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