This is an automated email from the ASF dual-hosted git repository.

rzo1 pushed a commit to branch TOMEE-4703
in repository https://gitbox.apache.org/repos/asf/tomee.git


The following commit(s) were added to refs/heads/TOMEE-4703 by this push:
     new 0ca8717454 TOMEE-4699 add a test for the ThreadContext copy race
0ca8717454 is described below

commit 0ca87174542d3b337686fbf0e8c9f8aec0766db5
Author: Richard Zowalla <[email protected]>
AuthorDate: Wed Sep 9 20:56:37 2026 +0200

    TOMEE-4699 add a test for the ThreadContext copy race
    
    Copying a ThreadContext iterates the source map. When the thread owning that
    context updates it at the same time the copy fails with a
    ConcurrentModificationException, which is what made tasks submitted to a 
managed
    executor disappear.
    
    The test copies a context on one thread while another updates it. It fails
    within a fraction of a second without the synchronized block in the copy
    constructor, and passes with it.
---
 .../openejb/threads/ThreadContextCopyRaceTest.java | 100 +++++++++++++++++++++
 1 file changed, 100 insertions(+)

diff --git 
a/container/openejb-core/src/test/java/org/apache/openejb/threads/ThreadContextCopyRaceTest.java
 
b/container/openejb-core/src/test/java/org/apache/openejb/threads/ThreadContextCopyRaceTest.java
new file mode 100644
index 0000000000..350a363747
--- /dev/null
+++ 
b/container/openejb-core/src/test/java/org/apache/openejb/threads/ThreadContextCopyRaceTest.java
@@ -0,0 +1,100 @@
+/**
+ * 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.openejb.threads;
+
+import org.apache.openejb.core.ThreadContext;
+import org.apache.openejb.jee.EnterpriseBean;
+import org.apache.openejb.jee.SingletonBean;
+import org.apache.openejb.junit.ApplicationComposer;
+import org.apache.openejb.testing.Module;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import jakarta.ejb.EJB;
+import jakarta.ejb.Singleton;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+/**
+ * TOMEE-4699: copying a ThreadContext iterates the source map, which fails 
with a
+ * ConcurrentModificationException when the owning thread updates it at the 
same time.
+ */
+@RunWith(ApplicationComposer.class)
+public class ThreadContextCopyRaceTest {
+    @Module
+    public EnterpriseBean bean() {
+        return new SingletonBean(Facade.class).localBean();
+    }
+
+    @EJB
+    private Facade facade;
+
+    @Test
+    public void copyingWhileTheOwnerUpdatesItsContext() throws Exception {
+        facade.hammer();
+    }
+
+    public static class Filler {
+    }
+
+    @Singleton
+    public static class Facade {
+        public void hammer() throws Exception {
+            final ThreadContext caller = ThreadContext.getThreadContext();
+            assertNotNull(caller);
+
+            // widen the window: an entrySet iterator over a bigger map spends 
longer exposed
+            for (int i = 0; i < 16; i++) {
+                caller.set(Filler.class, new Filler());
+            }
+
+            final AtomicBoolean running = new AtomicBoolean(true);
+            final AtomicReference<Throwable> failure = new AtomicReference<>();
+
+            // another thread updates the context while this one copies it, 
which is what the
+            // managed executor used to do
+            final Thread mutator = new Thread(() -> {
+                try {
+                    while (running.get()) {
+                        caller.set(Filler.class, new Filler());
+                        caller.remove(Filler.class);
+                    }
+                } catch (final Throwable t) {
+                    failure.compareAndSet(null, t);
+                }
+            }, "thread-context-mutator");
+            mutator.start();
+
+            try {
+                for (int i = 0; i < 200_000 && failure.get() == null; i++) {
+                    new ThreadContext(caller);
+                }
+            } catch (final Throwable t) {
+                failure.compareAndSet(null, t);
+            } finally {
+                running.set(false);
+                mutator.join(60_000L);
+            }
+
+            assertNull("copying a ThreadContext must not race with its owner: 
" + failure.get(),
+                failure.get());
+        }
+    }
+}

Reply via email to