You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@struts.apache.org by lu...@apache.org on 2026/10/18 10:51:08 UTC
(struts) 10/22: WW-3576 feat(core): run execAndWait background processes against a session sandbox
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 a573fc13e0e8532a3551b99b6d062fe22bc0efb5
Author: Lukasz Lenart <lu...@apache.org>
AuthorDate: Sun Oct 18 09:15:05 2026 +0200
WW-3576 feat(core): run execAndWait background processes against a session sandbox
Co-Authored-By: Claude Opus 5.5 <no...@anthropic.com>
---
.../interceptor/ExecuteAndWaitInterceptor.java | 18 ++++-
.../interceptor/exec/BackgroundProcess.java | 8 +++
.../interceptor/exec/StrutsBackgroundProcess.java | 26 +++++++
.../interceptor/ExecuteAndWaitInterceptorTest.java | 82 ++++++++++++++++++++++
.../interceptor/SessionWritingWaitAction.java | 60 ++++++++++++++++
5 files changed, 193 insertions(+), 1 deletion(-)
diff --git a/core/src/main/java/org/apache/struts2/interceptor/ExecuteAndWaitInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/ExecuteAndWaitInterceptor.java
index 5a073147f..7572a6d4e 100644
--- a/core/src/main/java/org/apache/struts2/interceptor/ExecuteAndWaitInterceptor.java
+++ b/core/src/main/java/org/apache/struts2/interceptor/ExecuteAndWaitInterceptor.java
@@ -22,6 +22,7 @@ import org.apache.struts2.action.Action;
import org.apache.struts2.ActionContext;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.ActionProxy;
+import org.apache.struts2.action.SessionAware;
import org.apache.struts2.config.entities.ResultConfig;
import org.apache.struts2.inject.Container;
import org.apache.struts2.inject.Inject;
@@ -31,6 +32,8 @@ import org.apache.logging.log4j.Logger;
import org.apache.struts2.ServletActionContext;
import org.apache.struts2.interceptor.exec.BackgroundProcess;
import org.apache.struts2.interceptor.exec.ExecutorProvider;
+import org.apache.struts2.interceptor.exec.SandboxedSessionMap;
+import org.apache.struts2.interceptor.exec.SessionSandboxReconciler;
import org.apache.struts2.interceptor.exec.StrutsBackgroundProcess;
import org.apache.struts2.interceptor.exec.StrutsExecutorProvider;
import org.apache.struts2.util.TokenHelper;
@@ -295,7 +298,8 @@ public class ExecuteAndWaitInterceptor extends MethodFilterInterceptor {
if ((!executeAfterValidationPass || secondTime) && bp == null) {
LOG.trace("BackgroundProcess instance is null (create new instance) - executeAfterValidationPass: {}, secondTime: {}.", executeAfterValidationPass, secondTime);
- bp = getNewBackgroundProcess(name, actionInvocation, threadPriority).prepare();
+ SandboxedSessionMap sandbox = new SandboxedSessionMap(httpSession, name);
+ bp = getNewBackgroundProcess(name, actionInvocation, threadPriority).withSessionSandbox(sandbox).prepare();
session.put(bp_SessionKey, bp);
if (executor == null || executor.isShutdown()) {
LOG.warn("Executor is shutting down (or null), cannot execute a new process, invoke next ActionInvocation step and return.");
@@ -334,6 +338,7 @@ public class ExecuteAndWaitInterceptor extends MethodFilterInterceptor {
LOG.trace("BackgroundProcess instance is done (remove key, return result) - executeAfterValidationPass: {}, secondTime: {}.", executeAfterValidationPass, secondTime);
session.remove(bp_SessionKey);
actionInvocation.getStack().push(bp.getAction());
+ writeBackSessionSandbox(bp, context);
// if an exception occurred during action execution, throw it here
if (bp.getException() != null) {
@@ -352,6 +357,17 @@ public class ExecuteAndWaitInterceptor extends MethodFilterInterceptor {
}
}
+ private void writeBackSessionSandbox(BackgroundProcess bp, ActionContext context) {
+ SandboxedSessionMap sandbox = bp.getSessionSandbox();
+ if (sandbox == null) {
+ return;
+ }
+ SessionSandboxReconciler.reconcile(sandbox, ServletActionContext.getRequest(), bp.getAction());
+ if (bp.getAction() instanceof SessionAware sessionAware) {
+ sessionAware.withSession(context.getSession());
+ }
+ }
+
/**
* <p>
* Performs the initial delay.
diff --git a/core/src/main/java/org/apache/struts2/interceptor/exec/BackgroundProcess.java b/core/src/main/java/org/apache/struts2/interceptor/exec/BackgroundProcess.java
index a9ca48d20..7b1392ac8 100644
--- a/core/src/main/java/org/apache/struts2/interceptor/exec/BackgroundProcess.java
+++ b/core/src/main/java/org/apache/struts2/interceptor/exec/BackgroundProcess.java
@@ -38,4 +38,12 @@ public interface BackgroundProcess extends Runnable {
Exception getException();
boolean isDone();
+
+ default BackgroundProcess withSessionSandbox(SandboxedSessionMap sandbox) {
+ return this;
+ }
+
+ default SandboxedSessionMap getSessionSandbox() {
+ return null;
+ }
}
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 adab43dc6..8c41c3a8a 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
@@ -22,9 +22,11 @@ import org.apache.struts2.ActionContext;
import org.apache.struts2.ActionInvocation;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import org.apache.struts2.action.SessionAware;
import java.io.Serial;
import java.io.Serializable;
+import java.util.HashMap;
/**
* Background thread to be executed by the ExecuteAndWaitInterceptor.
@@ -43,6 +45,7 @@ public class StrutsBackgroundProcess implements BackgroundProcess, Serializable
//WW-4900 transient since 2.5.15
protected transient ActionInvocation invocation;
protected transient Exception exception;
+ private transient SandboxedSessionMap sessionSandbox;
protected String result;
protected boolean done;
@@ -66,6 +69,7 @@ public class StrutsBackgroundProcess implements BackgroundProcess, Serializable
processThread = new Thread(() -> {
try {
beforeInvocation();
+ installSessionSandbox();
result = invocation.invokeActionOnly();
} catch (Exception e) {
LOG.warn("Exception during invokeActionOnly() execution", e);
@@ -91,6 +95,28 @@ public class StrutsBackgroundProcess implements BackgroundProcess, Serializable
return this;
}
+ @Override
+ public BackgroundProcess withSessionSandbox(SandboxedSessionMap sandbox) {
+ this.sessionSandbox = sandbox;
+ return this;
+ }
+
+ @Override
+ public SandboxedSessionMap getSessionSandbox() {
+ return sessionSandbox;
+ }
+
+ private void installSessionSandbox() {
+ if (sessionSandbox == null) {
+ return;
+ }
+ ActionContext.bind(ActionContext.of(new HashMap<>(invocation.getInvocationContext().getContextMap()))
+ .withSession(sessionSandbox));
+ if (invocation.getAction() instanceof SessionAware sessionAware) {
+ sessionAware.withSession(sessionSandbox);
+ }
+ }
+
@Override
public void run() {
if (processThread == null) {
diff --git a/core/src/test/java/org/apache/struts2/interceptor/ExecuteAndWaitInterceptorTest.java b/core/src/test/java/org/apache/struts2/interceptor/ExecuteAndWaitInterceptorTest.java
index 7701173eb..5216a6564 100644
--- a/core/src/test/java/org/apache/struts2/interceptor/ExecuteAndWaitInterceptorTest.java
+++ b/core/src/test/java/org/apache/struts2/interceptor/ExecuteAndWaitInterceptorTest.java
@@ -39,10 +39,14 @@ import org.apache.struts2.util.location.LocatableProperties;
import jakarta.servlet.http.HttpSession;
import org.apache.struts2.StrutsInternalTestCase;
import org.apache.struts2.dispatcher.HttpParameters;
+import org.apache.struts2.dispatcher.SessionMap;
+import org.apache.struts2.interceptor.exec.BackgroundProcess;
import org.apache.struts2.interceptor.exec.ExecutorProvider;
+import org.apache.struts2.interceptor.exec.SandboxedSessionMap;
import org.apache.struts2.interceptor.parameter.ParametersInterceptor;
import org.apache.struts2.views.jsp.StrutsMockHttpServletRequest;
import org.apache.struts2.views.jsp.StrutsMockHttpSession;
+import org.springframework.mock.web.MockHttpSession;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -67,6 +71,7 @@ public class ExecuteAndWaitInterceptorTest extends StrutsInternalTestCase {
private ParametersInterceptor parametersInterceptor;
private TokenInterceptor tokenInterceptor;
private TokenSessionStoreInterceptor tokenSessionInterceptor;
+ private ServletConfigInterceptor servletConfigInterceptor;
public void testOneWait() throws Exception {
waitInterceptor.setDelay(0);
@@ -275,6 +280,74 @@ public class ExecuteAndWaitInterceptorTest extends StrutsInternalTestCase {
assertNull(session.get(tokenSessionName));
}
+ public void testBackgroundWritesReachTheRealSession() throws Exception {
+ useLiveSession();
+
+ assertEquals("wait", buildProxy("sessionAction").execute());
+ awaitDone("sessionAction");
+
+ assertEquals("success", buildProxy("sessionAction").execute());
+ assertEquals("done", httpSession.getAttribute("result"));
+ assertEquals("yes", httpSession.getAttribute("aware"));
+ }
+
+ public void testBackgroundWritesAreInvisibleUntilPolled() throws Exception {
+ useLiveSession();
+
+ assertEquals("wait", buildProxy("sessionAction").execute());
+ awaitDone("sessionAction");
+
+ assertNull(httpSession.getAttribute("result"));
+ assertNull(httpSession.getAttribute("aware"));
+ }
+
+ public void testWritesOfAFailedActionAreApplied() throws Exception {
+ useLiveSession();
+ Map<String, String[]> requestParams = new HashMap<>();
+ requestParams.put("failWith", new String[]{"boom"});
+ withRequestParams(requestParams);
+
+ assertEquals("wait", buildProxy("sessionAction").execute());
+ awaitDone("sessionAction");
+
+ ActionProxy poll = buildProxy("sessionAction");
+ try {
+ poll.execute();
+ fail("the background failure should be rethrown by the polling request");
+ } catch (IllegalStateException failure) {
+ assertEquals("boom", failure.getMessage());
+ }
+ assertEquals("done", httpSession.getAttribute("result"));
+ }
+
+ public void testSessionAwareGetsTheLiveMapBackAfterWriteBack() throws Exception {
+ useLiveSession();
+
+ assertEquals("wait", buildProxy("sessionAction").execute());
+ SessionWritingWaitAction action = (SessionWritingWaitAction) awaitDone("sessionAction").getAction();
+
+ assertEquals("success", buildProxy("sessionAction").execute());
+ assertFalse(action.getSession() instanceof SandboxedSessionMap);
+ assertSame(session, action.getSession());
+ }
+
+ private void useLiveSession() {
+ httpSession = new MockHttpSession();
+ request.setSession(httpSession);
+ session = new SessionMap(request);
+ context = ActionContext.of(context).withSession(session).getContextMap();
+ }
+
+ private BackgroundProcess awaitDone(String actionName) throws InterruptedException {
+ BackgroundProcess process = (BackgroundProcess) httpSession.getAttribute(ExecuteAndWaitInterceptor.KEY + actionName);
+ long deadline = System.currentTimeMillis() + 5000;
+ while (!process.isDone() && System.currentTimeMillis() < deadline) {
+ Thread.sleep(20);
+ }
+ assertTrue("background process did not finish in time", process.isDone());
+ return process;
+ }
+
private void withRequestParams(Map<String, String[]> requestParams) {
context = ActionContext.of(context).withParameters(HttpParameters.create(requestParams).build()).getContextMap();
}
@@ -306,6 +379,7 @@ public class ExecuteAndWaitInterceptorTest extends StrutsInternalTestCase {
container.inject(parametersInterceptor);
container.inject(tokenInterceptor);
container.inject(tokenSessionInterceptor);
+ container.inject(servletConfigInterceptor);
waitInterceptor.init();
parametersInterceptor.init();
@@ -339,6 +413,7 @@ public class ExecuteAndWaitInterceptorTest extends StrutsInternalTestCase {
parametersInterceptor = new ParametersInterceptor();
tokenInterceptor = new TokenInterceptor();
tokenSessionInterceptor = new TokenSessionStoreInterceptor();
+ servletConfigInterceptor = new ServletConfigInterceptor();
PackageConfig wait = new PackageConfig.Builder("")
.addActionConfig("action1", new ActionConfig.Builder("", "action1", ExecuteAndWaitDelayAction.class.getName())
.addResultConfig(new ResultConfig.Builder(Action.SUCCESS, MockResult.class.getName()).build())
@@ -347,6 +422,13 @@ public class ExecuteAndWaitInterceptorTest extends StrutsInternalTestCase {
.addInterceptor(new InterceptorMapping("execAndWait", waitInterceptor))
.build())
.addActionConfig("plainWait", waitActionConfig("plainWait"))
+ .addActionConfig("sessionAction", new ActionConfig.Builder("", "sessionAction", SessionWritingWaitAction.class.getName())
+ .addResultConfig(new ResultConfig.Builder(Action.SUCCESS, MockResult.class.getName()).build())
+ .addResultConfig(new ResultConfig.Builder(ExecuteAndWaitInterceptor.WAIT, MockResult.class.getName()).build())
+ .addInterceptor(new InterceptorMapping("servletConfig", servletConfigInterceptor))
+ .addInterceptor(new InterceptorMapping("params", parametersInterceptor))
+ .addInterceptor(new InterceptorMapping("execAndWait", waitInterceptor))
+ .build())
.addActionConfig("tokenWait", waitActionConfig("tokenWait", new InterceptorMapping("token", tokenInterceptor)))
.addActionConfig("tokenSessionWait", waitActionConfig("tokenSessionWait", new InterceptorMapping("tokenSession", tokenSessionInterceptor)))
.build();
diff --git a/core/src/test/java/org/apache/struts2/interceptor/SessionWritingWaitAction.java b/core/src/test/java/org/apache/struts2/interceptor/SessionWritingWaitAction.java
new file mode 100644
index 000000000..4610d3786
--- /dev/null
+++ b/core/src/test/java/org/apache/struts2/interceptor/SessionWritingWaitAction.java
@@ -0,0 +1,60 @@
+/*
+ * 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.struts2.interceptor;
+
+import org.apache.struts2.ActionContext;
+import org.apache.struts2.action.Action;
+import org.apache.struts2.action.SessionAware;
+import org.apache.struts2.interceptor.parameter.StrutsParameter;
+
+import java.util.Map;
+
+/**
+ * Used by ExecuteAndWaitInterceptorTest to write to the session from the background thread.
+ */
+public class SessionWritingWaitAction implements Action, SessionAware {
+
+ private Map<String, Object> session;
+ private String failWith;
+
+ @Override
+ public String execute() throws Exception {
+ Thread.sleep(200);
+ ActionContext.getContext().getSession().put("result", "done");
+ session.put("aware", "yes");
+ if (failWith != null) {
+ throw new IllegalStateException(failWith);
+ }
+ return SUCCESS;
+ }
+
+ @Override
+ public void withSession(Map<String, Object> session) {
+ this.session = session;
+ }
+
+ public Map<String, Object> getSession() {
+ return session;
+ }
+
+ @StrutsParameter
+ public void setFailWith(String failWith) {
+ this.failWith = failWith;
+ }
+}