diff --git a/extensions/narayana-jta/deployment/src/main/java/io/quarkus/narayana/jta/deployment/NarayanaJtaProcessor.java b/extensions/narayana-jta/deployment/src/main/java/io/quarkus/narayana/jta/deployment/NarayanaJtaProcessor.java index 2ca5d25e3373cd..70ffcff8f4772e 100644 --- a/extensions/narayana-jta/deployment/src/main/java/io/quarkus/narayana/jta/deployment/NarayanaJtaProcessor.java +++ b/extensions/narayana-jta/deployment/src/main/java/io/quarkus/narayana/jta/deployment/NarayanaJtaProcessor.java @@ -50,7 +50,6 @@ import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; import io.quarkus.deployment.logging.LogCleanupFilterBuildItem; import io.quarkus.gizmo.ClassCreator; -import io.quarkus.narayana.jta.runtime.CDIDelegatingTransactionManager; import io.quarkus.narayana.jta.runtime.NarayanaJtaProducers; import io.quarkus.narayana.jta.runtime.NarayanaJtaRecorder; import io.quarkus.narayana.jta.runtime.TransactionManagerConfiguration; @@ -85,7 +84,6 @@ public void build(NarayanaJtaRecorder recorder, recorder.handleShutdown(shutdownContextBuildItem, transactions); feature.produce(new FeatureBuildItem(Feature.NARAYANA_JTA)); additionalBeans.produce(new AdditionalBeanBuildItem(NarayanaJtaProducers.class)); - additionalBeans.produce(new AdditionalBeanBuildItem(CDIDelegatingTransactionManager.class)); additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf("io.quarkus.narayana.jta.RequestScopedTransaction")); runtimeInit.produce(new RuntimeInitializedClassBuildItem( diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NarayanaJtaProducers.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NarayanaJtaProducers.java index 77bbaa4f0f27de..f2172253c13583 100644 --- a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NarayanaJtaProducers.java +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NarayanaJtaProducers.java @@ -5,6 +5,7 @@ import javax.enterprise.inject.Produces; import javax.inject.Singleton; import javax.transaction.TransactionSynchronizationRegistry; +import javax.transaction.UserTransaction; import org.jboss.tm.JBossXATerminator; import org.jboss.tm.XAResourceRecoveryRegistry; @@ -13,7 +14,6 @@ import com.arjuna.ats.internal.jbossatx.jta.jca.XATerminator; import com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionSynchronizationRegistryImple; import com.arjuna.ats.jbossatx.jta.RecoveryManagerService; -import com.arjuna.ats.jta.UserTransaction; import io.quarkus.arc.Unremovable; @@ -28,8 +28,15 @@ public UserTransactionRegistry userTransactionRegistry() { @Produces @ApplicationScoped - public javax.transaction.UserTransaction userTransaction() { - return UserTransaction.userTransaction(); + public UserTransaction userTransaction() { + return new NotifyingUserTransaction(com.arjuna.ats.jta.UserTransaction.userTransaction()); + } + + @Produces + @Unremovable + @Singleton + public javax.transaction.TransactionManager transactionManager() { + return new NotifyingTransactionManager(); } @Produces diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/CDIDelegatingTransactionManager.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NotifyingTransactionManager.java similarity index 65% rename from extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/CDIDelegatingTransactionManager.java rename to extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NotifyingTransactionManager.java index d8caf9dc874873..4cc53cbcf1f335 100644 --- a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/CDIDelegatingTransactionManager.java +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NotifyingTransactionManager.java @@ -6,8 +6,6 @@ import javax.enterprise.context.Destroyed; import javax.enterprise.context.Initialized; import javax.enterprise.event.Event; -import javax.inject.Inject; -import javax.inject.Singleton; import javax.transaction.HeuristicMixedException; import javax.transaction.HeuristicRollbackException; import javax.transaction.InvalidTransactionException; @@ -20,51 +18,20 @@ import org.jboss.logging.Logger; -import io.quarkus.arc.Unremovable; - /** * A delegating transaction manager which receives an instance of Narayana transaction manager * and delegates all calls to it. * On top of it the implementation adds the CDI events processing for {@link TransactionScoped}. */ -@Singleton -@Unremovable // used by Arc for transactional observers -public class CDIDelegatingTransactionManager implements TransactionManager, Serializable { - - private static final Logger log = Logger.getLogger(CDIDelegatingTransactionManager.class); +public class NotifyingTransactionManager extends TransactionScopedNotifier implements TransactionManager, Serializable { private static final long serialVersionUID = 1598L; - private final transient com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionManagerImple delegate; - - /** - * An {@link Event} that can {@linkplain Event#fire(Object) fire} - * {@link Transaction}s when the {@linkplain TransactionScoped transaction scope} is initialized. - */ - @Inject - @Initialized(TransactionScoped.class) - Event transactionScopeInitialized; + private static final Logger LOG = Logger.getLogger(NotifyingTransactionManager.class); - /** - * An {@link Event} that can {@linkplain Event#fire(Object) fire} - * {@link Object}s before the {@linkplain TransactionScoped transaction scope} is destroyed. - */ - @Inject - @BeforeDestroyed(TransactionScoped.class) - Event transactionScopeBeforeDestroyed; + private transient com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionManagerImple delegate; - /** - * An {@link Event} that can {@linkplain Event#fire(Object) fire} - * {@link Object}s when the {@linkplain TransactionScoped transaction scope} is destroyed. - */ - @Inject - @Destroyed(TransactionScoped.class) - Event transactionScopeDestroyed; - - /** - * Delegating transaction manager call to com.arjuna.ats.jta.{@link com.arjuna.ats.jta.TransactionManager} - */ - public CDIDelegatingTransactionManager() { + NotifyingTransactionManager() { delegate = (com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionManagerImple) com.arjuna.ats.jta.TransactionManager .transactionManager(); } @@ -80,9 +47,7 @@ public CDIDelegatingTransactionManager() { @Override public void begin() throws NotSupportedException, SystemException { delegate.begin(); - if (this.transactionScopeInitialized != null) { - this.transactionScopeInitialized.fire(this.getTransaction()); - } + initialized(getTransaction()); } /** @@ -97,16 +62,11 @@ public void begin() throws NotSupportedException, SystemException { @Override public void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SecurityException, IllegalStateException, SystemException { - if (this.transactionScopeBeforeDestroyed != null) { - this.transactionScopeBeforeDestroyed.fire(this.getTransaction()); - } - + beforeDestroyed(this.getTransaction()); try { delegate.commit(); } finally { - if (this.transactionScopeDestroyed != null) { - this.transactionScopeDestroyed.fire(this.toString()); - } + destroyed(this); } } @@ -122,20 +82,16 @@ public void commit() throws RollbackException, HeuristicMixedException, Heuristi @Override public void rollback() throws IllegalStateException, SecurityException, SystemException { try { - if (this.transactionScopeBeforeDestroyed != null) { - this.transactionScopeBeforeDestroyed.fire(this.getTransaction()); - } + beforeDestroyed(getTransaction()); } catch (Throwable t) { - log.error("Failed to fire @BeforeDestroyed(TransactionScoped.class)", t); + LOG.error("Failed to fire @BeforeDestroyed(TransactionScoped.class)", t); } try { delegate.rollback(); } finally { //we don't need a catch block here, if this one fails we just let the exception propagate - if (this.transactionScopeDestroyed != null) { - this.transactionScopeDestroyed.fire(this.toString()); - } + destroyed(this); } } diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NotifyingUserTransaction.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NotifyingUserTransaction.java new file mode 100644 index 00000000000000..0ed3eb04528b52 --- /dev/null +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/NotifyingUserTransaction.java @@ -0,0 +1,70 @@ +package io.quarkus.narayana.jta.runtime; + +import javax.transaction.HeuristicMixedException; +import javax.transaction.HeuristicRollbackException; +import javax.transaction.NotSupportedException; +import javax.transaction.RollbackException; +import javax.transaction.SystemException; +import javax.transaction.UserTransaction; + +import org.jboss.logging.Logger; + +public class NotifyingUserTransaction extends TransactionScopedNotifier implements UserTransaction { + + private static final Logger LOG = Logger.getLogger(NotifyingUserTransaction.class); + + private final UserTransaction delegate; + + public NotifyingUserTransaction(UserTransaction delegate) { + this.delegate = delegate; + } + + @Override + public void begin() throws NotSupportedException, SystemException { + delegate.begin(); + initialized(delegate); + } + + @Override + public void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SecurityException, + IllegalStateException, SystemException { + beforeDestroyed(delegate); + try { + delegate.commit(); + } finally { + // we don't use the tx object because when commit() completes the thread is no longer associated with a transaction + destroyed(this); + } + } + + @Override + public void rollback() throws IllegalStateException, SecurityException, SystemException { + try { + beforeDestroyed(delegate); + } catch (Throwable t) { + LOG.error("Failed to fire @BeforeDestroyed(TransactionScoped.class)", t); + } + try { + delegate.rollback(); + } finally { + // we don't use the tx object because when rollback() completes the thread is no longer associated with a transaction + destroyed(this); + } + } + + @Override + public void setRollbackOnly() throws IllegalStateException, SystemException { + delegate.setRollbackOnly(); + } + + @Override + public int getStatus() throws SystemException { + return delegate.getStatus(); + } + + @Override + public void setTransactionTimeout(int seconds) throws SystemException { + delegate.setTransactionTimeout(seconds); + } + +} diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/TransactionScopedNotifier.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/TransactionScopedNotifier.java new file mode 100644 index 00000000000000..28acab1d39682b --- /dev/null +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/TransactionScopedNotifier.java @@ -0,0 +1,41 @@ +package io.quarkus.narayana.jta.runtime; + +import javax.enterprise.context.BeforeDestroyed; +import javax.enterprise.context.Destroyed; +import javax.enterprise.context.Initialized; +import javax.enterprise.event.Event; +import javax.transaction.TransactionScoped; + +import io.quarkus.arc.Arc; + +abstract class TransactionScopedNotifier { + + private transient Event initialized; + private transient Event beforeDestroyed; + private transient Event destroyed; + + void initialized(Object payload) { + if (initialized == null) { + initialized = Arc.container().beanManager().getEvent() + .select(Initialized.Literal.of(TransactionScoped.class)); + } + initialized.fire(payload); + } + + void beforeDestroyed(Object payload) { + if (beforeDestroyed == null) { + beforeDestroyed = Arc.container().beanManager().getEvent() + .select(BeforeDestroyed.Literal.of(TransactionScoped.class)); + } + beforeDestroyed.fire(payload); + } + + void destroyed(Object payload) { + if (destroyed == null) { + destroyed = Arc.container().beanManager().getEvent() + .select(Destroyed.Literal.of(TransactionScoped.class)); + } + destroyed.fire(payload); + } + +} diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorBase.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorBase.java index cb3d8c49bf8ef5..5bc31cbb0cd8cd 100644 --- a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorBase.java +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TransactionalInterceptorBase.java @@ -27,7 +27,7 @@ import com.arjuna.ats.jta.logging.jtaLogger; import io.quarkus.arc.runtime.InterceptorBindings; -import io.quarkus.narayana.jta.runtime.CDIDelegatingTransactionManager; +import io.quarkus.narayana.jta.runtime.NotifyingTransactionManager; import io.quarkus.narayana.jta.runtime.TransactionConfiguration; import io.quarkus.transaction.annotations.Rollback; import io.smallrye.mutiny.Multi; @@ -110,7 +110,7 @@ protected Object invokeInOurTx(InvocationContext ic, TransactionManager tm, Runn int timeoutConfiguredForMethod = getTransactionTimeoutFromAnnotation(ic); - int currentTmTimeout = ((CDIDelegatingTransactionManager) transactionManager).getTransactionTimeout(); + int currentTmTimeout = ((NotifyingTransactionManager) transactionManager).getTransactionTimeout(); if (timeoutConfiguredForMethod > 0) { tm.setTransactionTimeout(timeoutConfiguredForMethod); diff --git a/integration-tests/narayana-jta/src/main/java/io/quarkus/narayana/jta/TransactionBeanWithEvents.java b/integration-tests/narayana-jta/src/main/java/io/quarkus/narayana/jta/TransactionBeanWithEvents.java index 59919e6c1b952e..7d0ede72be12c5 100644 --- a/integration-tests/narayana-jta/src/main/java/io/quarkus/narayana/jta/TransactionBeanWithEvents.java +++ b/integration-tests/narayana-jta/src/main/java/io/quarkus/narayana/jta/TransactionBeanWithEvents.java @@ -16,6 +16,7 @@ import javax.transaction.TransactionManager; import javax.transaction.TransactionScoped; import javax.transaction.Transactional; +import javax.transaction.UserTransaction; import org.jboss.logging.Logger; @@ -110,10 +111,13 @@ void transactionScopeActivated(@Observes @Initialized(TransactionScoped.class) f log.error("Context on @Initialized has to be active"); throw new IllegalStateException("Context on @Initialized has to be active"); } - if (!(event instanceof Transaction)) { - log.error("@Initialized scope expects event payload being the " + Transaction.class.getName()); + if (!(event instanceof Transaction) && !(event instanceof UserTransaction)) { + log.error( + "@Initialized scope expects event payload being the " + Transaction.class.getName() + " or " + + UserTransaction.class.getName()); throw new IllegalStateException( - "@Initialized scope expects event payload being the " + Transaction.class.getName()); + "@Initialized scope expects event payload being the " + Transaction.class.getName() + " or " + + UserTransaction.class.getName()); } initializedCount++; @@ -130,17 +134,19 @@ void transactionScopePreDestroy(@Observes @BeforeDestroyed(TransactionScoped.cla try { ctx = beanManager.getContext(TransactionScoped.class); } catch (Exception e) { - log.error("Context on @Initialized is not available"); + log.error("Context on @BeforeDestroyed is not available"); throw e; } if (!ctx.isActive()) { log.error("Context on @BeforeDestroyed has to be active"); throw new IllegalStateException("Context on @BeforeDestroyed has to be active"); } - if (!(event instanceof Transaction)) { - log.error("@Initialized scope expects event payload being the " + Transaction.class.getName()); + if (!(event instanceof Transaction) && !(event instanceof UserTransaction)) { + log.error("@BeforeDestroyed scope expects event payload being the " + Transaction.class.getName() + " or " + + UserTransaction.class.getName()); throw new IllegalStateException( - "@Initialized scope expects event payload being the " + Transaction.class.getName()); + "@BeforeDestroyed scope expects event payload being the " + Transaction.class.getName() + " or " + + UserTransaction.class.getName()); } beforeDestroyedCount++; diff --git a/integration-tests/narayana-jta/src/test/java/io/quarkus/narayana/jta/TransactionScopedTest.java b/integration-tests/narayana-jta/src/test/java/io/quarkus/narayana/jta/TransactionScopedTest.java index 6a08d4ae18f26a..0c4897dde9f708 100644 --- a/integration-tests/narayana-jta/src/test/java/io/quarkus/narayana/jta/TransactionScopedTest.java +++ b/integration-tests/narayana-jta/src/test/java/io/quarkus/narayana/jta/TransactionScopedTest.java @@ -59,8 +59,8 @@ void transactionScopedInTransaction() throws Exception { } @Test - void scopeEventsAreEmitted() { - beanEvents.cleanCounts(); + void scopeEventsAreEmitted() throws Exception { + TransactionBeanWithEvents.cleanCounts(); beanEvents.doInTransaction(true); @@ -70,11 +70,14 @@ void scopeEventsAreEmitted() { // expect runtime exception to rollback the call } - assertEquals(2, beanEvents.getInitialized(), "Expected @Initialized to be observed"); - assertEquals(2, beanEvents.getBeforeDestroyed(), "Expected @BeforeDestroyed to be observer"); - assertEquals(2, beanEvents.getDestroyed(), "Expected @Destroyed to be observer"); - assertEquals(1, beanEvents.getCommited(), "Expected commit to be called once"); - assertEquals(1, beanEvents.getRolledBack(), "Expected rollback to be called once"); + tx.begin(); + tx.commit(); + + assertEquals(3, TransactionBeanWithEvents.getInitialized(), "Expected @Initialized to be observed"); + assertEquals(3, TransactionBeanWithEvents.getBeforeDestroyed(), "Expected @BeforeDestroyed to be observer"); + assertEquals(3, TransactionBeanWithEvents.getDestroyed(), "Expected @Destroyed to be observer"); + assertEquals(1, TransactionBeanWithEvents.getCommited(), "Expected commit to be called once"); + assertEquals(1, TransactionBeanWithEvents.getRolledBack(), "Expected rollback to be called once"); } }