I have the following code:
class ServiceA {
def save(Object object) {
if (somethingBadComesBack) {
throw new CustomRuntimeException(data)
}
}
}
class ServiceB {
def serviceA
def save(Object object) {
try {
serviceA.save(object)
// do more stuff if good to go
} catch(CustomRuntimeException e) {
// populate some objects with errors based on exception
}
}
}
class ServiceC {
def serviceB
def process(Object object) {
serviceB.save(object)
if (object.hasErrors() {
// do some stuff
}else{
// do some stuff
}
def info = someMethod(object)
return info
}
}
class SomeController {
def serviceC
def process() {
def object = .....
serviceC.save(object) // UnexpectedRollbackException is thrown here
}
}
When ServiceA.save()
is called and an exception occurs, ServiceC.save()
is throwing an UnexpectedRollbackException
when it tries to return.
I did the following:
try {
serviceC.process(object)
}catch(UnexpectedRollbackException e) {
println e.getMostSpecificCause()
}
and I am getting:
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
I'm not sure where to start looking for how to fix this.
You're using a runtime exception to roll back the transaction, but that's cheating - it's taking advantage of a side effect. Runtime exceptions roll back transactions automatically since you don't need to catch them so it's assumed that if one is thrown, it wasn't expected and the default behavior is to roll back. You can configure methods to not roll back for specific expected runtime exceptions, but this is somewhat rare. Checked exceptions don't roll back exceptions because in Java they must be caught or declared in the throws
, so you have to have either explicitly thrown it or ducked it; either way you had a chance to try again.
The correct way to intentionally roll back a transaction is to call setRollbackOnly()
on the current TransactionStatus
but this isn't directly accessible in a service method (it is in a withTransaction
block since it's the argument to the closure). But it's easy to get to: import org.springframework.transaction.interceptor.TransactionAspectSupport
and call TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()
. This will required that you rework your code since there won't be an exception to catch, so you'll need to check that it was rolled back with TransactionAspectSupport.currentTransactionStatus().isRollbackOnly()
.
I'm not sure if it's a Grails issue or standard behavior, but when I was debugging this there were 3 commit calls with 3 different TransactionStatus
instances. Only the first had the rollback flag set, but the second was aware of the first and was ok. The third one was considered a new transaction and was the one that triggered the same exception that you were seeing. So to work around this I added this to the 2nd and 3rd service methods:
def status = TransactionAspectSupport.currentTransactionStatus()
if (!status.isRollbackOnly()) status.setRollbackOnly()
to chain the rollback flag. That worked and I didn't get the UnexpectedRollbackException
.
It might be easier to combine this with a checked exception. It's still overly expensive since it will fill in the stacktrace unnecessarily, but if you call setRollbackOnly()
and throw a checked exception, you will be able to use the same general workflow you have now.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With