Skip to content

Commit b12115b

Browse files
committed
Sophisticated lifecycle support for ThreadPoolTaskExecutor/Scheduler
Pause/resume capability through SmartLifecycle implementation. ContextClosedEvent triggers early executor/scheduler shutdown. Programmatic initiateShutdown() option for custom early shutdown. Closes gh-30831 Closes gh-27090 Closes gh-24497
1 parent 029a69f commit b12115b

File tree

7 files changed

+308
-38
lines changed

7 files changed

+308
-38
lines changed

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

+232-5
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,20 @@
2323
import java.util.concurrent.ThreadFactory;
2424
import java.util.concurrent.ThreadPoolExecutor;
2525
import java.util.concurrent.TimeUnit;
26+
import java.util.concurrent.locks.Condition;
27+
import java.util.concurrent.locks.ReentrantLock;
2628

2729
import org.apache.commons.logging.Log;
2830
import org.apache.commons.logging.LogFactory;
2931

3032
import org.springframework.beans.factory.BeanNameAware;
3133
import org.springframework.beans.factory.DisposableBean;
3234
import org.springframework.beans.factory.InitializingBean;
35+
import org.springframework.context.ApplicationContext;
36+
import org.springframework.context.ApplicationContextAware;
37+
import org.springframework.context.ApplicationListener;
38+
import org.springframework.context.SmartLifecycle;
39+
import org.springframework.context.event.ContextClosedEvent;
3340
import org.springframework.lang.Nullable;
3441

3542
/**
@@ -50,7 +57,8 @@
5057
*/
5158
@SuppressWarnings("serial")
5259
public abstract class ExecutorConfigurationSupport extends CustomizableThreadFactory
53-
implements BeanNameAware, InitializingBean, DisposableBean {
60+
implements BeanNameAware, ApplicationContextAware, InitializingBean, DisposableBean,
61+
SmartLifecycle, ApplicationListener<ContextClosedEvent> {
5462

5563
protected final Log logger = LogFactory.getLog(getClass());
5664

@@ -60,16 +68,34 @@ public abstract class ExecutorConfigurationSupport extends CustomizableThreadFac
6068

6169
private RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.AbortPolicy();
6270

71+
private boolean acceptTasksAfterContextClose = false;
72+
6373
private boolean waitForTasksToCompleteOnShutdown = false;
6474

6575
private long awaitTerminationMillis = 0;
6676

77+
private int phase = DEFAULT_PHASE;
78+
6779
@Nullable
6880
private String beanName;
6981

82+
@Nullable
83+
private ApplicationContext applicationContext;
84+
7085
@Nullable
7186
private ExecutorService executor;
7287

88+
private final ReentrantLock pauseLock = new ReentrantLock();
89+
90+
private final Condition unpaused = this.pauseLock.newCondition();
91+
92+
private volatile boolean paused;
93+
94+
private int executingTaskCount = 0;
95+
96+
@Nullable
97+
private Runnable stopCallback;
98+
7399

74100
/**
75101
* Set the ThreadFactory to use for the ExecutorService's thread pool.
@@ -105,12 +131,32 @@ public void setRejectedExecutionHandler(@Nullable RejectedExecutionHandler rejec
105131
(rejectedExecutionHandler != null ? rejectedExecutionHandler : new ThreadPoolExecutor.AbortPolicy());
106132
}
107133

134+
/**
135+
* Set whether to accept further tasks after the application context close phase
136+
* has begun.
137+
* <p>Default is {@code false} as of 6.1, triggering an early soft shutdown of
138+
* the executor and therefore rejecting any further task submissions. Switch this
139+
* to {@code true} in order to let other components submit tasks even during their
140+
* own destruction callbacks, at the expense of a longer shutdown phase.
141+
* This will usually go along with
142+
* {@link #setWaitForTasksToCompleteOnShutdown "waitForTasksToCompleteOnShutdown"}.
143+
* <p>This flag will only have effect when the executor is running in a Spring
144+
* application context and able to receive the {@link ContextClosedEvent}.
145+
* @since 6.1
146+
* @see org.springframework.context.ConfigurableApplicationContext#close()
147+
* @see DisposableBean#destroy()
148+
* @see #shutdown()
149+
*/
150+
public void setAcceptTasksAfterContextClose(boolean acceptTasksAfterContextClose) {
151+
this.acceptTasksAfterContextClose = acceptTasksAfterContextClose;
152+
}
153+
108154
/**
109155
* Set whether to wait for scheduled tasks to complete on shutdown,
110156
* not interrupting running tasks and executing all tasks in the queue.
111-
* <p>Default is "false", shutting down immediately through interrupting
112-
* ongoing tasks and clearing the queue. Switch this flag to "true" if you
113-
* prefer fully completed tasks at the expense of a longer shutdown phase.
157+
* <p>Default is {@code false}, shutting down immediately through interrupting
158+
* ongoing tasks and clearing the queue. Switch this flag to {@code true} if
159+
* you prefer fully completed tasks at the expense of a longer shutdown phase.
114160
* <p>Note that Spring's container shutdown continues while ongoing tasks
115161
* are being completed. If you want this executor to block and wait for the
116162
* termination of tasks before the rest of the container continues to shut
@@ -161,11 +207,36 @@ public void setAwaitTerminationMillis(long awaitTerminationMillis) {
161207
this.awaitTerminationMillis = awaitTerminationMillis;
162208
}
163209

210+
/**
211+
* Specify the lifecycle phase for pausing and resuming this executor.
212+
* The default is {@link #DEFAULT_PHASE}.
213+
* @since 6.1
214+
* @see SmartLifecycle#getPhase()
215+
*/
216+
public void setPhase(int phase) {
217+
this.phase = phase;
218+
}
219+
220+
/**
221+
* Return the lifecycle phase for pausing and resuming this executor.
222+
* @since 6.1
223+
* @see #setPhase
224+
*/
225+
@Override
226+
public int getPhase() {
227+
return this.phase;
228+
}
229+
164230
@Override
165231
public void setBeanName(String name) {
166232
this.beanName = name;
167233
}
168234

235+
@Override
236+
public void setApplicationContext(ApplicationContext applicationContext) {
237+
this.applicationContext = applicationContext;
238+
}
239+
169240

170241
/**
171242
* Calls {@code initialize()} after the container applied all property values.
@@ -211,9 +282,33 @@ public void destroy() {
211282
}
212283

213284
/**
214-
* Perform a shutdown on the underlying ExecutorService.
285+
* Initiate a shutdown on the underlying ExecutorService,
286+
* rejecting further task submissions.
287+
* <p>The executor will not accept further tasks and will prevent further
288+
* scheduling of periodic tasks, letting existing tasks complete still.
289+
* This step is non-blocking and can be applied as an early shutdown signal
290+
* before following up with a full {@link #shutdown()} call later on.
291+
* @since 6.1
292+
* @see #shutdown()
293+
* @see java.util.concurrent.ExecutorService#shutdown()
294+
*/
295+
public void initiateShutdown() {
296+
if (this.executor != null) {
297+
this.executor.shutdown();
298+
}
299+
}
300+
301+
/**
302+
* Perform a full shutdown on the underlying ExecutorService,
303+
* according to the corresponding configuration settings.
304+
* <p>This step potentially blocks for the configured termination period,
305+
* waiting for remaining tasks to complete. For an early shutdown signal
306+
* to not accept further tasks, call {@link #initiateShutdown()} first.
307+
* @see #setWaitForTasksToCompleteOnShutdown
308+
* @see #setAwaitTerminationMillis
215309
* @see java.util.concurrent.ExecutorService#shutdown()
216310
* @see java.util.concurrent.ExecutorService#shutdownNow()
311+
* @see java.util.concurrent.ExecutorService#awaitTermination
217312
*/
218313
public void shutdown() {
219314
if (logger.isDebugEnabled()) {
@@ -270,4 +365,136 @@ private void awaitTerminationIfNecessary(ExecutorService executor) {
270365
}
271366
}
272367

368+
369+
/**
370+
* Resume this executor if paused before (otherwise a no-op).
371+
* @since 6.1
372+
*/
373+
@Override
374+
public void start() {
375+
this.pauseLock.lock();
376+
try {
377+
this.paused = false;
378+
this.unpaused.signalAll();
379+
}
380+
finally {
381+
this.pauseLock.unlock();
382+
}
383+
}
384+
385+
/**
386+
* Pause this executor, not waiting for tasks to complete.
387+
* @since 6.1
388+
*/
389+
@Override
390+
public void stop() {
391+
this.pauseLock.lock();
392+
try {
393+
this.paused = true;
394+
this.stopCallback = null;
395+
}
396+
finally {
397+
this.pauseLock.unlock();
398+
}
399+
}
400+
401+
/**
402+
* Pause this executor, triggering the given callback
403+
* once all currently executing tasks have completed.
404+
* @since 6.1
405+
*/
406+
@Override
407+
public void stop(Runnable callback) {
408+
this.pauseLock.lock();
409+
try {
410+
this.paused = true;
411+
if (this.executingTaskCount == 0) {
412+
this.stopCallback = null;
413+
callback.run();
414+
}
415+
else {
416+
this.stopCallback = callback;
417+
}
418+
}
419+
finally {
420+
this.pauseLock.unlock();
421+
}
422+
}
423+
424+
/**
425+
* Check whether this executor is not paused and has not been shut down either.
426+
* @since 6.1
427+
* @see #start()
428+
* @see #stop()
429+
*/
430+
@Override
431+
public boolean isRunning() {
432+
return (this.executor != null && !this.executor.isShutdown() & !this.paused);
433+
}
434+
435+
/**
436+
* A before-execute callback for framework subclasses to delegate to
437+
* (for start/stop handling), and possibly also for custom subclasses
438+
* to extend (making sure to call this implementation as well).
439+
* @param thread the thread to run the task
440+
* @param task the task to be executed
441+
* @since 6.1
442+
* @see ThreadPoolExecutor#beforeExecute(Thread, Runnable)
443+
*/
444+
protected void beforeExecute(Thread thread, Runnable task) {
445+
this.pauseLock.lock();
446+
try {
447+
while (this.paused && this.executor != null && !this.executor.isShutdown()) {
448+
this.unpaused.await();
449+
}
450+
}
451+
catch (InterruptedException ex) {
452+
thread.interrupt();
453+
}
454+
finally {
455+
this.executingTaskCount++;
456+
this.pauseLock.unlock();
457+
}
458+
}
459+
460+
/**
461+
* An after-execute callback for framework subclasses to delegate to
462+
* (for start/stop handling), and possibly also for custom subclasses
463+
* to extend (making sure to call this implementation as well).
464+
* @param task the task that has been executed
465+
* @param ex the exception thrown during execution, if any
466+
* @since 6.1
467+
* @see ThreadPoolExecutor#afterExecute(Runnable, Throwable)
468+
*/
469+
protected void afterExecute(Runnable task, @Nullable Throwable ex) {
470+
this.pauseLock.lock();
471+
try {
472+
this.executingTaskCount--;
473+
if (this.executingTaskCount == 0) {
474+
Runnable callback = this.stopCallback;
475+
if (callback != null) {
476+
callback.run();
477+
this.stopCallback = null;
478+
}
479+
}
480+
}
481+
finally {
482+
this.pauseLock.unlock();
483+
}
484+
}
485+
486+
/**
487+
* {@link ContextClosedEvent} handler for initiating an early shutdown.
488+
* @since 6.1
489+
* @see #initiateShutdown()
490+
*/
491+
@Override
492+
public void onApplicationEvent(ContextClosedEvent event) {
493+
if (event.getApplicationContext() == this.applicationContext && !this.acceptTasksAfterContextClose) {
494+
// Early shutdown signal: accept no further tasks, let existing tasks complete
495+
// before hitting the actual destruction step in the shutdown() method above.
496+
initiateShutdown();
497+
}
498+
}
499+
273500
}

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

+10-1
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,16 @@ protected ExecutorService initializeExecutor(
187187
protected ScheduledExecutorService createExecutor(
188188
int poolSize, ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) {
189189

190-
return new ScheduledThreadPoolExecutor(poolSize, threadFactory, rejectedExecutionHandler);
190+
return new ScheduledThreadPoolExecutor(poolSize, threadFactory, rejectedExecutionHandler) {
191+
@Override
192+
protected void beforeExecute(Thread thread, Runnable task) {
193+
ScheduledExecutorFactoryBean.this.beforeExecute(thread, task);
194+
}
195+
@Override
196+
protected void afterExecute(Runnable task, Throwable ex) {
197+
ScheduledExecutorFactoryBean.this.afterExecute(task, ex);
198+
}
199+
};
191200
}
192201

193202
/**

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

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2023 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.
@@ -192,7 +192,16 @@ protected ThreadPoolExecutor createExecutor(
192192
ThreadFactory threadFactory, RejectedExecutionHandler rejectedExecutionHandler) {
193193

194194
return new ThreadPoolExecutor(corePoolSize, maxPoolSize,
195-
keepAliveSeconds, TimeUnit.SECONDS, queue, threadFactory, rejectedExecutionHandler);
195+
keepAliveSeconds, TimeUnit.SECONDS, queue, threadFactory, rejectedExecutionHandler) {
196+
@Override
197+
protected void beforeExecute(Thread thread, Runnable task) {
198+
ThreadPoolExecutorFactoryBean.this.beforeExecute(thread, task);
199+
}
200+
@Override
201+
protected void afterExecute(Runnable task, Throwable ex) {
202+
ThreadPoolExecutorFactoryBean.this.afterExecute(task, ex);
203+
}
204+
};
196205
}
197206

198207
/**

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

+17-15
Original file line numberDiff line numberDiff line change
@@ -254,27 +254,29 @@ protected ExecutorService initializeExecutor(
254254

255255
BlockingQueue<Runnable> queue = createQueue(this.queueCapacity);
256256

257-
ThreadPoolExecutor executor;
258-
if (this.taskDecorator != null) {
259-
executor = new ThreadPoolExecutor(
257+
ThreadPoolExecutor executor = new ThreadPoolExecutor(
260258
this.corePoolSize, this.maxPoolSize, this.keepAliveSeconds, TimeUnit.SECONDS,
261259
queue, threadFactory, rejectedExecutionHandler) {
262-
@Override
263-
public void execute(Runnable command) {
264-
Runnable decorated = taskDecorator.decorate(command);
260+
@Override
261+
public void execute(Runnable command) {
262+
Runnable decorated = command;
263+
if (taskDecorator != null) {
264+
decorated = taskDecorator.decorate(command);
265265
if (decorated != command) {
266266
decoratedTaskMap.put(decorated, command);
267267
}
268-
super.execute(decorated);
269268
}
270-
};
271-
}
272-
else {
273-
executor = new ThreadPoolExecutor(
274-
this.corePoolSize, this.maxPoolSize, this.keepAliveSeconds, TimeUnit.SECONDS,
275-
queue, threadFactory, rejectedExecutionHandler);
276-
277-
}
269+
super.execute(decorated);
270+
}
271+
@Override
272+
protected void beforeExecute(Thread thread, Runnable task) {
273+
ThreadPoolTaskExecutor.this.beforeExecute(thread, task);
274+
}
275+
@Override
276+
protected void afterExecute(Runnable task, Throwable ex) {
277+
ThreadPoolTaskExecutor.this.afterExecute(task, ex);
278+
}
279+
};
278280

279281
if (this.allowCoreThreadTimeOut) {
280282
executor.allowCoreThreadTimeOut(true);

0 commit comments

Comments
 (0)