From 5ebd1e102e1bcd5e52924d75d047fb0ff8954c16 Mon Sep 17 00:00:00 2001 From: Ondra Chaloupka Date: Fri, 18 Oct 2019 14:33:09 +0200 Subject: [PATCH] [#3878] adding transaction scope into JTA extension --- .../jta/deployment/NarayanaJtaProcessor.java | 47 ++-- .../CDIDelegatingTransactionManager.java | 186 +++++++++++++++ .../jta/runtime/NarayanaJtaProducers.java | 12 - .../runtime/context/TransactionContext.java | 216 ++++++++++++++++++ .../TransactionalInterceptorBase.java | 5 +- integration-tests/narayana-jta/pom.xml | 89 ++++++++ .../jta/TransactionBeanWithEvents.java | 151 ++++++++++++ .../narayana/jta/TransactionScopedBean.java | 16 ++ .../narayana/jta/TransactionScopedTest.java | 72 ++++++ integration-tests/pom.xml | 1 + tcks/microprofile-context-propagation/pom.xml | 4 - 11 files changed, 763 insertions(+), 36 deletions(-) create mode 100644 extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/CDIDelegatingTransactionManager.java create mode 100644 extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/context/TransactionContext.java create mode 100644 integration-tests/narayana-jta/pom.xml create mode 100644 integration-tests/narayana-jta/src/main/java/io/quarkus/narayana/jta/TransactionBeanWithEvents.java create mode 100644 integration-tests/narayana-jta/src/main/java/io/quarkus/narayana/jta/TransactionScopedBean.java create mode 100644 integration-tests/narayana-jta/src/test/java/io/quarkus/narayana/jta/TransactionScopedTest.java 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 fdc47dcdd..bc263c9bc 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 @@ -4,7 +4,7 @@ import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT; import java.util.Properties; -import javax.inject.Inject; +import javax.transaction.TransactionScoped; import com.arjuna.ats.internal.arjuna.coordinator.CheckedActionFactoryImple; import com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionManagerImple; @@ -14,6 +14,8 @@ import com.arjuna.ats.jta.common.JTAEnvironmentBean; import com.arjuna.common.util.propertyservice.PropertiesFactory; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.ContextRegistrarBuildItem; +import io.quarkus.arc.processor.ContextRegistrar; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -22,9 +24,11 @@ import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.substrate.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.substrate.RuntimeInitializedClassBuildItem; import io.quarkus.deployment.builditem.substrate.SubstrateSystemPropertyBuildItem; +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; +import io.quarkus.narayana.jta.runtime.context.TransactionContext; import io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorMandatory; import io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorNever; import io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorNotSupported; @@ -34,31 +38,27 @@ import io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorSuppo class NarayanaJtaProcessor { - @Inject - BuildProducer additionalBeans; - - @Inject - BuildProducer reflectiveClass; - - @Inject - BuildProducer runtimeInit; - - @BuildStep() - public SubstrateSystemPropertyBuildItem substrateSystemPropertyBuildItem() { - return new SubstrateSystemPropertyBuildItem("CoordinatorEnvironmentBean.transactionStatusManagerEnable", - String.valueOf(transactions.enableTransactionStatusManager)); - } - /** * The transactions configuration. */ TransactionManagerConfiguration transactions; + @BuildStep + public SubstrateSystemPropertyBuildItem substrateSystemPropertyBuildItem() { + return new SubstrateSystemPropertyBuildItem("CoordinatorEnvironmentBean.transactionStatusManagerEnable", + String.valueOf(transactions.enableTransactionStatusManager)); + } + @BuildStep(providesCapabilities = Capabilities.TRANSACTIONS) @Record(RUNTIME_INIT) - public void build(NarayanaJtaRecorder recorder, BuildProducer feature) { + public void build(NarayanaJtaRecorder recorder, + BuildProducer additionalBeans, + BuildProducer reflectiveClass, + BuildProducer runtimeInit, + BuildProducer feature) { feature.produce(new FeatureBuildItem(FeatureBuildItem.NARAYANA_JTA)); additionalBeans.produce(new AdditionalBeanBuildItem(NarayanaJtaProducers.class)); + additionalBeans.produce(new AdditionalBeanBuildItem(CDIDelegatingTransactionManager.class)); runtimeInit.produce(new RuntimeInitializedClassBuildItem( "com.arjuna.ats.internal.jta.resources.arjunacore.CommitMarkableResourceRecord")); reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, JTAEnvironmentBean.class.getName(), @@ -84,4 +84,17 @@ class NarayanaJtaProcessor { recorder.setNodeName(transactions); recorder.setDefaultTimeout(transactions); } + + @BuildStep + public void transactionContext( + BuildProducer contextRegistry) { + + contextRegistry.produce(new ContextRegistrarBuildItem(new ContextRegistrar() { + @Override + public void register(RegistrationContext registrationContext) { + registrationContext.configure(TransactionScoped.class).normal().contextClass(TransactionContext.class).done(); + } + })); + } + } 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/CDIDelegatingTransactionManager.java new file mode 100644 index 000000000..959178eb6 --- /dev/null +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/CDIDelegatingTransactionManager.java @@ -0,0 +1,186 @@ +package io.quarkus.narayana.jta.runtime; + +import java.io.Serializable; + +import javax.enterprise.context.BeforeDestroyed; +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; +import javax.transaction.NotSupportedException; +import javax.transaction.RollbackException; +import javax.transaction.SystemException; +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; +import javax.transaction.TransactionScoped; + +/** + * 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 +public class CDIDelegatingTransactionManager 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) + private Event transactionScopeInitialized; + + /** + * 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) + private Event transactionScopeBeforeDestroyed; + + /** + * 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) + private Event transactionScopeDestroyed; + + /** + * Delegating transaction manager call to com.arjuna.ats.jta.{@link com.arjuna.ats.jta.TransactionManager} + */ + public CDIDelegatingTransactionManager() { + delegate = (com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionManagerImple) com.arjuna.ats.jta.TransactionManager + .transactionManager(); + } + + /** + * Overrides {@link TransactionManager#begin()} to + * additionally {@linkplain Event#fire(Object) fire} an {@link Object} + * representing the {@linkplain Initialized initialization} + * of the {@linkplain TransactionScoped transaction scope}. + * + * @see TransactionManager#begin() + */ + @Override + public void begin() throws NotSupportedException, SystemException { + delegate.begin(); + if (this.transactionScopeInitialized != null) { + this.transactionScopeInitialized.fire(this.getTransaction()); + } + } + + /** + * Overrides {@link TransactionManager#commit()} to + * additionally {@linkplain Event#fire(Object) fire} an {@link Object} + * representing the {@linkplain BeforeDestroyed before destruction} and + * the {@linkplain Destroyed destruction} + * of the {@linkplain TransactionScoped transaction scope}. + * + * @see TransactionManager#commit() + */ + @Override + public void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SecurityException, + IllegalStateException, SystemException { + if (this.transactionScopeBeforeDestroyed != null) { + this.transactionScopeBeforeDestroyed.fire(this.getTransaction()); + } + + try { + delegate.commit(); + } finally { + if (this.transactionScopeDestroyed != null) { + this.transactionScopeDestroyed.fire(this.toString()); + } + } + } + + /** + * Overrides {@link TransactionManager#rollback()} to + * additionally {@linkplain Event#fire(Object) fire} an {@link Object} + * representing the {@linkplain BeforeDestroyed before destruction} and + * the {@linkplain Destroyed destruction} + * of the {@linkplain TransactionScoped transaction scope}. + * + * @see TransactionManager#rollback() + */ + @Override + public void rollback() throws IllegalStateException, SecurityException, SystemException { + if (this.transactionScopeBeforeDestroyed != null) { + this.transactionScopeBeforeDestroyed.fire(this.getTransaction()); + } + + try { + delegate.rollback(); + } finally { + if (this.transactionScopeDestroyed != null) { + this.transactionScopeDestroyed.fire(this.toString()); + } + } + } + + /** + * {@inheritDoc} + */ + @Override + public int getStatus() throws SystemException { + return delegate.getStatus(); + } + + /** + * {@inheritDoc} + */ + @Override + public Transaction getTransaction() throws SystemException { + return delegate.getTransaction(); + } + + /** + * {@inheritDoc} + */ + @Override + public void resume(Transaction transaction) throws InvalidTransactionException, IllegalStateException, SystemException { + delegate.resume(transaction); + } + + /** + * {@inheritDoc} + */ + @Override + public void setRollbackOnly() throws IllegalStateException, SystemException { + delegate.setRollbackOnly(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setTransactionTimeout(int seconds) throws SystemException { + delegate.setTransactionTimeout(seconds); + } + + /** + * Returns transaction timeout in seconds. + * + * @return transaction timeout set currently + * @throws SystemException on an undefined error + */ + public int getTransactionTimeout() throws SystemException { + return delegate.getTimeout(); + } + + /** + * {@inheritDoc} + */ + @Override + public Transaction suspend() throws SystemException { + return delegate.suspend(); + } +} 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 4f94f075b..419d68bbb 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 @@ -3,7 +3,6 @@ package io.quarkus.narayana.jta.runtime; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.context.Dependent; import javax.enterprise.inject.Produces; -import javax.inject.Singleton; import javax.transaction.TransactionSynchronizationRegistry; import org.jboss.tm.JBossXATerminator; @@ -11,18 +10,13 @@ import org.jboss.tm.XAResourceRecoveryRegistry; import org.jboss.tm.usertx.UserTransactionRegistry; import com.arjuna.ats.internal.jbossatx.jta.jca.XATerminator; -import com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionManagerImple; import com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionSynchronizationRegistryImple; import com.arjuna.ats.jbossatx.jta.RecoveryManagerService; -import com.arjuna.ats.jta.TransactionManager; import com.arjuna.ats.jta.UserTransaction; @Dependent public class NarayanaJtaProducers { - private static final javax.transaction.UserTransaction USER_TRANSACTION = UserTransaction.userTransaction(); - private static final com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionManagerImple TRANSACTION_MANAGER = (TransactionManagerImple) TransactionManager - .transactionManager(); @Produces @ApplicationScoped @@ -48,12 +42,6 @@ public class NarayanaJtaProducers { return new TransactionSynchronizationRegistryImple(); } - @Produces - @Singleton - public javax.transaction.TransactionManager transactionManager() { - return TRANSACTION_MANAGER; - } - @Produces @ApplicationScoped public JBossXATerminator xaTerminator() { diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/context/TransactionContext.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/context/TransactionContext.java new file mode 100644 index 000000000..4d6727313 --- /dev/null +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/context/TransactionContext.java @@ -0,0 +1,216 @@ +package io.quarkus.narayana.jta.runtime.context; + +import java.lang.annotation.Annotation; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.stream.Collectors; + +import javax.enterprise.context.ContextNotActiveException; +import javax.enterprise.context.spi.Contextual; +import javax.enterprise.context.spi.CreationalContext; +import javax.transaction.Status; +import javax.transaction.SystemException; +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; +import javax.transaction.TransactionScoped; +import javax.transaction.TransactionSynchronizationRegistry; + +import com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionSynchronizationRegistryImple; + +import io.quarkus.arc.ContextInstanceHandle; +import io.quarkus.arc.ContextInstanceHandleImpl; +import io.quarkus.arc.InjectableBean; +import io.quarkus.arc.InjectableContext; + +/** + * {@link javax.enterprise.context.spi.Context} class which defines the {@link TransactionScoped} context. + */ +public class TransactionContext implements InjectableContext { + // marker object to be put as a key for SynchronizationRegistry to gather all beans created in the scope + private static final Object TRANSACTION_CONTEXT_MARKER = new Object(); + + private final TransactionSynchronizationRegistry transactionSynchronizationRegistry = new TransactionSynchronizationRegistryImple(); + private final TransactionManager transactionManager = com.arjuna.ats.jta.TransactionManager.transactionManager(); + + @Override + public void destroy() { + if (!isActive()) { + return; + } + + TransactionContextState contextState = (TransactionContextState) transactionSynchronizationRegistry + .getResource(TRANSACTION_CONTEXT_MARKER); + if (contextState == null) { + return; + } + contextState.destroy(); + } + + @Override + public void destroy(Contextual contextual) { + if (!isActive()) { + return; + } + TransactionContextState contextState = (TransactionContextState) transactionSynchronizationRegistry + .getResource(TRANSACTION_CONTEXT_MARKER); + if (contextState == null) { + return; + } + contextState.remove(contextual); + } + + @Override + public ContextState getState() { + if (!isActive()) { + throw new ContextNotActiveException("No active transaction on the current thread"); + } + + ContextState result; + TransactionContextState contextState = (TransactionContextState) transactionSynchronizationRegistry + .getResource(TRANSACTION_CONTEXT_MARKER); + if (contextState == null) { + result = new TransactionContextState<>(); + } else { + result = contextState; + } + return result; + } + + @Override + public Class getScope() { + return TransactionScoped.class; + } + + @Override + @SuppressWarnings("unchecked") + public T get(Contextual contextual, CreationalContext creationalContext) { + if (!isActive()) { + throw new ContextNotActiveException(); + } + if (contextual == null) { + throw new IllegalArgumentException("Contextual parameter must not be null"); + } + + TransactionContextState contextState; + contextState = (TransactionContextState) transactionSynchronizationRegistry + .getResource(TRANSACTION_CONTEXT_MARKER); + + if (contextState == null) { + contextState = new TransactionContextState<>(); + transactionSynchronizationRegistry.putResource(TRANSACTION_CONTEXT_MARKER, contextState); + } + + ContextInstanceHandle instanceHandle = contextState.get(contextual); + if (instanceHandle != null) { + return instanceHandle.get(); + } else if (creationalContext != null) { + T createdInstance = contextual.create(creationalContext); + instanceHandle = new ContextInstanceHandleImpl<>((InjectableBean) contextual, createdInstance, + creationalContext); + + contextState.put(contextual, instanceHandle); + + return createdInstance; + } else { + return null; + } + } + + @Override + public T get(Contextual contextual) { + return get(contextual, null); + } + + /** + * The transaction scoped context is active when a transaction is active. + */ + @Override + public boolean isActive() { + Transaction transaction = getCurrentTransaction(); + if (transaction == null) { + return false; + } + + try { + int currentStatus = transaction.getStatus(); + return currentStatus == Status.STATUS_ACTIVE || + currentStatus == Status.STATUS_MARKED_ROLLBACK || + currentStatus == Status.STATUS_PREPARED || + currentStatus == Status.STATUS_UNKNOWN || + currentStatus == Status.STATUS_PREPARING || + currentStatus == Status.STATUS_COMMITTING || + currentStatus == Status.STATUS_ROLLING_BACK; + } catch (SystemException e) { + throw new RuntimeException("Error getting the status of the current transaction", e); + } + } + + private Transaction getCurrentTransaction() { + try { + return transactionManager.getTransaction(); + } catch (SystemException e) { + throw new RuntimeException("Error getting the current transaction", e); + } + } + + /** + * Representing of the context state. It's a container for all available beans in the context. + * It's filled during bean usage and cleared on destroy. + */ + private static class TransactionContextState implements ContextState { + + private final ConcurrentMap, ContextInstanceHandle> mapBeanToInstanceHandle = new ConcurrentHashMap<>(); + + /** + * Put the contextual bean and its handle to the container. + * + * @param bean bean to be added + * @param handle handle for the bean which incorporates the bean, contextual instance and the context + */ + void put(Contextual bean, ContextInstanceHandle handle) { + mapBeanToInstanceHandle.put(bean, handle); + } + + /** + * Remove the bean from the container. + * + * @param bean contextual bean instance + */ + void remove(Contextual bean) { + mapBeanToInstanceHandle.remove(bean); + } + + /** + * Retrieve the bean saved in the container. + * + * @param bean retrieving the bean from the container, otherwise {@code null} is returned + */ + ContextInstanceHandle get(Contextual bean) { + return mapBeanToInstanceHandle.get(bean); + } + + /** + * Destroying all the beans in the container and clearing the container. + */ + void destroy() { + for (ContextInstanceHandle handle : mapBeanToInstanceHandle.values()) { + handle.destroy(); + } + mapBeanToInstanceHandle.clear(); + } + + /** + * Method required by the {@link io.quarkus.arc.InjectableContext.ContextState} interface + * which is then used to get state of the scope in method {@link InjectableContext#getState()} + * + * @return list of context bean and the bean instances which are available in the container + */ + @Override + public Map, Object> getContextualInstances() { + return mapBeanToInstanceHandle.values().stream() + .collect(Collectors.toMap(ContextInstanceHandle::getBean, ContextInstanceHandle::get)); + } + + } +} 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 f6659654e..337882fe8 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 @@ -19,10 +19,10 @@ import org.eclipse.microprofile.reactive.streams.operators.ReactiveStreams; import org.jboss.tm.usertx.client.ServerVMClientUserTransaction; import org.reactivestreams.Publisher; -import com.arjuna.ats.internal.jta.transaction.arjunacore.TransactionManagerImple; 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.TransactionConfiguration; import io.smallrye.reactive.converters.ReactiveTypeConverter; import io.smallrye.reactive.converters.Registry; @@ -97,7 +97,7 @@ public abstract class TransactionalInterceptorBase implements Serializable { throws Exception { TransactionConfiguration configAnnotation = getTransactionConfiguration(ic); - int currentTmTimeout = ((TransactionManagerImple) transactionManager).getTimeout(); + int currentTmTimeout = ((CDIDelegatingTransactionManager) transactionManager).getTransactionTimeout(); if (configAnnotation != null && configAnnotation.timeout() != TransactionConfiguration.UNSET_TIMEOUT) { tm.setTransactionTimeout(configAnnotation.timeout()); } @@ -309,7 +309,6 @@ public abstract class TransactionalInterceptorBase implements Serializable { } protected boolean setUserTransactionAvailable(boolean available) { - boolean previousUserTransactionAvailability = ServerVMClientUserTransaction.isAvailable(); ServerVMClientUserTransaction.setAvailability(available); diff --git a/integration-tests/narayana-jta/pom.xml b/integration-tests/narayana-jta/pom.xml new file mode 100644 index 000000000..6e48d66d8 --- /dev/null +++ b/integration-tests/narayana-jta/pom.xml @@ -0,0 +1,89 @@ + + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + ../ + + 4.0.0 + + quarkus-integration-test-narayana-jta + Quarkus - Integration Tests - Narayana JTA CDI + + + + io.quarkus + quarkus-narayana-jta + + + io.quarkus + quarkus-junit5 + test + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + native-image + + + native + + + + + + io.quarkus + quarkus-maven-plugin + + + native-image + + native-image + + + false + true + true + ${graalvmHome} + + + + + + maven-failsafe-plugin + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + + + + + + + + + + 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 new file mode 100644 index 000000000..3edf5d598 --- /dev/null +++ b/integration-tests/narayana-jta/src/main/java/io/quarkus/narayana/jta/TransactionBeanWithEvents.java @@ -0,0 +1,151 @@ +package io.quarkus.narayana.jta; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.BeforeDestroyed; +import javax.enterprise.context.ContextNotActiveException; +import javax.enterprise.context.Destroyed; +import javax.enterprise.context.Initialized; +import javax.enterprise.context.spi.Context; +import javax.enterprise.event.Observes; +import javax.enterprise.inject.spi.BeanManager; +import javax.inject.Inject; +import javax.transaction.Status; +import javax.transaction.Synchronization; +import javax.transaction.SystemException; +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; +import javax.transaction.TransactionScoped; +import javax.transaction.Transactional; + +import org.jboss.logging.Logger; + +@ApplicationScoped +public class TransactionBeanWithEvents { + private static final Logger log = Logger.getLogger(TransactionBeanWithEvents.class); + + private static int initializedCount, beforeDestroyedCount, destroyedCount; + private static int commitCount, rollbackCount; + + @Inject + private TransactionManager tm; + + static int getInitialized() { + return initializedCount; + } + + static int getBeforeDestroyed() { + return beforeDestroyedCount; + } + + static int getDestroyed() { + return destroyedCount; + } + + static int getCommited() { + return commitCount; + } + + static int getRolledBack() { + return rollbackCount; + } + + @Transactional + void doInTransaction(boolean isCommit) { + log.debug("Running transactional bean method"); + + try { + tm.getTransaction().registerSynchronization(new Synchronization() { + @Override + public void beforeCompletion() { + } + + @Override + public void afterCompletion(int status) { + if (status == Status.STATUS_ROLLEDBACK) { + rollbackCount++; + } else if (status == Status.STATUS_COMMITTED) { + commitCount++; + } else { + throw new IllegalStateException("Expected commit or rollback on transaction synchronization callback"); + } + } + }); + } catch (Exception e) { + throw new IllegalStateException("Cannot get transaction to register synchronization on bean call", e); + } + + if (!isCommit) { + throw new RuntimeException("Rollback here!"); + } + } + + void transactionScopeActivated(@Observes @Initialized(TransactionScoped.class) final Object event, + final BeanManager beanManager) throws SystemException { + Transaction tx = tm.getTransaction(); + if (tx == null) { + log.error("@Intialized expects an active transaction"); + throw new IllegalStateException("@Intialized expects an active transaction"); + } + if (tx.getStatus() != Status.STATUS_ACTIVE) { + log.error("@Initialized expects transaction is Status.STATUS_ACTIVE"); + throw new IllegalStateException("@Initialized expects transaction is Status.STATUS_ACTIVE"); + } + Context ctx = null; + try { + ctx = beanManager.getContext(TransactionScoped.class); + } catch (Exception e) { + log.error("Context on @Initialized is not available"); + throw e; + } + if (!ctx.isActive()) { + 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("@Intialized scope expects event payload being the " + Transaction.class.getName()); + throw new IllegalStateException("@Intialized scope expects event payload being the " + Transaction.class.getName()); + } + + initializedCount++; + } + + void transactionScopePreDestroy(@Observes @BeforeDestroyed(TransactionScoped.class) final Object event, + final BeanManager beanManager) throws SystemException { + Transaction tx = tm.getTransaction(); + if (tx == null) { + log.error("@BeforeDestroyed expects an active transaction"); + throw new IllegalStateException("@BeforeDestroyed expects an active transaction"); + } + Context ctx = null; + try { + ctx = beanManager.getContext(TransactionScoped.class); + } catch (Exception e) { + log.error("Context on @Initialized 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("@Intialized scope expects event payload being the " + Transaction.class.getName()); + throw new IllegalStateException("@Intialized scope expects event payload being the " + Transaction.class.getName()); + } + + beforeDestroyedCount++; + } + + void transactionScopeDestroyed(@Observes @Destroyed(TransactionScoped.class) final Object event, + final BeanManager beanManager) throws SystemException { + Transaction tx = tm.getTransaction(); + if (tx != null) + throw new IllegalStateException("@Destroyed expects no transaction"); + try { + Context ctx = beanManager.getContext(TransactionScoped.class); + throw new IllegalStateException("No bean in context expected but it's " + ctx); + } catch (final ContextNotActiveException expected) { + } + + destroyedCount++; + } +} diff --git a/integration-tests/narayana-jta/src/main/java/io/quarkus/narayana/jta/TransactionScopedBean.java b/integration-tests/narayana-jta/src/main/java/io/quarkus/narayana/jta/TransactionScopedBean.java new file mode 100644 index 000000000..23b56164a --- /dev/null +++ b/integration-tests/narayana-jta/src/main/java/io/quarkus/narayana/jta/TransactionScopedBean.java @@ -0,0 +1,16 @@ +package io.quarkus.narayana.jta; + +import javax.transaction.TransactionScoped; + +@TransactionScoped +public class TransactionScopedBean { + private int value = 0; + + public int getValue() { + return value; + } + + public void setValue(int value) { + this.value = value; + } +} 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 new file mode 100644 index 000000000..5e6f67335 --- /dev/null +++ b/integration-tests/narayana-jta/src/test/java/io/quarkus/narayana/jta/TransactionScopedTest.java @@ -0,0 +1,72 @@ +package io.quarkus.narayana.jta; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import javax.enterprise.context.ContextNotActiveException; +import javax.inject.Inject; +import javax.transaction.Transaction; +import javax.transaction.TransactionManager; +import javax.transaction.UserTransaction; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +class TransactionScopedTest { + @Inject + private UserTransaction tx; + + @Inject + private TransactionManager tm; + + @Inject + private TransactionScopedBean beanTransactional; + + @Inject + private TransactionBeanWithEvents beanEvents; + + @Test + void transactionScopedInTransaction() throws Exception { + tx.begin(); + beanTransactional.setValue(42); + assertEquals(42, beanTransactional.getValue(), "Transaction scope did not save the value"); + Transaction suspendedTransaction = tm.suspend(); + + assertThrows(ContextNotActiveException.class, () -> { + beanTransactional.getValue(); + }, "Not expecting to have available TransactionScoped bean outside of the transaction"); + + tx.begin(); + beanTransactional.setValue(1); + assertEquals(1, beanTransactional.getValue(), "Transaction scope did not save the value"); + tx.commit(); + + assertThrows(ContextNotActiveException.class, () -> { + beanTransactional.getValue(); + }, "Not expecting to have available TransactionScoped bean outside of the transaction"); + + tm.resume(suspendedTransaction); + assertEquals(42, beanTransactional.getValue(), "Transaction scope did not resumed correctly"); + tx.rollback(); + } + + @Test + void scopeEventsAreEmitted() { + beanEvents.doInTransaction(true); + + try { + beanEvents.doInTransaction(false); + } catch (RuntimeException expected) { + // 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"); + } + +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index e0db7a3af..8e3330cad 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -77,6 +77,7 @@ kotlin mongodb-panache narayana-stm + narayana-jta elytron-security-jdbc diff --git a/tcks/microprofile-context-propagation/pom.xml b/tcks/microprofile-context-propagation/pom.xml index e109e2544..a17c96a50 100644 --- a/tcks/microprofile-context-propagation/pom.xml +++ b/tcks/microprofile-context-propagation/pom.xml @@ -29,10 +29,6 @@ org.eclipse.microprofile.context-propagation:microprofile-context-propagation-tck - - - **/JTACDITest.java -