Why does CMT commit on exit of EJB method, when transaction attribute is "Required"?

nclark picture nclark · Aug 25, 2011 · Viewed 11.4k times · Source

I am consistently finding that my already-existing transaction is getting committed inside any method of an EJB marked @ejb.transaction type="Required". Can this be correct?

My expectation is, an EJB "requiring" a transaction means: if there's one already there, it will politely leave it uncommitted when done so that whoever invoked begin() can continue to use it for further operations before invoking commit() or rollback(). [Of course, if there was no transaction in the first place, then the EJB method would invoke both begin() and commit()/rollback().]

Is my expectation wrong, or should I be looking for a configuration bug?

It might be relevant to add that I'm using Hibernate 3 inside the EJB. I'm obtaining a UserTransaction before calling the EJB method. The EJB generated wrapper invokes ServerTransaction.commit() on exit, which Hibernate hooks into and uses the opportunity to close its Session. The error I'm getting is a Hibernate lazy loading exception, because the session is closed when I try to access getters on a Hibernate-persisted object. So technically, I'm not 100% sure whether the ServerTransaction.commit() I observed necessarily committed the UserTransaction I started (maybe ServerTransaction.commit() doesn't always actually follow through with a "real" commit?), but if it did not -- then on what basis is Hibernate closing the Session?

Update: I believe my assumptions above were correct, but my observations were a bit off. See below for my self-supplied Answer.

Answer

David Blevins picture David Blevins · Aug 28, 2011

REQUIRED can be evil

I personally do not like the REQUIRED transaction attribute and would strongly discourage its use.

Lazily creating transactions (which is what REQUIRED does) results in not really knowing when and where a transaction was actually started and when it will commit. That's not a good thing. People should explicitly design transactional boundaries.

MANDATORY and UserTransaction are soul mates

Your desire to use a UserTransaction is very good and does work with CMT -- there is no difference between a JTA transaction started via UserTransaction supplied by the container or a JTA transaction started for you by the container.

The approach I would recommend is to switch all REQUIRED usage to MANDATORY. With MANDATORY the container will not start transactions for you. Instead it will protect your bean by ensuring it cannot be called unless a transaction is in progress. This is an amazing and underutilized feature. MANDATORY is the best friend of anyone who wants to create a truly deterministic transactional app and to enforce it. With this setup you might fall in love with CMT.

In this scenario you start the transactions with UserTransactions and then the container is like your big bodyguard kicking people to the curb unless they've appropriately started a transaction before attempting to invoke your code.

Considerations

  • A Servlet or a BMT EJB can use a UserTransaction.
  • CMT beans cannot use a UserTransaction, but they can participate in a transaction started by a UserTransaction (as noted above, a JTA transaction is a JTA transaction).
  • BMT beans cannot participate in an existing transaction. If you call a BMT bean while a transaction is already in progress, the transaction will be suspended by the container prior to invoking the BMT bean and resumed after the method completes.
  • @Resource UserTransaction will get you the user transaction via injection
  • java:comp/UserTransaction will get you the user transaction via lookup
  • @TransactionAttribute(MANDATORY) used at the class level will affect the methods of that exact class (i.e. the fooClass.getDecaredMethods() methods). Methods of super classes and subclasses will default to @TransactionAttribute(REQUIRED) unless those classes are also explicitly annotated @TransactionAttribute(MANDATORY)
  • If you get sick of calling userTransaction.begin() and userTransaction.commit() and doing the related exception handling, consider @TransactionAttribute(REQUIRES_NEW). Your transaction boundaries will still be documented and explicit.
  • RuntimeExceptions thrown from a method of a CMT bean will cause the transaction to be marked for rollback, even if you catch and handle the exception in the calling code. Use @ApplicationException to disable this on a case by case basis for custom runtime exception classes.

Losing your Transaction context

A couple things can cause your in-progress transaction to stop, suspend or otherwise not propagate to the called bean.

  • BMT beans stop transaction propagation. If a transaction in progress calls a BMT bean, that transaction will be suspended before the BMT bean is called and will be resumed after the bean returns. BMT beans can be the origin of transactions but cannot participate in existing transactions. If propagation is mysteriously failing, make sure there are not unintentional calls to BMT beans in mid-transaction.
  • Do not use anything other than a container-supplied UserTransaction or container supplied methods like SessionContext.setRollbackOnly to manage transactions. Use of a "resource-manager specific transaction demarcation API" such as JPA EntityTransaction or java.sql.Connection.commit() will circumvent transaction management.
  • Do not start your own threads. Transaction propagation happens on a per-thread basis. There are no standard APIs in Java EE that support a transaction spanning multiple threads. If you leave the thread either from starting your own thread or using @Asynchronous, you will leave your existing transaction behind.