So far my test case isn't very minimal. ;-)
The bug is that in some cases the TransactionAttribute is being ignored.
(It came up in the context of type NEVER and that's what I've been
testing with.)
The huge test case has a base interface TestTxBase<T,R> and derived
interface TestTx extends TestTxBase<Integer,Boolean>, and an abstract
bean TestTxBaseBean<T,R> implements TestTxBase<T,R>, and a concrete bean
TestTxBean extends TestTxBaseBean<Integer,Boolean> implements TestTx,
then if a method is all of:
A. declared in TestTxBase
B. has a generic argument or return value of type T or R in TestTxBase
C. implemented in TestTxBean
Then the transaction attribute is lost. But in the following cases it is
not lost:
- !A,B,C
declared in TestTx, has a generic arg, implemented in TestTxBean
(in the test, the neverDerived* methods)
- A,!B,C
declared in TestTxBase, has no generic arg, implemented in TestTxBean
(in the test, neverNongeneric(), neverNoReturnOrArg())
- A,B,!C
declared in TestTxBase, has a generic arg, implemented in
TestTxBaseBean
(in the test, neverInBase())
In the backtrace we were looking at earlier, the jboss AOP stuff had the
wrong interceptor in the proxy (had the one for REQUIRES instead of the
one for NEVER). So in the specific complicated case here, somehow jboss
AOP gets the wrong idea.
Havoc
[
log.txt 3K ]
03:09:51,491 DEBUG [com.dumbhippo.server.blocks.TestTxBase] (testTx): at start of TestTxTask run(), transaction status is NO_TRANSACTION-6
03:09:51,493 DEBUG [com.dumbhippo.server.blocks.TestTxBase] (testTx): at mandatory, transaction status is ACTIVE-0
03:09:51,494 DEBUG [com.dumbhippo.server.blocks.TestTxBase] (testTx): mandatoryInBase threw javax.ejb.EJBTransactionRequiredException: public java.lang.Object com.dumbhippo.server.blocks.TestTxBaseBean.mandatoryInBase(java.lang.Object )
03:09:51,495 DEBUG [com.dumbhippo.server.blocks.TestTxBase] (testTx): at never, transaction status is ACTIVE-0
03:09:51,496 DEBUG [com.dumbhippo.server.blocks.TestTxBase] (testTx): at neverNoReturn, transaction status is ACTIVE-0
03:09:51,496 DEBUG [com.dumbhippo.server.blocks.TestTxBase] (testTx): at neverNoArg, transaction status is ACTIVE-0
03:09:51,497 DEBUG [com.dumbhippo.server.blocks.TestTxBase] (testTx): at neverNoReturnOrArg, transaction status is NO_TRANSACTION-6
03:09:51,497 DEBUG [com.dumbhippo.server.blocks.TestTxBase] (testTx): at neverNongeneric, transaction status is NO_TRANSACTION-6
03:09:51,498 DEBUG [com.dumbhippo.server.blocks.TestTxBase] (testTx): at neverDerived, transaction status is NO_TRANSACTION-6
03:09:51,499 DEBUG [com.dumbhippo.server.blocks.TestTxBase] (testTx): at neverDerivedNoReturn, transaction status is NO_TRANSACTION-6
03:09:51,499 DEBUG [com.dumbhippo.server.blocks.TestTxBase] (testTx): at neverDerivedNoArg, transaction status is NO_TRANSACTION-6
03:09:51,499 DEBUG [com.dumbhippo.server.blocks.TestTxBase] (testTx): at neverDerivedNoReturnOrArg, transaction status is NO_TRANSACTION-6
03:09:51,500 DEBUG [com.dumbhippo.server.blocks.TestTxBase] (testTx): at neverDerivedNongeneric, transaction status is NO_TRANSACTION-6
03:09:51,500 DEBUG [com.dumbhippo.server.blocks.TestTxBase] (testTx): at neverInBase, transaction status is NO_TRANSACTION-6
03:09:51,500 DEBUG [com.dumbhippo.server.blocks.TestTxBase] (testTx): at neverNoReturnInBase, transaction status is NO_TRANSACTION-6
03:09:51,501 DEBUG [com.dumbhippo.server.blocks.TestTxBase] (testTx): at neverNoArgInBase, transaction status is NO_TRANSACTION-6
03:09:51,501 DEBUG [com.dumbhippo.server.blocks.TestTxBase] (testTx): at neverNoReturnOrArgInBase, transaction status is NO_TRANSACTION-6
03:09:51,501 DEBUG [com.dumbhippo.server.blocks.TestTxBase] (testTx): at neverNongenericInBase, transaction status is NO_TRANSACTION-6
03:09:51,502 DEBUG [com.dumbhippo.server.blocks.TestTxBase] (testTx): at noAnnotation, transaction status is ACTIVE-0
03:09:51,502 DEBUG [com.dumbhippo.server.blocks.TestTxBase] (testTx): at noAnnotationInBase, transaction status is ACTIVE-0
03:09:51,503 DEBUG [com.dumbhippo.server.blocks.TestTxBase] (testTx): at notSupported, transaction status is ACTIVE-0
03:09:51,503 DEBUG [com.dumbhippo.server.blocks.TestTxBase] (testTx): at notSupportedInBase, transaction status is NO_TRANSACTION-6
03:09:51,503 DEBUG [com.dumbhippo.server.blocks.TestTxBase] (testTx): at required, transaction status is ACTIVE-0
03:09:51,504 DEBUG [com.dumbhippo.server.blocks.TestTxBase] (testTx): at requiredInBase, transaction status is ACTIVE-0
03:09:51,504 DEBUG [com.dumbhippo.server.blocks.TestTxBase] (testTx): at requiresNew, transaction status is ACTIVE-0
03:09:51,505 DEBUG [com.dumbhippo.server.blocks.TestTxBase] (testTx): at requiresNewInBase, transaction status is ACTIVE-0
03:09:51,505 DEBUG [com.dumbhippo.server.blocks.TestTxBase] (testTx): at supports, transaction status is ACTIVE-0
03:09:51,505 DEBUG [com.dumbhippo.server.blocks.TestTxBase] (testTx): at supportsInBase, transaction status is NO_TRANSACTION-6
03:09:51,506 DEBUG [com.dumbhippo.server.blocks.TestTxBase] (testTx): at end of TestTxTask run(), transaction status is NO_TRANSACTION-6
[
TestTxBean.java 2K ]
package com.dumbhippo.server.blocks;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
@Stateless
public class TestTxBean extends TestTxBaseBean<Integer,Boolean> implements TestTx {
public TestTxBean() {
super(TestTx.class);
}
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public Boolean requiresNew(Integer t) {
logTxStatus("requiresNew");
return false;
}
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public Boolean required(Integer t) {
logTxStatus("required");
return false;
}
@TransactionAttribute(TransactionAttributeType.SUPPORTS)
public Boolean supports(Integer t) {
logTxStatus("supports");
return false;
}
@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
public Boolean notSupported(Integer t) {
logTxStatus("notSupported");
return false;
}
@TransactionAttribute(TransactionAttributeType.MANDATORY)
public Boolean mandatory(Integer t) {
logTxStatus("mandatory");
return false;
}
public Boolean noAnnotation(Integer t) {
logTxStatus("noAnnotation");
return false;
}
@TransactionAttribute(TransactionAttributeType.NEVER)
public Boolean never(Integer t) {
logTxStatus("never");
return false;
}
@TransactionAttribute(TransactionAttributeType.NEVER)
public void neverNoReturn(Integer t) {
logTxStatus("neverNoReturn");
}
@TransactionAttribute(TransactionAttributeType.NEVER)
public Boolean neverNoArg() {
logTxStatus("neverNoArg");
return false;
}
@TransactionAttribute(TransactionAttributeType.NEVER)
public void neverNoReturnOrArg() {
logTxStatus("neverNoReturnOrArg");
}
@TransactionAttribute(TransactionAttributeType.NEVER)
public <T,R> R neverDerived(T t) {
logTxStatus("neverDerived");
return null;
}
@TransactionAttribute(TransactionAttributeType.NEVER)
public <T> void neverDerivedNoReturn(T t) {
logTxStatus("neverDerivedNoReturn");
}
@TransactionAttribute(TransactionAttributeType.NEVER)
public <R> R neverDerivedNoArg() {
logTxStatus("neverDerivedNoArg");
return null;
}
@TransactionAttribute(TransactionAttributeType.NEVER)
public void neverDerivedNoReturnOrArg() {
logTxStatus("neverDerivedNoReturnOrArg");
}
@TransactionAttribute(TransactionAttributeType.NEVER)
public int neverDerivedNongeneric(boolean b) {
logTxStatus("neverDerivedNongeneric");
return 0;
}
@TransactionAttribute(TransactionAttributeType.NEVER)
public int neverNongeneric(boolean b) {
logTxStatus("neverNongeneric");
return 0;
}
}
[
TestTx.java < 1K ]
package com.dumbhippo.server.blocks;
import javax.ejb.Local;
@Local
public interface TestTx extends TestTxBase<Integer,Boolean> {
public <T,R> R neverDerived(T t);
public <T> void neverDerivedNoReturn(T t);
public <R> R neverDerivedNoArg();
public void neverDerivedNoReturnOrArg();
public int neverDerivedNongeneric(boolean b);
}
[
TestTxBase.java < 1K ]
package com.dumbhippo.server.blocks;
public interface TestTxBase<T,R> {
public R required(T t);
public R requiresNew(T t);
public R supports(T t);
public R notSupported(T t);
public R never(T t);
public R mandatory(T t);
public R noAnnotation(T t);
public void neverNoReturn(T t);
public R neverNoArg();
public void neverNoReturnOrArg();
public int neverNongeneric(boolean b);
public R requiredInBase(T t);
public R requiresNewInBase(T t);
public R supportsInBase(T t);
public R notSupportedInBase(T t);
public R neverInBase(T t);
public R mandatoryInBase(T t);
public R noAnnotationInBase(T t);
public void neverNoReturnInBase(T t);
public R neverNoArgInBase();
public void neverNoReturnOrArgInBase();
public int neverNongenericInBase(boolean b);
public void test(T t);
}
[
TestTxBaseBean.java 9K ]
package com.dumbhippo.server.blocks;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import javax.ejb.EJB;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.transaction.SystemException;
import javax.transaction.TransactionManager;
import org.slf4j.Logger;
import com.dumbhippo.GlobalSetup;
import com.dumbhippo.ThreadUtils;
import com.dumbhippo.server.TransactionRunner;
import com.dumbhippo.server.util.EJBUtil;
public abstract class TestTxBaseBean<T,R> implements TestTxBase<T,R> {
static final private Logger logger = GlobalSetup.getLogger(TestTxBase.class);
@EJB
private TransactionRunner runner;
private Class<? extends TestTxBase<T,R>> iface;
static protected void logTxStatus(String where) {
TransactionManager tm;
try {
tm = (TransactionManager) (new InitialContext()).lookup("java:/TransactionManager");
} catch (NamingException e) {
throw new RuntimeException("no TransactionManager found", e);
}
int txStatus;
try {
txStatus = tm.getStatus();
} catch (SystemException e) {
throw new RuntimeException("failed to get tx status", e);
}
logger.debug("at {}, transaction status is {}", where,
EJBUtil.transactionStatusString(txStatus));
}
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public R requiredInBase(T t) {
logTxStatus("requiredInBase");
return null;
}
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public R requiresNewInBase(T t) {
logTxStatus("requiresNewInBase");
return null;
}
@TransactionAttribute(TransactionAttributeType.SUPPORTS)
public R supportsInBase(T t) {
logTxStatus("supportsInBase");
return null;
}
@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
public R notSupportedInBase(T t) {
logTxStatus("notSupportedInBase");
return null;
}
@TransactionAttribute(TransactionAttributeType.MANDATORY)
public R mandatoryInBase(T t) {
logTxStatus("mandatoryInBase");
return null;
}
@TransactionAttribute(TransactionAttributeType.NEVER)
public R neverInBase(T t) {
logTxStatus("neverInBase");
return null;
}
public R noAnnotationInBase(T t) {
logTxStatus("noAnnotationInBase");
return null;
}
@TransactionAttribute(TransactionAttributeType.NEVER)
public void neverNoReturnInBase(T t) {
logTxStatus("neverNoReturnInBase");
}
@TransactionAttribute(TransactionAttributeType.NEVER)
public R neverNoArgInBase() {
logTxStatus("neverNoArgInBase");
return null;
}
@TransactionAttribute(TransactionAttributeType.NEVER)
public void neverNoReturnOrArgInBase() {
logTxStatus("neverNoReturnOrArgInBase");
}
@TransactionAttribute(TransactionAttributeType.NEVER)
public int neverNongenericInBase(boolean b) {
logTxStatus("neverNongenericInBase");
return 0;
}
static private class TestTxTask<T,R> implements Runnable, Callable<R> {
private T t;
private Class<? extends TestTxBase<T,R>> iface;
TestTxTask(T t, Class<? extends TestTxBase<T,R>> iface) {
this.t = t;
this.iface = iface;
}
public void run() {
call();
}
public R call() {
logTxStatus("start of TestTxTask run()");
TestTxBase<T,R> testTx = EJBUtil.defaultLookup(iface);
TestTx testTxDerived;
try {
testTxDerived = TestTx.class.cast(testTx);
} catch (ClassCastException e) {
logger.debug("can't cast to TestTx");
testTxDerived = null;
}
try {
testTx.mandatory(t);
} catch (RuntimeException e) {
logger.debug("mandatory threw {}: {}", e.getClass().getName(), e.getMessage());
}
try {
testTx.mandatoryInBase(t);
} catch (RuntimeException e) {
logger.debug("mandatoryInBase threw {}: {}", e.getClass().getName(), e.getMessage());
}
try {
testTx.never(t);
} catch (RuntimeException e) {
logger.debug("never threw {}: {}", e.getClass().getName(), e.getMessage());
}
try {
testTx.neverNoReturn(t);
} catch (RuntimeException e) {
logger.debug("neverNoReturn threw {}: {}", e.getClass().getName(), e.getMessage());
}
try {
testTx.neverNoArg();
} catch (RuntimeException e) {
logger.debug("neverNoArg threw {}: {}", e.getClass().getName(), e.getMessage());
}
try {
testTx.neverNoReturnOrArg();
} catch (RuntimeException e) {
logger.debug("neverNoReturnOrArg threw {}: {}", e.getClass().getName(), e.getMessage());
}
try {
testTx.neverNongeneric(true);
} catch (RuntimeException e) {
logger.debug("neverNongeneric threw {}: {}", e.getClass().getName(), e.getMessage());
}
try {
testTxDerived.neverDerived(42);
} catch (RuntimeException e) {
logger.debug("neverDerived threw {}: {}", e.getClass().getName(), e.getMessage());
}
try {
testTxDerived.neverDerivedNoReturn(42);
} catch (RuntimeException e) {
logger.debug("neverDerivedNoReturn threw {}: {}", e.getClass().getName(), e.getMessage());
}
try {
testTxDerived.neverDerivedNoArg();
} catch (RuntimeException e) {
logger.debug("neverDerivedNoArg threw {}: {}", e.getClass().getName(), e.getMessage());
}
try {
testTxDerived.neverDerivedNoReturnOrArg();
} catch (RuntimeException e) {
logger.debug("neverDerivedNoReturnOrArg threw {}: {}", e.getClass().getName(), e.getMessage());
}
try {
testTxDerived.neverDerivedNongeneric(true);
} catch (RuntimeException e) {
logger.debug("neverDerivedNongeneric threw {}: {}", e.getClass().getName(), e.getMessage());
}
try {
testTx.neverInBase(t);
} catch (RuntimeException e) {
logger.debug("neverInBase threw {}: {}", e.getClass().getName(), e.getMessage());
}
try {
testTxDerived.neverNoReturnInBase(42);
} catch (RuntimeException e) {
logger.debug("neverNoReturnInBase threw {}: {}", e.getClass().getName(), e.getMessage());
}
try {
testTxDerived.neverNoArgInBase();
} catch (RuntimeException e) {
logger.debug("neverNoArgInBase threw {}: {}", e.getClass().getName(), e.getMessage());
}
try {
testTxDerived.neverNoReturnOrArgInBase();
} catch (RuntimeException e) {
logger.debug("neverNoReturnOrArgInBase threw {}: {}", e.getClass().getName(), e.getMessage());
}
try {
testTxDerived.neverNongenericInBase(true);
} catch (RuntimeException e) {
logger.debug("neverNongenericInBase threw {}: {}", e.getClass().getName(), e.getMessage());
}
try {
testTx.noAnnotation(t);
} catch (RuntimeException e) {
logger.debug("noAnnotation threw {}: {}", e.getClass().getName(), e.getMessage());
}
try {
testTx.noAnnotationInBase(t);
} catch (RuntimeException e) {
logger.debug("noAnnotationInBase threw {}: {}", e.getClass().getName(), e.getMessage());
}
try {
testTx.notSupported(t);
} catch (RuntimeException e) {
logger.debug("notSupported threw {}: {}", e.getClass().getName(), e.getMessage());
}
try {
testTx.notSupportedInBase(t);
} catch (RuntimeException e) {
logger.debug("notSupportedInBase threw {}: {}", e.getClass().getName(), e.getMessage());
}
try {
testTx.required(t);
} catch (RuntimeException e) {
logger.debug("required threw {}: {}", e.getClass().getName(), e.getMessage());
}
try {
testTx.requiredInBase(t);
} catch (RuntimeException e) {
logger.debug("requiredInBase threw {}: {}", e.getClass().getName(), e.getMessage());
}
try {
testTx.requiresNew(t);
} catch (RuntimeException e) {
logger.debug("requiresNew threw {}: {}", e.getClass().getName(), e.getMessage());
}
try {
testTx.requiresNewInBase(t);
} catch (RuntimeException e) {
logger.debug("requiresNewInBase threw {}: {}", e.getClass().getName(), e.getMessage());
}
try {
testTx.supports(t);
} catch (RuntimeException e) {
logger.debug("supports threw {}: {}", e.getClass().getName(), e.getMessage());
}
try {
testTx.supportsInBase(t);
} catch (RuntimeException e) {
logger.debug("supportsInBase threw {}: {}", e.getClass().getName(), e.getMessage());
}
logTxStatus("end of TestTxTask run()");
return null;
}
}
protected TestTxBaseBean(Class<? extends TestTxBase<T,R>> iface) {
this.iface = iface;
}
public void test(T t) {
/*
try {
logger.debug("==Running tests inside a new transaction");
runner.runTaskInNewTransaction((Runnable)new TestTxTask<T,R>(null, iface));
} catch (RuntimeException e) {
logger.debug("==Error running tests in new transaction", e);
}
*/
try {
logger.debug("==Running tests outside a transaction in another thread");
ExecutorService executor = ThreadUtils.newSingleThreadExecutor("testTx");
executor.execute(new TestTxTask<T,R>(null, iface));
} catch (RuntimeException e) {
logger.debug("==Error running tests in thread", e);
}
/*
try {
logger.debug("==Running tests inside a UniqueTaskExecutor");
UniqueTaskExecutor<T,R> executor = new UniqueTaskExecutor<T,R>("testTx");
Future<R> fb = executor.execute(t, new TestTxTask<T,R>(null, iface));
Future<R> fb2 = executor.execute(t, new TestTxTask<T,R>(null, iface));
try {
fb.get();
} catch (InterruptedException e) {
logger.debug("get() exception", e);
} catch (ExecutionException e) {
logger.debug("get() exception", e);
}
logger.debug("== (doing a second get() on the future)");
try {
fb.get();
} catch (InterruptedException e) {
logger.debug("get() 2 exception", e);
} catch (ExecutionException e) {
logger.debug("get() 2 exception", e);
}
logger.debug("== (doing a get() on the second future)");
try {
fb2.get();
} catch (InterruptedException e) {
logger.debug("get() 3 exception", e);
} catch (ExecutionException e) {
logger.debug("get() 3 exception", e);
}
} catch (RuntimeException e) {
logger.debug("==Error running tests in UniqueTaskExecutor");
}
*/
}
}