This is an automated email from the ASF dual-hosted git repository.

rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit d2aa9866dfeb3cdfbb41961b2e18c7984f999469
Author: opencode <[email protected]>
AuthorDate: Thu Nov 5 16:27:03 2026 +0100

    Retire a statement whose recycle reset fails with an Error
    
    CachedStatement.closeInvoked() caught RuntimeException, 
ReflectiveOperationException and SQLException around the recycle reset, but not 
Error. A non-fatal Error from getResultSet, clearParameters or another reset 
call escaped the close path, so the provisional proxy was not removed and 
closed/delegate were never settled; the statement stayed open and unretired.
    
    Catch Throwable, apply handleThrowable, remove the provisional proxy and 
fall through to the normal close. Put the close and handle retirement in a 
finally so they always run. Add a test whose recycle reset throws a 
NoClassDefFoundError and assert the close does not throw and the physical 
statement is closed.
---
 .../jdbc/pool/interceptor/StatementCache.java      | 29 +++++++----
 .../TestStatementCacheRecycleFailure.java          | 60 ++++++++++++++++++++++
 2 files changed, 78 insertions(+), 11 deletions(-)

diff --git 
a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/StatementCache.java
 
b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/StatementCache.java
index 1407521711..2598a80d17 100644
--- 
a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/StatementCache.java
+++ 
b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/StatementCache.java
@@ -768,27 +768,34 @@ public class StatementCache extends 
StatementDecoratorInterceptor implements Sta
                             shouldClose = false;
                         }
                     }
-                } catch (RuntimeException | ReflectiveOperationException | 
SQLException x) {
+                } catch (Throwable x) {
                     // The throw always precedes the park attempt in the try
                     // above, so this proxy is not in the cache; the
                     // identity-aware removal is a no-op for it and leaves any
-                    // other statement parked under the same key alone.
+                    // other statement parked under the same key alone. A
+                    // non-fatal Error is the same failed recycle as an
+                    // unchecked exception: close the handle through the normal
+                    // path instead of leaving it open and unretired.
+                    ExceptionUtils.handleThrowable(x);
                     removeStatement(proxy);
                     if (log.isDebugEnabled()) {
                         log.debug("Unable to recycle statement: " + getSql(), 
x);
                     }
                 }
             }
-            if (shouldClose) {
-                // The physical close belongs to the superclass, and so does 
the
-                // retirement of this handle: a driver that refuses the close
-                // throws from there and the application hears about it. The
-                // statement never reached the cache in this branch, so nothing
-                // here has to be unwound for it.
-                super.closeInvoked();
+            try {
+                if (shouldClose) {
+                    // The physical close belongs to the superclass, and so 
does the
+                    // retirement of this handle: a driver that refuses the 
close
+                    // throws from there and the application hears about it. 
The
+                    // statement never reached the cache in this branch, so 
nothing
+                    // here has to be unwound for it.
+                    super.closeInvoked();
+                }
+            } finally {
+                closed = true;
+                delegate = null;
             }
-            closed = true;
-            delegate = null;
         }
 
         @Override
diff --git 
a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/interceptor/TestStatementCacheRecycleFailure.java
 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/interceptor/TestStatementCacheRecycleFailure.java
index 087b2f3aae..e203374e03 100644
--- 
a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/interceptor/TestStatementCacheRecycleFailure.java
+++ 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/interceptor/TestStatementCacheRecycleFailure.java
@@ -234,4 +234,64 @@ class TestStatementCacheRecycleFailure extends 
JdbcPoolBaseTest {
             pooled.close();
         }
     }
+
+    @Test
+    void anErrorDuringRecycleClosesTheStatementAndDoesNotEscape() throws 
Exception {
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setJdbcInterceptors(
+                ErrorRecycleStatementCache.class.getName() + 
"(prepared=true,max=5)");
+        ds.createPool();
+
+        try (Connection pooled = ds.getConnection()) {
+            ErrorRecycleStatementCache cache =
+                    interceptorOf(pooled, ErrorRecycleStatementCache.class);
+            cache.failNextRecycleReset();
+            PreparedStatement failing = pooled.prepareStatement("SELECT 1");
+            Statement physical = driverStatement(failing);
+
+            Assertions.assertDoesNotThrow(failing::close,
+                    "a non-fatal Error during the recycle reset must not 
escape the close");
+            Assertions.assertTrue(physical.isClosed(),
+                    "the statement must still be closed through the normal 
path");
+            Assertions.assertEquals(0, cache.getCacheSizePerConnection(),
+                    "the failed recycle must not have parked anything");
+        }
+    }
+
+    /** A cache whose next recycle reset fails with a non-fatal Error. */
+    public static class ErrorRecycleStatementCache extends StatementCache {
+
+        private boolean errorNextRecycleReset;
+
+        void failNextRecycleReset() {
+            errorNextRecycleReset = true;
+        }
+
+        @Override
+        protected Object createDecorator(Object proxy, Method method, Object[] 
args,
+                Object statement, Constructor<?> constructor, String sql)
+                throws InstantiationException, IllegalAccessException, 
InvocationTargetException {
+            if (errorNextRecycleReset && compare(PREPARE_STATEMENT, 
method.getName())) {
+                errorNextRecycleReset = false;
+                statement = errorClearParameters((PreparedStatement) 
statement);
+            }
+            return super.createDecorator(proxy, method, args, statement, 
constructor, sql);
+        }
+    }
+
+    private static PreparedStatement errorClearParameters(PreparedStatement 
delegate) {
+        return (PreparedStatement) Proxy.newProxyInstance(
+                TestStatementCacheRecycleFailure.class.getClassLoader(),
+                new Class<?>[] { PreparedStatement.class },
+                (proxy, method, args) -> {
+                    if ("clearParameters".equals(method.getName())) {
+                        throw new NoClassDefFoundError("simulated stopped 
context in clearParameters()");
+                    }
+                    try {
+                        return args == null ? method.invoke(delegate) : 
method.invoke(delegate, args);
+                    } catch (InvocationTargetException x) {
+                        throw x.getCause();
+                    }
+                });
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to