Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
710d9df
WW-3576 docs: design for SessionMap staleness and the execAndWait ses…
lukaszlenart Oct 18, 2026
0ee4abf
WW-3576 docs: implementation plan for SessionMap staleness and the ex…
lukaszlenart Oct 18, 2026
e652635
WW-3576 fix(core): read SessionMap's session once per call and drop t…
lukaszlenart Oct 18, 2026
499415e
WW-3576 fix(core): keep the pinned session in a local in SessionMap.p…
lukaszlenart Oct 18, 2026
a75621d
WW-3576 fix(core): treat an invalidated session as absent and stop ca…
lukaszlenart Oct 18, 2026
f78c62d
WW-3576 test(core): exercise each SessionMap operation against its ow…
lukaszlenart Oct 18, 2026
62c8ce3
WW-3576 docs: a put that finds its session invalidated pins a new one…
lukaszlenart Oct 18, 2026
170bf18
WW-3576 feat(core): add a sandboxed session map recording background …
lukaszlenart Oct 18, 2026
4561444
WW-3576 feat(core): reconcile a session sandbox with per-action confl…
lukaszlenart Oct 18, 2026
a573fc1
WW-3576 feat(core): run execAndWait background processes against a se…
lukaszlenart Oct 18, 2026
c09388b
WW-3576 feat(core): warn when a background process writes around its …
lukaszlenart Oct 18, 2026
04d8d18
WW-3576 docs: detach the request SessionMap at request end
lukaszlenart Oct 18, 2026
3221287
WW-3576 fix(core): detach the request SessionMap at request end
lukaszlenart Oct 18, 2026
6fba749
WW-3576 refactor(core): narrow the sandbox API and break the dispatch…
lukaszlenart Oct 18, 2026
52a2192
WW-3576 fix(core): copy the background context on the request thread
lukaszlenart Oct 18, 2026
7160db1
WW-3576 docs: drop the stale execAndWait ActionContext warning
lukaszlenart Oct 18, 2026
e79f945
WW-3576 fix(core): detach every request SessionMap at request end
lukaszlenart Oct 18, 2026
aa6277d
WW-3576 docs: request end detaches every SessionMap built for the req…
lukaszlenart Oct 18, 2026
8b1b440
WW-3576 fix(core): keep request SessionMaps attached while async is s…
lukaszlenart Oct 18, 2026
d313368
WW-3576 fix(core): keep the bound SessionMap attached while async is …
lukaszlenart Oct 18, 2026
8bd88a7
WW-3576 docs: reflow the async detach clause
lukaszlenart Oct 18, 2026
fb355c3
WW-3576 docs: tighten the detach rationale wording
lukaszlenart Oct 18, 2026
fd5988a
WW-3576 chore(core): address Sonar findings on the SessionMap changes
lukaszlenart Oct 18, 2026
0c1cc42
WW-3576 chore(core): resolve the remaining Sonar findings
lukaszlenart Oct 18, 2026
a713fe0
WW-3576 fix(core): address review findings on the execAndWait write-back
lukaszlenart Oct 18, 2026
82b4475
WW-3576 fix(core): discard write-back only for an invalidated session
lukaszlenart Oct 18, 2026
a66f76b
WW-3576 refactor(core): move the execAndWait session sandbox to WW-5787
lukaszlenart Oct 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ public void cleanupRequest(final HttpServletRequest request) {
try {
dispatcher.cleanUpRequest(request);
} finally {
detachSessionMaps(request);
ActionContext.clear();
Dispatcher.clearInstance();
ContainerHolder.clear();
Expand All @@ -83,6 +84,20 @@ public void cleanupRequest(final HttpServletRequest request) {
});
}

private static void detachSessionMaps(HttpServletRequest request) {
if (!request.isAsyncStarted()) {
SessionMap.detachAll(request);
detachSessionMap();
}
}

private static void detachSessionMap() {
ActionContext context = ActionContext.getContext();
if (context != null && context.getSession() instanceof SessionMap sessionMap) {
sessionMap.detach();
}
}

/**
* Creates the action context and initializes the thread local
*
Expand Down
194 changes: 137 additions & 57 deletions core/src/main/java/org/apache/struts2/dispatcher/SessionMap.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.Queue;

/**
* A simple implementation of the {@link java.util.Map} interface to handle a collection of HTTP session
Expand All @@ -39,10 +41,19 @@
@Serial
private static final long serialVersionUID = 4678843241638046854L;

protected HttpSession session;
private static final String REQUEST_MAPS = SessionMap.class.getName() + ".maps";

// volatile only publishes the pinned reference (HttpSession is thread-safe per the Servlet spec);

Check warning on line 46 in core/src/main/java/org/apache/struts2/dispatcher/SessionMap.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This block of commented-out lines of code should be removed.

See more on https://sonarcloud.io/project/issues?id=apache_struts&issues=AaFO89AhpdXNQ-N4E7hp&open=AaFO89AhpdXNQ-N4E7hp&pullRequest=2042
// non-transient as before, so subclass serialization is unchanged
@SuppressWarnings({"java:S3077", "java:S1948"})
protected volatile HttpSession session;
/**
* @deprecated no longer read or written; {@link #entrySet()} builds a fresh set on every call.
*/
@Deprecated(since = "7.5.0", forRemoval = true)
protected Set<Entry<String, Object>> entries;

Check warning on line 54 in core/src/main/java/org/apache/struts2/dispatcher/SessionMap.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=apache_struts&issues=AaFOqRX6BynKBKHRmVGv&open=AaFOqRX6BynKBKHRmVGv&pullRequest=2042
protected HttpServletRequest request;

private volatile boolean detached;

/**
* Creates a new session map given a http servlet request. Note, the enumeration of request
Expand All @@ -56,76 +67,109 @@
// "execAndWait" interceptor
this.request = request;
this.session = request.getSession(false);
register(request);
}

void detach() {
detached = true;
}

private void register(HttpServletRequest request) {
RequestMaps registered = request.getAttribute(REQUEST_MAPS) instanceof RequestMaps existing ? existing : null;
if (registered == null) {
registered = new RequestMaps();
request.setAttribute(REQUEST_MAPS, registered);
}
registered.maps.add(this);
}

static void detachAll(HttpServletRequest request) {
if (request.getAttribute(REQUEST_MAPS) instanceof RequestMaps registered) {
registered.maps.forEach(SessionMap::detach);
request.removeAttribute(REQUEST_MAPS);
}
}

private static final class RequestMaps {
private final Queue<SessionMap> maps = new ConcurrentLinkedQueue<>();

@Override
public String toString() {
return "RequestMaps";
}
}

/**
* Invalidate the http session.
*/
public void invalidate() {
if (session == null) {
HttpSession current = session;
if (current == null) {
return;
}

synchronized (session.getId().intern()) {
session.invalidate();
session = null;
entries = null;
try {
current.invalidate();
} catch (IllegalStateException e) {
// already invalidated elsewhere
}
unpin(current);
}

/**
* Removes all attributes from the session as well as clears entries in this
* map.
* Removes all attributes from the session.
*/
@Override
public void clear() {
if (session == null) {
HttpSession current = session;
if (current == null) {
return;
}

synchronized (session.getId().intern()) {
entries = null;
final Enumeration<String> attributeNamesEnum = session.getAttributeNames();
try {
final Enumeration<String> attributeNamesEnum = current.getAttributeNames();
while (attributeNamesEnum.hasMoreElements()) {
session.removeAttribute(attributeNamesEnum.nextElement());
current.removeAttribute(attributeNamesEnum.nextElement());
}
} catch (IllegalStateException e) {
unpin(current);
}

}

/**
* Returns a Set of attributes from the http session.
* Returns a Set of attributes from the http session, built fresh on every call.
*
* @return a Set of attributes from the http session.
*/
@Override
public Set<Entry<String, Object>> entrySet() {
if (session == null) {
HttpSession current = session;
if (current == null) {
return Collections.emptySet();
}

synchronized (session.getId().intern()) {
if (entries == null) {
entries = new HashSet<>();
final Set<Entry<String, Object>> result = new HashSet<>();
try {
final Enumeration<String> enumeration = current.getAttributeNames();

final Enumeration<String> enumeration = session.getAttributeNames();
while (enumeration.hasMoreElements()) {
final String key = enumeration.nextElement();
final Object value = current.getAttribute(key);
result.add(new StringObjectEntry(key, value) {
@Override
public Object setValue(final Object obj) {
current.setAttribute(key, obj);

while (enumeration.hasMoreElements()) {
final String key = enumeration.nextElement();
final Object value = session.getAttribute(key);
entries.add(new StringObjectEntry(key, value) {
@Override
public Object setValue(final Object obj) {
session.setAttribute(key, obj);

return value;
}
});
}
return value;
}
});
}
} catch (IllegalStateException e) {
unpin(current);
return Collections.emptySet();
}

return entries;
return result;
}

/**
Expand All @@ -139,12 +183,16 @@
*/
@Override
public Object get(final Object key) {
if (session == null) {
HttpSession current = session;
if (current == null) {
return null;
}

synchronized (session.getId().intern()) {
return session.getAttribute(key != null ? key.toString() : null);
try {
return current.getAttribute(key != null ? key.toString() : null);
} catch (IllegalStateException e) {
unpin(current);
return null;
}
}

Expand All @@ -157,16 +205,42 @@
*/
@Override
public Object put(final String key, final Object value) {
HttpSession current = session;
if (current == null) {
current = pin();
}

final Object oldValue = get(key);
try {
current.setAttribute(key, value);
} catch (IllegalStateException e) {
unpin(current);
pin().setAttribute(key, value);
return null;
}
return oldValue;
}

private HttpSession pin() {
synchronized (this) {
if (session == null) {
session = request.getSession(true);
HttpSession current = session;
if (current == null) {
if (detached) {
throw new IllegalStateException("The request of this session map has ended and its session is gone; "
+ "it cannot resolve a new session");
}
current = request.getSession(true);
session = current;
}
return current;
}
synchronized (session.getId().intern()) {
final Object oldValue = get(key);
entries = null;
session.setAttribute(key, value);
return oldValue;
}

private void unpin(final HttpSession stale) {
synchronized (this) {
if (session == stale) {
session = null;
}
}
}

Expand All @@ -181,19 +255,21 @@
*/
@Override
public Object remove(final Object key) {
if (session == null) {
HttpSession current = session;
if (current == null) {
return null;
}

synchronized (session.getId().intern()) {
entries = null;

final String keyAsString = (key != null ? key.toString() : null);
final Object value = get(keyAsString);
session.removeAttribute(keyAsString);

return value;
final String keyAsString = (key != null ? key.toString() : null);
final Object value = get(keyAsString);
try {
current.removeAttribute(keyAsString);
} catch (IllegalStateException e) {
unpin(current);
return null;
}

return value;
}


Expand All @@ -208,13 +284,17 @@
*/
@Override
public boolean containsKey(final Object key) {
if (session == null) {
HttpSession current = session;
if (current == null) {
return false;
}

synchronized (session.getId().intern()) {
final String keyAsString = (key != null ? key.toString() : null);
return (session.getAttribute(keyAsString) != null);
final String keyAsString = (key != null ? key.toString() : null);
try {
return current.getAttribute(keyAsString) != null;
} catch (IllegalStateException e) {
unpin(current);
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public class StrutsBackgroundProcess implements BackgroundProcess, Serializable
protected transient Exception exception;

protected String result;
protected boolean done;
protected volatile boolean done;

/**
* Constructs a background process
Expand Down
Loading
Loading