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 c57fd16d70e4984ab8624a9994bb95c9d0b39e7f 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 e42e83baea..ede6aa0dd6 100644 --- a/java/org/apache/catalina/valves/StuckThreadDetectionValve.java +++ b/java/org/apache/catalina/valves/StuckThreadDetectionValve.java @@ -303,8 +303,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; @@ -374,6 +375,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()) { @SuppressWarnings("deprecation") String msg = sm.getString("stuckThreadDetectionValve.notifyStuckThreadInterrupted", diff --git a/test/org/apache/catalina/valves/TestStuckThreadDetectionValve.java b/test/org/apache/catalina/valves/TestStuckThreadDetectionValve.java index 5b1139b9d1..492760234e 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 jakarta.servlet.http.HttpServlet; import jakarta.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]
