UnexpectedRollbackException overrides my own exception
Asked Answered
N

3

11

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

Nogas answered 14/7, 2010 at 11:56 Comment(0)
N
16

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.

Nogas answered 24/7, 2010 at 19:55 Comment(1)
I am having the same issue. Can we rollback for all checked and unchecked exceptions. Will there be any issue with this?Prospect
A
5

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

Anhwei answered 14/7, 2010 at 14:2 Comment(1)
+1 and thanks for the effort. failEarlyOnGlobalRollbackOnly is interesting, but I eventually found the cause of the problem (see my answer below)Nogas
B
2

Add a Custom Exception that extends Runtime Exception and take that exception till controller.

public class CustomException extends RuntimeException{

public String getExceptionMsg() {
    return exceptionMsg;
}
public void setExceptionMsg(String exceptionMsg) {
    this.exceptionMsg = exceptionMsg;
}
private String exceptionMsg;
public CustomException(String errorMsg)
{
    super(errorMsg);
}
public CustomException(Throwable cause){
    super(cause);
}
public CustomException(String message ,Throwable cause){
    super(message,cause);
}

}

Note:Remember only unchecked exceptions cause rollbacks in spring transactions.

What is happening is that you're catching the unchecked exception, converting it to a checked exception and then propogating it. The transaction manager does not rollback for RecordExistsException and thinks that your first transaction has succeded. Thats why it tries to save your child objects. You should annotate your service with.

Code: @Transactional (rollbackFor= RecordExistsException.class) or have your exception class extend RuntimeException.

Burbot answered 15/2, 2022 at 17:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.