This is an automated email from the ASF dual-hosted git repository. rzo1 pushed a commit to branch TOMEE-4731 in repository https://gitbox.apache.org/repos/asf/tomee.git
commit 953f3aa7f4e814f93d8cb45d18561ff52e3125dc Author: Richard Zowalla <[email protected]> AuthorDate: Thu Oct 22 09:54:50 2026 +0200 TOMEE-4731 - Do not resume a transaction when there is none or the TransactionManager is gone 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()); + } +}
