Skip to content

Commit 8cf3967

Browse files
committed
fix: Use ScheduledExecutorService with uncaughtExceptionHandler
1 parent efabdc5 commit 8cf3967

4 files changed

Lines changed: 88 additions & 49 deletions

File tree

src/dev/enola/be/task/TaskExecutor.java

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import static java.util.concurrent.TimeUnit.MILLISECONDS;
44
import static java.util.concurrent.TimeUnit.MINUTES;
55

6+
import dev.enola.common.concurrent.Executors;
7+
68
import java.time.Duration;
79
import java.time.Instant;
810
import java.util.Map;
@@ -11,7 +13,6 @@
1113
import java.util.concurrent.Callable;
1214
import java.util.concurrent.ConcurrentHashMap;
1315
import java.util.concurrent.ExecutorService;
14-
import java.util.concurrent.Executors;
1516
import java.util.concurrent.Future;
1617
import java.util.concurrent.FutureTask;
1718
import java.util.concurrent.ScheduledExecutorService;
@@ -29,15 +30,20 @@ public class TaskExecutor implements AutoCloseable {
2930
// can still find them later, with a separate eviction policy for that persistent store.
3031
private final Map<UUID, Task<?, ?>> tasks = new ConcurrentHashMap<>();
3132

32-
private final ExecutorService executor = TaskExecutorServices.newVirtualThreadPerTaskExecutor();
33-
34-
// TODO Use LoggingScheduledExecutorService from Enola Commons, for both.
33+
// Nota bene: In *THEORY* we should *NEVER* have *ANY* uncaught exceptions from Task,
34+
// because any exception thrown by the task's `execute()` method would be caught and
35+
// wrapped in an ExecutionException by the Future returned by ExecutorService.submit().
36+
//
37+
// But in practice, who knows what the future holds, so we better log them just in case;
38+
// just because "swallowed" lost exceptions are seriously the worst kind of bugs to diagnose!
39+
private final ExecutorService executor =
40+
Executors.newVirtualThreadPerTaskExecutorWithLoggingThreadUncaughtExceptionHandler(LOG);
3541

3642
private final ScheduledExecutorService timeoutScheduler =
37-
Executors.newSingleThreadScheduledExecutor();
43+
Executors.newSingleThreadScheduledExecutor("TaskExecutor-Timeout", LOG);
3844

3945
private final ScheduledExecutorService cleanupScheduler =
40-
Executors.newSingleThreadScheduledExecutor();
46+
Executors.newSingleThreadScheduledExecutor("TaskExecutor-Cleanup", LOG);
4147

4248
public TaskExecutor(Duration completedTaskEvictionInterval) {
4349
var m = completedTaskEvictionInterval.toMinutes();

src/dev/enola/be/task/TaskExecutorServices.java

Lines changed: 0 additions & 43 deletions
This file was deleted.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package dev.enola.common.concurrent;
2+
3+
import static dev.enola.common.concurrent.LoggingThreadUncaughtExceptionHandler.toLogger;
4+
5+
import java.util.concurrent.ExecutorService;
6+
import java.util.concurrent.ScheduledExecutorService;
7+
import java.util.concurrent.ThreadFactory;
8+
import java.util.logging.Logger;
9+
10+
public final class Executors {
11+
12+
public static ScheduledExecutorService newSingleThreadScheduledExecutor(
13+
String namePrefix, Logger logger) {
14+
var tf = createVirtualThreadFactory(namePrefix, logger);
15+
return java.util.concurrent.Executors.newSingleThreadScheduledExecutor(tf);
16+
}
17+
18+
public static ExecutorService
19+
newVirtualThreadPerTaskExecutorWithLoggingThreadUncaughtExceptionHandler(
20+
Logger logger) {
21+
var tf = createVirtualThreadFactory(logger);
22+
return java.util.concurrent.Executors.newThreadPerTaskExecutor(tf);
23+
}
24+
25+
private static ThreadFactory createVirtualThreadFactory(Logger logger) {
26+
return Thread.ofVirtual()
27+
.uncaughtExceptionHandler(toLogger(logger))
28+
// NB: Virtual threads are always daemon threads, so no: .setDaemon(true)
29+
// TODO new ContextAwareThreadFactory() how-to?
30+
// TODO .inheritInheritableThreadLocals(true) ?
31+
.factory();
32+
}
33+
34+
private static ThreadFactory createVirtualThreadFactory(String namePrefix, Logger logger) {
35+
return Thread.ofVirtual()
36+
.name(namePrefix, 1)
37+
.uncaughtExceptionHandler(toLogger(logger))
38+
// NB: Virtual threads are always daemon threads, so no: .setDaemon(true)
39+
// TODO new ContextAwareThreadFactory() how-to?
40+
// TODO .inheritInheritableThreadLocals(true) ?
41+
.factory();
42+
}
43+
44+
private Executors() {}
45+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package dev.enola.common.concurrent;
2+
3+
import static java.util.Objects.requireNonNull;
4+
5+
import java.lang.Thread.UncaughtExceptionHandler;
6+
import java.util.logging.Level;
7+
import java.util.logging.Logger;
8+
9+
class LoggingThreadUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
10+
11+
// Like dev.enola.common.concurrent.LoggingThreadUncaughtExceptionHandler in
12+
// https://github.com/enola-dev/enola, but without the dependency on dev.enola.common and using
13+
// JUL instead of SLF4J. TODO Maybe move this to dev.enola.common, later?
14+
15+
private final Logger logger;
16+
17+
private LoggingThreadUncaughtExceptionHandler(Logger logger) {
18+
this.logger = requireNonNull(logger, "logger");
19+
}
20+
21+
/** Factory method to obtain an instance of this bound to the passed JUL Logger. */
22+
public static UncaughtExceptionHandler toLogger(Logger logger) {
23+
return new LoggingThreadUncaughtExceptionHandler(logger);
24+
}
25+
26+
@Override
27+
public void uncaughtException(Thread thread, Throwable throwable) {
28+
logger.log(
29+
Level.SEVERE, "Uncaught exception in thread '" + thread.getName() + "'", throwable);
30+
}
31+
}

0 commit comments

Comments
 (0)