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

jungm pushed a commit to branch tomee-10.x
in repository https://gitbox.apache.org/repos/asf/tomee.git


The following commit(s) were added to refs/heads/tomee-10.x by this push:
     new 8eef035325 TOMEE-4731 - Do not resume a transaction when there is none 
or the TransactionManager is gone (#3122)
8eef035325 is described below

commit 8eef03532557a4edb109d39679d678b6cd2b2345
Author: Richard Zowalla <[email protected]>
AuthorDate: Sat Oct 24 09:20:55 2026 +0200

    TOMEE-4731 - Do not resume a transaction when there is none or the 
TransactionManager is gone (#3122)
    
    Threads created by ManagedThreadFactoryImpl run their whole life inside the
    default context service, which clears the transaction context. The restorer
    created when entering that context always called
    OpenEJB.getTransactionManager().resume(..) on exit, even if suspend() had
    returned null. When such a thread terminated after the container had been
    shut down the TransactionManager was gone and the thread died with a
    NullPointerException.
    
    The clearing snapshot now returns the no-op restorer if no transaction was
    suspended, and the restorer skips resume (logging a warning) if the
    TransactionManager is no longer available.
---
 .../threads/impl/TxThreadContextProvider.java      |  22 +++-
 .../threads/impl/TxThreadContextProviderTest.java  | 129 +++++++++++++++++++++
 2 files changed, 149 insertions(+), 2 deletions(-)

diff --git 
a/container/openejb-core/src/main/java/org/apache/openejb/threads/impl/TxThreadContextProvider.java
 
b/container/openejb-core/src/main/java/org/apache/openejb/threads/impl/TxThreadContextProvider.java
index fa867fb789..cac32b7767 100755
--- 
a/container/openejb-core/src/main/java/org/apache/openejb/threads/impl/TxThreadContextProvider.java
+++ 
b/container/openejb-core/src/main/java/org/apache/openejb/threads/impl/TxThreadContextProvider.java
@@ -26,6 +26,8 @@ import jakarta.transaction.Transaction;
 import jakarta.transaction.TransactionManager;
 import org.apache.openejb.OpenEJB;
 import org.apache.openejb.OpenEJBRuntimeException;
+import org.apache.openejb.util.LogCategory;
+import org.apache.openejb.util.Logger;
 
 import java.io.Serializable;
 import java.util.Map;
@@ -33,6 +35,8 @@ import java.util.Map;
 public class TxThreadContextProvider implements ThreadContextProvider, 
Serializable {
     public static final TxThreadContextProvider INSTANCE = new 
TxThreadContextProvider();
 
+    private static final Logger LOGGER = 
Logger.getInstance(LogCategory.OPENEJB, TxThreadContextProvider.class);
+
     @Override
     public ThreadContextSnapshot currentContext(final Map<String, String> 
props) {
         return ThreadContextProviderUtil.NOOP_SNAPSHOT;
@@ -56,7 +60,10 @@ public class TxThreadContextProvider implements 
ThreadContextProvider, Serializa
             try {
                 TransactionManager tm = OpenEJB.getTransactionManager();
                 if (tm != null) {
-                    return new TxThreadContextRestorer(tm.suspend());
+                    final Transaction suspended = tm.suspend();
+                    if (suspended != null) {
+                        return new TxThreadContextRestorer(suspended);
+                    }
                 }
 
                 return ThreadContextProviderUtil.NOOP_RESTORER;
@@ -75,8 +82,19 @@ public class TxThreadContextProvider implements 
ThreadContextProvider, Serializa
 
         @Override
         public void endContext() throws IllegalStateException {
+            if (transaction == null) {
+                return;
+            }
+
+            final TransactionManager tm = OpenEJB.getTransactionManager();
+            if (tm == null) {
+                // the container was shut down while the context was active, 
there is nothing left to resume into
+                LOGGER.warning("Cannot resume transaction " + transaction + ", 
the TransactionManager is no longer available");
+                return;
+            }
+
             try {
-                OpenEJB.getTransactionManager().resume(transaction);
+                tm.resume(transaction);
             } catch (SystemException | InvalidTransactionException e) {
                 throw new OpenEJBRuntimeException(e);
             }
diff --git 
a/container/openejb-core/src/test/java/org/apache/openejb/threads/impl/TxThreadContextProviderTest.java
 
b/container/openejb-core/src/test/java/org/apache/openejb/threads/impl/TxThreadContextProviderTest.java
new file mode 100644
index 0000000000..33b8244e04
--- /dev/null
+++ 
b/container/openejb-core/src/test/java/org/apache/openejb/threads/impl/TxThreadContextProviderTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.impl;
+
+import jakarta.enterprise.concurrent.spi.ThreadContextRestorer;
+import jakarta.transaction.Transaction;
+import jakarta.transaction.TransactionManager;
+import org.apache.geronimo.transaction.manager.GeronimoTransactionManager;
+import org.apache.openejb.loader.SystemInstance;
+import org.apache.openejb.ri.sp.PseudoSecurityService;
+import org.apache.openejb.spi.SecurityService;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * TOMEE-4731: clearing the transaction context must not fail when there is 
nothing to resume
+ * or when the container (and its TransactionManager) is already gone.
+ */
+public class TxThreadContextProviderTest {
+    private TransactionManager tm;
+
+    @Before
+    public void setUp() throws Exception {
+        SystemInstance.reset();
+        SystemInstance.get().setComponent(SecurityService.class, new 
PseudoSecurityService());
+        tm = new GeronimoTransactionManager();
+        SystemInstance.get().setComponent(TransactionManager.class, tm);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (tm.getTransaction() != null) {
+            tm.rollback();
+        }
+        SystemInstance.reset();
+    }
+
+    @Test
+    public void beginWithoutTransactionIsANoOp() {
+        final ThreadContextRestorer restorer = 
TxThreadContextProvider.TxThreadContextClearingSnapshot.INSTANCE.begin();
+        assertSame(ThreadContextProviderUtil.NOOP_RESTORER, restorer);
+    }
+
+    @Test
+    public void endContextToleratesAGoneTransactionManager() throws Exception {
+        tm.begin();
+        final Transaction transaction = tm.getTransaction();
+        final ThreadContextRestorer restorer = 
TxThreadContextProvider.TxThreadContextClearingSnapshot.INSTANCE.begin();
+        assertNull(tm.getTransaction());
+
+        // simulates the container shutting down while the context is still 
active
+        SystemInstance.reset();
+
+        try {
+            restorer.endContext();
+        } finally {
+            transaction.rollback();
+        }
+    }
+
+    @Test
+    public void suspendedTransactionIsResumed() throws Exception {
+        tm.begin();
+        final Transaction transaction = tm.getTransaction();
+        assertNotNull(transaction);
+
+        final ThreadContextRestorer restorer = 
TxThreadContextProvider.TxThreadContextClearingSnapshot.INSTANCE.begin();
+        assertNull("the transaction is cleared while the context is active", 
tm.getTransaction());
+
+        restorer.endContext();
+        assertSame("and resumed afterwards", transaction, tm.getTransaction());
+    }
+
+    @Test
+    public void managedThreadTerminatingAfterShutdownDoesNotFail() throws 
Exception {
+        final ManagedThreadFactoryImpl factory = new ManagedThreadFactoryImpl(
+            "tomee-4731-", null, 
ContextServiceImplFactory.newDefaultContextService());
+
+        final CountDownLatch running = new CountDownLatch(1);
+        final CountDownLatch release = new CountDownLatch(1);
+        final AtomicReference<Throwable> uncaught = new AtomicReference<>();
+
+        final Thread thread = factory.newThread(() -> {
+            running.countDown();
+            try {
+                release.await();
+            } catch (final InterruptedException e) {
+                Thread.currentThread().interrupt();
+            }
+        });
+        thread.setUncaughtExceptionHandler((t, e) -> uncaught.set(e));
+        thread.start();
+
+        assertTrue(running.await(1, TimeUnit.MINUTES));
+
+        // the container shuts down while the worker thread is still alive
+        SystemInstance.reset();
+
+        release.countDown();
+        thread.join(TimeUnit.MINUTES.toMillis(1));
+        assertFalse(thread.isAlive());
+        assertNull("the thread must terminate cleanly but got " + 
uncaught.get(), uncaught.get());
+    }
+}

Reply via email to