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 02b2f79d55f9a51d69ccefb0107d6d7a23a03442 Author: opencode <[email protected]> AuthorDate: Thu Nov 5 14:57:24 2026 +0100 Stop resetTimer stamps as soon as an abort starts Connection.abort() ends a borrow at ProxyConnection before the driver call, but ResetAbandonedTimer kept checkedOut true until the later release walk cleared the binding. In that window the JMX resetTimer() operation could stamp a connection whose borrow had already ended, pushing back the next abandonment or idle decision. Recognize ABORT_VAL in ResetAbandonedTimer.invoke() and lower checkedOut before forwarding. Add a test that parks inside the driver's abort and asserts resetTimer() already refuses to stamp. --- .../jdbc/pool/interceptor/ResetAbandonedTimer.java | 9 +++++ .../tomcat/jdbc/pool/TestAbortEndsTheBorrow.java | 46 ++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/ResetAbandonedTimer.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/ResetAbandonedTimer.java index a3f6662296..8beaf71f38 100644 --- a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/ResetAbandonedTimer.java +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/ResetAbandonedTimer.java @@ -231,6 +231,15 @@ public class ResetAbandonedTimer extends AbstractQueryReport implements ResetAba @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { final long epoch = borrowEpoch; + if (compare(ABORT_VAL, method)) { + // abort ends the borrow before the driver call and before the + // pool's release walk clears this binding. Take the flag down + // first, so the JMX resetTimer() operation cannot stamp a + // connection whose borrow has already ended. + synchronized (this) { + this.checkedOut = false; + } + } try { return super.invoke(proxy, method, args); } finally { diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestAbortEndsTheBorrow.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestAbortEndsTheBorrow.java index 64baa83107..54f943d746 100644 --- a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestAbortEndsTheBorrow.java +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestAbortEndsTheBorrow.java @@ -39,6 +39,7 @@ import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Test; import org.apache.tomcat.jdbc.JdbcPoolBaseTest; +import org.apache.tomcat.jdbc.pool.interceptor.ResetAbandonedTimer; /** * {@code Connection.abort(Executor)} ends a borrow. The specification has a @@ -187,6 +188,51 @@ class TestAbortEndsTheBorrow extends JdbcPoolBaseTest { "the late close must not reach the driver a second time"); } + @Test + void resetTimerStopsStampingAsSoonAsTheAbortStarts() throws Exception { + CountDownLatch abortInDriver = new CountDownLatch(1); + CountDownLatch releaseDriver = new CountDownLatch(1); + SlowAbortDriver.abortInDriver = abortInDriver; + SlowAbortDriver.releaseDriver = releaseDriver; + DataSource ds = newDataSource(); + ds.setDriverClassName(SlowAbortDriver.class.getName()); + ds.getPoolProperties().setInitialSize(0); + ds.getPoolProperties().setMaxActive(1); + ds.getPoolProperties().setJdbcInterceptors(ResetAbandonedTimer.class.getName()); + ds.createPool(); + + Connection held = ds.getConnection(); + ResetAbandonedTimer timer = interceptorOf(held, ResetAbandonedTimer.class); + Assertions.assertNotNull(timer, "precondition: the timer must sit in the chain"); + Assertions.assertTrue(timer.resetTimer(), + "precondition: the connection is checked out before the abort"); + + AtomicReference<Throwable> abortFailure = new AtomicReference<>(); + Thread aborter = new Thread(() -> { + try { + held.abort(DIRECT_EXECUTOR); + } catch (Throwable t) { + abortFailure.set(t); + } + }, "abort-with-reset-timer"); + aborter.start(); + try { + Assertions.assertTrue(abortInDriver.await(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS), + "precondition: the driver must be inside the abort"); + // The borrow has ended at the pool layer, but the release walk + // that clears the timer's binding has not run yet. The management + // operation must already treat the connection as not checked out. + Assertions.assertFalse(timer.resetTimer(), + "resetTimer() must not stamp a connection whose abort has started"); + } finally { + releaseDriver.countDown(); + aborter.join(AWAIT_TIMEOUT_MS); + } + Assertions.assertFalse(aborter.isAlive(), "the abort must finish once the gate opens"); + Assertions.assertNull(abortFailure.get(), () -> "the abort must not fail: " + abortFailure.get()); + awaitSessions(0); + } + @Test void abortingTheSameHandleTwiceAbortsOnce() throws Exception { SpecAbortDriver.ABORTS.set(0); --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
