This is an automated email from the ASF dual-hosted git repository.
lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git
The following commit(s) were added to refs/heads/main by this push:
new 486d535d4 WW-5826 fix(core): make ScopeInterceptor lock table
thread-safe
486d535d4 is described below
commit 486d535d47555aff5f0512014c3c00deb7ee3287
Author: ⳕⲛτⲉⲅⲥⲉⳏτⲟⲅ 🕵🏻 <[email protected]>
AuthorDate: Sun Nov 1 15:51:36 2026 +0530
WW-5826 fix(core): make ScopeInterceptor lock table thread-safe
The static lock table was an unsynchronized IdentityHashMap mutated from
concurrent requests, which do not share the per-request session monitor.
Beyond the corrupted size counter, this is a slow leak under concurrent
load: every entry left behind after unlock() pins a SessionMap (and
through it the request and session) plus the whole ActionInvocation, for
any application using the opt-in `scope` interceptor.
It is now wrapped with Collections.synchronizedMap.
---
.../struts2/interceptor/ScopeInterceptor.java | 7 ++-
.../struts2/interceptor/ScopeInterceptorTest.java | 67 ++++++++++++++++++++++
2 files changed, 73 insertions(+), 1 deletion(-)
diff --git
a/core/src/main/java/org/apache/struts2/interceptor/ScopeInterceptor.java
b/core/src/main/java/org/apache/struts2/interceptor/ScopeInterceptor.java
index c654316c2..083a1bf29 100644
--- a/core/src/main/java/org/apache/struts2/interceptor/ScopeInterceptor.java
+++ b/core/src/main/java/org/apache/struts2/interceptor/ScopeInterceptor.java
@@ -34,6 +34,7 @@ import org.apache.struts2.dispatcher.SessionMap;
import java.io.Serial;
import java.io.Serializable;
+import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Map;
@@ -234,7 +235,7 @@ public class ScopeInterceptor extends AbstractInterceptor
implements PreResultLi
return o;
}
- private static final Map<Object, Object> locks = new IdentityHashMap<>();
+ private static final Map<Object, Object> locks =
Collections.synchronizedMap(new IdentityHashMap<>());
static void lock(Object o, ActionInvocation invocation) throws Exception {
synchronized (o) {
@@ -272,6 +273,10 @@ public class ScopeInterceptor extends AbstractInterceptor
implements PreResultLi
}
}
+ static int lockCount() {
+ return locks.size();
+ }
+
protected void after(ActionInvocation invocation, String result) throws
Exception {
Map<String, Object> session = ActionContext.getContext().getSession();
if ( session != null) {
diff --git
a/core/src/test/java/org/apache/struts2/interceptor/ScopeInterceptorTest.java
b/core/src/test/java/org/apache/struts2/interceptor/ScopeInterceptorTest.java
new file mode 100644
index 000000000..571122bf2
--- /dev/null
+++
b/core/src/test/java/org/apache/struts2/interceptor/ScopeInterceptorTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.mock.MockActionInvocation;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+import static org.junit.Assert.assertEquals;
+
+public class ScopeInterceptorTest {
+
+ @Test
+ public void testConcurrentLockAndUnlockLeavesNoEntries() throws Exception {
+ ScopeInterceptor.clearLocks();
+
+ int threads = 8;
+ int iterations = 500;
+ CountDownLatch start = new CountDownLatch(1);
+ ExecutorService executor = Executors.newFixedThreadPool(threads);
+ try {
+ List<Future<?>> futures = new ArrayList<>();
+ for (int t = 0; t < threads; t++) {
+ futures.add(executor.submit(() -> {
+ start.await();
+ MockActionInvocation invocation = new
MockActionInvocation();
+ for (int i = 0; i < iterations; i++) {
+ Object session = new Object();
+ ScopeInterceptor.lock(session, invocation);
+ ScopeInterceptor.unlock(session);
+ }
+ return null;
+ }));
+ }
+ start.countDown();
+ for (Future<?> future : futures) {
+ future.get();
+ }
+ } finally {
+ executor.shutdownNow();
+ }
+
+ assertEquals(0, ScopeInterceptor.lockCount());
+ }
+}