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

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

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

Reply via email to