Skip to content

Virtual thread pinning in AbstractOpenApiResource.getOpenApi #2281

Closed
@mjagus

Description

@mjagus

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions