diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java index 786f42ae0049..5f20eb75aff7 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java @@ -139,7 +139,11 @@ public void setRejectedExecutionHandler(@Nullable RejectedExecutionHandler rejec * the executor's destruction step, with individual awaiting according to the * {@link #setAwaitTerminationSeconds "awaitTerminationSeconds"} property. *
This flag will only have effect when the executor is running in a Spring - * application context and able to receive the {@link ContextClosedEvent}. + * application context and able to receive the {@link ContextClosedEvent}. Also, + * note that {@link ThreadPoolTaskExecutor} effectively accepts tasks after context + * close by default, in combination with a coordinated lifecycle stop, unless + * {@link ThreadPoolTaskExecutor#setStrictEarlyShutdown "strictEarlyShutdown"} + * has been specified. * @since 6.1 * @see org.springframework.context.ConfigurableApplicationContext#close() * @see DisposableBean#destroy() @@ -294,6 +298,9 @@ public void destroy() { * scheduling of periodic tasks, letting existing tasks complete still. * This step is non-blocking and can be applied as an early shutdown signal * before following up with a full {@link #shutdown()} call later on. + *
Automatically called for early shutdown signals on + * {@link #onApplicationEvent(ContextClosedEvent) context close}. + * Can be manually called as well, in particular outside a container. * @since 6.1 * @see #shutdown() * @see java.util.concurrent.ExecutorService#shutdown() @@ -463,11 +470,29 @@ public void onApplicationEvent(ContextClosedEvent event) { this.lateShutdown = true; } else { - // Early shutdown signal: accept no further tasks, let existing tasks complete - // before hitting the actual destruction step in the shutdown() method above. - initiateShutdown(); + if (this.lifecycleDelegate != null) { + this.lifecycleDelegate.markShutdown(); + } + initiateEarlyShutdown(); } } } + /** + * Early shutdown signal: do not trigger further tasks, let existing tasks complete + * before hitting the actual destruction step in the {@link #shutdown()} method. + * This goes along with a {@link #stop(Runnable) coordinated lifecycle stop phase}. + *
Called from {@link #onApplicationEvent(ContextClosedEvent)} if no + * indications for a late shutdown have been determined, that is, if the + * {@link #setAcceptTasksAfterContextClose "acceptTasksAfterContextClose} and + * {@link #setWaitForTasksToCompleteOnShutdown "waitForTasksToCompleteOnShutdown"} + * flags have not been set. + *
The default implementation calls {@link #initiateShutdown()}. + * @since 6.1.4 + * @see #initiateShutdown() + */ + protected void initiateEarlyShutdown() { + initiateShutdown(); + } + } diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorLifecycleDelegate.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorLifecycleDelegate.java index 5405a71ba4ec..80b78ce314e9 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorLifecycleDelegate.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorLifecycleDelegate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,6 +42,8 @@ final class ExecutorLifecycleDelegate implements SmartLifecycle { private volatile boolean paused; + private volatile boolean shutdown; + private int executingTaskCount = 0; @Nullable @@ -100,10 +102,14 @@ public boolean isRunning() { return (!this.paused && !this.executor.isTerminated()); } + void markShutdown() { + this.shutdown = true; + } + void beforeExecute(Thread thread) { this.pauseLock.lock(); try { - while (this.paused && !this.executor.isShutdown()) { + while (this.paused && !this.shutdown && !this.executor.isShutdown()) { this.unpaused.await(); } } diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolExecutorFactoryBean.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolExecutorFactoryBean.java index 5486c2db6110..7680a2634f71 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolExecutorFactoryBean.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolExecutorFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -71,11 +71,13 @@ public class ThreadPoolExecutorFactoryBean extends ExecutorConfigurationSupport private int keepAliveSeconds = 60; + private int queueCapacity = Integer.MAX_VALUE; + private boolean allowCoreThreadTimeOut = false; private boolean prestartAllCoreThreads = false; - private int queueCapacity = Integer.MAX_VALUE; + private boolean strictEarlyShutdown = false; private boolean exposeUnconfigurableExecutor = false; @@ -107,6 +109,18 @@ public void setKeepAliveSeconds(int keepAliveSeconds) { this.keepAliveSeconds = keepAliveSeconds; } + /** + * Set the capacity for the ThreadPoolExecutor's BlockingQueue. + * Default is {@code Integer.MAX_VALUE}. + *
Any positive value will lead to a LinkedBlockingQueue instance; + * any other value will lead to a SynchronousQueue instance. + * @see java.util.concurrent.LinkedBlockingQueue + * @see java.util.concurrent.SynchronousQueue + */ + public void setQueueCapacity(int queueCapacity) { + this.queueCapacity = queueCapacity; + } + /** * Specify whether to allow core threads to time out. This enables dynamic * growing and shrinking even in combination with a non-zero queue (since @@ -129,15 +143,15 @@ public void setPrestartAllCoreThreads(boolean prestartAllCoreThreads) { } /** - * Set the capacity for the ThreadPoolExecutor's BlockingQueue. - * Default is {@code Integer.MAX_VALUE}. - *
Any positive value will lead to a LinkedBlockingQueue instance; - * any other value will lead to a SynchronousQueue instance. - * @see java.util.concurrent.LinkedBlockingQueue - * @see java.util.concurrent.SynchronousQueue + * Specify whether to initiate an early shutdown signal on context close, + * disposing all idle threads and rejecting further task submissions. + *
Default is "false".
+ * See {@link ThreadPoolTaskExecutor#setStrictEarlyShutdown} for details.
+ * @since 6.1.4
+ * @see #initiateShutdown()
*/
- public void setQueueCapacity(int queueCapacity) {
- this.queueCapacity = queueCapacity;
+ public void setStrictEarlyShutdown(boolean defaultEarlyShutdown) {
+ this.strictEarlyShutdown = defaultEarlyShutdown;
}
/**
@@ -222,6 +236,13 @@ protected BlockingQueue Default is "false".
+ * Default is "false", starting threads and adding them to the pool on demand.
* @since 5.3.14
* @see java.util.concurrent.ThreadPoolExecutor#prestartAllCoreThreads
*/
@@ -220,6 +222,30 @@ public void setPrestartAllCoreThreads(boolean prestartAllCoreThreads) {
this.prestartAllCoreThreads = prestartAllCoreThreads;
}
+ /**
+ * Specify whether to initiate an early shutdown signal on context close,
+ * disposing all idle threads and rejecting further task submissions.
+ * By default, existing tasks will be allowed to complete within the
+ * coordinated lifecycle stop phase in any case. This setting just controls
+ * whether an explicit {@link ThreadPoolExecutor#shutdown()} call will be
+ * triggered on context close, rejecting task submissions after that point.
+ * As of 6.1.4, the default is "false", leniently allowing for late tasks
+ * to arrive after context close, still participating in the lifecycle stop
+ * phase. Note that this differs from {@link #setAcceptTasksAfterContextClose}
+ * which completely bypasses the coordinated lifecycle stop phase, with no
+ * explicit waiting for the completion of existing tasks at all.
+ * Switch this to "true" for a strict early shutdown signal analogous to
+ * the 6.1-established default behavior of {@link ThreadPoolTaskScheduler}.
+ * Note that the related flags {@link #setAcceptTasksAfterContextClose} and
+ * {@link #setWaitForTasksToCompleteOnShutdown} will override this setting,
+ * leading to a late shutdown without a coordinated lifecycle stop phase.
+ * @since 6.1.4
+ * @see #initiateShutdown()
+ */
+ public void setStrictEarlyShutdown(boolean defaultEarlyShutdown) {
+ this.strictEarlyShutdown = defaultEarlyShutdown;
+ }
+
/**
* Specify a custom {@link TaskDecorator} to be applied to any {@link Runnable}
* about to be executed.
@@ -292,7 +318,7 @@ protected void afterExecute(Runnable task, Throwable ex) {
/**
* Create the BlockingQueue to use for the ThreadPoolExecutor.
* A LinkedBlockingQueue instance will be created for a positive
- * capacity value; a SynchronousQueue else.
+ * capacity value; a SynchronousQueue otherwise.
* @param queueCapacity the specified queue capacity
* @return the BlockingQueue instance
* @see java.util.concurrent.LinkedBlockingQueue
@@ -424,4 +450,11 @@ protected void cancelRemainingTask(Runnable task) {
}
}
+ @Override
+ protected void initiateEarlyShutdown() {
+ if (this.strictEarlyShutdown) {
+ super.initiateEarlyShutdown();
+ }
+ }
+
}
diff --git a/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java b/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java
index ce7eee8c1296..4206695f0b42 100644
--- a/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java
+++ b/spring-core/src/main/java/org/springframework/core/task/SimpleAsyncTaskExecutor.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2023 the original author or authors.
+ * Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -165,13 +165,16 @@ public void setTaskDecorator(TaskDecorator taskDecorator) {
}
/**
- * Specify a timeout for task termination when closing this executor.
- * The default is 0, not waiting for task termination at all.
+ * Specify a timeout (in milliseconds) for task termination when closing
+ * this executor. The default is 0, not waiting for task termination at all.
* Note that a concrete >0 timeout specified here will lead to the
* wrapping of every submitted task into a task-tracking runnable which
* involves considerable overhead in case of a high number of tasks.
* However, for a modest level of submissions with longer-running
* tasks, this is feasible in order to arrive at a graceful shutdown.
+ * Note that {@code SimpleAsyncTaskExecutor} does not participate in
+ * a coordinated lifecycle stop but rather just awaits task termination
+ * on {@link #close()}.
* @param timeout the timeout in milliseconds
* @since 6.1
* @see #close()
@@ -183,18 +186,6 @@ public void setTaskTerminationTimeout(long timeout) {
this.activeThreads = (timeout > 0 ? Collections.newSetFromMap(new ConcurrentHashMap<>()) : null);
}
- /**
- * Return whether this executor is still active, i.e. not closed yet,
- * and therefore accepts further task submissions. Otherwise, it is
- * either in the task termination phase or entirely shut down already.
- * @since 6.1
- * @see #setTaskTerminationTimeout
- * @see #close()
- */
- public boolean isActive() {
- return this.active;
- }
-
/**
* Set the maximum number of parallel task executions allowed.
* The default of -1 indicates no concurrency limit at all.
@@ -224,6 +215,18 @@ public final boolean isThrottleActive() {
return this.concurrencyThrottle.isThrottleActive();
}
+ /**
+ * Return whether this executor is still active, i.e. not closed yet,
+ * and therefore accepts further task submissions. Otherwise, it is
+ * either in the task termination phase or entirely shut down already.
+ * @since 6.1
+ * @see #setTaskTerminationTimeout
+ * @see #close()
+ */
+ public boolean isActive() {
+ return this.active;
+ }
+
/**
* Executes the given task, within a concurrency throttle