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());
+    }
+}

Reply via email to