Skip to content

Commit 9414c2d

Browse files
committed
Coordinated stop before destroy when ExecutorService not terminated yet
Closes gh-31549
1 parent 38724a1 commit 9414c2d

File tree

3 files changed

+30
-13
lines changed

3 files changed

+30
-13
lines changed

spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ public abstract class ExecutorConfigurationSupport extends CustomizableThreadFac
8686
@Nullable
8787
private ExecutorLifecycleDelegate lifecycleDelegate;
8888

89+
private volatile boolean lateShutdown;
90+
8991

9092
/**
9193
* Set the ThreadFactory to use for the ExecutorService's thread pool.
@@ -127,8 +129,9 @@ public void setRejectedExecutionHandler(@Nullable RejectedExecutionHandler rejec
127129
* <p>Default is {@code false} as of 6.1, triggering an early soft shutdown of
128130
* the executor and therefore rejecting any further task submissions. Switch this
129131
* to {@code true} in order to let other components submit tasks even during their
130-
* own destruction callbacks, at the expense of a longer shutdown phase.
131-
* This will usually go along with
132+
* own stop and destruction callbacks, at the expense of a longer shutdown phase.
133+
* The executor will not go through a coordinated lifecycle stop phase then
134+
* but rather only stop tasks on its own shutdown. This usually goes along with
132135
* {@link #setWaitForTasksToCompleteOnShutdown "waitForTasksToCompleteOnShutdown"}.
133136
* <p>This flag will only have effect when the executor is running in a Spring
134137
* application context and able to receive the {@link ContextClosedEvent}.
@@ -144,9 +147,13 @@ public void setAcceptTasksAfterContextClose(boolean acceptTasksAfterContextClose
144147
/**
145148
* Set whether to wait for scheduled tasks to complete on shutdown,
146149
* not interrupting running tasks and executing all tasks in the queue.
147-
* <p>Default is {@code false}, shutting down immediately through interrupting
148-
* ongoing tasks and clearing the queue. Switch this flag to {@code true} if
149-
* you prefer fully completed tasks at the expense of a longer shutdown phase.
150+
* <p>Default is {@code false}, with a coordinated lifecycle stop first (unless
151+
* {@link #setAcceptTasksAfterContextClose "acceptTasksAfterContextClose"}
152+
* has been set) and then an immediate shutdown through interrupting ongoing
153+
* tasks and clearing the queue. Switch this flag to {@code true} if you
154+
* prefer fully completed tasks at the expense of a longer shutdown phase.
155+
* The executor will not go through a coordinated lifecycle stop phase then
156+
* but rather only stop and wait for task completion on its own shutdown.
150157
* <p>Note that Spring's container shutdown continues while ongoing tasks
151158
* are being completed. If you want this executor to block and wait for the
152159
* termination of tasks before the rest of the container continues to shut
@@ -374,7 +381,7 @@ public void start() {
374381
*/
375382
@Override
376383
public void stop() {
377-
if (this.lifecycleDelegate != null) {
384+
if (this.lifecycleDelegate != null && !this.lateShutdown) {
378385
this.lifecycleDelegate.stop();
379386
}
380387
}
@@ -386,9 +393,12 @@ public void stop() {
386393
*/
387394
@Override
388395
public void stop(Runnable callback) {
389-
if (this.lifecycleDelegate != null) {
396+
if (this.lifecycleDelegate != null && !this.lateShutdown) {
390397
this.lifecycleDelegate.stop(callback);
391398
}
399+
else {
400+
callback.run();
401+
}
392402
}
393403

394404
/**
@@ -439,10 +449,16 @@ protected void afterExecute(Runnable task, @Nullable Throwable ex) {
439449
*/
440450
@Override
441451
public void onApplicationEvent(ContextClosedEvent event) {
442-
if (event.getApplicationContext() == this.applicationContext && !this.acceptTasksAfterContextClose) {
443-
// Early shutdown signal: accept no further tasks, let existing tasks complete
444-
// before hitting the actual destruction step in the shutdown() method above.
445-
initiateShutdown();
452+
if (event.getApplicationContext() == this.applicationContext) {
453+
if (this.acceptTasksAfterContextClose || this.waitForTasksToCompleteOnShutdown) {
454+
// Late shutdown without early stop lifecycle.
455+
this.lateShutdown = true;
456+
}
457+
else {
458+
// Early shutdown signal: accept no further tasks, let existing tasks complete
459+
// before hitting the actual destruction step in the shutdown() method above.
460+
initiateShutdown();
461+
}
446462
}
447463
}
448464

spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorLifecycleDelegate.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ public void stop(Runnable callback) {
9797

9898
@Override
9999
public boolean isRunning() {
100-
return (!this.executor.isShutdown() & !this.paused);
100+
return (!this.paused && !this.executor.isTerminated());
101101
}
102102

103103
void beforeExecute(Thread thread) {

spring-context/src/test/java/org/springframework/scheduling/annotation/EnableSchedulingTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,9 +344,10 @@ static class ExplicitSchedulerConfigSubclass extends ExplicitSchedulerConfig {
344344
@Override
345345
public TaskScheduler myTaskScheduler() {
346346
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
347+
scheduler.setPoolSize(2);
347348
scheduler.setThreadNamePrefix("explicitScheduler-");
349+
scheduler.setAcceptTasksAfterContextClose(true);
348350
scheduler.setAwaitTerminationMillis(1000);
349-
scheduler.setPoolSize(2);
350351
return scheduler;
351352
}
352353
}

0 commit comments

Comments
 (0)