Skip to content

Commit ce80637

Browse files
committed
Add option for graceful shutdown (setTaskTerminationTimeout)
See gh-30956
1 parent 78d0dbb commit ce80637

File tree

4 files changed

+205
-41
lines changed

4 files changed

+205
-41
lines changed

Diff for: framework-docs/modules/ROOT/pages/integration/scheduling.adoc

+21-13
Original file line numberDiff line numberDiff line change
@@ -62,22 +62,27 @@ The variants that Spring provides are as follows:
6262
`ConcurrentTaskExecutor` directly. However, if the `ThreadPoolTaskExecutor` is not
6363
flexible enough for your needs, `ConcurrentTaskExecutor` is an alternative.
6464
* `ThreadPoolTaskExecutor`:
65-
This implementation is most commonly used. It exposes bean properties for
66-
configuring a `java.util.concurrent.ThreadPoolExecutor` and wraps it in a `TaskExecutor`.
67-
If you need to adapt to a different kind of `java.util.concurrent.Executor`, we
68-
recommend that you use a `ConcurrentTaskExecutor` instead.
65+
This implementation is most commonly used. It exposes bean properties for configuring
66+
a `java.util.concurrent.ThreadPoolExecutor` and wraps it in a `TaskExecutor`.
67+
If you need to adapt to a different kind of `java.util.concurrent.Executor`,
68+
we recommend that you use a `ConcurrentTaskExecutor` instead.
6969
* `DefaultManagedTaskExecutor`:
7070
This implementation uses a JNDI-obtained `ManagedExecutorService` in a JSR-236
7171
compatible runtime environment (such as a Jakarta EE application server),
7272
replacing a CommonJ WorkManager for that purpose.
7373

74+
As of 6.1, `ThreadPoolTaskExecutor` provides a pause/resume capability and graceful
75+
shutdown through Spring's lifecycle management. There is also a new "virtualThreads"
76+
option on `SimpleAsyncTaskExecutor` which is aligned with JDK 21's Virtual Threads,
77+
as well as a graceful shutdown capability for `SimpleAsyncTaskExecutor` as well.
78+
7479

7580
[[scheduling-task-executor-usage]]
7681
=== Using a `TaskExecutor`
7782

78-
Spring's `TaskExecutor` implementations are used as simple JavaBeans. In the following example,
79-
we define a bean that uses the `ThreadPoolTaskExecutor` to asynchronously print
80-
out a set of messages:
83+
Spring's `TaskExecutor` implementations are commonly used with dependency injection.
84+
In the following example, we define a bean that uses the `ThreadPoolTaskExecutor`
85+
to asynchronously print out a set of messages:
8186

8287
[source,java,indent=0,subs="verbatim,quotes"]
8388
----
@@ -227,8 +232,8 @@ fixed delay, those methods should be used directly whenever possible. The value
227232
`PeriodicTrigger` implementation is that you can use it within components that rely on
228233
the `Trigger` abstraction. For example, it may be convenient to allow periodic triggers,
229234
cron-based triggers, and even custom trigger implementations to be used interchangeably.
230-
Such a component could take advantage of dependency injection so that you can configure such `Triggers`
231-
externally and, therefore, easily modify or extend them.
235+
Such a component could take advantage of dependency injection so that you can configure
236+
such `Triggers` externally and, therefore, easily modify or extend them.
232237

233238

234239
[[scheduling-task-scheduler-implementations]]
@@ -238,10 +243,8 @@ As with Spring's `TaskExecutor` abstraction, the primary benefit of the `TaskSch
238243
arrangement is that an application's scheduling needs are decoupled from the deployment
239244
environment. This abstraction level is particularly relevant when deploying to an
240245
application server environment where threads should not be created directly by the
241-
application itself. For such scenarios, Spring provides a `TimerManagerTaskScheduler`
242-
that delegates to a CommonJ `TimerManager` on WebLogic or WebSphere as well as a more recent
243-
`DefaultManagedTaskScheduler` that delegates to a JSR-236 `ManagedScheduledExecutorService`
244-
in a Jakarta EE environment. Both are typically configured with a JNDI lookup.
246+
application itself. For such scenarios, Spring provides a `DefaultManagedTaskScheduler`
247+
that delegates to a JSR-236 `ManagedScheduledExecutorService` in a Jakarta EE environment.
245248

246249
Whenever external thread management is not a requirement, a simpler alternative is
247250
a local `ScheduledExecutorService` setup within the application, which can be adapted
@@ -251,6 +254,11 @@ to provide common bean-style configuration along the lines of `ThreadPoolTaskExe
251254
These variants work perfectly fine for locally embedded thread pool setups in lenient
252255
application server environments, as well -- in particular on Tomcat and Jetty.
253256

257+
As of 6.1, `ThreadPoolTaskScheduler` provides a pause/resume capability and graceful
258+
shutdown through Spring's lifecycle management. There is also a new option called
259+
`SimpleAsyncTaskScheduler` which is aligned with JDK 21's Virtual Threads, using a
260+
single scheduler thread but firing up a new thread for every scheduled task execution.
261+
254262

255263

256264
[[scheduling-annotation-support]]

Diff for: spring-context/src/main/java/org/springframework/scheduling/concurrent/SimpleAsyncTaskScheduler.java

+17-3
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,19 @@
4444
* A simple implementation of Spring's {@link TaskScheduler} interface, using
4545
* a single scheduler thread and executing every scheduled task in an individual
4646
* separate thread. This is an attractive choice with virtual threads on JDK 21,
47-
* so it is commonly used with {@link #setVirtualThreads setVirtualThreads(true)}.
47+
* expecting common usage with {@link #setVirtualThreads setVirtualThreads(true)}.
48+
*
49+
* <p>Supports a graceful shutdown through {@link #setTaskTerminationTimeout},
50+
* at the expense of task tracking overhead per execution thread at runtime.
51+
* Supports limiting concurrent threads through {@link #setConcurrencyLimit}.
52+
* By default, the number of concurrent task executions is unlimited.
53+
* This allows for dynamic concurrency of scheduled task executions, in contrast
54+
* to {@link ThreadPoolTaskScheduler} which requires a fixed pool size.
55+
*
56+
* <p><b>NOTE: This implementation does not reuse threads!</b> Consider a
57+
* thread-pooling TaskScheduler implementation instead, in particular for
58+
* scheduling a large number of short-lived tasks. Alternatively, on JDK 21,
59+
* consider setting {@link #setVirtualThreads} to {@code true}.
4860
*
4961
* <p>Extends {@link SimpleAsyncTaskExecutor} and can serve as a fully capable
5062
* replacement for it, e.g. as a single shared instance serving as a
@@ -64,13 +76,14 @@
6476
* @author Juergen Hoeller
6577
* @since 6.1
6678
* @see #setVirtualThreads
67-
* @see #setTargetTaskExecutor
79+
* @see #setTaskTerminationTimeout
80+
* @see #setConcurrencyLimit
6881
* @see SimpleAsyncTaskExecutor
6982
* @see ThreadPoolTaskScheduler
7083
*/
7184
@SuppressWarnings("serial")
7285
public class SimpleAsyncTaskScheduler extends SimpleAsyncTaskExecutor implements TaskScheduler,
73-
ApplicationContextAware, SmartLifecycle, ApplicationListener<ContextClosedEvent>, AutoCloseable {
86+
ApplicationContextAware, SmartLifecycle, ApplicationListener<ContextClosedEvent> {
7487

7588
private static final TimeUnit NANO = TimeUnit.NANOSECONDS;
7689

@@ -275,6 +288,7 @@ public void close() {
275288
future.cancel(true);
276289
}
277290
}
291+
super.close();
278292
}
279293

280294
}

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

+51-2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
import org.junit.jupiter.api.AfterEach;
2727
import org.junit.jupiter.api.Test;
28+
import org.junit.jupiter.api.Timeout;
2829
import org.junit.jupiter.params.ParameterizedTest;
2930
import org.junit.jupiter.params.provider.ValueSource;
3031

@@ -33,6 +34,7 @@
3334
import org.springframework.context.annotation.Bean;
3435
import org.springframework.context.annotation.Configuration;
3536
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
37+
import org.springframework.core.task.TaskExecutor;
3638
import org.springframework.core.testfixture.EnabledForTestGroups;
3739
import org.springframework.scheduling.TaskScheduler;
3840
import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler;
@@ -66,6 +68,10 @@ public void tearDown() {
6668
}
6769

6870

71+
/*
72+
* Tests compatibility between default executor in TaskSchedulerRouter
73+
* and explicit ThreadPoolTaskScheduler in configuration subclass.
74+
*/
6975
@ParameterizedTest
7076
@ValueSource(classes = {FixedRateTaskConfig.class, FixedRateTaskConfigSubclass.class})
7177
@EnabledForTestGroups(LONG_RUNNING)
@@ -77,8 +83,14 @@ public void withFixedRateTask(Class<?> configClass) throws InterruptedException
7783
assertThat(ctx.getBean(AtomicInteger.class).get()).isGreaterThanOrEqualTo(10);
7884
}
7985

86+
/*
87+
* Tests compatibility between SimpleAsyncTaskScheduler in regular configuration
88+
* and explicit ThreadPoolTaskScheduler in configuration subclass. This includes
89+
* pause/resume behavior and a controlled shutdown with a 1s termination timeout.
90+
*/
8091
@ParameterizedTest
8192
@ValueSource(classes = {ExplicitSchedulerConfig.class, ExplicitSchedulerConfigSubclass.class})
93+
@Timeout(2) // should actually complete within 1s
8294
@EnabledForTestGroups(LONG_RUNNING)
8395
public void withExplicitScheduler(Class<?> configClass) throws InterruptedException {
8496
ctx = new AnnotationConfigApplicationContext(configClass);
@@ -96,9 +108,35 @@ public void withExplicitScheduler(Class<?> configClass) throws InterruptedExcept
96108
int count3 = ctx.getBean(AtomicInteger.class).get();
97109
assertThat(count3).isGreaterThanOrEqualTo(20);
98110

111+
TaskExecutor executor = ctx.getBean(TaskExecutor.class);
112+
AtomicInteger count = new AtomicInteger(0);
113+
for (int i = 0; i < 2; i++) {
114+
executor.execute(() -> {
115+
try {
116+
Thread.sleep(10000); // try to break test timeout
117+
}
118+
catch (InterruptedException ex) {
119+
// expected during executor shutdown
120+
try {
121+
Thread.sleep(500);
122+
// should get here within task termination timeout (1000)
123+
count.incrementAndGet();
124+
}
125+
catch (InterruptedException ex2) {
126+
// not expected
127+
}
128+
}
129+
});
130+
}
131+
99132
assertThat(ctx.getBean(ExplicitSchedulerConfig.class).threadName).startsWith("explicitScheduler-");
100-
assertThat(Arrays.asList(ctx.getDefaultListableBeanFactory().getDependentBeans("myTaskScheduler")).contains(
101-
TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)).isTrue();
133+
assertThat(Arrays.asList(ctx.getDefaultListableBeanFactory().getDependentBeans("myTaskScheduler"))
134+
.contains(TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)).isTrue();
135+
136+
// Include executor shutdown in test timeout (2 seconds),
137+
// expecting interruption of the sleeping thread...
138+
ctx.close();
139+
assertThat(count.intValue()).isEqualTo(2);
102140
}
103141

104142
@Test
@@ -226,6 +264,11 @@ public void task() {
226264

227265
@Configuration
228266
static class FixedRateTaskConfigSubclass extends FixedRateTaskConfig {
267+
268+
@Bean
269+
public TaskScheduler taskScheduler() {
270+
return new ThreadPoolTaskScheduler();
271+
}
229272
}
230273

231274

@@ -239,6 +282,7 @@ static class ExplicitSchedulerConfig {
239282
public TaskScheduler myTaskScheduler() {
240283
SimpleAsyncTaskScheduler scheduler = new SimpleAsyncTaskScheduler();
241284
scheduler.setThreadNamePrefix("explicitScheduler-");
285+
scheduler.setTaskTerminationTimeout(1000);
242286
return scheduler;
243287
}
244288

@@ -263,6 +307,8 @@ static class ExplicitSchedulerConfigSubclass extends ExplicitSchedulerConfig {
263307
public TaskScheduler myTaskScheduler() {
264308
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
265309
scheduler.setThreadNamePrefix("explicitScheduler-");
310+
scheduler.setAwaitTerminationMillis(1000);
311+
scheduler.setPoolSize(2);
266312
return scheduler;
267313
}
268314
}
@@ -437,6 +483,7 @@ public void task() {
437483
public TaskScheduler taskScheduler1() {
438484
SimpleAsyncTaskScheduler scheduler = new SimpleAsyncTaskScheduler();
439485
scheduler.setThreadNamePrefix("explicitScheduler1");
486+
scheduler.setConcurrencyLimit(1);
440487
return scheduler;
441488
}
442489

@@ -478,6 +525,7 @@ public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
478525
public TaskScheduler taskScheduler1() {
479526
SimpleAsyncTaskScheduler scheduler = new SimpleAsyncTaskScheduler();
480527
scheduler.setThreadNamePrefix("explicitScheduler1-");
528+
scheduler.setConcurrencyLimit(1);
481529
return scheduler;
482530
}
483531

@@ -508,6 +556,7 @@ public ThreadAwareWorker worker() {
508556
public TaskScheduler taskScheduler1() {
509557
SimpleAsyncTaskScheduler scheduler = new SimpleAsyncTaskScheduler();
510558
scheduler.setThreadNamePrefix("explicitScheduler1-");
559+
scheduler.setConcurrencyLimit(1);
511560
return scheduler;
512561
}
513562

0 commit comments

Comments
 (0)