I have a spring boot project version 3.5 with virtual threads (spring.virtual.threads.enabled=true) and @EnableAsyncConfig enabled.
I have a simple RestController which is calling the service layer function which is annotated with @Async
The function is simply sleeping for 30 seconds before sending a response back. I see that the controller also gets blocked and does not return immediately. Since the service layer function does the computation asynchronously, I expect the controller to return the response immediately and dont wait till the service layer function gets finished.
If I disable virtual threads and start use ThreadPoolTaskExecutor, everything works fine. The controller sends back the response immediately and in the background, I see the logs after 30 seconds. How to fix this issue?
Below is the sample code
Controller
@RestController
@RequestMapping("/hello")
@RequiredArgsConstructor
@Slf4j
public class HelloController {
private final HelloService helloService;
@GetMapping
public ResponseEntity<String> hello() {
log.info("Received request in controller. Handled by thread: {}. Is Virtual Thread?: {}",
Thread.currentThread().getName(), Thread.currentThread().isVirtual());
helloService.hello();
return ResponseEntity.ok("Hello, World!");
}
}
This is calling the service layer function
@Service
@Slf4j
public class HelloService {
@Async
public CompletableFuture<Void> hello() {
log.info("HelloService called");
try {
// Simulate some processing
Thread.sleep(30000);
log.info("In Thread: {}", Thread.currentThread().getName());
log.info("Is Virtual Thread?: {}", Thread.currentThread().isVirtual());
log.info("Hello service completed");
} catch (InterruptedException e) {
log.error("Error in HelloService", e);
Thread.currentThread().interrupt();
}
return CompletableFuture.completedFuture(null);
}
}
Async config is present in its own configuration file
@Configuration
@EnableAsync
@RequiredArgsConstructor
public class AsyncConfig {
private final ThreadContextDecorator threadContextDecorator;
@Bean
public ConcurrentTaskExecutor taskExecutor() {
ThreadFactory threadFactory = Thread.ofVirtual()
.name("async-executor-", 0)
.factory();
Executor virtualThreadExecutor = task -> {
Runnable runnable = threadContextDecorator.decorate(task);
try (var executor = Executors.newThreadPerTaskExecutor(threadFactory)) {
executor.execute(runnable);
}
};
return new ConcurrentTaskExecutor(virtualThreadExecutor);
}
}
Im creating a task decorator to add MDC support
@Component
@Slf4j
public class ThreadContextDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
Map<String, String> contextMap = MDC.getCopyOfContextMap();
return () -> {
try {
if (contextMap != null) {
MDC.setContextMap(contextMap);
}
log.info("Decorating Thread: {}", Thread.currentThread().getName());
runnable.run();
} finally {
// Clear the MDC context to prevent memory leaks
MDC.clear();
}
};
}
}
Below are the logs from the controller requst
[omcat-handler-0] Received request in controller. Handled by thread: tomcat-handler-0. Is Virtual Thread?: true
[sync-executor-0] Decorating Thread: async-executor-0
[sync-executor-0] HelloService called
[sync-executor-0] In Thread: async-executor-0
[sync-executor-0] Is Virtual Thread?: true
[sync-executor-0] Hello service completed
As you can see, the controller is waiting for 30 seconds before sending the response back but I expect it to return response immediately
Just to extend a little bit perfectly correct @M.Deinum's comment, the AsyncConfig.taskExecutor method could look like follows:
@Bean
public TaskExecutor taskExecutor() {
final SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor();
taskExecutor.setVirtualThreads(true);
taskExecutor.setTaskDecorator(threadContextDecorator);
return taskExecutor;
}
Note that the line
taskExecutor.setVirtualThreads(true);
makes spring.threads.virtual.enabled=true unnecessary once @Async-annotated methods are concerned. Which can make sense if your don't want wholesale virtual thread usage, for example if you don't want HelloController.hello method to be run on virtual threads.
Here is what is wrong with your version of TaskExecutor.
TaskExecutor.execute(Runnable task) contract gives you a full freedom of how to execute the task. The implementation could execute it on the same thread, like org.springframework.core.task.SyncTaskExecutor is doing
task.run();
, blocking the execution, or execute it on new thread, like aforementioned SimpleAsyncTaskExecutor is doing:
newThread(task).start();
which does not block the current thread.
Your virtualThreadExecutor is in fact, blocking Executor, although it executes the task on separate (and virtual) thread.
Now the question is about ConcurrentTaskExecutor delegator - does it itself orchestrates the concurrent execution? No, it does not! Instead it only delegates the execution to a concurrent Executor, hence the name of its setter - setConcurrentExecutor. But the delegatee Executor you provided ConcurrentTaskExecutor with was not a concurrent one.
If you for whatever reason would still want to have your own implementation of Executor it could be the following:
@Bean
public TaskExecutor taskExecutor() {
final ThreadFactory threadFactory = Thread.ofVirtual()
.name("async-executor-", 0)
.factory();
final Executor virtualThreadExecutor = task -> {
final Runnable runnable = threadContextDecorator.decorate(task);
threadFactory.newThread(runnable).start();
};
return new ConcurrentTaskExecutor(virtualThreadExecutor);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With