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 278e9029480f5b642e8a305db2a0f10ae977fc35
Author: opencode <[email protected]>
AuthorDate: Mon Oct 12 13:43:35 2026 +0200

    Test that StuckThreadDetectionValve leaves no interrupt flag behind
    
    When the work of a stuck thread is not interruptible, the thread
    completes its request with the interrupt flag still set. The semaphore
    handshake in MonitoredThread.markAsDone(), which consumes the flag via
    the documented interrupt behavior of Semaphore.acquire(), is what
    prevents that flag from surviving the valve and affecting the next
    request processed by the pooled worker thread.
    
    Add an integration test which, with a servlet that completes with the
    interrupt flag set, verifies using a valve positioned before the stuck
    thread detection valve that no interrupt flag is left on the worker
    thread once the detection valve is done with the request.
---
 .../valves/TestStuckThreadDetectionValve.java      | 91 ++++++++++++++++++++++
 1 file changed, 91 insertions(+)

diff --git a/test/org/apache/catalina/valves/TestStuckThreadDetectionValve.java 
b/test/org/apache/catalina/valves/TestStuckThreadDetectionValve.java
index 492760234e..ff104ca837 100644
--- a/test/org/apache/catalina/valves/TestStuckThreadDetectionValve.java
+++ b/test/org/apache/catalina/valves/TestStuckThreadDetectionValve.java
@@ -20,7 +20,9 @@ import java.io.File;
 import java.io.IOException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
 
+import jakarta.servlet.ServletException;
 import jakarta.servlet.http.HttpServlet;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
@@ -30,6 +32,8 @@ import org.junit.Before;
 import org.junit.Test;
 
 import org.apache.catalina.Wrapper;
+import org.apache.catalina.connector.Request;
+import org.apache.catalina.connector.Response;
 import org.apache.catalina.core.StandardContext;
 import org.apache.catalina.startup.Tomcat;
 import org.apache.catalina.startup.TomcatBaseTest;
@@ -174,6 +178,93 @@ public class TestStuckThreadDetectionValve extends 
TomcatBaseTest {
         Assert.assertTrue(result.toString().startsWith("OK"));
     }
 
+    @Test
+    public void testInterruptFlagNotLeftSetForNextRequest() throws Exception {
+        // A servlet that works until interrupted and then completes without
+        // consuming the interrupt flag, as work that is not interruptible
+        // would leave it
+        InterruptUntilDoneServlet stickingServlet = new 
InterruptUntilDoneServlet();
+        Wrapper servlet = Tomcat.addServlet(context, "myservlet", 
stickingServlet);
+        servlet.addMapping("/myservlet");
+
+        // A valve added before the detection valve sees the state of the
+        // worker thread once the detection valve is done with the request,
+        // which is the state the thread would carry into the next request
+        AtomicReference<Boolean> flagAfterRequest = new AtomicReference<>();
+        ValveBase observer = new ValveBase(true) {
+            @Override
+            public void invoke(Request request, Response response) throws 
IOException, ServletException {
+                try {
+                    getNext().invoke(request, response);
+                } finally {
+                    // Consume any flag so the pooled worker thread is left
+                    // clean for the next request
+                    
flagAfterRequest.set(Boolean.valueOf(Thread.interrupted()));
+                }
+            }
+        };
+
+        context.addValve(observer);
+
+        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(30000);
+        // check that we did not reach the join timeout
+        Assert.assertFalse(asyncThread.isAlive());
+
+        Assert.assertTrue(stickingServlet.wasInterrupted);
+        // The detection valve must not leave the interrupt flag it set on the
+        // worker thread set once it is done with the request
+        Assert.assertEquals(Boolean.FALSE, flagAfterRequest.get());
+        Assert.assertTrue(result.toString().startsWith("OK"));
+    }
+
+    private static class InterruptUntilDoneServlet extends HttpServlet {
+
+        private static final long serialVersionUID = 1L;
+        volatile boolean wasInterrupted = false;
+
+        @Override
+        protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
throws IOException {
+            // Wait for the interrupt without consuming it, then complete.
+            // Non-interruptible work that ignores the interrupt leaves the
+            // flag set when it finishes.
+            long end = System.currentTimeMillis() + 
TimeUnit.SECONDS.toMillis(25L);
+            while (!Thread.currentThread().isInterrupted() && 
System.currentTimeMillis() < end) {
+                try {
+                    Thread.sleep(50L);
+                } catch (InterruptedException e) {
+                    // The loop condition observes the flag below. Re-arm the
+                    // flag since this test checks that the valve clears the
+                    // interrupt it set.
+                    Thread.currentThread().interrupt();
+                }
+            }
+            wasInterrupted = Thread.currentThread().isInterrupted();
+            // Deliberately do not consume the interrupt flag here
+            resp.setContentType("text/plain");
+            resp.getWriter().println("OK");
+        }
+    }
+
     private static class UninterruptibleStickingServlet 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