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 e972bd5e910aa0d7424b81282cdfb15e84d5b706 Author: Mark Thomas <[email protected]> AuthorDate: Wed Jun 3 17:55:33 2026 +0100 Add test case for BZ 70048 written by CoPilot / GPT-5.4 --- .../catalina/valves/TestPersistentValveAsync.java | 339 +++++++++++++++++++++ 1 file changed, 339 insertions(+) diff --git a/test/org/apache/catalina/valves/TestPersistentValveAsync.java b/test/org/apache/catalina/valves/TestPersistentValveAsync.java new file mode 100644 index 0000000000..9910982f89 --- /dev/null +++ b/test/org/apache/catalina/valves/TestPersistentValveAsync.java @@ -0,0 +1,339 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.catalina.valves; + +import java.beans.PropertyChangeListener; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import jakarta.servlet.AsyncContext; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.Manager; +import org.apache.catalina.Session; +import org.apache.catalina.Store; +import org.apache.catalina.Wrapper; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.session.PersistentManager; +import org.apache.catalina.startup.Tomcat; +import org.apache.catalina.startup.TomcatBaseTest; +import org.apache.tomcat.util.buf.ByteChunk; + +public class TestPersistentValveAsync extends TomcatBaseTest { + + @Test + public void testAsyncRequestStoresSessionOnComplete() throws Exception { + Tomcat tomcat = getTomcatInstance(); + StandardContext context = (StandardContext) getProgrammaticRootContext(); + context.setDistributable(true); + + Wrapper wrapper = Tomcat.addServlet(context, "async-complete", new AsyncCompleteServlet()); + wrapper.setAsyncSupported(true); + context.addServletMappingDecoded("/async-complete", "async-complete"); + + TesterStore store = new TesterStore(); + PersistentManager manager = configurePersistentManager(context, store, new PersistentValve()); + + tomcat.start(); + + ByteChunk responseBody = getUrl("http://localhost:" + getPort() + "/async-complete"); + String sessionId = responseBody.toString(); + + Assert.assertEquals(List.of(sessionId), store.getSavedIds()); + Assert.assertNotNull(store.load(sessionId)); + Assert.assertEquals(0, manager.getActiveSessions()); + } + + + @Test + public void testSecondAsyncCycleStoresSessionOnFinalComplete() throws Exception { + Tomcat tomcat = getTomcatInstance(); + StandardContext context = (StandardContext) getProgrammaticRootContext(); + context.setDistributable(true); + + Wrapper wrapper = Tomcat.addServlet(context, "async-dispatch", new AsyncDispatchServlet()); + wrapper.setAsyncSupported(true); + context.addServletMappingDecoded("/async-dispatch", "async-dispatch"); + + TesterStore store = new TesterStore(); + PersistentManager manager = configurePersistentManager(context, store, new PersistentValve()); + + tomcat.start(); + + ByteChunk responseBody = getUrl("http://localhost:" + getPort() + "/async-dispatch"); + String sessionId = responseBody.toString(); + + Assert.assertEquals(List.of(sessionId), store.getSavedIds()); + Assert.assertNotNull(store.load(sessionId)); + Assert.assertEquals(0, manager.getActiveSessions()); + } + + + @Test + public void testSemaphoreHeldWhileAsyncRequestInProgress() throws Exception { + Tomcat tomcat = getTomcatInstance(); + StandardContext context = (StandardContext) getProgrammaticRootContext(); + context.setDistributable(true); + + Tomcat.addServlet(context, "session", new SessionServlet()); + context.addServletMappingDecoded("/session", "session"); + + 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.start(); + + String sessionId = "TEST-SESSION-ID"; + + Map<String,List<String>> requestHeaders = cookieHeaders(sessionId); + + 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(); + + Assert.assertTrue(asyncStarted.await(10, TimeUnit.SECONDS)); + + ByteChunk rejectedOne = new ByteChunk(); + int rejectedOneStatus = getUrl("http://localhost:" + getPort() + "/session", rejectedOne, requestHeaders, null); + Assert.assertEquals(HttpServletResponse.SC_TOO_MANY_REQUESTS, rejectedOneStatus); + + 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); + + Assert.assertNull(asyncFailure.get()); + Assert.assertEquals(HttpServletResponse.SC_OK, asyncResponseCode.get()); + Assert.assertEquals(sessionId, asyncResponseBody.toString()); + + ByteChunk success = new ByteChunk(); + int successStatus = getUrl("http://localhost:" + getPort() + "/session", success, requestHeaders, null); + Assert.assertEquals(HttpServletResponse.SC_OK, successStatus); + Assert.assertFalse(success.isNull()); + } + + + private PersistentManager configurePersistentManager(StandardContext context, TesterStore store, + PersistentValve valve) { + PersistentManager manager = new PersistentManager(); + manager.setStore(store); + manager.setMaxIdleBackup(0); + manager.setSessionActivityCheck(true); + context.setManager(manager); + context.addValve(valve); + return manager; + } + + + private Map<String,List<String>> cookieHeaders(String sessionId) { + Map<String,List<String>> result = new HashMap<>(); + result.put("Cookie", List.of("JSESSIONID=" + sessionId)); + return result; + } + + + private static class SessionServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { + HttpSession session = request.getSession(); + response.getWriter().print(session.getId()); + } + } + + + private static class AsyncCompleteServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) { + HttpSession session = request.getSession(); + AsyncContext asyncContext = request.startAsync(); + asyncContext.start(() -> { + try { + response.getWriter().print(session.getId()); + } catch (IOException e) { + throw new IllegalStateException(e); + } finally { + asyncContext.complete(); + } + }); + } + } + + + private static class AsyncDispatchServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) { + HttpSession session = request.getSession(); + + if (request.getAttribute("asyncCycleStarted") == null) { + request.setAttribute("asyncCycleStarted", Boolean.TRUE); + AsyncContext asyncContext = request.startAsync(); + asyncContext.start(asyncContext::dispatch); + } else { + AsyncContext asyncContext = request.startAsync(); + asyncContext.start(() -> { + try { + response.getWriter().print(session.getId()); + } catch (IOException e) { + throw new IllegalStateException(e); + } finally { + asyncContext.complete(); + } + }); + } + } + } + + + private static class BlockingAsyncServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private final CountDownLatch asyncStarted; + private final CountDownLatch allowAsyncComplete; + + private BlockingAsyncServlet(CountDownLatch asyncStarted, CountDownLatch allowAsyncComplete) { + this.asyncStarted = asyncStarted; + this.allowAsyncComplete = allowAsyncComplete; + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) { + String sessionId = request.getRequestedSessionId(); + AsyncContext asyncContext = request.startAsync(); + asyncContext.start(() -> { + asyncStarted.countDown(); + try { + Assert.assertTrue(allowAsyncComplete.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 TesterStore implements Store { + + private Manager manager; + private final Map<String,Session> sessions = new HashMap<>(); + private final List<String> savedIds = new ArrayList<>(); + + private List<String> getSavedIds() { + return savedIds; + } + + @Override + public Manager getManager() { + return manager; + } + + @Override + public void setManager(Manager manager) { + this.manager = manager; + } + + @Override + public int getSize() { + return sessions.size(); + } + + @Override + public void addPropertyChangeListener(PropertyChangeListener listener) { + // NO-OP + } + + @Override + public String[] keys() { + return sessions.keySet().toArray(new String[0]); + } + + @Override + public Session load(String id) { + return sessions.get(id); + } + + @Override + public void remove(String id) { + sessions.remove(id); + } + + @Override + public void clear() { + sessions.clear(); + } + + @Override + public void removePropertyChangeListener(PropertyChangeListener listener) { + // NO-OP + } + + @Override + public void save(Session session) { + sessions.put(session.getId(), session); + savedIds.add(session.getId()); + } + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
