Skip to content

Commit a2000db

Browse files
committed
Leniently accept tasks after context close in lifecycle stop phase
Schedulers remain strict, just plain executors are lenient on shutdown now. An early shutdown for executors can be enforced via setStrictEarlyShutdown. Closes gh-32226
1 parent 4a3ef3e commit a2000db

File tree

5 files changed

+122
-34
lines changed

5 files changed

+122
-34
lines changed

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

+29-4
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,11 @@ public void setRejectedExecutionHandler(@Nullable RejectedExecutionHandler rejec
139139
* the executor's destruction step, with individual awaiting according to the
140140
* {@link #setAwaitTerminationSeconds "awaitTerminationSeconds"} property.
141141
* <p>This flag will only have effect when the executor is running in a Spring
142-
* application context and able to receive the {@link ContextClosedEvent}.
142+
* application context and able to receive the {@link ContextClosedEvent}. Also,
143+
* note that {@link ThreadPoolTaskExecutor} effectively accepts tasks after context
144+
* close by default, in combination with a coordinated lifecycle stop, unless
145+
* {@link ThreadPoolTaskExecutor#setStrictEarlyShutdown "strictEarlyShutdown"}
146+
* has been specified.
143147
* @since 6.1
144148
* @see org.springframework.context.ConfigurableApplicationContext#close()
145149
* @see DisposableBean#destroy()
@@ -294,6 +298,9 @@ public void destroy() {
294298
* scheduling of periodic tasks, letting existing tasks complete still.
295299
* This step is non-blocking and can be applied as an early shutdown signal
296300
* before following up with a full {@link #shutdown()} call later on.
301+
* <p>Automatically called for early shutdown signals on
302+
* {@link #onApplicationEvent(ContextClosedEvent) context close}.
303+
* Can be manually called as well, in particular outside a container.
297304
* @since 6.1
298305
* @see #shutdown()
299306
* @see java.util.concurrent.ExecutorService#shutdown()
@@ -463,11 +470,29 @@ public void onApplicationEvent(ContextClosedEvent event) {
463470
this.lateShutdown = true;
464471
}
465472
else {
466-
// Early shutdown signal: accept no further tasks, let existing tasks complete
467-
// before hitting the actual destruction step in the shutdown() method above.
468-
initiateShutdown();
473+
if (this.lifecycleDelegate != null) {
474+
this.lifecycleDelegate.markShutdown();
475+
}
476+
initiateEarlyShutdown();
469477
}
470478
}
471479
}
472480

481+
/**
482+
* Early shutdown signal: do not trigger further tasks, let existing tasks complete
483+
* before hitting the actual destruction step in the {@link #shutdown()} method.
484+
* This goes along with a {@link #stop(Runnable) coordinated lifecycle stop phase}.
485+
* <p>Called from {@link #onApplicationEvent(ContextClosedEvent)} if no
486+
* indications for a late shutdown have been determined, that is, if the
487+
* {@link #setAcceptTasksAfterContextClose "acceptTasksAfterContextClose} and
488+
* {@link #setWaitForTasksToCompleteOnShutdown "waitForTasksToCompleteOnShutdown"}
489+
* flags have not been set.
490+
* <p>The default implementation calls {@link #initiateShutdown()}.
491+
* @since 6.1.4
492+
* @see #initiateShutdown()
493+
*/
494+
protected void initiateEarlyShutdown() {
495+
initiateShutdown();
496+
}
497+
473498
}

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

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -42,6 +42,8 @@ final class ExecutorLifecycleDelegate implements SmartLifecycle {
4242

4343
private volatile boolean paused;
4444

45+
private volatile boolean shutdown;
46+
4547
private int executingTaskCount = 0;
4648

4749
@Nullable
@@ -100,10 +102,14 @@ public boolean isRunning() {
100102
return (!this.paused && !this.executor.isTerminated());
101103
}
102104

105+
void markShutdown() {
106+
this.shutdown = true;
107+
}
108+
103109
void beforeExecute(Thread thread) {
104110
this.pauseLock.lock();
105111
try {
106-
while (this.paused && !this.executor.isShutdown()) {
112+
while (this.paused && !this.shutdown && !this.executor.isShutdown()) {
107113
this.unpaused.await();
108114
}
109115
}

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

+31-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -71,11 +71,13 @@ public class ThreadPoolExecutorFactoryBean extends ExecutorConfigurationSupport
7171

7272
private int keepAliveSeconds = 60;
7373

74+
private int queueCapacity = Integer.MAX_VALUE;
75+
7476
private boolean allowCoreThreadTimeOut = false;
7577

7678
private boolean prestartAllCoreThreads = false;
7779

78-
private int queueCapacity = Integer.MAX_VALUE;
80+
private boolean strictEarlyShutdown = false;
7981

8082
private boolean exposeUnconfigurableExecutor = false;
8183

@@ -107,6 +109,18 @@ public void setKeepAliveSeconds(int keepAliveSeconds) {
107109
this.keepAliveSeconds = keepAliveSeconds;
108110
}
109111

112+
/**
113+
* Set the capacity for the ThreadPoolExecutor's BlockingQueue.
114+
* Default is {@code Integer.MAX_VALUE}.
115+
* <p>Any positive value will lead to a LinkedBlockingQueue instance;
116+
* any other value will lead to a SynchronousQueue instance.
117+
* @see java.util.concurrent.LinkedBlockingQueue
118+
* @see java.util.concurrent.SynchronousQueue
119+
*/
120+
public void setQueueCapacity(int queueCapacity) {
121+
this.queueCapacity = queueCapacity;
122+
}
123+
110124
/**
111125
* Specify whether to allow core threads to time out. This enables dynamic
112126
* growing and shrinking even in combination with a non-zero queue (since
@@ -129,15 +143,15 @@ public void setPrestartAllCoreThreads(boolean prestartAllCoreThreads) {
129143
}
130144

131145
/**
132-
* Set the capacity for the ThreadPoolExecutor's BlockingQueue.
133-
* Default is {@code Integer.MAX_VALUE}.
134-
* <p>Any positive value will lead to a LinkedBlockingQueue instance;
135-
* any other value will lead to a SynchronousQueue instance.
136-
* @see java.util.concurrent.LinkedBlockingQueue
137-
* @see java.util.concurrent.SynchronousQueue
146+
* Specify whether to initiate an early shutdown signal on context close,
147+
* disposing all idle threads and rejecting further task submissions.
148+
* <p>Default is "false".
149+
* See {@link ThreadPoolTaskExecutor#setStrictEarlyShutdown} for details.
150+
* @since 6.1.4
151+
* @see #initiateShutdown()
138152
*/
139-
public void setQueueCapacity(int queueCapacity) {
140-
this.queueCapacity = queueCapacity;
153+
public void setStrictEarlyShutdown(boolean defaultEarlyShutdown) {
154+
this.strictEarlyShutdown = defaultEarlyShutdown;
141155
}
142156

143157
/**
@@ -222,6 +236,13 @@ protected BlockingQueue<Runnable> createQueue(int queueCapacity) {
222236
}
223237
}
224238

239+
@Override
240+
protected void initiateEarlyShutdown() {
241+
if (this.strictEarlyShutdown) {
242+
super.initiateEarlyShutdown();
243+
}
244+
}
245+
225246

226247
@Override
227248
@Nullable

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

+36-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -98,6 +98,8 @@ public class ThreadPoolTaskExecutor extends ExecutorConfigurationSupport
9898

9999
private boolean prestartAllCoreThreads = false;
100100

101+
private boolean strictEarlyShutdown = false;
102+
101103
@Nullable
102104
private TaskDecorator taskDecorator;
103105

@@ -212,14 +214,38 @@ public void setAllowCoreThreadTimeOut(boolean allowCoreThreadTimeOut) {
212214

213215
/**
214216
* Specify whether to start all core threads, causing them to idly wait for work.
215-
* <p>Default is "false".
217+
* <p>Default is "false", starting threads and adding them to the pool on demand.
216218
* @since 5.3.14
217219
* @see java.util.concurrent.ThreadPoolExecutor#prestartAllCoreThreads
218220
*/
219221
public void setPrestartAllCoreThreads(boolean prestartAllCoreThreads) {
220222
this.prestartAllCoreThreads = prestartAllCoreThreads;
221223
}
222224

225+
/**
226+
* Specify whether to initiate an early shutdown signal on context close,
227+
* disposing all idle threads and rejecting further task submissions.
228+
* <p>By default, existing tasks will be allowed to complete within the
229+
* coordinated lifecycle stop phase in any case. This setting just controls
230+
* whether an explicit {@link ThreadPoolExecutor#shutdown()} call will be
231+
* triggered on context close, rejecting task submissions after that point.
232+
* <p>As of 6.1.4, the default is "false", leniently allowing for late tasks
233+
* to arrive after context close, still participating in the lifecycle stop
234+
* phase. Note that this differs from {@link #setAcceptTasksAfterContextClose}
235+
* which completely bypasses the coordinated lifecycle stop phase, with no
236+
* explicit waiting for the completion of existing tasks at all.
237+
* <p>Switch this to "true" for a strict early shutdown signal analogous to
238+
* the 6.1-established default behavior of {@link ThreadPoolTaskScheduler}.
239+
* Note that the related flags {@link #setAcceptTasksAfterContextClose} and
240+
* {@link #setWaitForTasksToCompleteOnShutdown} will override this setting,
241+
* leading to a late shutdown without a coordinated lifecycle stop phase.
242+
* @since 6.1.4
243+
* @see #initiateShutdown()
244+
*/
245+
public void setStrictEarlyShutdown(boolean defaultEarlyShutdown) {
246+
this.strictEarlyShutdown = defaultEarlyShutdown;
247+
}
248+
223249
/**
224250
* Specify a custom {@link TaskDecorator} to be applied to any {@link Runnable}
225251
* about to be executed.
@@ -292,7 +318,7 @@ protected void afterExecute(Runnable task, Throwable ex) {
292318
/**
293319
* Create the BlockingQueue to use for the ThreadPoolExecutor.
294320
* <p>A LinkedBlockingQueue instance will be created for a positive
295-
* capacity value; a SynchronousQueue else.
321+
* capacity value; a SynchronousQueue otherwise.
296322
* @param queueCapacity the specified queue capacity
297323
* @return the BlockingQueue instance
298324
* @see java.util.concurrent.LinkedBlockingQueue
@@ -424,4 +450,11 @@ protected void cancelRemainingTask(Runnable task) {
424450
}
425451
}
426452

453+
@Override
454+
protected void initiateEarlyShutdown() {
455+
if (this.strictEarlyShutdown) {
456+
super.initiateEarlyShutdown();
457+
}
458+
}
459+
427460
}

spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java

+18-15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -165,13 +165,16 @@ public void setTaskDecorator(TaskDecorator taskDecorator) {
165165
}
166166

167167
/**
168-
* Specify a timeout for task termination when closing this executor.
169-
* The default is 0, not waiting for task termination at all.
168+
* Specify a timeout (in milliseconds) for task termination when closing
169+
* this executor. The default is 0, not waiting for task termination at all.
170170
* <p>Note that a concrete >0 timeout specified here will lead to the
171171
* wrapping of every submitted task into a task-tracking runnable which
172172
* involves considerable overhead in case of a high number of tasks.
173173
* However, for a modest level of submissions with longer-running
174174
* tasks, this is feasible in order to arrive at a graceful shutdown.
175+
* <p>Note that {@code SimpleAsyncTaskExecutor} does not participate in
176+
* a coordinated lifecycle stop but rather just awaits task termination
177+
* on {@link #close()}.
175178
* @param timeout the timeout in milliseconds
176179
* @since 6.1
177180
* @see #close()
@@ -183,18 +186,6 @@ public void setTaskTerminationTimeout(long timeout) {
183186
this.activeThreads = (timeout > 0 ? Collections.newSetFromMap(new ConcurrentHashMap<>()) : null);
184187
}
185188

186-
/**
187-
* Return whether this executor is still active, i.e. not closed yet,
188-
* and therefore accepts further task submissions. Otherwise, it is
189-
* either in the task termination phase or entirely shut down already.
190-
* @since 6.1
191-
* @see #setTaskTerminationTimeout
192-
* @see #close()
193-
*/
194-
public boolean isActive() {
195-
return this.active;
196-
}
197-
198189
/**
199190
* Set the maximum number of parallel task executions allowed.
200191
* The default of -1 indicates no concurrency limit at all.
@@ -224,6 +215,18 @@ public final boolean isThrottleActive() {
224215
return this.concurrencyThrottle.isThrottleActive();
225216
}
226217

218+
/**
219+
* Return whether this executor is still active, i.e. not closed yet,
220+
* and therefore accepts further task submissions. Otherwise, it is
221+
* either in the task termination phase or entirely shut down already.
222+
* @since 6.1
223+
* @see #setTaskTerminationTimeout
224+
* @see #close()
225+
*/
226+
public boolean isActive() {
227+
return this.active;
228+
}
229+
227230

228231
/**
229232
* Executes the given task, within a concurrency throttle

0 commit comments

Comments
 (0)