Add support for SimpleTaskExecutor#cancel-remaining-tasks-on-close

See https://github.com/spring-projects/spring-framework/issues/35372
See gh-47244

Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>
This commit is contained in:
Yanming Zhou 2025-09-17 14:56:56 +08:00 committed by Stéphane Nicoll
parent bc60749949
commit 95305691ab
5 changed files with 64 additions and 20 deletions

View File

@ -79,6 +79,11 @@ public class TaskExecutionProperties {
public static class Simple { public static class Simple {
/**
* Whether to cancel remaining tasks on close.
*/
private boolean cancelRemainingTasksOnClose;
/** /**
* Whether to reject tasks when the concurrency limit has been reached. * Whether to reject tasks when the concurrency limit has been reached.
*/ */
@ -90,6 +95,14 @@ public class TaskExecutionProperties {
*/ */
private @Nullable Integer concurrencyLimit; private @Nullable Integer concurrencyLimit;
public boolean isCancelRemainingTasksOnClose() {
return this.cancelRemainingTasksOnClose;
}
public void setCancelRemainingTasksOnClose(boolean cancelRemainingTasksOnClose) {
this.cancelRemainingTasksOnClose = cancelRemainingTasksOnClose;
}
public boolean isRejectTasksWhenLimitReached() { public boolean isRejectTasksWhenLimitReached() {
return this.rejectTasksWhenLimitReached; return this.rejectTasksWhenLimitReached;
} }

View File

@ -148,6 +148,7 @@ class TaskExecutorConfigurations {
builder = builder.customizers(this.taskExecutorCustomizers.orderedStream()::iterator); builder = builder.customizers(this.taskExecutorCustomizers.orderedStream()::iterator);
builder = builder.taskDecorator(getTaskDecorator(this.taskDecorator)); builder = builder.taskDecorator(getTaskDecorator(this.taskDecorator));
TaskExecutionProperties.Simple simple = this.properties.getSimple(); TaskExecutionProperties.Simple simple = this.properties.getSimple();
builder = builder.cancelRemainingTasksOnClose(simple.isCancelRemainingTasksOnClose());
builder = builder.rejectTasksWhenLimitReached(simple.isRejectTasksWhenLimitReached()); builder = builder.rejectTasksWhenLimitReached(simple.isRejectTasksWhenLimitReached());
builder = builder.concurrencyLimit(simple.getConcurrencyLimit()); builder = builder.concurrencyLimit(simple.getConcurrencyLimit());
TaskExecutionProperties.Shutdown shutdown = this.properties.getShutdown(); TaskExecutionProperties.Shutdown shutdown = this.properties.getShutdown();

View File

@ -83,11 +83,13 @@ class TaskExecutionAutoConfigurationTests {
void simpleAsyncTaskExecutorBuilderShouldReadProperties() { void simpleAsyncTaskExecutorBuilderShouldReadProperties() {
this.contextRunner this.contextRunner
.withPropertyValues("spring.task.execution.thread-name-prefix=mytest-", .withPropertyValues("spring.task.execution.thread-name-prefix=mytest-",
"spring.task.execution.simple.cancel-remaining-tasks-on-close=true",
"spring.task.execution.simple.reject-tasks-when-limit-reached=true", "spring.task.execution.simple.reject-tasks-when-limit-reached=true",
"spring.task.execution.simple.concurrency-limit=1", "spring.task.execution.simple.concurrency-limit=1",
"spring.task.execution.shutdown.await-termination=true", "spring.task.execution.shutdown.await-termination=true",
"spring.task.execution.shutdown.await-termination-period=30s") "spring.task.execution.shutdown.await-termination-period=30s")
.run(assertSimpleAsyncTaskExecutor((taskExecutor) -> { .run(assertSimpleAsyncTaskExecutor((taskExecutor) -> {
assertThat(taskExecutor).hasFieldOrPropertyWithValue("cancelRemainingTasksOnClose", true);
assertThat(taskExecutor).hasFieldOrPropertyWithValue("rejectTasksWhenLimitReached", true); assertThat(taskExecutor).hasFieldOrPropertyWithValue("rejectTasksWhenLimitReached", true);
assertThat(taskExecutor.getConcurrencyLimit()).isEqualTo(1); assertThat(taskExecutor.getConcurrencyLimit()).isEqualTo(1);
assertThat(taskExecutor.getThreadNamePrefix()).isEqualTo("mytest-"); assertThat(taskExecutor.getThreadNamePrefix()).isEqualTo("mytest-");

View File

@ -52,6 +52,8 @@ public class SimpleAsyncTaskExecutorBuilder {
private final @Nullable String threadNamePrefix; private final @Nullable String threadNamePrefix;
private final boolean cancelRemainingTasksOnClose;
private final boolean rejectTasksWhenLimitReached; private final boolean rejectTasksWhenLimitReached;
private final @Nullable Integer concurrencyLimit; private final @Nullable Integer concurrencyLimit;
@ -63,15 +65,16 @@ public class SimpleAsyncTaskExecutorBuilder {
private final @Nullable Duration taskTerminationTimeout; private final @Nullable Duration taskTerminationTimeout;
public SimpleAsyncTaskExecutorBuilder() { public SimpleAsyncTaskExecutorBuilder() {
this(null, null, false, null, null, null, null); this(null, null, false, false, null, null, null, null);
} }
private SimpleAsyncTaskExecutorBuilder(@Nullable Boolean virtualThreads, @Nullable String threadNamePrefix, private SimpleAsyncTaskExecutorBuilder(@Nullable Boolean virtualThreads, @Nullable String threadNamePrefix,
boolean rejectTasksWhenLimitReached, @Nullable Integer concurrencyLimit, boolean cancelRemainingTasksOnClose, boolean rejectTasksWhenLimitReached,
@Nullable TaskDecorator taskDecorator, @Nullable Set<SimpleAsyncTaskExecutorCustomizer> customizers, @Nullable Integer concurrencyLimit, @Nullable TaskDecorator taskDecorator,
@Nullable Duration taskTerminationTimeout) { @Nullable Set<SimpleAsyncTaskExecutorCustomizer> customizers, @Nullable Duration taskTerminationTimeout) {
this.virtualThreads = virtualThreads; this.virtualThreads = virtualThreads;
this.threadNamePrefix = threadNamePrefix; this.threadNamePrefix = threadNamePrefix;
this.cancelRemainingTasksOnClose = cancelRemainingTasksOnClose;
this.rejectTasksWhenLimitReached = rejectTasksWhenLimitReached; this.rejectTasksWhenLimitReached = rejectTasksWhenLimitReached;
this.concurrencyLimit = concurrencyLimit; this.concurrencyLimit = concurrencyLimit;
this.taskDecorator = taskDecorator; this.taskDecorator = taskDecorator;
@ -86,8 +89,8 @@ public class SimpleAsyncTaskExecutorBuilder {
*/ */
public SimpleAsyncTaskExecutorBuilder threadNamePrefix(@Nullable String threadNamePrefix) { public SimpleAsyncTaskExecutorBuilder threadNamePrefix(@Nullable String threadNamePrefix) {
return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, threadNamePrefix, return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, threadNamePrefix,
this.rejectTasksWhenLimitReached, this.concurrencyLimit, this.taskDecorator, this.customizers, this.cancelRemainingTasksOnClose, this.rejectTasksWhenLimitReached, this.concurrencyLimit,
this.taskTerminationTimeout); this.taskDecorator, this.customizers, this.taskTerminationTimeout);
} }
/** /**
@ -97,8 +100,26 @@ public class SimpleAsyncTaskExecutorBuilder {
*/ */
public SimpleAsyncTaskExecutorBuilder virtualThreads(@Nullable Boolean virtualThreads) { public SimpleAsyncTaskExecutorBuilder virtualThreads(@Nullable Boolean virtualThreads) {
return new SimpleAsyncTaskExecutorBuilder(virtualThreads, this.threadNamePrefix, return new SimpleAsyncTaskExecutorBuilder(virtualThreads, this.threadNamePrefix,
this.rejectTasksWhenLimitReached, this.concurrencyLimit, this.taskDecorator, this.customizers, this.cancelRemainingTasksOnClose, this.rejectTasksWhenLimitReached, this.concurrencyLimit,
this.taskTerminationTimeout); this.taskDecorator, this.customizers, this.taskTerminationTimeout);
}
/**
* Set whether to cancel remaining tasks on close. By default {@code false} not
* tracking active threads at all or just interrupting any remaining threads that
* still have not finished after the specified {@link #taskTerminationTimeout
* taskTerminationTimeout}. Switch this to {@code true} for immediate interruption on
* close, either in combination with a subsequent termination timeout or without any
* waiting at all, depending on whether a {@code taskTerminationTimeout} has been
* specified as well.
* @param cancelRemainingTasksOnClose whether to cancel remaining tasks on close
* @return a new builder instance
* @since 4.0.0
*/
public SimpleAsyncTaskExecutorBuilder cancelRemainingTasksOnClose(boolean cancelRemainingTasksOnClose) {
return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, this.threadNamePrefix,
cancelRemainingTasksOnClose, this.rejectTasksWhenLimitReached, this.concurrencyLimit,
this.taskDecorator, this.customizers, this.taskTerminationTimeout);
} }
/** /**
@ -112,8 +133,8 @@ public class SimpleAsyncTaskExecutorBuilder {
*/ */
public SimpleAsyncTaskExecutorBuilder rejectTasksWhenLimitReached(boolean rejectTasksWhenLimitReached) { public SimpleAsyncTaskExecutorBuilder rejectTasksWhenLimitReached(boolean rejectTasksWhenLimitReached) {
return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, this.threadNamePrefix, return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, this.threadNamePrefix,
rejectTasksWhenLimitReached, this.concurrencyLimit, this.taskDecorator, this.customizers, this.cancelRemainingTasksOnClose, rejectTasksWhenLimitReached, this.concurrencyLimit,
this.taskTerminationTimeout); this.taskDecorator, this.customizers, this.taskTerminationTimeout);
} }
/** /**
@ -123,8 +144,8 @@ public class SimpleAsyncTaskExecutorBuilder {
*/ */
public SimpleAsyncTaskExecutorBuilder concurrencyLimit(@Nullable Integer concurrencyLimit) { public SimpleAsyncTaskExecutorBuilder concurrencyLimit(@Nullable Integer concurrencyLimit) {
return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, this.threadNamePrefix, return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, this.threadNamePrefix,
this.rejectTasksWhenLimitReached, concurrencyLimit, this.taskDecorator, this.customizers, this.cancelRemainingTasksOnClose, this.rejectTasksWhenLimitReached, concurrencyLimit,
this.taskTerminationTimeout); this.taskDecorator, this.customizers, this.taskTerminationTimeout);
} }
/** /**
@ -134,8 +155,8 @@ public class SimpleAsyncTaskExecutorBuilder {
*/ */
public SimpleAsyncTaskExecutorBuilder taskDecorator(@Nullable TaskDecorator taskDecorator) { public SimpleAsyncTaskExecutorBuilder taskDecorator(@Nullable TaskDecorator taskDecorator) {
return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, this.threadNamePrefix, return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, this.threadNamePrefix,
this.rejectTasksWhenLimitReached, this.concurrencyLimit, taskDecorator, this.customizers, this.cancelRemainingTasksOnClose, this.rejectTasksWhenLimitReached, this.concurrencyLimit,
this.taskTerminationTimeout); taskDecorator, this.customizers, this.taskTerminationTimeout);
} }
/** /**
@ -146,8 +167,8 @@ public class SimpleAsyncTaskExecutorBuilder {
*/ */
public SimpleAsyncTaskExecutorBuilder taskTerminationTimeout(@Nullable Duration taskTerminationTimeout) { public SimpleAsyncTaskExecutorBuilder taskTerminationTimeout(@Nullable Duration taskTerminationTimeout) {
return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, this.threadNamePrefix, return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, this.threadNamePrefix,
this.rejectTasksWhenLimitReached, this.concurrencyLimit, this.taskDecorator, this.customizers, this.cancelRemainingTasksOnClose, this.rejectTasksWhenLimitReached, this.concurrencyLimit,
taskTerminationTimeout); this.taskDecorator, this.customizers, taskTerminationTimeout);
} }
/** /**
@ -177,8 +198,8 @@ public class SimpleAsyncTaskExecutorBuilder {
Iterable<? extends SimpleAsyncTaskExecutorCustomizer> customizers) { Iterable<? extends SimpleAsyncTaskExecutorCustomizer> customizers) {
Assert.notNull(customizers, "'customizers' must not be null"); Assert.notNull(customizers, "'customizers' must not be null");
return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, this.threadNamePrefix, return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, this.threadNamePrefix,
this.rejectTasksWhenLimitReached, this.concurrencyLimit, this.taskDecorator, append(null, customizers), this.cancelRemainingTasksOnClose, this.rejectTasksWhenLimitReached, this.concurrencyLimit,
this.taskTerminationTimeout); this.taskDecorator, append(null, customizers), this.taskTerminationTimeout);
} }
/** /**
@ -206,8 +227,8 @@ public class SimpleAsyncTaskExecutorBuilder {
Iterable<? extends SimpleAsyncTaskExecutorCustomizer> customizers) { Iterable<? extends SimpleAsyncTaskExecutorCustomizer> customizers) {
Assert.notNull(customizers, "'customizers' must not be null"); Assert.notNull(customizers, "'customizers' must not be null");
return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, this.threadNamePrefix, return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, this.threadNamePrefix,
this.rejectTasksWhenLimitReached, this.concurrencyLimit, this.taskDecorator, this.cancelRemainingTasksOnClose, this.rejectTasksWhenLimitReached, this.concurrencyLimit,
append(this.customizers, customizers), this.taskTerminationTimeout); this.taskDecorator, append(this.customizers, customizers), this.taskTerminationTimeout);
} }
/** /**
@ -246,6 +267,7 @@ public class SimpleAsyncTaskExecutorBuilder {
PropertyMapper map = PropertyMapper.get(); PropertyMapper map = PropertyMapper.get();
map.from(this.virtualThreads).to(taskExecutor::setVirtualThreads); map.from(this.virtualThreads).to(taskExecutor::setVirtualThreads);
map.from(this.threadNamePrefix).whenHasText().to(taskExecutor::setThreadNamePrefix); map.from(this.threadNamePrefix).whenHasText().to(taskExecutor::setThreadNamePrefix);
map.from(this.cancelRemainingTasksOnClose).to(taskExecutor::setCancelRemainingTasksOnClose);
map.from(this.rejectTasksWhenLimitReached).to(taskExecutor::setRejectTasksWhenLimitReached); map.from(this.rejectTasksWhenLimitReached).to(taskExecutor::setRejectTasksWhenLimitReached);
map.from(this.concurrencyLimit).to(taskExecutor::setConcurrencyLimit); map.from(this.concurrencyLimit).to(taskExecutor::setConcurrencyLimit);
map.from(this.taskDecorator).to(taskExecutor::setTaskDecorator); map.from(this.taskDecorator).to(taskExecutor::setTaskDecorator);

View File

@ -59,6 +59,12 @@ class SimpleAsyncTaskExecutorBuilderTests {
SimpleAsyncTaskExecutorAssert.assertThat(executor).usesVirtualThreads(); SimpleAsyncTaskExecutorAssert.assertThat(executor).usesVirtualThreads();
} }
@Test
void cancelRemainingTasksOnCloseShouldApply() {
SimpleAsyncTaskExecutor executor = this.builder.cancelRemainingTasksOnClose(true).build();
assertThat(executor).extracting("cancelRemainingTasksOnClose").isEqualTo(true);
}
@Test @Test
void rejectTasksWhenLimitReachedShouldApply() { void rejectTasksWhenLimitReachedShouldApply() {
SimpleAsyncTaskExecutor executor = this.builder.rejectTasksWhenLimitReached(true).build(); SimpleAsyncTaskExecutor executor = this.builder.rejectTasksWhenLimitReached(true).build();