spring-projects / spring-data-jpa

Simplifies the development of creating a JPA-based data access layer.
https://spring.io/projects/spring-data-jpa/
Apache License 2.0
2.92k stars 1.39k forks source link

[Virtual Threads] Possible Thread Pinning in `PartTreeJpaQuery.QueryPreparer#createQuery()` #3505

Closed kzander91 closed 2 weeks ago

kzander91 commented 2 weeks ago

Version: Spring Data JPA 3.3.0.

The method uses a synchronized block, causing thread pinning: https://github.com/spring-projects/spring-data-jpa/blob/23167429949292b203ad86f3c7da30f807c912b5/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/PartTreeJpaQuery.java#L300-L302

In my application, I'm intercepting calls to EntityManager#createQuery and perform blocking operations in that interceptor. Due to the synchronized block, my virtual thread cannot dismount from the platform thread, therefore pinning it. Running with -Djdk.tracePinnedThreads=full produces stack traces that point to the snippet above:

Thread[#55,ForkJoinPool-1-worker-2,5,CarrierThreads]
    java.base/java.lang.VirtualThread$VThreadContinuation.onPinned(Unknown Source)
    java.base/jdk.internal.vm.Continuation.onPinned0(Unknown Source)
    java.base/java.lang.VirtualThread.park(Unknown Source)
    java.base/java.lang.System$2.parkVirtualThread(Unknown Source)
    java.base/jdk.internal.misc.VirtualThreads.park(Unknown Source)
    java.base/java.util.concurrent.locks.LockSupport.park(Unknown Source)
    java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(Unknown Source)
    java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(Unknown Source)
    java.base/java.util.concurrent.CountDownLatch.await(Unknown Source)
    reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:90)
    reactor.core.publisher.Mono.block(Mono.java:1728)
    my.company.MyInterceptor.blockingCall() //---------------  <---- My code here
    java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
    java.base/java.lang.reflect.Method.invoke(Unknown Source)
    org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:637)
    org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:620)
    org.springframework.aop.aspectj.AspectJMethodBeforeAdvice.before(AspectJMethodBeforeAdvice.java:44)
    org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor.invoke(MethodBeforeAdviceInterceptor.java:57)
    org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
    org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
    org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
    org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:220)
    jdk.proxy2/jdk.proxy2.$Proxy266.createQuery(Unknown Source)
    org.springframework.data.jpa.repository.query.PartTreeJpaQuery$QueryPreparer.createQuery(PartTreeJpaQuery.java:297) <== monitors:1
    org.springframework.data.jpa.repository.query.PartTreeJpaQuery$QueryPreparer.createQuery(PartTreeJpaQuery.java:242)
    org.springframework.data.jpa.repository.query.PartTreeJpaQuery.doCreateQuery(PartTreeJpaQuery.java:113)
    org.springframework.data.jpa.repository.query.AbstractJpaQuery.createQuery(AbstractJpaQuery.java:239)
    org.springframework.data.jpa.repository.query.JpaQueryExecution$SingleEntityExecution.doExecute(JpaQueryExecution.java:223)
    org.springframework.data.jpa.repository.query.JpaQueryExecution.execute(JpaQueryExecution.java:92)
    org.springframework.data.jpa.repository.query.AbstractJpaQuery.doExecute(AbstractJpaQuery.java:149)
    org.springframework.data.jpa.repository.query.AbstractJpaQuery.execute(AbstractJpaQuery.java:137)
    org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:170)
    org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:158)
    org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:164)
    org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:143)

Please consider replacing this (and other synchronized blocks/methods) with ReentrantLock to avoid thread pinning.

mp911de commented 2 weeks ago

That's fixed now.

kzander91 commented 2 weeks ago

Awesome, thanks Mark!