Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

java.lang.Error should rollback the JTA transaction #16459

Merged
merged 1 commit into from
Apr 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package io.quarkus.narayana.interceptor;

public class TestException extends Exception {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package io.quarkus.narayana.interceptor;

import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;

public class TestXAResource implements XAResource {
final TxAssertionData txAssertionData;

TestXAResource(TxAssertionData txAssertionData) {
this.txAssertionData = txAssertionData;
}

@Override
public void commit(Xid xid, boolean b) throws XAException {
txAssertionData.addCommit();
}

@Override
public void end(Xid xid, int i) throws XAException {
}

@Override
public void forget(Xid xid) throws XAException {
}

@Override
public int getTransactionTimeout() throws XAException {
return 0;
}

@Override
public boolean isSameRM(XAResource xaResource) throws XAException {
return false;
}

@Override
public int prepare(Xid xid) throws XAException {
return XA_OK;
}

@Override
public Xid[] recover(int i) throws XAException {
return new Xid[0];
}

@Override
public void rollback(Xid xid) throws XAException {
txAssertionData.addRollback();
}

@Override
public boolean setTransactionTimeout(int i) throws XAException {
return false;
}

@Override
public void start(Xid xid, int i) throws XAException {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
package io.quarkus.narayana.interceptor;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.transaction.RollbackException;
import javax.transaction.Status;
import javax.transaction.SystemException;
import javax.transaction.TransactionManager;
import javax.transaction.Transactional;
import javax.transaction.UserTransaction;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;

public class TransactionalTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addClasses(TransactionalTest.TransactionalBean.class, TestXAResource.class,
TxAssertionData.class, TestException.class));

@Inject
private TransactionManager tm;

@Inject
private UserTransaction userTransaction;

@Inject
private TransactionalTest.TransactionalBean testTransactionalBean;

@Inject
private TxAssertionData txAssertionData;

@AfterEach
public void tearDown() {
try {
userTransaction.rollback();
} catch (Exception e) {
// do nothing
} finally {
txAssertionData.reset();
}
}

@Test
public void transactionalRequiresToCommit() throws Exception {
assertTransactionInactive();
testTransactionalBean.executeTransactional();
assertTransactionInactive();
Assertions.assertEquals(1, txAssertionData.getCommit());
Assertions.assertEquals(0, txAssertionData.getRollback());
}

@Test
public void transactionalThrowRuntimeException() {
assertTransactionInactive();
try {
testTransactionalBean.executeTransactionalThrowException(RuntimeException.class);
Assertions.fail("Expecting RuntimeException 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 transactionalThrowApplicationException() {
assertTransactionInactive();
try {
testTransactionalBean.executeTransactionalThrowException(TestException.class);
Assertions.fail("Expecting TestException 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 transactionalThrowError() {
assertTransactionInactive();
try {
testTransactionalBean.executeTransactionalThrowException(Error.class);
Assertions.fail("Expecting Error 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 transactionalThrowApplicationExceptionWithRollbackOn() {
assertTransactionInactive();
try {
testTransactionalBean.executeTransactionalRollbackOnException(TestException.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 transactionalThrowRuntimeExceptionWithDontRollbackOn() {
assertTransactionInactive();
try {
testTransactionalBean.executeTransactionalDontRollbackOnRuntimeException(RuntimeException.class);
Assertions.fail("Expecting RuntimeException 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 transactionalThrowErrorWithDontRollbackOn() {
assertTransactionInactive();
try {
testTransactionalBean.executeTransactionalDontRollbackOnError(Error.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 transactionalThrowApplicationExceptionDontRollbackOnPriority() {
assertTransactionInactive();
try {
testTransactionalBean.executeTransactionalRollbackOnPriority(TestException.class);
Assertions.fail("Expecting TestException 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());
}

private void assertTransactionInactive() {
try {
if (tm.getTransaction() != null) {
Assertions.assertNotEquals(Status.STATUS_ACTIVE, tm.getTransaction().getStatus());
}
} catch (Exception e) {
Assertions.fail(e.getMessage());
}
}

@ApplicationScoped
static class TransactionalBean {
@Inject
private TransactionManager transactionManager;

@Inject
private TxAssertionData txAssertionData;

private void enlist() throws SystemException, RollbackException {
transactionManager.getTransaction()
.enlistResource(new TestXAResource(txAssertionData));
}

@Transactional
public void executeTransactional() throws Exception {
enlist();
}

@Transactional
public void executeTransactionalThrowException(Class<? extends Throwable> throwable) throws Throwable {
enlist();
throw throwable.newInstance();
}

@Transactional(rollbackOn = Exception.class)
public void executeTransactionalRollbackOnException(Class<? extends Throwable> throwable) throws Throwable {
enlist();
throw throwable.newInstance();
}

@Transactional(dontRollbackOn = RuntimeException.class)
public void executeTransactionalDontRollbackOnRuntimeException(Class<? extends Throwable> throwable) throws Throwable {
enlist();
throw throwable.newInstance();
}

@Transactional(dontRollbackOn = Error.class)
public void executeTransactionalDontRollbackOnError(Class<? extends Throwable> throwable) throws Throwable {
enlist();
throw throwable.newInstance();
}

@Transactional(dontRollbackOn = Exception.class, rollbackOn = Exception.class)
public void executeTransactionalRollbackOnPriority(Class<? extends Throwable> throwable) throws Throwable {
enlist();
throw throwable.newInstance();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.quarkus.narayana.interceptor;

import java.util.concurrent.atomic.AtomicInteger;

import javax.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class TxAssertionData {
private AtomicInteger commitNumber = new AtomicInteger();
private AtomicInteger rollbackNumber = new AtomicInteger();

public void reset() {
commitNumber.set(0);
rollbackNumber.set(0);
}

public int addCommit() {
return commitNumber.incrementAndGet();
}

public int addRollback() {
return rollbackNumber.incrementAndGet();
}

public int getCommit() {
return commitNumber.get();
}

public int getRollback() {
return rollbackNumber.get();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,9 @@ protected Object invokeInOurTx(InvocationContext ic, TransactionManager tm, Runn

try {
ret = ic.proceed();
} catch (Exception e) {
} catch (Throwable t) {
throwing = true;
handleException(ic, e, tx);
handleException(ic, t, tx);
} finally {
// handle asynchronously if not throwing
if (!throwing && ret != null) {
Expand Down Expand Up @@ -250,8 +250,8 @@ protected Object invokeInCallerTx(InvocationContext ic, Transaction tx) throws E
try {
checkConfiguration(ic);
return ic.proceed();
} catch (Exception e) {
handleException(ic, e, tx);
} catch (Throwable t) {
handleException(ic, t, tx);
}
throw new RuntimeException("UNREACHABLE");
}
Expand All @@ -269,34 +269,35 @@ private void checkConfiguration(InvocationContext ic) {
}
}

protected void handleExceptionNoThrow(InvocationContext ic, Throwable e, Transaction tx)
protected void handleExceptionNoThrow(InvocationContext ic, Throwable t, Transaction tx)
throws IllegalStateException, SystemException {

Transactional transactional = getTransactional(ic);

for (Class<?> dontRollbackOnClass : transactional.dontRollbackOn()) {
if (dontRollbackOnClass.isAssignableFrom(e.getClass())) {
if (dontRollbackOnClass.isAssignableFrom(t.getClass())) {
return;
}
}

for (Class<?> rollbackOnClass : transactional.rollbackOn()) {
if (rollbackOnClass.isAssignableFrom(e.getClass())) {
if (rollbackOnClass.isAssignableFrom(t.getClass())) {
tx.setRollbackOnly();
return;
}
}

if (e instanceof RuntimeException) {
// RuntimeException and Error are un-checked exceptions and rollback is expected
if (t instanceof RuntimeException || t instanceof Error) {
tx.setRollbackOnly();
return;
}
}

protected void handleException(InvocationContext ic, Exception e, Transaction tx) throws Exception {
protected void handleException(InvocationContext ic, Throwable t, Transaction tx) throws Exception {

handleExceptionNoThrow(ic, e, tx);
throw e;
handleExceptionNoThrow(ic, t, tx);
sneakyThrow(t);
}

protected void endTransaction(TransactionManager tm, Transaction tx, RunnableWithException afterEndTransaction)
Expand Down Expand Up @@ -327,4 +328,16 @@ protected boolean setUserTransactionAvailable(boolean available) {
protected void resetUserTransactionAvailability(boolean previousUserTransactionAvailability) {
ServerVMClientUserTransaction.setAvailability(previousUserTransactionAvailability);
}

/**
* An utility method to throw any exception as a {@link RuntimeException}.
* We may throw a checked exception (subtype of {@code Throwable} or {@code Exception}) as un-checked exception.
* This considers the Java 8 inference rule that states that a {@code throws E} is inferred as {@code RuntimeException}.
*
* This method can be used in {@code throw} statement such as: {@code throw sneakyThrow(exception);}.
*/
@SuppressWarnings("unchecked")
private static <E extends Throwable> void sneakyThrow(Throwable e) throws E {
throw (E) e;
}
}