Affects: Spring - 5.3.24 Affects: Spring boot - 2.7.6
I am using spring events to communicate between modules. I want the processing of an event to be asynchronous hence I am using @EnableAsync
and @Async
annotations. When I do not register any executor service, then spring automagically registers an executor service for async processing with some defaults. The default thread pool is a size of 8. The async processing works as expected spawning 8 threads.
The problem appears when I register another executor service and do not use it for @Async
method. Then for each execution of the @Async
method a new thread is spawned.
Consider the following minimal setup:
package com.example.demo
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.ApplicationEvent
import org.springframework.context.ApplicationEventPublisher
import org.springframework.context.annotation.Bean
import org.springframework.context.event.EventListener
import org.springframework.scheduling.annotation.Async
import org.springframework.scheduling.annotation.EnableAsync
import org.springframework.scheduling.annotation.EnableScheduling
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.scheduling.concurrent.CustomizableThreadFactory
import org.springframework.stereotype.Component
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
@EnableAsync
@EnableScheduling
@SpringBootApplication
class DemoApplication {
@Bean
fun scheduledExecutorService(): ScheduledExecutorService {
return Executors.newScheduledThreadPool(2, CustomizableThreadFactory("scheduler-"))
}
}
fun main(args: Array<String>) {
runApplication<DemoApplication>(*args)
}
class Event(source: Any, val message: String) : ApplicationEvent(source)
@Component
class EventEmitter(
private val applicationEventPublisher: ApplicationEventPublisher
) {
@Scheduled(fixedRate = 100L)
fun emitEvent() {
println("Sending event ${Thread.currentThread().name}")
applicationEventPublisher.publishEvent(Event(this, "Dummy message"))
}
}
@Component
class EventConsumer {
@Async
@EventListener
fun consume(event: Event) {
println("Received event ${Thread.currentThread().name}")
someHeavyProcessing()
}
fun someHeavyProcessing() {
Thread.sleep(1000)
}
}
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "2.7.6"
id("io.spring.dependency-management") version "1.0.15.RELEASE"
kotlin("jvm") version "1.6.21"
kotlin("plugin.spring") version "1.6.21"
}
group = "com.example"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_17
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "17"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
My logs say:
(...)
Sending event scheduler-1
Received event SimpleAsyncTaskExecutor-201
Sending event scheduler-1
Received event SimpleAsyncTaskExecutor-202
Sending event scheduler-1
Received event SimpleAsyncTaskExecutor-203
Sending event scheduler-1
Received event SimpleAsyncTaskExecutor-204
Sending event scheduler-2
Received event SimpleAsyncTaskExecutor-205
(...)
````
Once I register an ExecutorService for the ```@Async``` processing then it works just fine. Here are the two changes that needs to be added:
```kotlin
@Bean
fun asyncExecutorService() : ExecutorService {
return Executors.newFixedThreadPool(4, CustomizableThreadFactory("async-"));
}
(...)
@Async("asyncExecutorService")
Logs: ``` (...) Received event async-1 Sending event scheduler-2 Received event async-2 Sending event scheduler-2 Received event async-3 Sending event scheduler-2 Received event async-4 Sending event scheduler-2 Sending event scheduler-2 Sending event scheduler-2 Sending event scheduler-2 Sending event scheduler-2 Sending event scheduler-2 Sending event scheduler-2 Received event async-1 Sending event scheduler-2 Received event async-2 Sending event scheduler-2 Received event async-3 Sending event scheduler-2 Received event async-4 Sending event scheduler-2 Sending event scheduler-2 Sending event scheduler-2 Sending event scheduler-2 (...)
Comment From: QuantumXiecao
This problem is due to spring-boot autoconfiguration class:TaskExecutionAutoConfiguration(code below)
@Lazy
@Bean(name = { APPLICATION_TASK_EXECUTOR_BEAN_NAME,
AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME })
@ConditionalOnMissingBean(Executor.class)
public ThreadPoolTaskExecutor applicationTaskExecutor(TaskExecutorBuilder builder) {
return builder.build();
}
when you add your own bean, spring-boot would not include DEFAULT_TASK_EXECUTOR_BEAN_NAME.
This would lead to spring to use SimpleAsyncTaskExecutor which would always start one new Thread(spring-aop AsyncExecutionInterceptor.java)
@Override
@Nullable
protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) {
Executor defaultExecutor = super.getDefaultExecutor(beanFactory);
return (defaultExecutor != null ? defaultExecutor : new SimpleAsyncTaskExecutor());
}
Comment From: snicoll
When I do not register any executor service, then spring automagically registers an executor service for async processing with some defaults. The default thread pool is a size of 8. The async processing works as expected spawning 8 threads.
If you don't understand this behavior, please consider reading the reference guide before raising an issue. You creating your own executor means this behavior backs off and you're back with what the framework does by default.
@EnableAsync
describes what happens. Note that the number of active threads does not increase, the default implementation just creates a new thread for every task that's reclaimed once processing is over.