This is an automated email from the ASF dual-hosted git repository. lukaszlenart pushed a commit to branch feature/WW-3576-session-map-staleness in repository https://gitbox.apache.org/repos/asf/struts.git
commit 52a21921e18fc8d96e7b4752f44960cbd4fc252e Author: Lukasz Lenart <[email protected]> AuthorDate: Sun Oct 18 10:34:27 2026 +0200 WW-3576 fix(core): copy the background context on the request thread The background process's context map is copied when the sandbox is handed over, on the request thread, instead of on the background thread while the request may still be rendering the wait page. An OgnlContext is copied as an OgnlContext. A context bound by an overridden beforeInvocation() is honoured and copied on the background thread. done is volatile, since it gates the write-back. Co-Authored-By: Claude Opus 5.5 <[email protected]> --- .../interceptor/exec/StrutsBackgroundProcess.java | 30 +++++++++-- .../exec/StrutsBackgroundProcessTest.java | 58 ++++++++++++++++++++++ 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/org/apache/struts2/interceptor/exec/StrutsBackgroundProcess.java b/core/src/main/java/org/apache/struts2/interceptor/exec/StrutsBackgroundProcess.java index 6ad0f2967..5bf7e6a11 100644 --- a/core/src/main/java/org/apache/struts2/interceptor/exec/StrutsBackgroundProcess.java +++ b/core/src/main/java/org/apache/struts2/interceptor/exec/StrutsBackgroundProcess.java @@ -18,6 +18,8 @@ */ package org.apache.struts2.interceptor.exec; +import ognl.Ognl; +import ognl.OgnlContext; import org.apache.struts2.ActionContext; import org.apache.struts2.ActionInvocation; import org.apache.logging.log4j.LogManager; @@ -27,6 +29,7 @@ import org.apache.struts2.action.SessionAware; import java.io.Serial; import java.io.Serializable; import java.util.HashMap; +import java.util.Map; /** * Background thread to be executed by the ExecuteAndWaitInterceptor. @@ -46,9 +49,10 @@ public class StrutsBackgroundProcess implements BackgroundProcess, Serializable protected transient ActionInvocation invocation; protected transient Exception exception; private transient SandboxedSessionMap sessionSandbox; + private transient Map<String, Object> sandboxContext; protected String result; - protected boolean done; + protected volatile boolean done; /** * Constructs a background process @@ -98,6 +102,7 @@ public class StrutsBackgroundProcess implements BackgroundProcess, Serializable @Override public BackgroundProcess withSessionSandbox(SandboxedSessionMap sandbox) { this.sessionSandbox = sandbox; + this.sandboxContext = copyOf(invocation.getInvocationContext().getContextMap()); return this; } @@ -117,13 +122,32 @@ public class StrutsBackgroundProcess implements BackgroundProcess, Serializable if (sessionSandbox == null) { return; } - ActionContext.bind(ActionContext.of(new HashMap<>(invocation.getInvocationContext().getContextMap())) - .withSession(sessionSandbox)); + ActionContext.bind(ActionContext.of(backgroundContextMap()).withSession(sessionSandbox)); if (invocation.getAction() instanceof SessionAware sessionAware) { sessionAware.withSession(sessionSandbox); } } + private Map<String, Object> backgroundContextMap() { + ActionContext bound = ActionContext.getContext(); + if (bound == null || bound.getContextMap() == invocation.getInvocationContext().getContextMap()) { + return sandboxContext; + } + return copyOf(bound.getContextMap()); + } + + private static Map<String, Object> copyOf(Map<String, Object> context) { + if (context instanceof OgnlContext ognlContext) { + OgnlContext copy = Ognl.createDefaultContext(ognlContext.getRoot(), ognlContext.getMemberAccess(), + ognlContext.getClassResolver(), ognlContext.getTypeConverter()); + copy.setTraceEvaluations(ognlContext.isTraceEvaluations()); + copy.setKeepLastEvaluation(ognlContext.isKeepLastEvaluation()); + copy.putAll(ognlContext.getValues()); + return copy; + } + return new HashMap<>(context); + } + @Override public void run() { if (processThread == null) { diff --git a/core/src/test/java/org/apache/struts2/interceptor/exec/StrutsBackgroundProcessTest.java b/core/src/test/java/org/apache/struts2/interceptor/exec/StrutsBackgroundProcessTest.java index ad3409bd1..39c30db57 100644 --- a/core/src/test/java/org/apache/struts2/interceptor/exec/StrutsBackgroundProcessTest.java +++ b/core/src/test/java/org/apache/struts2/interceptor/exec/StrutsBackgroundProcessTest.java @@ -24,7 +24,9 @@ import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.security.SecureRandom; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Random; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; @@ -34,9 +36,14 @@ import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import ognl.OgnlContext; import org.apache.struts2.StrutsInternalTestCase; +import org.apache.struts2.util.ValueStack; +import org.apache.struts2.util.ValueStackFactory; import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpSession; import org.apache.struts2.ActionContext; import org.apache.struts2.ActionInvocation; @@ -161,6 +168,57 @@ public class StrutsBackgroundProcessTest extends StrutsInternalTestCase { assertEquals("Background thread Unprepared has not been prepared!", bp.getException().getMessage()); } + public void testSandboxContextIsCopiedAtHandoverAndStaysAnOgnlContext() { + ValueStack stack = container.getInstance(ValueStackFactory.class).createValueStack(); + ActionContext requestContext = ActionContext.of(stack.getContext()).withSession(new HashMap<>()); + AtomicReference<Map<String, Object>> seen = new AtomicReference<>(); + MockActionInvocationWithActionInvoker invocation = new MockActionInvocationWithActionInvoker(() -> { + seen.set(ActionContext.getContext().getContextMap()); + return "done"; + }); + invocation.setInvocationContext(requestContext); + SandboxedSessionMap sandbox = new SandboxedSessionMap(new MockHttpSession(), "copy"); + + BackgroundProcess bp = new StrutsBackgroundProcess(invocation, "copy", Thread.NORM_PRIORITY) + .withSessionSandbox(sandbox) + .prepare(); + requestContext.put("afterHandover", "x"); + executor.execute(bp); + await().atMost(5, TimeUnit.SECONDS).until(bp::isDone); + + assertNull(bp.getException()); + Map<String, Object> background = seen.get(); + assertTrue(background instanceof OgnlContext); + assertNotSame(requestContext.getContextMap(), background); + assertFalse(background.containsKey("afterHandover")); + assertSame(sandbox, ActionContext.of(background).getSession()); + assertSame(stack, ActionContext.of(background).getValueStack()); + assertNotSame(sandbox, requestContext.getSession()); + } + + public void testSandboxHonoursTheContextBoundByBeforeInvocation() { + AtomicReference<Map<String, Object>> seen = new AtomicReference<>(); + MockActionInvocationWithActionInvoker invocation = new MockActionInvocationWithActionInvoker(() -> { + seen.set(ActionContext.getContext().getContextMap()); + return "done"; + }); + invocation.setInvocationContext(ActionContext.of()); + SandboxedSessionMap sandbox = new SandboxedSessionMap(new MockHttpSession(), "custom"); + + BackgroundProcess bp = new StrutsBackgroundProcess(invocation, "custom", Thread.NORM_PRIORITY) { + @Override + protected void beforeInvocation() { + ActionContext.bind(ActionContext.of().with("custom", "yes")); + } + }.withSessionSandbox(sandbox).prepare(); + executor.execute(bp); + await().atMost(5, TimeUnit.SECONDS).until(bp::isDone); + + assertNull(bp.getException()); + assertEquals("yes", seen.get().get("custom")); + assertSame(sandbox, ActionContext.of(seen.get()).getSession()); + } + private static class MockActionInvocationWithActionInvoker extends MockActionInvocation { private final Callable<String> actionInvoker;
