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

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


The following commit(s) were added to refs/heads/main by this push:
     new 56ef71a67e Review Fix BZ 69236 - reset JDBC interceptors in 
declaration order
56ef71a67e is described below

commit 56ef71a67e7833fa8d9043ba46f44f9c7604d9f9
Author: Mark Thomas <[email protected]>
AuthorDate: Fri Oct 23 20:53:32 2026 +0100

    Review Fix BZ 69236 - reset JDBC interceptors in declaration order
    
    Add a changelog entry
    Fix a few leaks
    Improve error reporting
    Refactor the test
---
 .../apache/tomcat/jdbc/pool/ConnectionPool.java    |  28 +++-
 .../interceptor/TestInterceptorResetOrder.java     | 175 +++++++++++++++++++++
 .../jdbc/test/InterceptorResetOrderTest.java       | 106 -------------
 webapps/docs/changelog.xml                         |  10 ++
 4 files changed, 211 insertions(+), 108 deletions(-)

diff --git 
a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ConnectionPool.java
 
b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ConnectionPool.java
index a0460cbf3c..b021fb7d40 100644
--- 
a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ConnectionPool.java
+++ 
b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ConnectionPool.java
@@ -314,6 +314,8 @@ public class ConnectionPool {
                 }catch(Exception x) {
                     SQLException sx = new SQLException("Unable to instantiate 
interceptor chain.");
                     sx.initCause(x);
+                    //nothing has been done to the connection, it can go back 
to the pool
+                    abandonSetup(con, sx, false);
                     throw sx;
                 }
             }
@@ -321,15 +323,18 @@ public class ConnectionPool {
             con.setHandler(handler);
         }
         //reset the chain in the order the interceptors are declared, for new 
and cached handlers alike
+        JdbcInterceptor next = handler;
         try {
-            JdbcInterceptor next = handler;
             while (next!=null) {
                 next.reset(this, con);
                 next = next.getNext();
             }
         }catch(Exception x) {
-            SQLException sx = new SQLException("Unable to reset interceptor 
chain.");
+            SQLException sx = new SQLException("Unable to reset interceptor [" 
+
+                    (next!=null?next.getClass().getName():"unknown") + "].");
             sx.initCause(x);
+            //the state of the connection is unknown, discard it so its 
partially reset chain is dropped
+            abandonSetup(con, sx, true);
             throw sx;
         }
         // setup statement proxy
@@ -351,11 +356,30 @@ public class ConnectionPool {
         }catch (Exception x) {
             SQLException s = new SQLException();
             s.initCause(x);
+            abandonSetup(con, s, false);
             throw s;
         }
 
     }
 
+    /**
+     * Gives up a connection that was borrowed but could not be set up, so 
that it does not stay borrowed forever.
+     * Any failure to do so is added to the exception that is about to be 
thrown rather than hiding it.
+     * @param con the borrowed connection
+     * @param cause the exception that is about to be thrown to the caller
+     * @param discard {@code true} if the connection must be closed, {@code 
false} if it can go back to the pool
+     */
+    private void abandonSetup(PooledConnection con, SQLException cause, 
boolean discard) {
+        try {
+            if (discard) {
+                con.setDiscarded(true);
+            }
+            returnConnection(con);
+        }catch(Exception y) {
+            cause.addSuppressed(y);
+        }
+    }
+
     /**
      * Creates and caches a {@link java.lang.reflect.Constructor} used to 
instantiate the proxy object.
      * We cache this, since the creation of a constructor is fairly slow.
diff --git 
a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/interceptor/TestInterceptorResetOrder.java
 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/interceptor/TestInterceptorResetOrder.java
new file mode 100644
index 0000000000..41c439fad6
--- /dev/null
+++ 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/interceptor/TestInterceptorResetOrder.java
@@ -0,0 +1,175 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.tomcat.jdbc.pool.interceptor;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.apache.tomcat.jdbc.JdbcPoolBaseTest;
+import org.apache.tomcat.jdbc.pool.ConnectionPool;
+import org.apache.tomcat.jdbc.pool.DataSource;
+import org.apache.tomcat.jdbc.pool.JdbcInterceptor;
+import org.apache.tomcat.jdbc.pool.PooledConnection;
+
+/**
+ * The interceptors of a connection must be reset in the order in which they 
are declared, whether the connection is
+ * new or is taken from the pool, and a failure to reset them must not leave 
the connection borrowed.
+ */
+class TestInterceptorResetOrder extends JdbcPoolBaseTest {
+
+    private static final List<String> RESETS = 
Collections.synchronizedList(new ArrayList<>());
+
+    @BeforeEach
+    void clearResets() {
+        RESETS.clear();
+        Unconstructable.fail = false;
+    }
+
+    private DataSource pool(Class<?>... interceptors) {
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setMaxActive(1);
+        StringBuilder chain = new StringBuilder();
+        for (Class<?> interceptor : interceptors) {
+            if (chain.length() > 0) {
+                chain.append(';');
+            }
+            chain.append(interceptor.getName());
+        }
+        ds.getPoolProperties().setJdbcInterceptors(chain.toString());
+        return ds;
+    }
+
+    @Test
+    void resetOrderOnNewConnection() throws Exception {
+        DataSource ds = pool(First.class, Second.class);
+
+        ds.getConnection().close();
+
+        assertEquals(Arrays.asList("first", "second"), RESETS);
+    }
+
+    @Test
+    void resetOrderOnReusedConnection() throws Exception {
+        DataSource ds = pool(First.class, Second.class);
+        try (Connection first = ds.getConnection()) {
+            setSessionMarker(first, 1);
+        }
+        RESETS.clear();
+
+        try (Connection reused = ds.getConnection()) {
+            assertEquals(Integer.valueOf(1), sessionMarker(reused),
+                    "the cached interceptor chain must be exercised on the 
same physical session");
+            assertEquals(Arrays.asList("first", "second"), RESETS);
+        }
+    }
+
+    @Test
+    void resetFailureIsReportedAndTheConnectionDiscarded() throws Exception {
+        DataSource ds = pool(First.class, Failing.class, Second.class);
+
+        SQLException e = assertThrows(SQLException.class, () -> 
ds.getConnection());
+
+        assertInstanceOf(IllegalStateException.class, e.getCause());
+        assertTrue(e.getMessage().contains(Failing.class.getName()), 
e.getMessage());
+        // The interceptors after the one that failed are not reset
+        assertEquals(Arrays.asList("first"), RESETS);
+        // The connection that could not be set up must not stay borrowed ...
+        assertEquals(0, ds.getPool().getActive());
+        // ... and must have been closed rather than returned to the pool
+        assertEquals(0, ds.getPool().getIdle());
+        awaitSessions(0);
+    }
+
+    @Test
+    void instantiationFailureIsReportedAndTheConnectionReturned() throws 
Exception {
+        DataSource ds = pool(Unconstructable.class);
+        // With a single connection a leaked borrow would make the second 
attempt wait for the connection
+        ds.getPoolProperties().setMaxWait(500);
+        // The pool instantiates the interceptors when it starts so only fail 
after that
+        ds.createPool();
+        Unconstructable.fail = true;
+
+        for (int i = 0; i < 2; i++) {
+            SQLException e = assertThrows(SQLException.class, () -> 
ds.getConnection());
+
+            assertTrue(e.getMessage().contains("instantiate"), e.getMessage());
+            // Nothing was done to the connection so it goes back to the pool 
rather than being closed
+            assertEquals(0, ds.getPool().getActive());
+            assertEquals(1, ds.getPool().getIdle());
+        }
+        awaitSessions(1);
+    }
+
+    public static class First extends JdbcInterceptor {
+
+        @Override
+        public void reset(ConnectionPool parent, PooledConnection con) {
+            // the pool also resets interceptors without a parent when a 
connection is discarded
+            if (parent != null) {
+                RESETS.add("first");
+            }
+        }
+    }
+
+    public static class Second extends JdbcInterceptor {
+
+        @Override
+        public void reset(ConnectionPool parent, PooledConnection con) {
+            if (parent != null) {
+                RESETS.add("second");
+            }
+        }
+    }
+
+    public static class Unconstructable extends JdbcInterceptor {
+
+        static volatile boolean fail;
+
+        public Unconstructable() {
+            if (fail) {
+                throw new IllegalStateException();
+            }
+        }
+
+        @Override
+        public void reset(ConnectionPool parent, PooledConnection con) {
+            // NO-OP
+        }
+    }
+
+    public static class Failing extends JdbcInterceptor {
+
+        @Override
+        public void reset(ConnectionPool parent, PooledConnection con) {
+            if (parent != null) {
+                throw new IllegalStateException();
+            }
+        }
+    }
+}
diff --git 
a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/InterceptorResetOrderTest.java
 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/InterceptorResetOrderTest.java
deleted file mode 100644
index a4824702cf..0000000000
--- 
a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/InterceptorResetOrderTest.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- *  Licensed to the Apache Software Foundation (ASF) under one or more
- *  contributor license agreements.  See the NOTICE file distributed with
- *  this work for additional information regarding copyright ownership.
- *  The ASF licenses this file to You under the Apache License, Version 2.0
- *  (the "License"); you may not use this file except in compliance with
- *  the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License.
- */
-package org.apache.tomcat.jdbc.test;
-
-import java.sql.SQLException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
-
-import org.apache.tomcat.jdbc.pool.ConnectionPool;
-import org.apache.tomcat.jdbc.pool.JdbcInterceptor;
-import org.apache.tomcat.jdbc.pool.PooledConnection;
-import org.apache.tomcat.jdbc.test.driver.Driver;
-
-/**
- * The reset order of the interceptors must follow the order in which they are 
declared, whether the connection is new
- * or is taken from the pool.
- */
-public class InterceptorResetOrderTest extends DefaultTestCase {
-
-    private static final List<String> RESETS = 
Collections.synchronizedList(new ArrayList<>());
-
-    @Before
-    public void setUp() {
-        RESETS.clear();
-        datasource.setDriverClassName(Driver.class.getName());
-        datasource.setUrl("jdbc:tomcat:test");
-        datasource.setMaxActive(1);
-        datasource.setInitialSize(0);
-        datasource.setMinIdle(0);
-        datasource.setJdbcInterceptors(First.class.getName() + ";" + 
Second.class.getName());
-    }
-
-    @Test
-    public void testResetOrderOnNewConnection() throws SQLException {
-        datasource.getConnection().close();
-        Assert.assertEquals(Arrays.asList("first", "second"), RESETS);
-    }
-
-    @Test
-    public void testResetOrderOnReusedConnection() throws SQLException {
-        datasource.getConnection().close();
-        RESETS.clear();
-        datasource.getConnection().close();
-        Assert.assertEquals(Arrays.asList("first", "second"), RESETS);
-    }
-
-    @Test
-    public void testResetFailureIsReportedAsSQLException() {
-        datasource.setJdbcInterceptors(Failing.class.getName());
-
-        SQLException e = Assert.assertThrows(SQLException.class, () -> 
datasource.getConnection());
-
-        Assert.assertTrue(e.getCause() instanceof IllegalStateException);
-    }
-
-    public static class Failing extends JdbcInterceptor {
-
-        @Override
-        public void reset(ConnectionPool parent, PooledConnection con) {
-            if (parent != null) {
-                throw new IllegalStateException();
-            }
-        }
-    }
-
-    public static class First extends JdbcInterceptor {
-
-        @Override
-        public void reset(ConnectionPool parent, PooledConnection con) {
-            // the pool also resets interceptors without a parent when a 
connection is discarded
-            if (parent != null) {
-                RESETS.add("first");
-            }
-        }
-    }
-
-    public static class Second extends JdbcInterceptor {
-
-        @Override
-        public void reset(ConnectionPool parent, PooledConnection con) {
-            if (parent != null) {
-                RESETS.add("second");
-            }
-        }
-    }
-}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 73b924209b..0b8bb8f803 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -986,6 +986,16 @@
         configuration. Pull request <pr>1084</pr> provided by Raphael 
Vullriede.
         (markt)
       </fix>
+      <fix>
+        <bug>69236</bug>: Reset the interceptors of a new connection in the
+        order they are declared, as is already done for a connection taken from
+        the pool, rather than in reverse order. A failure to reset an
+        interceptor is now reported as an <code>SQLException</code> that names
+        the interceptor and the connection is discarded rather than left
+        borrowed. A connection is also no longer left borrowed if the
+        interceptors cannot be instantiated or the connection proxy cannot be
+        created. Pull request provided by Arturo Bernal. (markt)
+      </fix>
       <fix>
         <bug>70269</bug>: Correct a regression in the change that reset the
         properties of statements cached by the <code>StatementCache</code>


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

Reply via email to