views:

65

answers:

2

I have the following strange scenario with spring's transaction management:

I have method A which calls method B which calls method C, each of them in a different class. Methods B and C are both wrapped with transactions. Both use PROPAGATION_REQUIRED, so while spring creates two logical transactions, there is one physical transaction in the db.

Now, in method C I throw a RuntimeException. This sets the inner logical transaction as rollbackOnly and the physical transaction as well. In method B, I am aware of the possibility of UnexpectedRollbackException, so I don't proceed to commit normally. I catch the exception from C and I throw another RuntimeException.

I expect that the outer RuntimeException will cause a rollback to the outer transaction, However the actual behavior is this:

  • The outer transaction appears to try to commit, or at least check its status, and then it throws the UnexpectedRollbackException because the physical transaction was already marked as rollbackOnly.
  • Before throwing that exception, it prints to the logs another exception, stating that "Application exception overridden by commit exception". Thus, Caller A receives the UnexpectedRollbackException, not the exception that B throws.

I found a workaround for it, which is to actively set the outer transaction as rollback only before throwing the exception

public ModelAndView methodB(HttpServletRequest req, HttpServletResponse resp) {
  try{
    other.methodC();
  } catch (RuntimeException e){
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    throw new RuntimeException ("outer exception");
  }
  return handleGetRequest(req, resp);
}

However, this workaround strongly couples the code with transactions api and I'd like to avoid this. Any suggestions?

p.s. both transactions are meant to rollback on runtime exceptions. I didn't define any rollbackFor exception or anything like that

+1  A: 

You could try to set failEarlyOnGlobalRollbackOnly flag to true in your AbstractPlatformTransactionManager inheritor (HibernateTransactionManager, for example). Here is its description:

Set whether to fail early in case of the transaction being globally marked as rollback-only.

Default is "false", only causing an UnexpectedRollbackException at the outermost transaction boundary. Switch this flag on to cause an UnexpectedRollbackException as early as the global rollback-only marker has been first detected, even from within an inner transaction boundary. ote that, as of Spring 2.0, the fail-early behavior for global rollback-only markers has been unified: All transaction managers will by default only cause UnexpectedRollbackException at the outermost transaction boundary. This allows, for example, to continue unit tests even after an operation failed and the transaction will never be completed. All transaction managers will only fail earlier if this flag has explicitly been set to "true".

wax
+1 and thanks for the effort. failEarlyOnGlobalRollbackOnly is interesting, but I eventually found the cause of the problem (see my answer below)
Yoni
+1  A: 

I found the cause of this problem. It turns out that methodB was wrapped with a cglib-based proxy (using spring old way, pre 2.0) before being wrapped in transaction. so when I throw a RuntimeException from methodB, cglib ends up throwing an InvocationTargetException, which is actually a checked exception.

Spring's transaction manager ends up catching the checked exception and tries to commit the transaction, unaware of the nested runtime exception that methodB threw. Once I discovered this, I set up the transaction wrapper to rollback for checked exceptions as well, now it works as expected.

Yoni