This is an automated email from the ASF dual-hosted git repository.
jungm pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomee.git
The following commit(s) were added to refs/heads/main by this push:
new d56fbf3b99 TOMEE-4731 - Do not resume a transaction when there is none
or the TransactionManager is gone (#3121)
d56fbf3b99 is described below
commit d56fbf3b99a6d9f19617666efbd43eab28cc9eb6
Author: Richard Zowalla <[email protected]>
AuthorDate: Sat Oct 24 09:20:47 2026 +0200
TOMEE-4731 - Do not resume a transaction when there is none or the
TransactionManager is gone (#3121)
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());
+ }
+}