• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

openmrs / openmrs-core / 37518343506

06 Oct 2026 07:21PM UTC coverage: 66.431% (-0.03%) from 66.459%
37518343506

push

github

dkayiwa
TRUNK-6819: Start the JobRunr job server only once OpenMRS has started (#6654)

The background job server was started while its application context was
still being created, so jobs could run before the context was ready. On
jetty:run this logged "EntityManagerFactory is closed" for a job that ran
during the context refresh after modules start, against the closed session
factory of the context being replaced, and "Task class ... not found" for a
module's job that ran after the first, core-only refresh, before modules
were loaded.

The server is now started by a SmartLifecycle once its context has finished
refreshing and stopped before the context closes. The webapp defers that
start until SchedulerService.onStartup(), which runs once modules have
started; contexts refreshed after startup start their server right away.

The webapp never closes its application context, so Context.shutdown() now
calls SchedulerService.onShutdown() first, as it did before the move to
JobRunr, and JobRunrSchedulerService stops the job server there, before the
modules and the session factory are shut down.

On 2.9.x, JobRunr 6 does not return the server in its configuration
result, so the lifecycle takes it from JobRunr, and the scheduler service
is declared in applicationContext-service.xml, so its lifecycle is set
there rather than with @Autowired.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NK4kFG8qyevx9Bk9jWoEgS
Claude-Session: https://claude.ai/code/session_01PdDdZrB8Q5eMgej5FmRx4R
Claude-Session: https://claude.ai/code/session_01LZ92BDLHrn6xACczVMtSiR

33 of 37 new or added lines in 5 files covered. (89.19%)

38 existing lines in 9 files now uncovered.

25960 of 39078 relevant lines covered (66.43%)

0.66 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

50.63
/api/src/main/java/org/openmrs/api/context/Daemon.java
1
/**
2
 * This Source Code Form is subject to the terms of the Mozilla Public License,
3
 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
4
 * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
5
 * the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
6
 *
7
 * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
8
 * graphic logo is a trademark of OpenMRS Inc.
9
 */
10
package org.openmrs.api.context;
11

12
import java.util.List;
13
import java.util.concurrent.Callable;
14
import java.util.concurrent.CountDownLatch;
15
import java.util.concurrent.ExecutionException;
16
import java.util.concurrent.Future;
17
import java.util.stream.Collectors;
18

19
import org.apache.commons.collections.CollectionUtils;
20
import org.openmrs.Role;
21
import org.openmrs.User;
22
import org.openmrs.api.APIAuthenticationException;
23
import org.openmrs.api.APIException;
24
import org.openmrs.api.OpenmrsService;
25
import org.openmrs.api.cache.GlobalPropertyCache;
26
import org.openmrs.api.cache.RolePrivilegeCache;
27
import org.openmrs.api.db.ContextDAO;
28
import org.openmrs.api.db.hibernate.HibernateContextDAO;
29
import org.openmrs.module.DaemonToken;
30
import org.openmrs.module.Module;
31
import org.openmrs.module.ModuleException;
32
import org.openmrs.module.ModuleFactory;
33
import org.openmrs.scheduler.jobrunr.JobRequestHandlerAdapter;
34
import org.openmrs.util.OpenmrsThreadPoolHolder;
35
import org.slf4j.Logger;
36
import org.slf4j.LoggerFactory;
37
import org.springframework.context.support.AbstractRefreshableApplicationContext;
38

39
/**
40
 * This class allows certain tasks to run with elevated privileges. Primary use is scheduling and
41
 * module startup when there is no user to authenticate as.
42
 */
43
public final class Daemon {
44

45
        /**
46
         * The uuid defined for the daemon user object
47
         */
48
        static final String DAEMON_USER_UUID = "A4F30A1B-5EB9-11DF-A648-37A07F9C90FB";
49

50
        private static final ThreadLocal<Boolean> isDaemonThread = new ThreadLocal<>();
1 ✔
51

52
        private static final ThreadLocal<User> daemonThreadUser = new ThreadLocal<>();
1 ✔
53

54
        private static final Logger log = LoggerFactory.getLogger("org.openmrs.api");
1 ✔
55

56
        /**
57
         * Capability token passed to expected callers allowed to create DaemonThreads.
58
         *
59
         * @since 3.0.0, 2.9.0, 2.8.9
60
         */
61
        public static final class CallerKey {
62

63
                private CallerKey() {
64
                }
65
        }
66

67
        /**
68
         * The single {@link CallerKey} instance. Must not be exposed to the public API
69
         */
70
        private static final CallerKey CALLER_KEY = new CallerKey();
1 ✔
71

72
        static {
73
                // send the key to our known collaborators
74
                HibernateContextDAO.setDaemonCallerKey(CALLER_KEY);
1 ✔
75
                ModuleFactory.setDaemonCallerKey(CALLER_KEY);
1 ✔
76
                JobRequestHandlerAdapter.setDaemonCallerKey(CALLER_KEY);
1 ✔
77
                RolePrivilegeCache.setDaemonCallerKey(CALLER_KEY);
1 ✔
78
                GlobalPropertyCache.setDaemonCallerKey(CALLER_KEY);
1 ✔
79
                // WebDaemon lives in the web module, which the api module cannot reference at compile time, so
80
                // hand it the key reflectively.
81
                try {
82
                        Class.forName("org.openmrs.web.WebDaemon").getMethod("setDaemonCallerKey", CallerKey.class).invoke(null,
×
83
                            CALLER_KEY);
84
                }
85
                catch (ClassNotFoundException e) {
1 ✔
86
                        log.debug("Could not load WebDaemon class", e);
1 ✔
87
                }
88
                catch (ReflectiveOperationException e) {
×
89
                        log.error("Exception caught while trying to provide DaemonCallerKey to WebDaemon", e);
×
90
                }
1 ✔
91
        }
1 ✔
92

93
        /**
94
         * private constructor to override the default constructor to prevent it from being instantiated.
95
         */
96
        private Daemon() {
97
        }
98

99
        /**
100
         * Forces this class to initialize, which distributes the caller key to its trusted collaborators.
101
         *
102
         * @since 3.0.0, 2.9.0, 2.8.9
103
         */
104
        public static void ensureInitialized() {
105
                // Merely invoking a static method guarantees Daemon's static initializer has run; there is
106
                // deliberately nothing else to do here.
107
        }
×
108

109
        /**
110
         * @see #startModule(Module, boolean, AbstractRefreshableApplicationContext, CallerKey)
111
         */
112
        public static Module startModule(Module module) throws ModuleException {
113
                return startModule(module, false, null, CALLER_KEY);
×
114
        }
115

116
        /**
117
         * This method should not be called directly. The {@link ModuleFactory#startModule(Module)} method
118
         * uses this to start the given module in a new thread that is authenticated as the daemon user.
119
         * <br>
120
         * If a non-null application context is passed in, it gets refreshed to make the module's services
121
         * available
122
         *
123
         * @param module the module to start
124
         * @param isOpenmrsStartup Specifies whether this module is being started at application startup or
125
         *            not
126
         * @param applicationContext the spring application context instance to refresh
127
         * @param callerKey the {@link CallerKey} proving the caller is permitted to start modules
128
         * @return the module returned from {@link ModuleFactory#startModuleInternal(Module)}
129
         */
130
        public static Module startModule(final Module module, final boolean isOpenmrsStartup,
131
                final AbstractRefreshableApplicationContext applicationContext, CallerKey callerKey) throws ModuleException {
132
                requireDaemonCaller(callerKey, "Module.factory.only");
1 ✔
133

134
                Future<Module> moduleStartFuture = runInDaemonThreadInternal(
1 ✔
135
                    () -> ModuleFactory.startModuleInternal(module, isOpenmrsStartup, applicationContext));
1 ✔
136

137
                // wait for the "startModule" thread to finish
138
                try {
139
                        return moduleStartFuture.get();
1 ✔
140
                } catch (InterruptedException e) {
×
141
                        // ignore
142
                } catch (ExecutionException e) {
×
143
                        if (e.getCause() instanceof ModuleException) {
×
144
                                throw (ModuleException) e.getCause();
×
145
                        } else {
146
                                throw new ModuleException("Unable to start module " + module.getName(), e);
×
147
                        }
148
                }
×
149

150
                return module;
×
151
        }
152

153
        /**
154
         * This method should not be called directly; it is guarded by the daemon caller key, which is
155
         * issued only to the {@link ContextDAO} implementation.
156
         * <p>
157
         * <strong>Should</strong> only allow the creation of new users, not the edition of existing ones
158
         *
159
         * @param user A new user to be created.
160
         * @param password The password to set for the new user.
161
         * @param roleNames A list of role names to fetch the roles to add to the user.
162
         * @param callerKey the {@link CallerKey} proving the caller is permitted to create users
163
         * @return The newly created user
164
         * @since 2.3.0
165
         */
166
        public static User createUser(User user, String password, List<String> roleNames, CallerKey callerKey) throws Exception {
167
                requireDaemonCaller(callerKey, "Context.DAO.only");
1 ✔
168

169
                // create a new thread and execute that task in it
170
                Future<User> userFuture = runInDaemonThreadInternal(() -> {
1 ✔
171
                        if ((user.getId() != null && Context.getUserService().getUser(user.getId()) != null)
1 ✔
172
                                || Context.getUserService().getUserByUuid(user.getUuid()) != null
1 ✔
173
                                || Context.getUserService().getUserByUsername(user.getUsername()) != null || (user.getEmail() != null
1 ✔
174
                                        && Context.getUserService().getUserByUsernameOrEmail(user.getEmail()) != null)) {
×
175
                                throw new APIException("User.creating.already.exists", new Object[] { user.getDisplayString() });
1 ✔
176
                        }
177

178
                        if (!CollectionUtils.isEmpty(roleNames)) {
1 ✔
179
                                List<Role> roles = roleNames.stream().map(roleName -> Context.getUserService().getRole(roleName))
1 ✔
180
                                        .collect(Collectors.toList());
1 ✔
181
                                roles.forEach(user::addRole);
1 ✔
182
                        }
183

184
                        return Context.getUserService().createUser(user, password);
1 ✔
185
                });
186

187
                // wait for the 'create user' thread to finish
188
                try {
189
                        return userFuture.get();
1 ✔
190
                } catch (InterruptedException e) {
×
191
                        // ignore
192
                } catch (ExecutionException e) {
1 ✔
193
                        if (e.getCause() instanceof Exception) {
1 ✔
194
                                throw (Exception) e.getCause();
1 ✔
195
                        } else {
196
                                throw new RuntimeException(e.getCause());
×
197
                        }
198
                }
×
199

200
                return null;
×
201
        }
202

203
        /**
204
         * Call this method if you are inside a Daemon thread (for example in a Module activator or a
205
         * scheduled task) and you want to start up a new parallel Daemon thread. You may only call this
206
         * method from a Daemon thread.
207
         *
208
         * @param runnable what to run in a new thread
209
         * @return the newly spawned {@link Thread}
210
         * @deprecated As of 2.7.0, consider using {@link #runNewDaemonTask(Runnable)} instead
211
         */
212
        @Deprecated
213
        public static Thread runInNewDaemonThread(final Runnable runnable) {
214
                // make sure we're already in a daemon thread
215
                if (!isDaemonThread()) {
1 ✔
216
                        throw new APIAuthenticationException("Only daemon threads can spawn new daemon threads");
1 ✔
217
                }
218

219
                // the previous implementation ensured that Thread.start() was called before this function returned
220
                // since we cannot guarantee that the executor will run the thread when `execute()` is called, we need another
221
                // mechanism to ensure the submitted Runnable was actually started.
222
                final CountDownLatch countDownLatch = new CountDownLatch(1);
×
223

224
                // we should consider making DaemonThread public, so the caller can access returnedObject and exceptionThrown
225
                DaemonThread thread = new DaemonThread() {
×
226

227
                        @Override
228
                        public void run() {
229
                                isDaemonThread.set(true);
×
230
                                try {
231
                                        Context.openSession();
×
232
                                        countDownLatch.countDown();
×
233
                                        //Suppressing sonar issue "squid:S1217"
234
                                        //We intentionally do not start a new thread yet, rather wrap the run call in a session.
235
                                        runnable.run();
×
236
                                } finally {
237
                                        try {
238
                                                Context.closeSession();
×
239
                                        } finally {
240
                                                isDaemonThread.remove();
×
241
                                                daemonThreadUser.remove();
×
242
                                        }
243
                                }
244
                        }
×
245
                };
246

247
                OpenmrsThreadPoolHolder.threadExecutor.execute(thread);
×
248

249
                // do not return until the thread is actually started to emulate the previous behaviour
250
                try {
251
                        countDownLatch.await();
×
252
                } catch (InterruptedException ignored) {}
×
253

254
                return thread;
×
255
        }
256

257
        /**
258
         * Call this method if you are inside a Daemon thread (for example in a Module activator or a
259
         * scheduled task) and you want to start up a new parallel Daemon thread. You may only call this
260
         * method from a Daemon thread.
261
         *
262
         * @param callable what to run in a new thread
263
         * @return a future that completes when the task is done;
264
         * @since 2.7.0
265
         */
266
        @SuppressWarnings({ "squid:S1217", "unused" })
267
        public static <T> Future<T> runInNewDaemonThread(final Callable<T> callable) {
268
                // make sure we're already in a daemon thread
269
                if (!isDaemonThread()) {
1 ✔
270
                        throw new APIAuthenticationException("Only daemon threads can spawn new daemon threads");
1 ✔
271
                }
272

273
                return runInDaemonThreadInternal(callable);
×
274
        }
275

276
        /**
277
         * Call this method if you are inside a Daemon thread (for example in a Module activator or a
278
         * scheduled task) and you want to start up a new parallel Daemon thread. You may only call this
279
         * method from a Daemon thread.
280
         *
281
         * @param runnable what to run in a new thread
282
         * @return a future that completes when the task is done;
283
         * @since 2.7.0
284
         */
285
        @SuppressWarnings({ "squid:S1217", "unused" })
286
        public static Future<?> runNewDaemonTask(final Runnable runnable) {
287
                // make sure we're already in a daemon thread
288
                if (!isDaemonThread()) {
1 ✔
289
                        throw new APIAuthenticationException("Only daemon threads can spawn new daemon threads");
1 ✔
290
                }
291

292
                return runInDaemonThreadInternal(runnable);
×
293
        }
294

295
        /**
296
         * Runs the given task on a new daemon thread, authorized by a {@link CallerKey}. This exists
297
         * strictly for internal use by trusted core entry points (such as {@code WebDaemon} startup) that
298
         * must launch daemon work from a non-daemon thread.
299
         *
300
         * @param runnable what to run in a new daemon thread
301
         * @param callerKey the {@link CallerKey} proving the caller is a trusted daemon entry point
302
         * @return a future that completes when the task is done
303
         * @since 3.0.0, 2.9.0, 2.8.9
304
         */
305
        @SuppressWarnings({ "squid:S1217", "unused" })
306
        public static Future<?> runNewDaemonTask(final Runnable runnable, CallerKey callerKey) {
307
                requireDaemonCaller(callerKey, "runNewDaemonTask can only be called by an authorized daemon entry point");
1 ✔
308

309
                return runInDaemonThreadInternal(runnable);
1 ✔
310
        }
311

312
        /**
313
         * Runs the given task on a new daemon thread, authorized by a {@link CallerKey}, and returns a
314
         * {@link Future} for its result without blocking. This exists strictly for internal use by trusted
315
         * core entry points that must launch daemon work from a non-daemon thread and want to await the
316
         * result themselves (for example coalescing concurrent callers onto a single future).
317
         *
318
         * @param callable what to run in a new daemon thread
319
         * @param callerKey the {@link CallerKey} proving the caller is a trusted daemon entry point
320
         * @return a future that completes with the task's result
321
         * @since 3.0.0, 2.9.0, 2.8.9
322
         */
323
        @SuppressWarnings("squid:S1217")
324
        public static <T> Future<T> runNewDaemonTask(final Callable<T> callable, CallerKey callerKey) {
325
                requireDaemonCaller(callerKey, "runNewDaemonTask can only be called by an authorized daemon entry point");
×
326

327
                return runInDaemonThreadInternal(callable);
×
328
        }
329

330
        /**
331
         * @return true if the current thread was started by this class and so is a daemon thread that has
332
         *         all privileges
333
         * @see Context#hasPrivilege(String)
334
         */
335
        public static boolean isDaemonThread() {
336
                Boolean b = isDaemonThread.get();
1 ✔
337
                return b != null && b;
1 ✔
338
        }
339

340
        /**
341
         * Calls the {@link OpenmrsService#onStartup()} method, as a daemon, for an instance implementing
342
         * the {@link OpenmrsService} interface.
343
         *
344
         * @param service instance implementing the {@link OpenmrsService} interface.
345
         * @param callerKey the {@link CallerKey} proving the caller is permitted to run service startup
346
         * @since 1.9
347
         */
348
        public static void runStartupForService(final OpenmrsService service, CallerKey callerKey) throws ModuleException {
349
                requireDaemonCaller(callerKey, "Service.context.only");
1 ✔
350

351
                Future<?> future = runInDaemonThreadInternal(service::onStartup);
1 ✔
352

353
                // wait for the "onStartup" thread to finish
354
                try {
355
                        future.get();
1 ✔
356
                } catch (InterruptedException e) {
×
357
                        // ignore
358
                } catch (ExecutionException e) {
×
359
                        if (e.getCause() instanceof ModuleException) {
×
360
                                throw (ModuleException) e.getCause();
×
361
                        } else {
362
                                throw new ModuleException("Unable to run onStartup() method of service {}",
×
363
                                        service.getClass().getSimpleName(), e);
×
364
                        }
365
                }
1 ✔
366
        }
1 ✔
367

368
        /**
369
         * Executes the given runnable in a new thread that is authenticated as the daemon user.
370
         *
371
         * @param runnable an object implementing the {@link Runnable} interface.
372
         * @param token the token required to run code as the daemon user
373
         * @return the newly spawned {@link Thread}
374
         * @since 1.9.2
375
         * @deprecated Since 2.7.0 use {@link #runInDaemonThreadWithoutResult(Runnable, DaemonToken)}
376
         *             instead
377
         */
378
        @Deprecated
379
        @SuppressWarnings({ "squid:S1217", "unused" })
380
        public static Thread runInDaemonThread(final Runnable runnable, DaemonToken token) {
381
                if (!ModuleFactory.isTokenValid(token)) {
×
382
                        throw new ContextAuthenticationException("Invalid token " + token);
×
383
                }
384

385
                DaemonThread thread = new DaemonThread() {
×
386

387
                        @Override
388
                        public void run() {
389
                                isDaemonThread.set(true);
×
390
                                try {
391
                                        Context.openSession();
×
392
                                        //Suppressing sonar issue "squid:S1217"
393
                                        //We intentionally do not start a new thread yet, rather wrap the run call in a session.
394
                                        runnable.run();
×
395
                                } finally {
396
                                        try {
397
                                                Context.closeSession();
×
398
                                        } finally {
399
                                                isDaemonThread.remove();
×
400
                                                daemonThreadUser.remove();
×
401
                                        }
402
                                }
403
                        }
×
404
                };
405

406
                OpenmrsThreadPoolHolder.threadExecutor.execute(thread);
×
407
                return thread;
×
408
        }
409

410
        /**
411
         * Executes the given runnable in a new thread that is authenticated as the daemon user.
412
         *
413
         * @param callable an object implementing the {@link Callable<T>} interface to be run
414
         * @param token the token required to run code as the daemon user
415
         * @return the newly spawned {@link Thread}
416
         * @since 2.7.0
417
         */
418
        @SuppressWarnings({ "squid:S1217", "unused" })
419
        public static <T> Future<T> runInDaemonThread(final Callable<T> callable, DaemonToken token) {
420
                if (!ModuleFactory.isTokenValid(token)) {
×
421
                        throw new ContextAuthenticationException("Invalid token");
×
422
                }
423

424
                return runInDaemonThreadInternal(callable);
×
425
        }
426

427
        /**
428
         * Executes the given runnable in a new thread that is authenticated as the daemon user.
429
         *
430
         * @param runnable an object implementing the {@link Runnable} interface to be run
431
         * @param token the token required to run code as the daemon user
432
         * @return the newly spawned {@link Thread}
433
         * @since 2.7.0
434
         */
435
        @SuppressWarnings("squid:S1217")
436
        public static Future<?> runInDaemonThreadWithoutResult(final Runnable runnable, DaemonToken token) {
437
                if (!ModuleFactory.isTokenValid(token)) {
×
438
                        throw new ContextAuthenticationException("Invalid token");
×
439
                }
440

441
                return runInDaemonThreadInternal(runnable);
×
442
        }
443

444
        /**
445
         * Executes the given runnable in a new thread that is authenticated as the daemon user and wait for
446
         * the thread to finish.
447
         *
448
         * @param runnable an object implementing the {@link Runnable} interface.
449
         * @param token the token required to run code as the daemon user
450
         * @since 2.7.0
451
         */
452
        public static void runInDaemonThreadAndWait(final Runnable runnable, DaemonToken token) {
453
                Future<?> daemonThread = runInDaemonThreadWithoutResult(runnable, token);
×
454

455
                try {
456
                        daemonThread.get();
×
457
                } catch (InterruptedException | ExecutionException e) {
×
458
                        // Ignored
459
                }
×
460
        }
×
461

462
        private static <T> Future<T> runInDaemonThreadInternal(Callable<T> callable) {
463
                return OpenmrsThreadPoolHolder.threadExecutor.submit(() -> {
1 ✔
464
                        isDaemonThread.set(true);
1 ✔
465
                        try {
466
                                Context.openSession();
1 ✔
467
                                return callable.call();
1 ✔
468
                        } finally {
469
                                try {
470
                                        Context.closeSession();
1 ✔
471
                                } finally {
472
                                        isDaemonThread.remove();
1 ✔
473
                                        daemonThreadUser.remove();
1 ✔
474
                                }
475
                        }
476
                });
477
        }
478

479
        private static Future<?> runInDaemonThreadInternal(Runnable runnable) {
480
                // for Threads, we used to guarantee that Thread.start() was called before the function returned
481
                // since we cannot guarantee that the executor actually started executing the thread, we use a CountDownLatch
482
                // to emulate this behaviour when the user submits a Thread. Other runnables are unaffected.
483
                CountDownLatch countDownLatch = getCountDownLatch(runnable instanceof Thread);
1 ✔
484

485
                Future<?> result = OpenmrsThreadPoolHolder.threadExecutor.submit(() -> {
1 ✔
486
                        isDaemonThread.set(true);
1 ✔
487
                        try {
488
                                Context.openSession();
1 ✔
489
                                countDownLatch.countDown();
1 ✔
490
                                runnable.run();
1 ✔
491
                        } finally {
492
                                try {
493
                                        Context.closeSession();
1 ✔
494
                                } finally {
495
                                        isDaemonThread.remove();
1 ✔
496
                                        daemonThreadUser.remove();
1 ✔
497
                                }
498
                        }
499
                });
1 ✔
500

501
                try {
502
                        countDownLatch.await();
1 ✔
503
                } catch (InterruptedException ignored) {}
1 ✔
504

505
                return result;
1 ✔
506
        }
507

508
        private static CountDownLatch getCountDownLatch(boolean isThread) {
509
                return isThread ? new CountDownLatch(1) : new CountDownLatch(0);
1 ✔
510
        }
511

512
        /**
513
         * @return the capability required to invoke Daemon's guarded entry points. Package-private, so it
514
         *         is reachable only by trusted collaborators.
515
         */
516
        static CallerKey callerKey() {
517
                return CALLER_KEY;
1 ✔
518
        }
519

520
        /**
521
         * Rejects the call unless the supplied key is the genuine {@link #CALLER_KEY}. Fails closed for any
522
         * other value (including null).
523
         *
524
         * @param callerKey the key presented by the caller
525
         * @param messageCode the message (code) identifying the guarded operation
526
         */
527
        private static void requireDaemonCaller(CallerKey callerKey, String messageCode) {
528
                if (callerKey != CALLER_KEY) {
1 ✔
529
                        throw new APIException(messageCode, new Object[] { "an unauthorized caller" });
1 ✔
530
                }
531
        }
1 ✔
532

533
        /**
534
         * Executes the given task as the given user. <br>
535
         * <br>
536
         * This is guarded by the daemon caller key, which is issued only to {@link JobRequestHandlerAdapter}.
537
         * <p>
538
         * <strong>Should</strong> not be called from other methods other than JobRequestHandlerAdapter
539
         * <strong>Should</strong> not throw error if called from a JobRequestHandlerAdapter class
540
         *
541
         * @param userSystemId the user to run as
542
         * @param runnable the task to run
543
         * @param callerKey the {@link CallerKey} proving the caller is permitted to execute scheduled tasks
544
         * @since 2.9.0
545
         */
546
        public static void executeScheduledTaskAsUser(String userSystemId, DaemonTask runnable, CallerKey callerKey)
547
                throws Exception {
UNCOV
548
                requireDaemonCaller(callerKey, "executeScheduledTaskAsUser can only be called from JobRequestHandlerAdapter");
×
549

UNCOV
550
                isDaemonThread.set(true);
×
551
                try {
UNCOV
552
                        Context.openSession();
×
553
                        Context.getUserContext().becomeUser(userSystemId);
×
554
                        isDaemonThread.remove();
×
555
                        runnable.run();
×
556
                } finally {
UNCOV
557
                        isDaemonThread.remove();
×
UNCOV
558
                        Context.closeSession();
×
559
                }
560
        }
×
561

562
        /**
563
         * Thread class used by the {@link Daemon#startModule(Module)} method so that the returned object
564
         * and the exception thrown can be returned to calling class
565
         */
566
        protected static class DaemonThread extends Thread {
×
567

568
                /**
569
                 * The object returned from the method called in {@link #run()}
570
                 */
571
                protected Object returnedObject = null;
×
572

573
                /**
574
                 * The exception thrown (if any) by the method called in {@link #run()}
575
                 */
576
                protected Exception exceptionThrown = null;
×
577

578
                /**
579
                 * Gets the exception thrown (if any) by the method called in {@link #run()}
580
                 *
581
                 * @return the thrown exception (if any).
582
                 */
583
                public Exception getExceptionThrown() {
584
                        return exceptionThrown;
×
585
                }
586
        }
587

588
        @FunctionalInterface
589
        public interface DaemonTask {
590

591
                void run() throws Exception;
592
        }
593

594
        /**
595
         * Checks whether user is Daemon. However, this is not the preferred method for checking to see if
596
         * the current thread is a daemon thread, rather use {@link #isDaemonThread()}. isDaemonThread is
597
         * preferred for checking to see if you are in that thread or if the current thread is daemon.
598
         *
599
         * @param user user whom we are checking if daemon
600
         * @return true if user is Daemon
601
         */
602
        public static boolean isDaemonUser(User user) {
603
                return DAEMON_USER_UUID.equals(user.getUuid());
1 ✔
604
        }
605

606
        /**
607
         * @return the current thread daemon user or null if not assigned
608
         * @since 2.0.0, 1.12.0, 1.11.6, 1.10.4, 1.9.11
609
         */
610
        public static User getDaemonThreadUser() {
611
                if (isDaemonThread()) {
1 ✔
612
                        User user = daemonThreadUser.get();
1 ✔
613
                        if (user == null) {
1 ✔
614
                                user = Context.getContextDAO().getUserByUuid(DAEMON_USER_UUID);
1 ✔
615
                                daemonThreadUser.set(user);
1 ✔
616
                        }
617
                        return user;
1 ✔
618
                } else {
619
                        return null;
×
620
                }
621
        }
622

623
        public static String getDaemonUserUuid() {
624
                return DAEMON_USER_UUID;
1 ✔
625
        }
626
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc