diff --git a/java/org/apache/catalina/ha/context/ReplicatedContext.java b/java/org/apache/catalina/ha/context/ReplicatedContext.java index 5ba2583fb0a0..8db3b70394cf 100644 --- a/java/org/apache/catalina/ha/context/ReplicatedContext.java +++ b/java/org/apache/catalina/ha/context/ReplicatedContext.java @@ -118,9 +118,13 @@ public ClassLoader[] getClassLoaders() { @Override public ServletContext getServletContext() { if (context == null) { - context = new ReplApplContext(this); - if (getAltDDName() != null) { - context.setAttribute(Globals.ALT_DD_ATTR, getAltDDName()); + synchronized (this) { + if (context == null) { + context = new ReplApplContext(this); + if (getAltDDName() != null) { + context.setAttribute(Globals.ALT_DD_ATTR, getAltDDName()); + } + } } } diff --git a/test/org/apache/catalina/ha/context/TestReplicatedContext.java b/test/org/apache/catalina/ha/context/TestReplicatedContext.java index 3840a19249cd..dffdab05dbf5 100644 --- a/test/org/apache/catalina/ha/context/TestReplicatedContext.java +++ b/test/org/apache/catalina/ha/context/TestReplicatedContext.java @@ -18,7 +18,12 @@ import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.atomic.AtomicReference; +import jakarta.servlet.ServletContext; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; @@ -37,6 +42,58 @@ public class TestReplicatedContext extends TomcatBaseTest { + @Test + public void testGetServletContextReturnsSameInstanceUnderConcurrency() throws Exception { + Tomcat tomcat = getTomcatInstance(); + Host host = tomcat.getHost(); + if (host instanceof StandardHost) { + ((StandardHost) host).setContextClass(ReplicatedContext.class.getName()); + } + + File root = new File("test/webapp"); + ReplicatedContext replicatedContext = + (ReplicatedContext) tomcat.addWebapp(host, "", root.getAbsolutePath()); + tomcat.start(); + + // Null the context field to simulate the window during reload + replicatedContext.context = null; + + int numThreads = 20; + CyclicBarrier barrier = new CyclicBarrier(numThreads); + List threads = new ArrayList<>(); + ServletContext[] results = new ServletContext[numThreads]; + AtomicReference failure = new AtomicReference<>(); + + for (int i = 0; i < numThreads; i++) { + final int index = i; + Thread thread = new Thread(() -> { + try { + barrier.await(); + results[index] = replicatedContext.getServletContext(); + } catch (Throwable ex) { + failure.set(ex); + } + }); + thread.start(); + threads.add(thread); + } + + for (Thread thread : threads) { + thread.join(5000); + } + + if (failure.get() != null) { + Assert.fail("Thread failed: " + failure.get()); + } + + ServletContext first = results[0]; + Assert.assertNotNull(first); + for (int i = 1; i < numThreads; i++) { + Assert.assertSame(first, results[i]); + } + } + + @Test public void testBug57425() throws LifecycleException, IOException { Tomcat tomcat = getTomcatInstance();