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

markt-asf pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 1aacdb5bd8149da9767f64eb07efaa56517f38bf
Author: Mark Thomas <[email protected]>
AuthorDate: Wed Jun 3 19:27:33 2026 +0100

    Additional tests written by CoPilot / GPT-5.4
---
 .../catalina/valves/TestPersistentValveAsync.java  | 301 ++++++++++++++++++---
 1 file changed, 257 insertions(+), 44 deletions(-)

diff --git a/test/org/apache/catalina/valves/TestPersistentValveAsync.java 
b/test/org/apache/catalina/valves/TestPersistentValveAsync.java
index 9910982f89..b852917857 100644
--- a/test/org/apache/catalina/valves/TestPersistentValveAsync.java
+++ b/test/org/apache/catalina/valves/TestPersistentValveAsync.java
@@ -28,6 +28,7 @@ import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 
 import jakarta.servlet.AsyncContext;
+import jakarta.servlet.ServletException;
 import jakarta.servlet.http.HttpServlet;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
@@ -48,6 +49,9 @@ import org.apache.tomcat.util.buf.ByteChunk;
 
 public class TestPersistentValveAsync extends TomcatBaseTest {
 
+    private static final String TEST_SESSION_ID = "TEST-SESSION-ID";
+    private static final long TEST_TIMEOUT_MS = 1000;
+
     @Test
     public void testAsyncRequestStoresSessionOnComplete() throws Exception {
         Tomcat tomcat = getTomcatInstance();
@@ -98,66 +102,61 @@ public class TestPersistentValveAsync extends 
TomcatBaseTest {
 
     @Test
     public void testSemaphoreHeldWhileAsyncRequestInProgress() throws 
Exception {
+        CountDownLatch asyncStarted = new CountDownLatch(1);
+        CountDownLatch allowAsyncComplete = new CountDownLatch(1);
+
         Tomcat tomcat = getTomcatInstance();
-        StandardContext context = (StandardContext) 
getProgrammaticRootContext();
-        context.setDistributable(true);
+        addSemaphoreTestServlets("async-block",
+                new BlockingAsyncServlet(asyncStarted, allowAsyncComplete), 
"/async-block");
+        tomcat.start();
+
+        assertSemaphoreHeldUntilAsyncRequestCompletes("/async-block", 
asyncStarted, allowAsyncComplete,
+                HttpServletResponse.SC_OK, TEST_SESSION_ID);
+    }
 
-        Tomcat.addServlet(context, "session", new SessionServlet());
-        context.addServletMappingDecoded("/session", "session");
 
+    @Test
+    public void testSemaphoreHeldAcrossAsyncDispatchDispatchComplete() throws 
Exception {
         CountDownLatch asyncStarted = new CountDownLatch(1);
         CountDownLatch allowAsyncComplete = new CountDownLatch(1);
-        Wrapper asyncWrapper =
-                Tomcat.addServlet(context, "async-block", new 
BlockingAsyncServlet(asyncStarted, allowAsyncComplete));
-        asyncWrapper.setAsyncSupported(true);
-        context.addServletMappingDecoded("/async-block", "async-block");
-
-        TesterStore store = new TesterStore();
-        PersistentValve persistentValve = new PersistentValve();
-        persistentValve.setSemaphoreBlockOnAcquire(false);
-        configurePersistentManager(context, store, persistentValve);
 
+        Tomcat tomcat = getTomcatInstance();
+        addSemaphoreTestServlets("async-dispatch-dispatch-complete",
+                new DispatchingAsyncServlet(2, asyncStarted, 
allowAsyncComplete, TerminalAction.COMPLETE), "/async-ddc");
         tomcat.start();
 
-        String sessionId = "TEST-SESSION-ID";
-
-        Map<String,List<String>> requestHeaders = cookieHeaders(sessionId);
+        assertSemaphoreHeldUntilAsyncRequestCompletes("/async-ddc", 
asyncStarted, allowAsyncComplete,
+                HttpServletResponse.SC_OK, TEST_SESSION_ID);
+    }
 
-        ByteChunk asyncResponseBody = new ByteChunk();
-        AtomicInteger asyncResponseCode = new AtomicInteger(-1);
-        AtomicReference<Throwable> asyncFailure = new AtomicReference<>();
-        Thread asyncClientThread = new Thread(() -> {
-            try {
-                asyncResponseCode.set(getUrl("http://localhost:"; + getPort() + 
"/async-block", asyncResponseBody,
-                        requestHeaders, null));
-            } catch (Throwable t) {
-                asyncFailure.set(t);
-            }
-        });
 
-        asyncClientThread.start();
+    @Test
+    public void testSemaphoreHeldUntilAsyncError() throws Exception {
+        CountDownLatch asyncStarted = new CountDownLatch(1);
+        CountDownLatch allowAsyncError = new CountDownLatch(1);
 
-        Assert.assertTrue(asyncStarted.await(10, TimeUnit.SECONDS));
+        Tomcat tomcat = getTomcatInstance();
+        StandardContext context = addSemaphoreTestServlets("async-error",
+                new ErrorDispatchingAsyncServlet(asyncStarted, 
allowAsyncError), "/async-error");
+        Tomcat.addServlet(context, "async-error-target", new ErrorServlet());
+        context.addServletMappingDecoded("/async-error-target", 
"async-error-target");
+        tomcat.start();
 
-        ByteChunk rejectedOne = new ByteChunk();
-        int rejectedOneStatus = getUrl("http://localhost:"; + getPort() + 
"/session", rejectedOne, requestHeaders, null);
-        Assert.assertEquals(HttpServletResponse.SC_TOO_MANY_REQUESTS, 
rejectedOneStatus);
+        assertSemaphoreHeldUntilAsyncRequestCompletes("/async-error", 
asyncStarted, allowAsyncError,
+                HttpServletResponse.SC_INTERNAL_SERVER_ERROR, null);
+    }
 
-        ByteChunk rejectedTwo = new ByteChunk();
-        int rejectedTwoStatus = getUrl("http://localhost:"; + getPort() + 
"/session", rejectedTwo, requestHeaders, null);
-        Assert.assertEquals(HttpServletResponse.SC_TOO_MANY_REQUESTS, 
rejectedTwoStatus);
 
-        allowAsyncComplete.countDown();
-        asyncClientThread.join(10000);
+    @Test
+    public void testSemaphoreHeldUntilAsyncTimeout() throws Exception {
+        CountDownLatch asyncStarted = new CountDownLatch(1);
 
-        Assert.assertNull(asyncFailure.get());
-        Assert.assertEquals(HttpServletResponse.SC_OK, 
asyncResponseCode.get());
-        Assert.assertEquals(sessionId, asyncResponseBody.toString());
+        Tomcat tomcat = getTomcatInstance();
+        addSemaphoreTestServlets("async-timeout", new 
TimeoutAsyncServlet(asyncStarted), "/async-timeout");
+        tomcat.start();
 
-        ByteChunk success = new ByteChunk();
-        int successStatus = getUrl("http://localhost:"; + getPort() + 
"/session", success, requestHeaders, null);
-        Assert.assertEquals(HttpServletResponse.SC_OK, successStatus);
-        Assert.assertFalse(success.isNull());
+        assertSemaphoreHeldUntilAsyncRequestCompletes("/async-timeout", 
asyncStarted, null,
+                HttpServletResponse.SC_INTERNAL_SERVER_ERROR, null);
     }
 
 
@@ -173,6 +172,27 @@ public class TestPersistentValveAsync extends 
TomcatBaseTest {
     }
 
 
+    private StandardContext addSemaphoreTestServlets(String asyncServletName, 
HttpServlet asyncServlet,
+            String asyncPath) {
+        StandardContext context = (StandardContext) 
getProgrammaticRootContext();
+        context.setDistributable(true);
+
+        Tomcat.addServlet(context, "session", new SessionServlet());
+        context.addServletMappingDecoded("/session", "session");
+
+        Wrapper asyncWrapper = Tomcat.addServlet(context, asyncServletName, 
asyncServlet);
+        asyncWrapper.setAsyncSupported(true);
+        context.addServletMappingDecoded(asyncPath, asyncServletName);
+
+        TesterStore store = new TesterStore();
+        PersistentValve persistentValve = new PersistentValve();
+        persistentValve.setSemaphoreBlockOnAcquire(false);
+        configurePersistentManager(context, store, persistentValve);
+
+        return context;
+    }
+
+
     private Map<String,List<String>> cookieHeaders(String sessionId) {
         Map<String,List<String>> result = new HashMap<>();
         result.put("Cookie", List.of("JSESSIONID=" + sessionId));
@@ -180,6 +200,43 @@ public class TestPersistentValveAsync extends 
TomcatBaseTest {
     }
 
 
+    private void assertSemaphoreHeldUntilAsyncRequestCompletes(String path, 
CountDownLatch asyncStarted,
+            CountDownLatch allowTerminalAction, int expectedStatus, String 
expectedResponseBody) throws Exception {
+        Map<String,List<String>> requestHeaders = 
cookieHeaders(TEST_SESSION_ID);
+
+        AsyncRequest asyncRequest = new AsyncRequest(path, requestHeaders);
+        asyncRequest.start();
+
+        Assert.assertTrue(asyncStarted.await(10, TimeUnit.SECONDS));
+
+        assertSessionRequestRejected(requestHeaders);
+        assertSessionRequestRejected(requestHeaders);
+
+        if (allowTerminalAction != null) {
+            allowTerminalAction.countDown();
+        }
+
+        asyncRequest.await();
+        asyncRequest.assertResponse(expectedStatus, expectedResponseBody);
+        assertSessionRequestSucceeds(requestHeaders);
+    }
+
+
+    private void assertSessionRequestRejected(Map<String,List<String>> 
requestHeaders) throws Exception {
+        ByteChunk rejected = new ByteChunk();
+        int status = getUrl("http://localhost:"; + getPort() + "/session", 
rejected, requestHeaders, null);
+        Assert.assertEquals(429, status);
+    }
+
+
+    private void assertSessionRequestSucceeds(Map<String,List<String>> 
requestHeaders) throws Exception {
+        ByteChunk success = new ByteChunk();
+        int status = getUrl("http://localhost:"; + getPort() + "/session", 
success, requestHeaders, null);
+        Assert.assertEquals(HttpServletResponse.SC_OK, status);
+        Assert.assertFalse(success.isNull());
+    }
+
+
     private static class SessionServlet extends HttpServlet {
 
         private static final long serialVersionUID = 1L;
@@ -275,6 +332,162 @@ public class TestPersistentValveAsync extends 
TomcatBaseTest {
     }
 
 
+    private static class DispatchingAsyncServlet extends HttpServlet {
+
+        private static final long serialVersionUID = 1L;
+
+        private final int dispatchCount;
+        private final CountDownLatch asyncStarted;
+        private final CountDownLatch allowTerminalAction;
+        private final TerminalAction terminalAction;
+
+        private DispatchingAsyncServlet(int dispatchCount, CountDownLatch 
asyncStarted, CountDownLatch allowTerminalAction,
+                TerminalAction terminalAction) {
+            this.dispatchCount = dispatchCount;
+            this.asyncStarted = asyncStarted;
+            this.allowTerminalAction = allowTerminalAction;
+            this.terminalAction = terminalAction;
+        }
+
+        @Override
+        protected void doGet(HttpServletRequest request, HttpServletResponse 
response) {
+            Integer dispatches = (Integer) request.getAttribute("dispatches");
+            int dispatchesSoFar = dispatches == null ? 0 : 
dispatches.intValue();
+
+            if (dispatchesSoFar < dispatchCount) {
+                request.setAttribute("dispatches", 
Integer.valueOf(dispatchesSoFar + 1));
+                AsyncContext asyncContext = request.startAsync();
+                asyncContext.start(asyncContext::dispatch);
+                return;
+            }
+
+            if (terminalAction == TerminalAction.COMPLETE) {
+                String sessionId = request.getRequestedSessionId();
+                AsyncContext asyncContext = request.startAsync();
+                asyncContext.start(() -> {
+                    asyncStarted.countDown();
+                    try {
+                        Assert.assertTrue(allowTerminalAction.await(10, 
TimeUnit.SECONDS));
+                        response.getWriter().print(sessionId);
+                    } catch (InterruptedException e) {
+                        Thread.currentThread().interrupt();
+                        throw new IllegalStateException(e);
+                    } catch (IOException e) {
+                        throw new IllegalStateException(e);
+                    } finally {
+                        asyncContext.complete();
+                    }
+                });
+            }
+        }
+    }
+
+
+    private static class ErrorDispatchingAsyncServlet extends HttpServlet {
+
+        private static final long serialVersionUID = 1L;
+
+        private final CountDownLatch asyncStarted;
+        private final CountDownLatch allowAsyncError;
+
+        private ErrorDispatchingAsyncServlet(CountDownLatch asyncStarted, 
CountDownLatch allowAsyncError) {
+            this.asyncStarted = asyncStarted;
+            this.allowAsyncError = allowAsyncError;
+        }
+
+        @Override
+        protected void doGet(HttpServletRequest request, HttpServletResponse 
response) {
+            AsyncContext asyncContext = request.startAsync();
+            asyncContext.start(() -> {
+                asyncStarted.countDown();
+                try {
+                    Assert.assertTrue(allowAsyncError.await(10, 
TimeUnit.SECONDS));
+                    asyncContext.dispatch("/async-error-target");
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                    throw new IllegalStateException(e);
+                }
+            });
+        }
+    }
+
+
+    private static class TimeoutAsyncServlet extends HttpServlet {
+
+        private static final long serialVersionUID = 1L;
+
+        private final CountDownLatch asyncStarted;
+
+        private TimeoutAsyncServlet(CountDownLatch asyncStarted) {
+            this.asyncStarted = asyncStarted;
+        }
+
+        @Override
+        protected void doGet(HttpServletRequest request, HttpServletResponse 
response) {
+            AsyncContext asyncContext = request.startAsync();
+            asyncContext.setTimeout(TEST_TIMEOUT_MS);
+            asyncStarted.countDown();
+        }
+    }
+
+
+    private static class ErrorServlet extends HttpServlet {
+
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        protected void doGet(HttpServletRequest request, HttpServletResponse 
response) throws ServletException {
+            throw new ServletException("Async test error");
+        }
+    }
+
+
+    private class AsyncRequest {
+
+        private final String path;
+        private final Map<String,List<String>> requestHeaders;
+        private final ByteChunk responseBody = new ByteChunk();
+        private final AtomicInteger responseCode = new AtomicInteger(-1);
+        private final AtomicReference<Throwable> failure = new 
AtomicReference<>();
+        private final Thread thread;
+
+        private AsyncRequest(String path, Map<String,List<String>> 
requestHeaders) {
+            this.path = path;
+            this.requestHeaders = requestHeaders;
+            this.thread = new Thread(this::doRequest);
+        }
+
+        private void start() {
+            thread.start();
+        }
+
+        private void await() throws InterruptedException {
+            thread.join(10000);
+        }
+
+        private void assertResponse(int expectedStatus, String 
expectedResponseBody) {
+            Assert.assertNull(failure.get());
+            Assert.assertEquals(expectedStatus, responseCode.get());
+            if (expectedResponseBody != null) {
+                Assert.assertEquals(expectedResponseBody, 
responseBody.toString());
+            }
+        }
+
+        private void doRequest() {
+            try {
+                responseCode.set(getUrl("http://localhost:"; + getPort() + 
path, responseBody, requestHeaders, null));
+            } catch (Throwable t) {
+                failure.set(t);
+            }
+        }
+    }
+
+
+    private enum TerminalAction {
+        COMPLETE
+    }
+
+
     private static class TesterStore implements Store {
 
         private Manager manager;


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

Reply via email to