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

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

commit d5ed0124c0fa2e2a64c0a770135b3350bbf023e5
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]

Reply via email to