diff --git a/extensions/narayana-jta/deployment/src/test/java/io/quarkus/narayana/interceptor/AnnotatedError.java b/extensions/narayana-jta/deployment/src/test/java/io/quarkus/narayana/interceptor/AnnotatedError.java new file mode 100644 index 0000000000000..15bdf44db911a --- /dev/null +++ b/extensions/narayana-jta/deployment/src/test/java/io/quarkus/narayana/interceptor/AnnotatedError.java @@ -0,0 +1,8 @@ +package io.quarkus.narayana.interceptor; + +import io.quarkus.narayana.jta.Rollback; + +// prevent a rollback to counter the default +@Rollback(false) +public class AnnotatedError extends Error { +} diff --git a/extensions/narayana-jta/deployment/src/test/java/io/quarkus/narayana/interceptor/AnnotatedTestException.java b/extensions/narayana-jta/deployment/src/test/java/io/quarkus/narayana/interceptor/AnnotatedTestException.java new file mode 100644 index 0000000000000..0bb9303b3484f --- /dev/null +++ b/extensions/narayana-jta/deployment/src/test/java/io/quarkus/narayana/interceptor/AnnotatedTestException.java @@ -0,0 +1,8 @@ +package io.quarkus.narayana.interceptor; + +import io.quarkus.narayana.jta.Rollback; + +// force a rollback to counter the default +@Rollback +public class AnnotatedTestException extends Exception { +} diff --git a/extensions/narayana-jta/deployment/src/test/java/io/quarkus/narayana/interceptor/TransactionalTest.java b/extensions/narayana-jta/deployment/src/test/java/io/quarkus/narayana/interceptor/TransactionalTest.java index bb7efbd941bdb..a8d8608aef2a4 100644 --- a/extensions/narayana-jta/deployment/src/test/java/io/quarkus/narayana/interceptor/TransactionalTest.java +++ b/extensions/narayana-jta/deployment/src/test/java/io/quarkus/narayana/interceptor/TransactionalTest.java @@ -23,7 +23,7 @@ public class TransactionalTest { static final QuarkusUnitTest config = new QuarkusUnitTest() .withApplicationRoot((jar) -> jar .addClasses(TransactionalTest.TransactionalBean.class, TestXAResource.class, - TxAssertionData.class, TestException.class)) + TxAssertionData.class, TestException.class, AnnotatedTestException.class, AnnotatedError.class)) .addClassLoaderEventListener(ClassLoaderLimiter.builder() .neverLoadedRuntimeClassName("javax.xml.stream.XMLInputFactory").build()); @@ -98,6 +98,32 @@ public void transactionalThrowError() { Assertions.assertEquals(1, txAssertionData.getRollback()); } + @Test + public void transactionalThrowAnnotatedApplicationException() { + assertTransactionInactive(); + try { + testTransactionalBean.executeTransactionalThrowException(AnnotatedTestException.class); + Assertions.fail("Expecting TestException to be thrown and the execution does not reach this point"); + } catch (Throwable expected) { + } + assertTransactionInactive(); + Assertions.assertEquals(0, txAssertionData.getCommit()); + Assertions.assertEquals(1, txAssertionData.getRollback()); + } + + @Test + public void transactionalThrowAnnotatedError() { + assertTransactionInactive(); + try { + testTransactionalBean.executeTransactionalThrowException(AnnotatedError.class); + Assertions.fail("Expecting Error to be thrown and the execution does not reach this point"); + } catch (Throwable expected) { + } + assertTransactionInactive(); + Assertions.assertEquals(1, txAssertionData.getCommit()); + Assertions.assertEquals(0, txAssertionData.getRollback()); + } + @Test public void transactionalThrowApplicationExceptionWithRollbackOn() { assertTransactionInactive(); diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/Rollback.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/Rollback.java new file mode 100644 index 0000000000000..874825547db35 --- /dev/null +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/Rollback.java @@ -0,0 +1,27 @@ +package io.quarkus.narayana.jta; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * If you annotate your exception with this annotation, the transactional interceptor will + * use the exception's instructions to force a rollback or not. + * + * FIXME: move to SPI so it can be used by TransactionalReactive + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface Rollback { + /** + * Specify whether the annotated exception should cause a rollback or not. Defaults to true. + * + * @return true if the annotated exception should cause a rollback or not. + */ + boolean value() default true; +} 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 2aed87b4d758d..2c6a0ead10ee3 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,6 +27,7 @@ import com.arjuna.ats.jta.logging.jtaLogger; import io.quarkus.arc.runtime.InterceptorBindings; +import io.quarkus.narayana.jta.Rollback; import io.quarkus.narayana.jta.runtime.CDIDelegatingTransactionManager; import io.quarkus.narayana.jta.runtime.TransactionConfiguration; import io.smallrye.mutiny.Multi; @@ -323,7 +324,6 @@ private void checkConfiguration(InvocationContext ic) { protected void handleExceptionNoThrow(InvocationContext ic, Throwable t, Transaction tx) throws IllegalStateException, SystemException { - Transactional transactional = getTransactional(ic); for (Class dontRollbackOnClass : transactional.dontRollbackOn()) { @@ -339,6 +339,15 @@ protected void handleExceptionNoThrow(InvocationContext ic, Throwable t, Transac } } + Rollback rollbackAnnotation = t.getClass().getAnnotation(Rollback.class); + if (rollbackAnnotation != null) { + if (rollbackAnnotation.value()) { + tx.setRollbackOnly(); + } + // in both cases, behaviour is specified by the annotation + return; + } + // RuntimeException and Error are un-checked exceptions and rollback is expected if (t instanceof RuntimeException || t instanceof Error) { tx.setRollbackOnly();