This is an automated email from the ASF dual-hosted git repository. rmaucher pushed a commit to branch 9.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit eab8bf0c975b50a7a90f3b2617d0b8bae912a9fe Author: opencode <[email protected]> AuthorDate: Thu Nov 5 14:30:52 2026 +0100 Restore the served session when a credential reconnect fails with an Error PooledConnection.reconnectWith cleared the fields for the new attempt before connect(), but caught only SQLException and RuntimeException. A non-fatal Error from a driver or wrapper therefore skipped the restore, leaving the served physical session open but unreachable and the pool holding a connection whose fields were null. Catch Throwable, handle fatal errors, close any partial new session (retaining a close failure as suppressed), and restore the served handles, timestamp and credentials in a finally block. Rethrow by type. Add a test with an Error during an alternate-credential reconnect: the served session keeps its slot and is reused by the next borrow. --- .../apache/tomcat/jdbc/pool/PooledConnection.java | 52 +++++++++++++++------- .../pool/TestThrowingDriverOnClaimedPaths.java | 38 ++++++++++++++++ 2 files changed, 74 insertions(+), 16 deletions(-) diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PooledConnection.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PooledConnection.java index 52430a9173..b599903c1a 100644 --- a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PooledConnection.java +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PooledConnection.java @@ -705,29 +705,49 @@ public class PooledConnection implements PooledConnectionMBean { lastConnected = -1; try { connect(); - } catch (SQLException | RuntimeException x) { + } catch (Throwable x) { // A connect can fail after it established the session - the - // default state it applies to a new session can be refused - so + // default state it applies to a new session can be refused, or a + // driver wrapper can fail with a non-fatal LinkageError - so // whatever is in the fields is closed here rather than dropped. // The interceptor chain is told nothing: this session never // served anyone, and the state the chain holds belongs to the // session that is about to be back in service. - closeSession(connection, xaConnection, cpdsConnection); - connection = served; - xaConnection = servedXa; - cpdsConnection = servedCpds; - lastConnected = servedLastConnected; - if (hadUser) { - sessionAttributes.put(PROP_USER, servedUser); - } else { - sessionAttributes.remove(PROP_USER); + ExceptionUtils.handleThrowable(x); + try { + closeSession(connection, xaConnection, cpdsConnection); + } catch (Throwable closeFailure) { + ExceptionUtils.handleThrowable(closeFailure); + x.addSuppressed(closeFailure); + } finally { + // Restore even if the partial session could not be closed: + // the served session is what the pool still owns, and a null + // field would leave it open but unreachable. + connection = served; + xaConnection = servedXa; + cpdsConnection = servedCpds; + lastConnected = servedLastConnected; + if (hadUser) { + sessionAttributes.put(PROP_USER, servedUser); + } else { + sessionAttributes.remove(PROP_USER); + } + if (hadPassword) { + sessionAttributes.put(PROP_PASSWORD, servedPassword); + } else { + sessionAttributes.remove(PROP_PASSWORD); + } } - if (hadPassword) { - sessionAttributes.put(PROP_PASSWORD, servedPassword); - } else { - sessionAttributes.remove(PROP_PASSWORD); + if (x instanceof SQLException) { + throw (SQLException) x; + } else if (x instanceof RuntimeException) { + throw (RuntimeException) x; + } else if (x instanceof Error) { + throw (Error) x; } - throw x; + SQLException ex = new SQLException(x.getMessage()); + ex.initCause(x); + throw ex; } if (served == null) { diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestThrowingDriverOnClaimedPaths.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestThrowingDriverOnClaimedPaths.java index 7e02f28523..28efc0eea2 100644 --- a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestThrowingDriverOnClaimedPaths.java +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestThrowingDriverOnClaimedPaths.java @@ -220,6 +220,44 @@ class TestThrowingDriverOnClaimedPaths extends JdbcPoolBaseTest { assertNothingStranded(ds); } + @Test + void anErrorWhileReconnectingWithOtherCredentialsKeepsTheServedSession() throws Exception { + resetDriver(); + DataSource ds = oneSlotPool(); + ds.getPoolProperties().setAlternateUsernameAllowed(true); + + // One idle session of the pool's own user for the alternate-credential + // borrow below to claim. + try (Connection first = ds.getConnection()) { + Assertions.assertNotNull(first); + } + awaitSessions(1); + + FAIL_NEXT_CONNECT.set(true); + try { + Assertions.assertThrows(SQLException.class, + () -> ds.getConnection("other-user", "other-password")); + } finally { + FAIL_NEXT_CONNECT.set(false); + } + + // The Error did not destroy the session: it was put back into the + // pool, so the slot is still accounted for and the next borrow uses + // the same physical session instead of leaking it. + Assertions.assertEquals(1, ds.getSize(), + "the served session must still hold its maxActive slot"); + Assertions.assertEquals(0, ds.getActive()); + Assertions.assertEquals(1, poolSessions(), + "the served session must still be open after the failed reconnect"); + try (Connection again = ds.getConnection()) { + Assertions.assertNotNull(again); + } + Assertions.assertEquals(0, ds.getActive()); + + ds.close(); + awaitSessions(0); + } + @Test void anErrorFromTheDriverOnInitialCreationFailsTheBorrowAndKeepsTheSlot() throws Exception { resetDriver(); --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
