You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by ma...@apache.org on 2026/06/03 18:34:39 UTC
(tomcat) 03/04: Additional tests written by CoPilot / GPT-5.4
This is an automated email from the ASF dual-hosted git repository.
markt-asf pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit c45028af067aaeea9d0ccca00ea9d83fce2c4102
Author: Mark Thomas <ma...@apache.org>
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..d146b2c1b2 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(HttpServletResponse.SC_TOO_MANY_REQUESTS, 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: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org