jdaugherty commented on code in PR #16763:
URL: https://github.com/apache/grails-core/pull/16763#discussion_r4377789786


##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/GrailsHibernateTemplate.java:
##########
@@ -236,6 +239,30 @@ public <T> T executeWithNewSession(final Closure<T> 
callable) {
                 txResources.initSynchronization();
             }
 
+            // If the outer session is disconnected, acquire its physical 
connection now before
+            // unbinding and entering the new session scope. Otherwise, lazy 
loading on outer session
+            // entities inside withNewSession { withTransaction { ... } } 
would borrow the inner
+            // transaction's connection and be left with a closed connection 
when the inner transaction commits.
+            if (previousHolder != null && previousHolder.getSession() != null 
&& previousHolder.getSession().isOpen()) {
+                Session previousSession = previousHolder.getSession();
+                if (previousSession instanceof SessionImplementor sessionImpl) 
{
+                    JdbcCoordinator jdbcCoordinator = 
sessionImpl.getJdbcCoordinator();
+                    if (jdbcCoordinator != null) {
+                        LogicalConnectionImplementor logicalConnection = 
jdbcCoordinator.getLogicalConnection();
+                        if (logicalConnection != null && 
!logicalConnection.isPhysicallyConnected()) {
+                            Connection con = 
logicalConnection.getPhysicalConnection();
+                            if (con != null) {
+                                try {
+                                    con.getMetaData();

Review Comment:
   This call is what makes the fix work, and the comment above doesn't say why. 
`getPhysicalConnection()` returns the `TransactionAwareDataSourceProxy` handle, 
and the proxy only takes a target connection on its first real method call. 
`getMetaData()` is that call. When I replaced it with a call the proxy answers 
itself (`hashCode()`), `WithNewSessionLazyLoadSpec` failed again with 
`PooledConnection has already been closed`. Please say that in the comment, so 
the call doesn't get removed as dead code.
   
   `getMetaData()` also makes the default `LazyConnectionDataSourceProxy` 
(`dataSource.lazy: true`) check out a pooled connection, and the request 
session holds it until it closes. That happens on every `withNewSession` while 
the request session is disconnected. It includes a request whose first database 
work is the `withNewSession` and whose session has nothing to lazy-load. After 
`withNewSession { WnsllAuthor.count() }` on an unused request session, the 
Tomcat pool reports 1 active connection on this branch and 0 on 8.0.x. 
`con.getAutoCommit()` binds the proxy's target the same way, but the lazy proxy 
answers it without fetching from the pool. With that change the spec still 
passes, and the pool stays at 0 until the request session runs a statement. 
(With `dataSource.lazy: false`, both calls check out a connection.)
   
   The `SQLException` is logged at debug. If this call fails, the original bug 
comes back with nothing in the log at the default level. Logging it at warn 
would make that visible.



##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/GrailsHibernateTemplate.java:
##########
@@ -680,10 +708,12 @@ protected void flushIfNecessary(Session session, boolean 
existingTransaction) th
     @SuppressWarnings("ConstantConditions")
     protected DataAccessException 
convertHibernateAccessException(HibernateException ex) {
         if (ex instanceof JDBCException) {
-            return convertJdbcAccessException((JDBCException) ex, 
jdbcExceptionTranslator);
+            DataAccessException dae = 
convertJdbcAccessException((JDBCException) ex, jdbcExceptionTranslator);
+            return dae != null ? dae : 
SessionFactoryUtils.convertHibernateAccessException(ex);
         }
         if (GenericJDBCException.class.equals(ex.getClass())) {

Review Comment:
   This branch can't run. `GenericJDBCException` extends `JDBCException`, so 
the `instanceof JDBCException` check above catches it first. The new test 
passes a `GenericJDBCException`, but it goes through the first branch. Could 
you delete this branch instead of adding the fallback to it?



##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/GrailsHibernateTemplate.java:
##########
@@ -410,7 +437,8 @@ protected <T> T doExecute(HibernateCallback<T> action, 
boolean enforceNativeSess
             flushIfNecessary(session, existingTransaction);
             return result;
         } catch (HibernateException ex) {
-            throw convertHibernateAccessException(ex);
+            DataAccessException dae = convertHibernateAccessException(ex);
+            throw (dae != null ? dae : new HibernateSystemException(ex));

Review Comment:
   After the change to `convertHibernateAccessException`, it can't return null. 
Both branches fall back to 
`SessionFactoryUtils.convertHibernateAccessException`, which ends with `new 
HibernateSystemException(ex)`, and nothing in the repository overrides the 
method. So this second fallback never runs, and no test covers it. I'd drop it.
   
   This path does need a test, though, because the issue's 
`NullPointerException` came from here. With the fix in place, the new spec no 
longer reaches it, and the new `GrailsHibernateTemplateSpec` test calls 
`convertHibernateAccessException` directly. Next to `doExecute wraps 
HibernateException as DataAccessException`, something like this:
   
   ```groovy
   when:
   template.execute { sess -> throw new GenericJDBCException('msg', new 
SQLException('untranslatable')) }
   
   then:
   thrown(HibernateJdbcException)
   ```
   
   On 8.0.x that throws the `NullPointerException`. On this branch it throws 
`HibernateJdbcException`.



##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/GrailsHibernateTemplate.java:
##########
@@ -236,6 +239,30 @@ public <T> T executeWithNewSession(final Closure<T> 
callable) {
                 txResources.initSynchronization();
             }
 
+            // If the outer session is disconnected, acquire its physical 
connection now before
+            // unbinding and entering the new session scope. Otherwise, lazy 
loading on outer session
+            // entities inside withNewSession { withTransaction { ... } } 
would borrow the inner
+            // transaction's connection and be left with a closed connection 
when the inner transaction commits.
+            if (previousHolder != null && previousHolder.getSession() != null 
&& previousHolder.getSession().isOpen()) {
+                Session previousSession = previousHolder.getSession();
+                if (previousSession instanceof SessionImplementor sessionImpl) 
{
+                    JdbcCoordinator jdbcCoordinator = 
sessionImpl.getJdbcCoordinator();
+                    if (jdbcCoordinator != null) {
+                        LogicalConnectionImplementor logicalConnection = 
jdbcCoordinator.getLogicalConnection();
+                        if (logicalConnection != null && 
!logicalConnection.isPhysicallyConnected()) {
+                            Connection con = 
logicalConnection.getPhysicalConnection();

Review Comment:
   Reproduced. It is also a regression. This works on 8.0.x and fails on this 
branch, with no lazy load involved:
   
   ```groovy
   WnsllAuthor.withTransaction { WnsllAuthor.count() }   // disconnects the 
request session
   WnsllBook.withTransaction(propagationBehavior: 
TransactionDefinition.PROPAGATION_SUPPORTS, readOnly: true) {
       WnsllBook.withNewSession {
           WnsllBook.withTransaction { new WnsllAuthor(name: 
'Tenar').save(flush: true) }
       }
       WnsllBook.count()   // 8.0.x: 1. This branch: HibernateJdbcException, 
PooledConnection has already been closed
   }
   ```
   
   `getMetaData()` runs after the synchronization is re-initialised, so 
`DataSourceUtils` binds a `ConnectionHolder` for the request session's 
connection. `previousConnectionHolder` was already read as null, so that holder 
stays bound through the new-session scope. The `finally` block's 
`unbindResourceIfPossible(dataSource)` then finds it, and `releaseConnection` 
closes the connection the request session is still holding. In my runs the 
nested `withTransaction` itself got through, because its `suspend` unbinds that 
holder before `doBegin` binds its own. What fails is the next statement on the 
request session.
   
   Moving the acquisition above the `previousConnectionHolder` and 
`getSynchronizations()` reads fixes it. With only that change, three things 
pass: the case above, the same case with `book.author.name` read inside the 
inner transaction (which throws the `NullPointerException` on 8.0.x), and 
`WithNewSessionLazyLoadSpec`.
   
   For the spec, put `readOnly: true` on the SUPPORTS scope. A read-write 
SUPPORTS scope that used the request session ends with 
`TransactionRequiredException: No active transaction`, from the flush at 
commit. That happens on 8.0.x too, and it would hide the result.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to