Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Asynchronous processing using virtual threads and @Async annotation

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

like image 526
rakesh Avatar asked Oct 24 '25 03:10

rakesh


1 Answers

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);
    }
like image 168
igor.zh Avatar answered Oct 26 '25 20:10

igor.zh