This PR aims to fix virtual thread pinning that occurs with spring-orm and EclipseLink: The code inside the two synchronized blocks in org.springframework.orm.jpa.vendor.EclipseLinkJpaDialect potentially blocks when interacting with a database.

This is the (truncated) Stacktrace (Spring 6.1.12, OpenJDK 21.0.3+9, EclipseLink 4.0.4, PostgreSQL 42.7.4, Tomcat 10.1.26) as logged with -Djdk.tracePinnedThreads=full:

Thread[#61,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:393)
    java.base/java.lang.VirtualThread.parkNanos(VirtualThread.java:621)
    java.base/java.lang.System$2.parkVirtualThread(System.java:2652)
    java.base/jdk.internal.misc.VirtualThreads.park(VirtualThreads.java:67)
    java.base/java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:408)
    java.base/sun.nio.ch.Poller.pollIndirect(Poller.java:137)
    java.base/sun.nio.ch.Poller.poll(Poller.java:102)
    java.base/sun.nio.ch.Poller.poll(Poller.java:87)
    java.base/sun.nio.ch.NioSocketImpl.park(NioSocketImpl.java:175)
    java.base/sun.nio.ch.NioSocketImpl.timedRead(NioSocketImpl.java:280)
    java.base/sun.nio.ch.NioSocketImpl.implRead(NioSocketImpl.java:304)
    java.base/sun.nio.ch.NioSocketImpl.read(NioSocketImpl.java:346)
    java.base/sun.nio.ch.NioSocketImpl$1.read(NioSocketImpl.java:796)
    java.base/java.net.Socket$SocketInputStream.read(Socket.java:1099)
    java.base/sun.security.ssl.SSLSocketInputRecord.read(SSLSocketInputRecord.java:489)
    java.base/sun.security.ssl.SSLSocketInputRecord.readHeader(SSLSocketInputRecord.java:483)
    java.base/sun.security.ssl.SSLSocketInputRecord.bytesInCompletePacket(SSLSocketInputRecord.java:70)
    java.base/sun.security.ssl.SSLSocketImpl.readApplicationRecord(SSLSocketImpl.java:1461)
    java.base/sun.security.ssl.SSLSocketImpl$AppInputStream.read(SSLSocketImpl.java:1066)
    org.postgresql.core.VisibleBufferedInputStream.readMore(VisibleBufferedInputStream.java:192)
    org.postgresql.core.VisibleBufferedInputStream.ensureBytes(VisibleBufferedInputStream.java:159)
    org.postgresql.core.VisibleBufferedInputStream.ensureBytes(VisibleBufferedInputStream.java:144)
    org.postgresql.core.VisibleBufferedInputStream.read(VisibleBufferedInputStream.java:76)
    org.postgresql.core.PGStream.receiveChar(PGStream.java:476)
    org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2174)
    org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:372)
    org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:517)
    org.postgresql.jdbc.PgStatement.execute(PgStatement.java:434)
    org.postgresql.jdbc.PgStatement.executeWithFlags(PgStatement.java:356)
    org.postgresql.jdbc.PgStatement.executeCachedSql(PgStatement.java:341)
    org.postgresql.jdbc.PgStatement.executeWithFlags(PgStatement.java:317)
    org.postgresql.jdbc.PgStatement.execute(PgStatement.java:312)
    org.apache.tomcat.jdbc.pool.PooledConnection.validate(PooledConnection.java:576)
    org.apache.tomcat.jdbc.pool.PooledConnection.validate(PooledConnection.java:478)
    org.apache.tomcat.jdbc.pool.ConnectionPool.borrowConnection(ConnectionPool.java:847)
    org.apache.tomcat.jdbc.pool.ConnectionPool.borrowConnection(ConnectionPool.java:689)
    org.apache.tomcat.jdbc.pool.ConnectionPool.getConnection(ConnectionPool.java:199)
    org.apache.tomcat.jdbc.pool.DataSourceProxy.getConnection(DataSourceProxy.java:133)
    org.eclipse.persistence.sessions.JNDIConnector.connect(JNDIConnector.java:140)
    org.eclipse.persistence.sessions.DatasourceLogin.connectToDatasource(DatasourceLogin.java:174)
    org.eclipse.persistence.internal.databaseaccess.DatasourceAccessor.connectInternal(DatasourceAccessor.java:374)
    org.eclipse.persistence.internal.databaseaccess.DatabaseAccessor.connectInternal(DatabaseAccessor.java:318)
    org.eclipse.persistence.internal.databaseaccess.DatasourceAccessor.reconnect(DatasourceAccessor.java:617)
    org.eclipse.persistence.internal.databaseaccess.DatabaseAccessor.reconnect(DatabaseAccessor.java:1820)
    org.eclipse.persistence.internal.databaseaccess.DatasourceAccessor.incrementCallCount(DatasourceAccessor.java:345)
    org.eclipse.persistence.internal.databaseaccess.DatasourceAccessor.beginTransaction(DatasourceAccessor.java:269)
    org.eclipse.persistence.internal.sessions.AbstractSession.basicBeginTransaction(AbstractSession.java:742)
    org.eclipse.persistence.sessions.server.ClientSession.addWriteConnection(ClientSession.java:757)
    org.eclipse.persistence.sessions.server.ServerSession.acquireClientConnection(ServerSession.java:271)
    org.eclipse.persistence.sessions.server.ClientSession.getAccessor(ClientSession.java:391)
    org.eclipse.persistence.internal.sessions.UnitOfWorkImpl.getAccessor(UnitOfWorkImpl.java:1960)
    org.eclipse.persistence.internal.jpa.EntityManagerImpl.unwrap(EntityManagerImpl.java:2906)
    org.springframework.orm.jpa.vendor.EclipseLinkJpaDialect.beginTransaction(EclipseLinkJpaDialect.java:131) <== monitors:1
    <...>

Synchronizing on object instances that are part of a public API isn't a good idea in general, so using private locks in EclipseLinkJpaDialect also improves isolation.

The PR may be considered a breaking change if EclipseLink or some other code synchronizes on the DatabaseLogin instance.

Comment From: jhoeller

Good point! I'm addressing this slightly differently with an instance-level lock in EclipseLinkJpaDialect rather than managing Lock instances per DatabaseLogin since there is usually one EclipseLinkJpaDialect instance per persistence unit (at the same granularity as the DatabaseLogin). Thanks for the pull request, in any case!

Comment From: chschu

Thanks for having a look and for the fix.