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 7cfeca8dffc4a15be403edd204e1bcd21e45b332
Author: opencode <[email protected]>
AuthorDate: Mon Oct 12 13:26:42 2026 +0200

    Interrupt a stuck thread only once per request
    
    A thread that remained stuck despite having been interrupted, because
    its work was not interruptible or the application ignored interrupts,
    was interrupted again and WARN logged with a full stack trace on every
    background process cycle for the remaining lifetime of the request.
    
    Record that the thread of a monitored request has been interrupted and
    skip subsequent interruption attempts. The thread is now interrupted,
    and logged, once per request. Add a test using a servlet that performs
    non-interruptible work and counts every interrupt delivered to it.
---
 .../catalina/valves/StuckThreadDetectionValve.java | 12 +++-
 .../valves/TestStuckThreadDetectionValve.java      | 73 ++++++++++++++++++++++
 2 files changed, 83 insertions(+), 2 deletions(-)

diff --git a/java/org/apache/catalina/valves/StuckThreadDetectionValve.java 
b/java/org/apache/catalina/valves/StuckThreadDetectionValve.java
index df1c1ace50..51a58c8931 100644
--- a/java/org/apache/catalina/valves/StuckThreadDetectionValve.java
+++ b/java/org/apache/catalina/valves/StuckThreadDetectionValve.java
@@ -300,8 +300,9 @@ public class StuckThreadDetectionValve extends ValveBase {
          */
         private final Semaphore interruptionSemaphore;
         /**
-         * Set to true after the thread is interrupted. No need to make it 
volatile since it is accessed right after
-         * acquiring the semaphore.
+         * Set to true after the thread is interrupted. Also checked to avoid 
interrupting, and logging, a thread that
+         * remains stuck on every background process cycle. No need to make it 
volatile since it is read and written
+         * while holding the interruption semaphore, and read right after 
acquiring it.
          */
         private boolean interrupted;
 
@@ -371,6 +372,13 @@ public class StuckThreadDetectionValve extends ValveBase {
                 return false;
             }
             try {
+                if (this.interrupted) {
+                    // The thread has already been interrupted for this 
request.
+                    // A thread that is still stuck despite the interrupt would
+                    // otherwise be interrupted, and logged, again on every
+                    // background process cycle.
+                    return false;
+                }
                 if (log.isWarnEnabled()) {
                     String msg = 
sm.getString("stuckThreadDetectionValve.notifyStuckThreadInterrupted",
                             this.getThread().getName(), 
Long.valueOf(getActiveTimeInMillis()), this.getStartTime(),
diff --git a/test/org/apache/catalina/valves/TestStuckThreadDetectionValve.java 
b/test/org/apache/catalina/valves/TestStuckThreadDetectionValve.java
index 039870e9a2..95b493ea54 100644
--- a/test/org/apache/catalina/valves/TestStuckThreadDetectionValve.java
+++ b/test/org/apache/catalina/valves/TestStuckThreadDetectionValve.java
@@ -19,6 +19,7 @@ package org.apache.catalina.valves;
 import java.io.File;
 import java.io.IOException;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
@@ -133,6 +134,78 @@ public class TestStuckThreadDetectionValve extends 
TomcatBaseTest {
         Assert.assertTrue(result.toString().startsWith("OK"));
     }
 
+    @Test
+    public void testInterruptStuckThreadOnlyOnce() throws Exception {
+        // A servlet whose work is not interruptible: it stays busy even after
+        // being interrupted and records every interrupt that is delivered
+        UninterruptibleStickingServlet stickingServlet =
+                new 
UninterruptibleStickingServlet(TimeUnit.SECONDS.toMillis(8L));
+        Wrapper servlet = Tomcat.addServlet(context, "myservlet", 
stickingServlet);
+        servlet.addMapping("/myservlet");
+
+        StuckThreadDetectionValve valve = new StuckThreadDetectionValve();
+        valve.setThreshold(2);
+        valve.setInterruptThreadThreshold(3);
+        context.addValve(valve);
+        context.setBackgroundProcessorDelay(1);
+        tomcat.start();
+
+        final ByteChunk result = new ByteChunk();
+        Thread asyncThread = new Thread() {
+            @Override
+            public void run() {
+                try {
+                    getUrl("http://localhost:"; + getPort() + "/myservlet", 
result, null);
+                } catch (IOException ioe) {
+                    ioe.printStackTrace();
+                }
+            }
+
+        };
+        asyncThread.start();
+        asyncThread.join(20000);
+        // check that we did not reach the join timeout
+        Assert.assertFalse(asyncThread.isAlive());
+
+        // The thread should have been interrupted, but only once. A thread
+        // that remains stuck despite the interrupt must not be interrupted,
+        // and logged, again on every background process cycle.
+        Assert.assertEquals(1, stickingServlet.getInterruptCount());
+        Assert.assertTrue(result.toString().startsWith("OK"));
+    }
+
+    private static class UninterruptibleStickingServlet extends HttpServlet {
+
+        private static final long serialVersionUID = 1L;
+        private final long duration;
+        private final AtomicInteger interruptCount = new AtomicInteger();
+
+        UninterruptibleStickingServlet(long duration) {
+            this.duration = duration;
+        }
+
+        @Override
+        protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
throws IOException {
+            // Busy wait, like work that ignores the interrupt. Each delivered
+            // interrupt is observed once and cleared, so a new interrupt of
+            // the same request is counted separately.
+            long end = System.currentTimeMillis() + duration;
+            while (System.currentTimeMillis() < end) {
+                if (Thread.interrupted()) {
+                    interruptCount.incrementAndGet();
+                }
+            }
+            // Do not hand an interrupt flag back to the container
+            Thread.interrupted();
+            resp.setContentType("text/plain");
+            resp.getWriter().println("OK");
+        }
+
+        public int getInterruptCount() {
+            return interruptCount.get();
+        }
+    }
+
     private static class StickingServlet extends HttpServlet {
 
         private static final long serialVersionUID = 1L;


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

Reply via email to