Spring 6.2.9
When Spring Boot auto-configures a SimpleAsyncTaskScheduler
(with virtual threads enabled) to be used for running @Scheduled
tasks, closing the application context will immediately interrupt the threads executing those tasks.
I was expecting the taskTerminationTimeout
to be respected before hard-killing running tasks.
The offending code is here (calling Future.cancel
with true
):
https://github.com/spring-projects/spring-framework/blob/09a5ca3e747af3dacd2bbb42ae4f356db26b57d3/spring-context/src/main/java/org/springframework/scheduling/concurrent/SimpleAsyncTaskScheduler.java#L397-L401
and here (explicitly calling Thread.interrupt()
, not allowing graceful completion):
https://github.com/spring-projects/spring-framework/blob/09a5ca3e747af3dacd2bbb42ae4f356db26b57d3/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java#L362-L380
Note how running threads are interrupted immediately, and then we wait for them to finish their work.
A similar issue was fixed in the past (#31019), but the fix made there doesn't help if the underlying TaskExecutor
is itself also interrupting threads.
The end result is that scheduled tasks currently waiting for the database or some other service will be interrupted and fail immediately, thus preventing graceful shutdown and making the taskTerminationTimeout
almost useless.
Comment From: jhoeller
SimpleAsyncTaskScheduler
is trying to stop all triggers and fixed-delay tasks in its stop
implementation within Spring's lifecycle management; is this being called in your scenario? Are you alternatively or additionally relying on taskTerminationTimeout
there?
Comment From: kzander91
@jhoeller No. I put breakpoints into close()
and both stop
methods: Only close()
is being called during shutdown (during singleton destruction through DisposableBeanAdapter
).
I stepped through DefaultLifecycleProcessor#doStop
and found that the stop
method is skipped because SimpleAsyncTaskScheduler#isRunning
returns false
. That method delegates to triggerLifecycle.isRunning()
here:
https://github.com/spring-projects/spring-framework/blob/09a5ca3e747af3dacd2bbb42ae4f356db26b57d3/spring-context/src/main/java/org/springframework/scheduling/concurrent/SimpleAsyncTaskScheduler.java#L377-L380
That in turn checks if the underlying ExecutorService
is running:
https://github.com/spring-projects/spring-framework/blob/09a5ca3e747af3dacd2bbb42ae4f356db26b57d3/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorLifecycleDelegate.java#L101-L104
Which it isn't, because the ContextClosedEvent
has already shut it down here:
https://github.com/spring-projects/spring-framework/blob/09a5ca3e747af3dacd2bbb42ae4f356db26b57d3/spring-context/src/main/java/org/springframework/scheduling/concurrent/SimpleAsyncTaskScheduler.java#L382-L388
Comment From: jhoeller
Thanks, that helps a lot. I guess we need to check this.fixedDelayLifecycle.isRunning()
there as well since the trigger lifecycle usually ends quicker than the fixed-delay lifecycle. I'll try to bring this into SimpleAsyncTaskScheduler
in a 6.2.10 snapshot ASAP.
Comment From: kzander91
Note that even with stop
being called, non-fixed-delay tasks (cron, fixed-rate) are still being interrupted.
I just tested this by skipping the executor shutdown in the debugger to ensure that isRunning()
returns true
...
Comment From: jhoeller
Indeed, we still need to do something about the pro-active interruption in SimpleAsyncTaskExecutor.close()
- either integrating that tracking into SimpleAsyncTaskScheduler.stop()
or interrupting only after the taskTerminationTimeout
has been exceeded. The latter would help plain SimpleAsyncTaskExecutor
usage as well.
Comment From: github-actions[bot]
Fixed via 67e88f3c2023e6b5ec42995b412ccf1a8247c911
Comment From: jhoeller
This is available in the latest 6.2.10 snapshot now. Please give it an early try and let me know whether it works for you!
Comment From: kzander91
@jhoeller Thanks a lot, with the latest snapshot, the termination timeout is now being respected without prematurely interrupting my threads :)
Comment From: jhoeller
Great to hear! Thanks for the immediate feedback.
Comment From: JohnNiang
Hi @jhoeller , https://github.com/spring-projects/spring-framework/commit/67e88f3c2023e6b5ec42995b412ccf1a8247c911 is a break change for my case.
I set the termination timeout to 10s for the SimepleAsyncTaskScheduler before, and now my application have to wait for 10s to shutdown because of the rearrange execution order between Thread::interrupt
and threads.wait(this.taskTerminationTimeout)
.
IMO, the termination timeout setting is only for graceful shutdown instead of being stuck thread.
Please review the following code and you will know the exact use of terminaltion timeout:
https://github.com/spring-projects/spring-framework/blob/19d5ec67811e2f74bf08b1c909a609ead4380acc/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java#L440