Add option for graceful shutdown (setTaskTerminationTimeout)

See gh-30956
This commit is contained in:
Juergen Hoeller
2023-07-27 21:39:58 +02:00
parent 78d0dbb519
commit ce80637891
4 changed files with 205 additions and 41 deletions

View File

@@ -17,7 +17,10 @@
package org.springframework.core.task;
import java.io.Serializable;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.ThreadFactory;
@@ -31,10 +34,12 @@ import org.springframework.util.concurrent.ListenableFutureTask;
/**
* {@link TaskExecutor} implementation that fires up a new Thread for each task,
* executing it asynchronously. Supports a virtual thread option on JDK 21.
* executing it asynchronously. Provides a virtual thread option on JDK 21.
*
* <p>Supports limiting concurrent threads through the "concurrencyLimit"
* bean property. By default, the number of concurrent threads is unlimited.
* <p>Supports a graceful shutdown through {@link #setTaskTerminationTimeout},
* at the expense of task tracking overhead per execution thread at runtime.
* Supports limiting concurrent threads through {@link #setConcurrencyLimit}.
* By default, the number of concurrent task executions is unlimited.
*
* <p><b>NOTE: This implementation does not reuse threads!</b> Consider a
* thread-pooling TaskExecutor implementation instead, in particular for
@@ -44,13 +49,14 @@ import org.springframework.util.concurrent.ListenableFutureTask;
* @author Juergen Hoeller
* @since 2.0
* @see #setVirtualThreads
* @see #setTaskTerminationTimeout
* @see #setConcurrencyLimit
* @see SyncTaskExecutor
* @see org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler
* @see org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
*/
@SuppressWarnings({"serial", "deprecation"})
public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
implements AsyncListenableTaskExecutor, Serializable {
implements AsyncListenableTaskExecutor, Serializable, AutoCloseable {
/**
* Permit any number of concurrent invocations: that is, don't throttle concurrency.
@@ -77,6 +83,13 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
@Nullable
private TaskDecorator taskDecorator;
private long taskTerminationTimeout;
@Nullable
private Set<Thread> activeThreads;
private volatile boolean active = true;
/**
* Create a new SimpleAsyncTaskExecutor with default thread name prefix.
@@ -147,33 +160,62 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
* have to cast it and call {@code Future#get} to evaluate exceptions.
* @since 4.3
*/
public final void setTaskDecorator(TaskDecorator taskDecorator) {
public void setTaskDecorator(TaskDecorator taskDecorator) {
this.taskDecorator = taskDecorator;
}
/**
* Set the maximum number of parallel accesses allowed.
* -1 indicates no concurrency limit at all.
* <p>In principle, this limit can be changed at runtime,
* although it is generally designed as a config time setting.
* NOTE: Do not switch between -1 and any concrete limit at runtime,
* as this will lead to inconsistent concurrency counts: A limit
* of -1 effectively turns off concurrency counting completely.
* Specify a timeout for task termination when closing this executor.
* The default is 0, not waiting for task termination at all.
* <p>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.
* @param timeout the timeout in milliseconds
* @since 6.1
* @see #close()
* @see org.springframework.scheduling.concurrent.ExecutorConfigurationSupport#setAwaitTerminationMillis
*/
public void setTaskTerminationTimeout(long timeout) {
Assert.isTrue(timeout >= 0, "Timeout value must be >=0");
this.taskTerminationTimeout = 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.
* <p>This is the equivalent of a maximum pool size in a thread pool,
* preventing temporary overload of the thread management system.
* @see #UNBOUNDED_CONCURRENCY
* @see org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor#setMaxPoolSize
*/
public void setConcurrencyLimit(int concurrencyLimit) {
this.concurrencyThrottle.setConcurrencyLimit(concurrencyLimit);
}
/**
* Return the maximum number of parallel accesses allowed.
* Return the maximum number of parallel task executions allowed.
*/
public final int getConcurrencyLimit() {
return this.concurrencyThrottle.getConcurrencyLimit();
}
/**
* Return whether this throttle is currently active.
* Return whether the concurrency throttle is currently active.
* @return {@code true} if the concurrency limit for this instance is active
* @see #getConcurrencyLimit()
* @see #setConcurrencyLimit
@@ -207,10 +249,17 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
@Override
public void execute(Runnable task, long startTimeout) {
Assert.notNull(task, "Runnable must not be null");
if (!isActive()) {
throw new TaskRejectedException(getClass().getSimpleName() + " has been closed already");
}
Runnable taskToUse = (this.taskDecorator != null ? this.taskDecorator.decorate(task) : task);
if (isThrottleActive() && startTimeout > TIMEOUT_IMMEDIATE) {
this.concurrencyThrottle.beforeAccess();
doExecute(new ConcurrencyThrottlingRunnable(taskToUse));
doExecute(new TaskTrackingRunnable(taskToUse));
}
else if (this.activeThreads != null) {
doExecute(new TaskTrackingRunnable(taskToUse));
}
else {
doExecute(taskToUse);
@@ -278,6 +327,33 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
}
}
/**
* This close methods tracks the termination of active threads if a concrete
* {@link #setTaskTerminationTimeout task termination timeout} has been set.
* Otherwise, it is not necessary to close this executor.
* @since 6.1
*/
@Override
public void close() {
if (this.active) {
this.active = false;
Set<Thread> threads = this.activeThreads;
if (threads != null) {
threads.forEach(Thread::interrupt);
synchronized (threads) {
try {
if (!threads.isEmpty()) {
threads.wait(this.taskTerminationTimeout);
}
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
}
}
/**
* Subclass of the general ConcurrencyThrottleSupport class,
@@ -299,23 +375,40 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
/**
* This Runnable calls {@code afterAccess()} after the
* target Runnable has finished its execution.
* Decorates a target task with active thread tracking
* and concurrency throttle management, if necessary.
*/
private class ConcurrencyThrottlingRunnable implements Runnable {
private class TaskTrackingRunnable implements Runnable {
private final Runnable target;
private final Runnable task;
public ConcurrencyThrottlingRunnable(Runnable target) {
this.target = target;
public TaskTrackingRunnable(Runnable task) {
Assert.notNull(task, "Task must not be null");
this.task = task;
}
@Override
public void run() {
Set<Thread> threads = activeThreads;
Thread thread = null;
if (threads != null) {
thread = Thread.currentThread();
threads.add(thread);
}
try {
this.target.run();
this.task.run();
}
finally {
if (threads != null) {
threads.remove(thread);
if (!isActive()) {
synchronized (threads) {
if (threads.isEmpty()) {
threads.notify();
}
}
}
}
concurrencyThrottle.afterAccess();
}
}