diff --git a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionProperties.java b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionProperties.java index b2c7d15963aa..adc57797d00b 100644 --- a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionProperties.java +++ b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionProperties.java @@ -79,6 +79,11 @@ public void setThreadNamePrefix(String threadNamePrefix) { 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. */ @@ -90,6 +95,14 @@ public static class Simple { */ private @Nullable Integer concurrencyLimit; + public boolean isCancelRemainingTasksOnClose() { + return this.cancelRemainingTasksOnClose; + } + + public void setCancelRemainingTasksOnClose(boolean cancelRemainingTasksOnClose) { + this.cancelRemainingTasksOnClose = cancelRemainingTasksOnClose; + } + public boolean isRejectTasksWhenLimitReached() { return this.rejectTasksWhenLimitReached; } diff --git a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java index a9687034e64b..9a634aba2591 100644 --- a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java +++ b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java @@ -148,6 +148,7 @@ private SimpleAsyncTaskExecutorBuilder builder() { builder = builder.customizers(this.taskExecutorCustomizers.orderedStream()::iterator); builder = builder.taskDecorator(getTaskDecorator(this.taskDecorator)); TaskExecutionProperties.Simple simple = this.properties.getSimple(); + builder = builder.cancelRemainingTasksOnClose(simple.isCancelRemainingTasksOnClose()); builder = builder.rejectTasksWhenLimitReached(simple.isRejectTasksWhenLimitReached()); builder = builder.concurrencyLimit(simple.getConcurrencyLimit()); TaskExecutionProperties.Shutdown shutdown = this.properties.getShutdown(); diff --git a/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java b/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java index 2f5468558e0a..7f1aacd59172 100644 --- a/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java +++ b/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java @@ -83,11 +83,13 @@ void shouldSupplyBeans() { void simpleAsyncTaskExecutorBuilderShouldReadProperties() { this.contextRunner .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.concurrency-limit=1", "spring.task.execution.shutdown.await-termination=true", "spring.task.execution.shutdown.await-termination-period=30s") .run(assertSimpleAsyncTaskExecutor((taskExecutor) -> { + assertThat(taskExecutor).hasFieldOrPropertyWithValue("cancelRemainingTasksOnClose", true); assertThat(taskExecutor).hasFieldOrPropertyWithValue("rejectTasksWhenLimitReached", true); assertThat(taskExecutor.getConcurrencyLimit()).isEqualTo(1); assertThat(taskExecutor.getThreadNamePrefix()).isEqualTo("mytest-"); diff --git a/core/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilder.java b/core/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilder.java index 6d328edcb205..7fc1f5f9025d 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilder.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilder.java @@ -52,6 +52,8 @@ public class SimpleAsyncTaskExecutorBuilder { private final @Nullable String threadNamePrefix; + private final boolean cancelRemainingTasksOnClose; + private final boolean rejectTasksWhenLimitReached; private final @Nullable Integer concurrencyLimit; @@ -63,15 +65,16 @@ public class SimpleAsyncTaskExecutorBuilder { private final @Nullable Duration taskTerminationTimeout; 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, - boolean rejectTasksWhenLimitReached, @Nullable Integer concurrencyLimit, - @Nullable TaskDecorator taskDecorator, @Nullable Set customizers, - @Nullable Duration taskTerminationTimeout) { + boolean cancelRemainingTasksOnClose, boolean rejectTasksWhenLimitReached, + @Nullable Integer concurrencyLimit, @Nullable TaskDecorator taskDecorator, + @Nullable Set customizers, @Nullable Duration taskTerminationTimeout) { this.virtualThreads = virtualThreads; this.threadNamePrefix = threadNamePrefix; + this.cancelRemainingTasksOnClose = cancelRemainingTasksOnClose; this.rejectTasksWhenLimitReached = rejectTasksWhenLimitReached; this.concurrencyLimit = concurrencyLimit; this.taskDecorator = taskDecorator; @@ -86,8 +89,8 @@ private SimpleAsyncTaskExecutorBuilder(@Nullable Boolean virtualThreads, @Nullab */ public SimpleAsyncTaskExecutorBuilder threadNamePrefix(@Nullable String threadNamePrefix) { return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, threadNamePrefix, - this.rejectTasksWhenLimitReached, this.concurrencyLimit, this.taskDecorator, this.customizers, - this.taskTerminationTimeout); + this.cancelRemainingTasksOnClose, this.rejectTasksWhenLimitReached, this.concurrencyLimit, + this.taskDecorator, this.customizers, this.taskTerminationTimeout); } /** @@ -97,8 +100,26 @@ public SimpleAsyncTaskExecutorBuilder threadNamePrefix(@Nullable String threadNa */ public SimpleAsyncTaskExecutorBuilder virtualThreads(@Nullable Boolean virtualThreads) { return new SimpleAsyncTaskExecutorBuilder(virtualThreads, this.threadNamePrefix, - this.rejectTasksWhenLimitReached, this.concurrencyLimit, this.taskDecorator, this.customizers, - this.taskTerminationTimeout); + this.cancelRemainingTasksOnClose, this.rejectTasksWhenLimitReached, this.concurrencyLimit, + 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 SimpleAsyncTaskExecutorBuilder virtualThreads(@Nullable Boolean virtualTh */ public SimpleAsyncTaskExecutorBuilder rejectTasksWhenLimitReached(boolean rejectTasksWhenLimitReached) { return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, this.threadNamePrefix, - rejectTasksWhenLimitReached, this.concurrencyLimit, this.taskDecorator, this.customizers, - this.taskTerminationTimeout); + this.cancelRemainingTasksOnClose, rejectTasksWhenLimitReached, this.concurrencyLimit, + this.taskDecorator, this.customizers, this.taskTerminationTimeout); } /** @@ -123,8 +144,8 @@ public SimpleAsyncTaskExecutorBuilder rejectTasksWhenLimitReached(boolean reject */ public SimpleAsyncTaskExecutorBuilder concurrencyLimit(@Nullable Integer concurrencyLimit) { return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, this.threadNamePrefix, - this.rejectTasksWhenLimitReached, concurrencyLimit, this.taskDecorator, this.customizers, - this.taskTerminationTimeout); + this.cancelRemainingTasksOnClose, this.rejectTasksWhenLimitReached, concurrencyLimit, + this.taskDecorator, this.customizers, this.taskTerminationTimeout); } /** @@ -134,8 +155,8 @@ public SimpleAsyncTaskExecutorBuilder concurrencyLimit(@Nullable Integer concurr */ public SimpleAsyncTaskExecutorBuilder taskDecorator(@Nullable TaskDecorator taskDecorator) { return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, this.threadNamePrefix, - this.rejectTasksWhenLimitReached, this.concurrencyLimit, taskDecorator, this.customizers, - this.taskTerminationTimeout); + this.cancelRemainingTasksOnClose, this.rejectTasksWhenLimitReached, this.concurrencyLimit, + taskDecorator, this.customizers, this.taskTerminationTimeout); } /** @@ -146,8 +167,8 @@ public SimpleAsyncTaskExecutorBuilder taskDecorator(@Nullable TaskDecorator task */ public SimpleAsyncTaskExecutorBuilder taskTerminationTimeout(@Nullable Duration taskTerminationTimeout) { return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, this.threadNamePrefix, - this.rejectTasksWhenLimitReached, this.concurrencyLimit, this.taskDecorator, this.customizers, - taskTerminationTimeout); + this.cancelRemainingTasksOnClose, this.rejectTasksWhenLimitReached, this.concurrencyLimit, + this.taskDecorator, this.customizers, taskTerminationTimeout); } /** @@ -177,8 +198,8 @@ public SimpleAsyncTaskExecutorBuilder customizers( Iterable customizers) { Assert.notNull(customizers, "'customizers' must not be null"); return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, this.threadNamePrefix, - this.rejectTasksWhenLimitReached, this.concurrencyLimit, this.taskDecorator, append(null, customizers), - this.taskTerminationTimeout); + this.cancelRemainingTasksOnClose, this.rejectTasksWhenLimitReached, this.concurrencyLimit, + this.taskDecorator, append(null, customizers), this.taskTerminationTimeout); } /** @@ -206,8 +227,8 @@ public SimpleAsyncTaskExecutorBuilder additionalCustomizers( Iterable customizers) { Assert.notNull(customizers, "'customizers' must not be null"); return new SimpleAsyncTaskExecutorBuilder(this.virtualThreads, this.threadNamePrefix, - this.rejectTasksWhenLimitReached, this.concurrencyLimit, this.taskDecorator, - append(this.customizers, customizers), this.taskTerminationTimeout); + this.cancelRemainingTasksOnClose, this.rejectTasksWhenLimitReached, this.concurrencyLimit, + this.taskDecorator, append(this.customizers, customizers), this.taskTerminationTimeout); } /** @@ -246,6 +267,7 @@ public T configure(T taskExecutor) { PropertyMapper map = PropertyMapper.get(); map.from(this.virtualThreads).to(taskExecutor::setVirtualThreads); 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.concurrencyLimit).to(taskExecutor::setConcurrencyLimit); map.from(this.taskDecorator).to(taskExecutor::setTaskDecorator); diff --git a/core/spring-boot/src/test/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilderTests.java b/core/spring-boot/src/test/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilderTests.java index 32f985854de8..5764d9736da7 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilderTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/task/SimpleAsyncTaskExecutorBuilderTests.java @@ -59,6 +59,12 @@ void virtualThreadsShouldApply() { SimpleAsyncTaskExecutorAssert.assertThat(executor).usesVirtualThreads(); } + @Test + void cancelRemainingTasksOnCloseShouldApply() { + SimpleAsyncTaskExecutor executor = this.builder.cancelRemainingTasksOnClose(true).build(); + assertThat(executor).extracting("cancelRemainingTasksOnClose").isEqualTo(true); + } + @Test void rejectTasksWhenLimitReachedShouldApply() { SimpleAsyncTaskExecutor executor = this.builder.rejectTasksWhenLimitReached(true).build();