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