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]
