Search code examples
javaspringtransactions

UnexpectedRollbackException overrides my own exception


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


Solution

  • 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.