Description
Describe the bug
I've been experimenting recently with project Loom by replacing various thread pools with virtual thread executors and running my application with -Djdk.tracePinnedThreads=full JVM option to see if there are any problems with this setup. The stack trace below popped up when I replaced Tomcat's default thread pool with virtual thread executor and went to the /swagger-ui.html page via internet browser:
Thread[#64,ForkJoinPool-1-worker-2,5,CarrierThreads]
java.base/java.lang.VirtualThread$VThreadContinuation.onPinned(VirtualThread.java:183)
java.base/jdk.internal.vm.Continuation.onPinned0(Continuation.java:398)
java.base/jdk.internal.vm.Continuation.yield0(Continuation.java:390)
java.base/jdk.internal.vm.Continuation.yield(Continuation.java:357)
java.base/java.lang.VirtualThread.yieldContinuation(VirtualThread.java:428)
java.base/java.lang.VirtualThread.park(VirtualThread.java:566)
java.base/java.lang.System$2.parkVirtualThread(System.java:2630)
java.base/jdk.internal.misc.VirtualThreads.park(VirtualThreads.java:54)
java.base/java.util.concurrent.locks.LockSupport.park(LockSupport.java:369)
java.base/java.util.concurrent.ForkJoinTask.awaitDone(ForkJoinTask.java:461)
java.base/java.util.concurrent.ForkJoinTask.invoke(ForkJoinTask.java:668)
java.base/java.util.stream.ReduceOps$ReduceOp.evaluateParallel(ReduceOps.java:927)
java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:233)
java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682)
org.springdoc.core.service.OpenAPIService.initializeHiddenRestController(OpenAPIService.java:289)
org.springdoc.core.service.OpenAPIService.build(OpenAPIService.java:271)
org.springdoc.api.AbstractOpenApiResource.getOpenApi(AbstractOpenApiResource.java:319) <== monitors:1
org.springdoc.webmvc.api.OpenApiResource.openapiJson(OpenApiResource.java:124)
org.springdoc.webmvc.api.OpenApiWebMvcResource.openapiJson(OpenApiWebMvcResource.java:111)
java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
java.base/java.lang.reflect.Method.invoke(Method.java:578)
org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:207)
org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:152)
org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118)
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:884)
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797)
org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1081)
org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:974)
The virtual thread becomes pinned because AbstractOpenApiResource.getOpenApi
method is synchronized and it indirectly calls OpenAPIService.initializeHiddenRestController
which blocks the thread by waiting for parallel stream.
Please consider replacing synchronized
modifier with ReentrantLock
to circumvent thread pinning.
To Reproduce
- JDK 20 with
--enable-preview
and-Djdk.tracePinnedThreads=full
switches - spring-boot version: 3.0.6
- springdoc-openapi version: 2.1.0
Replace Tomcat's thread pool with virtual thread executor. In my case I defined following Spring Bean to achieve this:
@Bean
TomcatProtocolHandlerCustomizer<?> tomcatProtocolHandlerCustomizer() {
return handler -> {
ThreadFactory factory = Thread.ofVirtual().name("http").factory();
handler.setExecutor(Executors.newThreadPerTaskExecutor(factory));
};
}
Expected behavior
Thread pinning should not happen i.e. rendering swagger-ui.html page should not produce any stacktrace in standard output when -Djdk.tracePinnedThreads=full
switch is provided to the JVM.
Additional context
Thread pinning only happens the first time swagger-ui.html page is rendered or every time if springdoc.cache.disabled
property is set to true
.