Auto-configure SimpleAsyncTaskScheduler when virtual threads are enabled

This auto-configures a new SimpleAsyncTaskSchedulerBuilder bean in the
context. This bean is configured to use virtual threads, if enabled.

SimpleAsyncTaskSchedulerCustomizers can be used to customize the built
SimpleAsyncTaskScheduler.

If virtual threads are enabled, the application task scheduler is
configured to be a SimpleAsyncTaskScheduler.

Adds a new configuration property spring.task.scheduling.simple
.concurrency-limit

Closes gh-36609
This commit is contained in:
Moritz Halbritter 2023-08-01 15:00:44 +02:00
parent 3c12f39adb
commit 5b00d5f89b
8 changed files with 509 additions and 7 deletions

View File

@ -40,7 +40,8 @@ import org.springframework.scheduling.config.TaskManagementConfigUtils;
@EnableConfigurationProperties(TaskSchedulingProperties.class)
@Import({ TaskSchedulingConfigurations.ThreadPoolTaskSchedulerBuilderConfiguration.class,
TaskSchedulingConfigurations.TaskSchedulerBuilderConfiguration.class,
TaskSchedulingConfigurations.ThreadPoolTaskSchedulerConfiguration.class })
TaskSchedulingConfigurations.SimpleAsyncTaskSchedulerBuilderConfiguration.class,
TaskSchedulingConfigurations.TaskSchedulerConfiguration.class })
public class TaskSchedulingAutoConfiguration {
@Bean

View File

@ -21,6 +21,10 @@ import java.util.concurrent.ScheduledExecutorService;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading;
import org.springframework.boot.autoconfigure.thread.Threading;
import org.springframework.boot.task.SimpleAsyncTaskSchedulerBuilder;
import org.springframework.boot.task.SimpleAsyncTaskSchedulerCustomizer;
import org.springframework.boot.task.TaskSchedulerBuilder;
import org.springframework.boot.task.TaskSchedulerCustomizer;
import org.springframework.boot.task.ThreadPoolTaskSchedulerBuilder;
@ -28,6 +32,7 @@ import org.springframework.boot.task.ThreadPoolTaskSchedulerCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.TaskManagementConfigUtils;
@ -43,9 +48,16 @@ class TaskSchedulingConfigurations {
@ConditionalOnBean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
@ConditionalOnMissingBean({ TaskScheduler.class, ScheduledExecutorService.class })
@SuppressWarnings("removal")
static class ThreadPoolTaskSchedulerConfiguration {
static class TaskSchedulerConfiguration {
@Bean(name = "taskScheduler")
@ConditionalOnThreading(Threading.VIRTUAL)
SimpleAsyncTaskScheduler taskSchedulerVirtualThreads(SimpleAsyncTaskSchedulerBuilder builder) {
return builder.build();
}
@Bean
@ConditionalOnThreading(Threading.PLATFORM)
ThreadPoolTaskScheduler taskScheduler(TaskSchedulerBuilder taskSchedulerBuilder,
ObjectProvider<ThreadPoolTaskSchedulerBuilder> threadPoolTaskSchedulerBuilderProvider) {
ThreadPoolTaskSchedulerBuilder threadPoolTaskSchedulerBuilder = threadPoolTaskSchedulerBuilderProvider
@ -105,4 +117,44 @@ class TaskSchedulingConfigurations {
}
@Configuration(proxyBeanMethods = false)
static class SimpleAsyncTaskSchedulerBuilderConfiguration {
private final TaskSchedulingProperties properties;
private final ObjectProvider<SimpleAsyncTaskSchedulerCustomizer> taskSchedulerCustomizers;
SimpleAsyncTaskSchedulerBuilderConfiguration(TaskSchedulingProperties properties,
ObjectProvider<SimpleAsyncTaskSchedulerCustomizer> taskSchedulerCustomizers) {
this.properties = properties;
this.taskSchedulerCustomizers = taskSchedulerCustomizers;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnThreading(Threading.PLATFORM)
SimpleAsyncTaskSchedulerBuilder simpleAsyncTaskSchedulerBuilder() {
return builder();
}
@Bean(name = "simpleAsyncTaskSchedulerBuilder")
@ConditionalOnMissingBean
@ConditionalOnThreading(Threading.VIRTUAL)
SimpleAsyncTaskSchedulerBuilder simpleAsyncTaskSchedulerBuilderVirtualThreads() {
SimpleAsyncTaskSchedulerBuilder builder = builder();
builder = builder.virtualThreads(true);
return builder;
}
private SimpleAsyncTaskSchedulerBuilder builder() {
SimpleAsyncTaskSchedulerBuilder builder = new SimpleAsyncTaskSchedulerBuilder();
builder = builder.threadNamePrefix(this.properties.getThreadNamePrefix());
builder = builder.customizers(this.taskSchedulerCustomizers.orderedStream()::iterator);
TaskSchedulingProperties.Simple simple = this.properties.getSimple();
builder = builder.concurrencyLimit(simple.getConcurrencyLimit());
return builder;
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2023 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.
@ -31,6 +31,8 @@ public class TaskSchedulingProperties {
private final Pool pool = new Pool();
private final Simple simple = new Simple();
private final Shutdown shutdown = new Shutdown();
/**
@ -42,6 +44,10 @@ public class TaskSchedulingProperties {
return this.pool;
}
public Simple getSimple() {
return this.simple;
}
public Shutdown getShutdown() {
return this.shutdown;
}
@ -71,6 +77,24 @@ public class TaskSchedulingProperties {
}
public static class Simple {
/**
* Set the maximum number of parallel accesses allowed. -1 indicates no
* concurrency limit at all.
*/
private Integer concurrencyLimit;
public Integer getConcurrencyLimit() {
return this.concurrencyLimit;
}
public void setConcurrencyLimit(Integer concurrencyLimit) {
this.concurrencyLimit = concurrencyLimit;
}
}
public static class Shutdown {
/**

View File

@ -28,9 +28,13 @@ import java.util.concurrent.TimeUnit;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
import org.springframework.boot.LazyInitializationBeanFactoryPostProcessor;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.task.SimpleAsyncTaskSchedulerBuilder;
import org.springframework.boot.task.SimpleAsyncTaskSchedulerCustomizer;
import org.springframework.boot.task.TaskSchedulerBuilder;
import org.springframework.boot.task.TaskSchedulerCustomizer;
import org.springframework.boot.task.ThreadPoolTaskSchedulerBuilder;
@ -45,6 +49,7 @@ import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
@ -110,6 +115,58 @@ class TaskSchedulingAutoConfigurationTests {
});
}
@Test
void simpleAsyncTaskSchedulerBuilderShouldReadProperties() {
this.contextRunner
.withPropertyValues("spring.task.scheduling.simple.concurrency-limit=1",
"spring.task.scheduling.thread-name-prefix=scheduling-test-")
.withUserConfiguration(SchedulingConfiguration.class)
.run((context) -> {
assertThat(context).hasSingleBean(SimpleAsyncTaskSchedulerBuilder.class);
SimpleAsyncTaskSchedulerBuilder builder = context.getBean(SimpleAsyncTaskSchedulerBuilder.class);
assertThat(builder).hasFieldOrPropertyWithValue("threadNamePrefix", "scheduling-test-");
assertThat(builder).hasFieldOrPropertyWithValue("concurrencyLimit", 1);
});
}
@Test
@EnabledForJreRange(min = JRE.JAVA_21)
void simpleAsyncTaskSchedulerBuilderShouldUseVirtualThreadsIfEnabled() {
this.contextRunner.withPropertyValues("spring.threads.virtual.enabled=true")
.withUserConfiguration(SchedulingConfiguration.class)
.run((context) -> {
assertThat(context).hasSingleBean(SimpleAsyncTaskSchedulerBuilder.class);
SimpleAsyncTaskSchedulerBuilder builder = context.getBean(SimpleAsyncTaskSchedulerBuilder.class);
assertThat(builder).hasFieldOrPropertyWithValue("virtualThreads", true);
});
}
@Test
@EnabledForJreRange(min = JRE.JAVA_21)
void simpleAsyncTaskSchedulerBuilderShouldUsePlatformThreadsByDefault() {
this.contextRunner.withUserConfiguration(SchedulingConfiguration.class).run((context) -> {
assertThat(context).hasSingleBean(SimpleAsyncTaskSchedulerBuilder.class);
SimpleAsyncTaskSchedulerBuilder builder = context.getBean(SimpleAsyncTaskSchedulerBuilder.class);
assertThat(builder).hasFieldOrPropertyWithValue("virtualThreads", null);
});
}
@Test
@SuppressWarnings("unchecked")
void simpleAsyncTaskSchedulerBuilderShouldApplyCustomizers() {
SimpleAsyncTaskSchedulerCustomizer customizer = (scheduler) -> {
};
this.contextRunner.withBean(SimpleAsyncTaskSchedulerCustomizer.class, () -> customizer)
.withUserConfiguration(SchedulingConfiguration.class)
.run((context) -> {
assertThat(context).hasSingleBean(SimpleAsyncTaskSchedulerBuilder.class);
SimpleAsyncTaskSchedulerBuilder builder = context.getBean(SimpleAsyncTaskSchedulerBuilder.class);
Set<SimpleAsyncTaskSchedulerCustomizer> customizers = (Set<SimpleAsyncTaskSchedulerCustomizer>) ReflectionTestUtils
.getField(builder, "customizers");
assertThat(customizers).as("SimpleAsyncTaskSchedulerBuilder.customizers").contains(customizer);
});
}
@Test
void enableSchedulingWithNoTaskExecutorAppliesTaskSchedulerCustomizers() {
this.contextRunner.withPropertyValues("spring.task.scheduling.thread-name-prefix=scheduling-test-")

View File

@ -36,8 +36,11 @@ Those default settings can be fine-tuned using the `spring.task.execution` names
This changes the thread pool to use a bounded queue so that when the queue is full (100 tasks), the thread pool increases to maximum 16 threads.
Shrinking of the pool is more aggressive as threads are reclaimed when they are idle for 10 seconds (rather than 60 seconds by default).
A `ThreadPoolTaskScheduler` can also be auto-configured if need to be associated to scheduled task execution (using `@EnableScheduling` for instance).
The thread pool uses one thread by default and its settings can be fine-tuned using the `spring.task.scheduling` namespace, as shown in the following example:
A scheduler can also be auto-configured if need to be associated to scheduled task execution (using `@EnableScheduling` for instance).
When virtual threads are enabled (using Java 21+ and configprop:spring.threads.virtual.enabled[] set to `true`) this will be a `SimpleAsyncTaskScheduler` that uses virtual threads.
Otherwise, it will be a `ThreadPoolTaskScheduler` with sensible defaults.
The `ThreadPoolTaskScheduler` uses one thread by default and its settings can be fine-tuned using the `spring.task.scheduling` namespace, as shown in the following example:
[source,yaml,indent=0,subs="verbatim",configprops,configblocks]
----
@ -49,5 +52,5 @@ The thread pool uses one thread by default and its settings can be fine-tuned us
size: 2
----
A `ThreadPoolTaskExecutorBuilder` bean, a `SimpleAsyncTaskExecutorBuilder` bean and a `ThreadPoolTaskSchedulerBuilder` bean are made available in the context if a custom executor or scheduler needs to be created.
The `SimpleAsyncTaskExecutorBuilder` is auto-configured to use virtual threads if they are enabled (using Java 21+ and configprop:spring.threads.virtual.enabled[] set to `true`).
A `ThreadPoolTaskExecutorBuilder` bean, a `SimpleAsyncTaskExecutorBuilder` bean, a `ThreadPoolTaskSchedulerBuilder` bean and a `SimpleAsyncTaskSchedulerBuilder` are made available in the context if a custom executor or scheduler needs to be created.
The `SimpleAsyncTaskExecutorBuilder` and `SimpleAsyncTaskSchedulerBuilder` beans are auto-configured to use virtual threads if they are enabled (using Java 21+ and configprop:spring.threads.virtual.enabled[] set to `true`).

View File

@ -0,0 +1,193 @@
/*
* Copyright 2012-2023 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.task;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* Builder that can be used to configure and create a {@link SimpleAsyncTaskScheduler}.
* Provides convenience methods to set common {@link SimpleAsyncTaskScheduler} settings.
* For advanced configuration, consider using {@link SimpleAsyncTaskSchedulerCustomizer}.
* <p>
* In a typical auto-configured Spring Boot application this builder is available as a
* bean and can be injected whenever a {@link SimpleAsyncTaskScheduler} is needed.
*
* @author Stephane Nicoll
* @author Moritz Halbritter
* @since 3.2.0
*/
public class SimpleAsyncTaskSchedulerBuilder {
private final String threadNamePrefix;
private final Integer concurrencyLimit;
private final Boolean virtualThreads;
private final Set<SimpleAsyncTaskSchedulerCustomizer> customizers;
public SimpleAsyncTaskSchedulerBuilder() {
this.threadNamePrefix = null;
this.customizers = null;
this.concurrencyLimit = null;
this.virtualThreads = null;
}
private SimpleAsyncTaskSchedulerBuilder(String threadNamePrefix, Integer concurrencyLimit, Boolean virtualThreads,
Set<SimpleAsyncTaskSchedulerCustomizer> taskSchedulerCustomizers) {
this.threadNamePrefix = threadNamePrefix;
this.concurrencyLimit = concurrencyLimit;
this.virtualThreads = virtualThreads;
this.customizers = taskSchedulerCustomizers;
}
/**
* Set the prefix to use for the names of newly created threads.
* @param threadNamePrefix the thread name prefix to set
* @return a new builder instance
*/
public SimpleAsyncTaskSchedulerBuilder threadNamePrefix(String threadNamePrefix) {
return new SimpleAsyncTaskSchedulerBuilder(threadNamePrefix, this.concurrencyLimit, this.virtualThreads,
this.customizers);
}
/**
* Set the concurrency limit.
* @param concurrencyLimit the concurrency limit
* @return a new builder instance
*/
public SimpleAsyncTaskSchedulerBuilder concurrencyLimit(Integer concurrencyLimit) {
return new SimpleAsyncTaskSchedulerBuilder(this.threadNamePrefix, concurrencyLimit, this.virtualThreads,
this.customizers);
}
/**
* Set whether to use virtual threads.
* @param virtualThreads whether to use virtual threads
* @return a new builder instance
*/
public SimpleAsyncTaskSchedulerBuilder virtualThreads(Boolean virtualThreads) {
return new SimpleAsyncTaskSchedulerBuilder(this.threadNamePrefix, this.concurrencyLimit, virtualThreads,
this.customizers);
}
/**
* Set the {@link SimpleAsyncTaskSchedulerCustomizer
* threadPoolTaskSchedulerCustomizers} that should be applied to the
* {@link SimpleAsyncTaskScheduler}. Customizers are applied in the order that they
* were added after builder configuration has been applied. Setting this value will
* replace any previously configured customizers.
* @param customizers the customizers to set
* @return a new builder instance
* @see #additionalCustomizers(SimpleAsyncTaskSchedulerCustomizer...)
*/
public SimpleAsyncTaskSchedulerBuilder customizers(SimpleAsyncTaskSchedulerCustomizer... customizers) {
Assert.notNull(customizers, "Customizers must not be null");
return customizers(Arrays.asList(customizers));
}
/**
* Set the {@link SimpleAsyncTaskSchedulerCustomizer
* threadPoolTaskSchedulerCustomizers} that should be applied to the
* {@link SimpleAsyncTaskScheduler}. Customizers are applied in the order that they
* were added after builder configuration has been applied. Setting this value will
* replace any previously configured customizers.
* @param customizers the customizers to set
* @return a new builder instance
* @see #additionalCustomizers(SimpleAsyncTaskSchedulerCustomizer...)
*/
public SimpleAsyncTaskSchedulerBuilder customizers(
Iterable<? extends SimpleAsyncTaskSchedulerCustomizer> customizers) {
Assert.notNull(customizers, "Customizers must not be null");
return new SimpleAsyncTaskSchedulerBuilder(this.threadNamePrefix, this.concurrencyLimit, this.virtualThreads,
append(null, customizers));
}
/**
* Add {@link SimpleAsyncTaskSchedulerCustomizer threadPoolTaskSchedulerCustomizers}
* that should be applied to the {@link SimpleAsyncTaskScheduler}. Customizers are
* applied in the order that they were added after builder configuration has been
* applied.
* @param customizers the customizers to add
* @return a new builder instance
* @see #customizers(SimpleAsyncTaskSchedulerCustomizer...)
*/
public SimpleAsyncTaskSchedulerBuilder additionalCustomizers(SimpleAsyncTaskSchedulerCustomizer... customizers) {
Assert.notNull(customizers, "Customizers must not be null");
return additionalCustomizers(Arrays.asList(customizers));
}
/**
* Add {@link SimpleAsyncTaskSchedulerCustomizer threadPoolTaskSchedulerCustomizers}
* that should be applied to the {@link SimpleAsyncTaskScheduler}. Customizers are
* applied in the order that they were added after builder configuration has been
* applied.
* @param customizers the customizers to add
* @return a new builder instance
* @see #customizers(SimpleAsyncTaskSchedulerCustomizer...)
*/
public SimpleAsyncTaskSchedulerBuilder additionalCustomizers(
Iterable<? extends SimpleAsyncTaskSchedulerCustomizer> customizers) {
Assert.notNull(customizers, "Customizers must not be null");
return new SimpleAsyncTaskSchedulerBuilder(this.threadNamePrefix, this.concurrencyLimit, this.virtualThreads,
append(this.customizers, customizers));
}
/**
* Build a new {@link SimpleAsyncTaskScheduler} instance and configure it using this
* builder.
* @return a configured {@link SimpleAsyncTaskScheduler} instance.
* @see #configure(SimpleAsyncTaskScheduler)
*/
public SimpleAsyncTaskScheduler build() {
return configure(new SimpleAsyncTaskScheduler());
}
/**
* Configure the provided {@link SimpleAsyncTaskScheduler} instance using this
* builder.
* @param <T> the type of task scheduler
* @param taskScheduler the {@link SimpleAsyncTaskScheduler} to configure
* @return the task scheduler instance
* @see #build()
*/
public <T extends SimpleAsyncTaskScheduler> T configure(T taskScheduler) {
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(this.threadNamePrefix).to(taskScheduler::setThreadNamePrefix);
map.from(this.concurrencyLimit).to(taskScheduler::setConcurrencyLimit);
map.from(this.virtualThreads).to(taskScheduler::setVirtualThreads);
if (!CollectionUtils.isEmpty(this.customizers)) {
this.customizers.forEach((customizer) -> customizer.customize(taskScheduler));
}
return taskScheduler;
}
private <T> Set<T> append(Set<T> set, Iterable<? extends T> additions) {
Set<T> result = new LinkedHashSet<>((set != null) ? set : Collections.emptySet());
additions.forEach(result::add);
return Collections.unmodifiableSet(result);
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright 2012-2023 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.task;
import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler;
/**
* Callback interface that can be used to customize a {@link SimpleAsyncTaskScheduler}.
*
* @author Moritz Halbritter
* @since 3.2.0
*/
@FunctionalInterface
public interface SimpleAsyncTaskSchedulerCustomizer {
/**
* Callback to customize a {@link SimpleAsyncTaskScheduler} instance.
* @param taskScheduler the task scheduler to customize
*/
void customize(SimpleAsyncTaskScheduler taskScheduler);
}

View File

@ -0,0 +1,136 @@
/*
* Copyright 2012-2023 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.task;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
/**
* Tests for {@link SimpleAsyncTaskSchedulerBuilder}.
*
* @author Stephane Nicoll
* @author Moritz Halbritter
*/
class SimpleAsyncTaskSchedulerBuilderTests {
private final SimpleAsyncTaskSchedulerBuilder builder = new SimpleAsyncTaskSchedulerBuilder();
@Test
void threadNamePrefixShouldApply() {
SimpleAsyncTaskScheduler scheduler = this.builder.threadNamePrefix("test-").build();
assertThat(scheduler.getThreadNamePrefix()).isEqualTo("test-");
}
@Test
void concurrencyLimitShouldApply() {
SimpleAsyncTaskScheduler scheduler = this.builder.concurrencyLimit(1).build();
assertThat(scheduler.getConcurrencyLimit()).isEqualTo(1);
}
@Test
@EnabledForJreRange(min = JRE.JAVA_21)
void virtualThreadsShouldApply() {
SimpleAsyncTaskScheduler scheduler = this.builder.virtualThreads(true).build();
Field field = ReflectionUtils.findField(SimpleAsyncTaskScheduler.class, "virtualThreadDelegate");
assertThat(field).as("SimpleAsyncTaskScheduler.virtualThreadDelegate").isNotNull();
field.setAccessible(true);
Object virtualThreadDelegate = ReflectionUtils.getField(field, scheduler);
assertThat(virtualThreadDelegate).as("SimpleAsyncTaskScheduler.virtualThreadDelegate").isNotNull();
}
@Test
void customizersWhenCustomizersAreNullShouldThrowException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.builder.customizers((SimpleAsyncTaskSchedulerCustomizer[]) null))
.withMessageContaining("Customizers must not be null");
}
@Test
void customizersCollectionWhenCustomizersAreNullShouldThrowException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.builder.customizers((Set<SimpleAsyncTaskSchedulerCustomizer>) null))
.withMessageContaining("Customizers must not be null");
}
@Test
void customizersShouldApply() {
SimpleAsyncTaskSchedulerCustomizer customizer = mock(SimpleAsyncTaskSchedulerCustomizer.class);
SimpleAsyncTaskScheduler scheduler = this.builder.customizers(customizer).build();
then(customizer).should().customize(scheduler);
}
@Test
void customizersShouldBeAppliedLast() {
SimpleAsyncTaskScheduler scheduler = spy(new SimpleAsyncTaskScheduler());
this.builder.concurrencyLimit(1).threadNamePrefix("test-").additionalCustomizers((taskScheduler) -> {
then(taskScheduler).should().setConcurrencyLimit(1);
then(taskScheduler).should().setThreadNamePrefix("test-");
});
this.builder.configure(scheduler);
}
@Test
void customizersShouldReplaceExisting() {
SimpleAsyncTaskSchedulerCustomizer customizer1 = mock(SimpleAsyncTaskSchedulerCustomizer.class);
SimpleAsyncTaskSchedulerCustomizer customizer2 = mock(SimpleAsyncTaskSchedulerCustomizer.class);
SimpleAsyncTaskScheduler scheduler = this.builder.customizers(customizer1)
.customizers(Collections.singleton(customizer2))
.build();
then(customizer1).shouldHaveNoInteractions();
then(customizer2).should().customize(scheduler);
}
@Test
void additionalCustomizersWhenCustomizersAreNullShouldThrowException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.builder.additionalCustomizers((SimpleAsyncTaskSchedulerCustomizer[]) null))
.withMessageContaining("Customizers must not be null");
}
@Test
void additionalCustomizersCollectionWhenCustomizersAreNullShouldThrowException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> this.builder.additionalCustomizers((Set<SimpleAsyncTaskSchedulerCustomizer>) null))
.withMessageContaining("Customizers must not be null");
}
@Test
void additionalCustomizersShouldAddToExisting() {
SimpleAsyncTaskSchedulerCustomizer customizer1 = mock(SimpleAsyncTaskSchedulerCustomizer.class);
SimpleAsyncTaskSchedulerCustomizer customizer2 = mock(SimpleAsyncTaskSchedulerCustomizer.class);
SimpleAsyncTaskScheduler scheduler = this.builder.customizers(customizer1)
.additionalCustomizers(customizer2)
.build();
then(customizer1).should().customize(scheduler);
then(customizer2).should().customize(scheduler);
}
}