Search code examples
spring-bootspring-webfluxjava-21virtual-threads

Virtual Threads don't work for POST request in Spring Boot WebFlux


I'm having trouble enabling virtual threads for POST requests. I use Spring Boot WebFlux v. 3.2.2. My pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.apricoz</groupId>
    <artifactId>Virtual-Threads</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>Virtual-Threads</name>
    <properties>
        <java.version>21</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

I enabled Virtual Threads in the application.properties file using spring.threads.virtual.enabled=true. I created a test RestController:

@RestController
@RequestMapping
@Slf4j
public class TestController {
    
    @GetMapping
    public String getHello() {
        log.info("{}", Thread.currentThread());
        return "Hello World!";
    }
    
    @PostMapping
    public String getHello(@RequestBody String name) {
        log.info("{}", Thread.currentThread());
        return String.format("Hello %s!", name);
    }
}

So, when executing a GET request, I see in the console VirtualThread[#57,task-1]/runnable@ForkJoinPool-1-worker-1, and when executing a POST request, I see Thread[#56,reactor-http-nio-2,5,main]. How can I enable virtual threads for POST requests?


Solution

  • The problem appears for every @RequestMapping-like-annotated method, which accepts a parameter, annotated with @RequestBody. In this case the method is not executed on the WebFlux's ExecutorScheduler/TaskExecutor-managed thread (task-<N>), but instead - on WebFlux Event Loop (reactor-http-nio-<N>).

    Thus, this problem is not specific to virtual threads whatsoever, obviously it appears for platform threads, if platform TaskExecutor, e.g. ThreadPoolTaskExecutor, is configured to run Controllers' methods. Instead, this problem is specific to an interaction of Spring-WebFlux bridge with WebFlux itself, possibly to complex hierarchy of Mono subclasses (GET request lands on Just, requests with a body - on something else). This bridge, WebFluxConfigurationSupport, sets so called blocking executor to RequestMappingHandlerAdapter, which is responsible for invoking Controllers' methods.

    In fact, such a configuration is quite recent introduction to Spring Web-Flux integration. Rossen Stoyanchev wrote in his post Blocking execution for WebFlux controller methods on Jun 15, 2023:

    We can enhance WebFuxConfigurer with options for blocking execution, e.g. to configure an AsyncTaskExecutor such as the new VirtualThreadTaskExecutor.

    Therefore, if we use Spring Boot version 2.5.0, which brings WebFlux 3.4.6, then we could see that even GET requests are not executed on an executor, but on WebFlux Event Loop. So, this ideology is relatively new to Spring WebFlux and we might expect further improvement to it, at the same time reporting this situation as a bug.

    In the meantime, as a temporary walkaround, I could offer simple and wholesale (дубовoe) solution, which still allows to run problematic methods on executor-managed threads.

    The AOP Aspect intercepts all calls to these methods and invokes them on the threads it manages.

    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.core.task.AsyncTaskExecutor;
    import org.springframework.stereotype.Component;
    
    @Component
    @Aspect
    public class ManagedThreadAspect {
    
        private static class Worker implements Runnable {
        
            private final ProceedingJoinPoint pjp;
            private volatile Object result;
            private volatile Throwable exception;
    
            public Worker(final ProceedingJoinPoint pjp) {
                this.pjp = pjp;
            }
        
            @Override
            public void run() {
                try {
                    result = pjp.proceed();
                } catch (Throwable e) {
                    exception = e;
                }
            }
    
            public Object getResult() throws Throwable {
                if (exception != null)
                    throw exception;
                return result;
            }
    
        }
    
        @Around("execution(* *(.., @org.springframework.web.bind.annotation.RequestBody (*), ..))")
        public Object invokeOnManagedThread(final ProceedingJoinPoint pjp) throws Throwable {
            final Worker worker = new Worker(pjp); // this is nothing more than a result holder
            Thread.ofVirtual().start( () -> {
                worker.run();
            }).join();
            return worker.getResult();
        }
        
    }
    

    Or, if you want to reuse the stock executor, and, consequentially, employ spring.threads.virtual.enabled=true setting, inject the executor and call it :

    @Autowired
    private AsyncTaskExecutor taskExecutor;
    ...
    final Worker worker = new Worker(pjp);
    taskExecutor.submit(worker).get();              
    return worker.getResult();
    

    The effect will be roughly the same as if this method is invoked by WebFlux-managed executor: @RequestBody methods will be invoked on virtual (or, in general, Aspect-managed) threads.

    Spring AOP should be configured appropriately, e.g. one of the @Configuration classes should be annotated with @EnableAspectJAutoProxy(proxyTargetClass=true).

    I could post an example on github or make a PR to yours if the above works for you.

    Conceptually, however, the usage of virtual threads with WebFlux raises some questions. The key idea of WebFlux is non-blocking handling of the requests in its Event Loop, while what Virtual Threads essentially bring is blocking. Therefore, WebFlux and Virtual Threads are considered by many as concurrent approaches, if not mutually exclusive. For example, an author of a Baldun blog, remarkably titled Reactor WebFlux vs Virtual Threads, concludes:

    we compared two different approaches to concurrency and asynchronous processing.

    This does not, of course, mean that virtual threads should be considered a nonsense in WebFlux, only running a real world web application could reveal pros and contras of such architecture.