I'm writing an application that requires me to dynamically spawn tasks based on user input. There can be hundreds of such tasks, and they will have different combinations of individual task timeouts, being one-off tasks, running at specific intervals, etc. As I'm using Java 21, virtual threads seem to be the best tool for spawning these individual tasks.
For each task, I need to be able to store some handle to it (so the submission needs to be non-blocking) and track its state (running, completed, failed), as well as be able to cancel it on demand.
To that end, I found SimpleAsyncTaskScheduler
, which seems to support most of what I need:
A simple implementation of Spring's TaskScheduler interface, using a single scheduler thread and executing every scheduled task in an individual separate thread. This is an attractive choice with virtual threads on JDK 21, expecting common usage with
setVirtualThreads(true)
.
There are various schedule/submit methods on the class, which either return a ScheduledFuture
/Future
respectively. These can be cancelled and tracked on demand if stored in a map, presumably. However, I do not see a way to timeout individual tasks. There is the setTaskTerminationTimeout
method, but that does not seem to support individual tasks.
For the problem I'm trying to solve, is this the right class to use, and if so, how can I adapt it for individual task termination timeouts, along with the other requirements mentioned above? What would be the idiomatic solution for Spring Boot applications?
The only way to timeout an individual task, represented by ScheduledFuture
/Future
returned by a TaskScheduler
implementation is to call Future.get(long, TimeUnit)
and catch TimeoutException
. Upon the exception you may want to cancel
the task, but be aware that this, depending on the nature of the task, may or may not lead to its immediate termination. That's all you can do in respect of timing out an individual Future
task. For example,
Future<?> future = simpleAsyncTaskScheduler.schedule( () -> {
// ...
}, Instant.now());
try {
future.get(TIMEOUT, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException e) {
// ... handle IE or EE
} catch (TimeoutException e) {
future.cancel(true);
// ... mark task as timed out
} catch (CancellationException e) {
// ... successfully cancelled
}
In that sense, @banitm's answer goes along the right lines.
The waiting on Future.get
can be arranged in dedicated virtual threads. If task completes normally, by timeout or somehow else, then the thread makes a correspondent mark in a task-related structure and terminates. If the task should be cancelled immediately, for example upon user request, another thread that handles such request should call Future.cancel(true)
, in this case, if cancellation is successful, the thread, waiting on Future.get,
receives CancellationException
and also terminates.
With such design, there would be two threads associated with a single task: the one that is managed by the scheduler, which actually executes the task, and other one that waits for completion of Future.get
. While such multiplication could hardly be acceptable for platform threads, virtual threads give us this freedom, moreover simple waiting on Future.get
is a best fit for them. Please check out a simple implementation of this design.
setTaskTerminationTimeout
method of SimpleAsyncTaskExecutor
does not only set a timeout for an individual task, it does not set a timeout for any task in a sense you expect it to set. Instead, it sets a timeout after expiration of which the SimpleAsyncTaskExecutor
can be closed. On close
method it just interrupts all tasks (essentially the same what Future.cancel
is doing) and then waits for this timeout for the tasks' completion. So, if you are not going to close your SimpleAsyncTaskExecutor
, taskTerminationTimeout
is totally irrelevant.
Also, SimpleAsyncTaskScheduler
is not suitable for timeout-based waiting on Future
because, according to its documentation,
This scheduler variant does not track the actual completion of tasks, but rather just the hand-off to an execution thread. As a consequence,
ScheduledFuture
... represents that and-off rather than the actual completion of the provided task (or series of repeated tasks)
It means, in particular, that Future
, returned by the methods of this class, returns immediately on Future.get
, even though the actual task is not yet completed. As an alternative, ConcurrentTaskScheduler
implementation of TaskScheduler
interface can be used as it returns a "wait-able" Future
. There are, however, some issue with configuring it with virtual threads, discussed in the details in SO thread How to use virtual threads with Scheduled Executor Service. A custom, virtual thread-compatible implementation of this interface is provided in the demo app.