Affects: demonstrated against 5.2.6.RELEASE
ThreadPoolTaskExecutor.setTaskDecorator()
allows you to, for example, catch and log any exceptions thrown by tasks submitted to the executor. In my application this functionality is very important, because otherwise bugs that cause exceptions would be completely swallowed and lurk indefinitely, unnoticed.
The bug is that this works if you use ThreadPoolTaskExecutor.execute()
to submit your task, but it doesn't work if you use ThreadPoolTaskExecutor.submit()
to submit your task.
Basically, when invoking any ThreadPoolTaskExecutor
method that returns a Future
, there is extra wrapping involved that catches and swallows thrown exceptions before they can reach the configured TaskDecorator
. This is a reversed wrapping order from that one would expect.
Here's a program that demonstrates the problem:
import org.springframework.core.task.TaskDecorator;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
public class ThreadPoolTaskExecutorBug {
public static void main(String[] args) throws Exception {
// Setup executor
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setTaskDecorator(action -> () -> {
try {
System.out.println("invoking " + action + ".run()");
action.run();
System.out.println("successful return from " + action + ".run()");
} catch (Throwable t) {
System.out.println("caught exception from " + action + ".run(): " + t.getMessage());
}
});
executor.afterPropertiesSet();
System.out.println();
System.out.println("TEST #1");
executor.execute(new Action(1));
Thread.sleep(500);
System.out.println();
System.out.println("TEST #2");
executor.submit(new Action(2));
Thread.sleep(500);
System.out.println();
executor.shutdown();
}
public static class Action implements Runnable {
private final int id;
public Action(int id) {
this.id = id;
}
@Override
public void run() {
System.out.println(this + ": run() invoked");
System.out.println(this + ": run() throwing exception");
throw new RuntimeException("exception thrown by " + this);
}
@Override
public String toString() {
return "Action#" + this.id;
}
}
}
Here's the output:
Jun 10, 2020 10:32:18 AM org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor initialize
INFO: Initializing ExecutorService
TEST #1
invoking Action#1.run()
Action#1: run() invoked
Action#1: run() throwing exception
caught exception from Action#1.run(): exception thrown by Action#1
TEST #2
invoking java.util.concurrent.FutureTask@62f7c498.run()
Action#2: run() invoked
Action#2: run() throwing exception
successful return from java.util.concurrent.FutureTask@62f7c498.run()
Jun 10, 2020 10:32:19 AM org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor shutdown
INFO: Shutting down ExecutorService
Note that under TEST #2
the action throws an exception, just like in TEST #1
, but the configured TaskDecorator
never gets a chance to catch and log the exception.
Comment From: jhoeller
This seems to be a common problem with the JDK's ThreadPoolExecutor
that we're building on there: see the note in its afterExecute
javadoc: https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ThreadPoolExecutor.html#afterExecute-java.lang.Runnable-java.lang.Throwable-
ThreadPoolExecutor.before/afterExecute
is essentially a variant of our TaskDecorator
, we suffer from the same limitations. Since TaskDecorator
does not imply exception handling directly, we cannot manually force Future
exceptions there. I'm afraid there is not much we can do about it; this unfortunately will have to be handled within affected TaskDecorator
implementations :-(
Comment From: archiecobbs
Thanks for the quick triage. If it can't be fixed then perhaps at least some additional documentation could be added to clarify the semantics of TaskDecorator
, in particular, that exceptions may not reach it.
Comment From: archiecobbs
Excellent - thank you.