No rollback on Error
GuiSim opened this issue · comments
Hello!
We chased a nasty bug in our environment recently and one thing that we realized is that JdbcLocalTxnInterceptor doesn't rollback on Error
.
This only catches Exception
.
If you have code annotated with @Transactional
that is instrumented by the interceptor and that code throws an Error
, you end up in a very strange state:
conn.setAutoCommit(false);
Object result;
try {
result = methodInvocation.proceed();
} catch (Exception e) {
//commit transaction only if rollback didn't occur
if (rollbackIfNecessary(transactional, e, conn)) {
logger.debug("Committing JDBC transaction");
conn.commit();
}
logger.debug("Enabling auto commit for this thread");
conn.setAutoCommit(true);
//propagate whatever exception is thrown anyway
throw e;
} finally {
// Close the em if necessary (guarded so this code doesn't run unless catch fired).
if (null != didWeStartWork.get() && conn.getAutoCommit()) {
didWeStartWork.remove();
unitOfWork.end();
}
}
The connection will have autoCommit
set to false
since the catch
will not be triggered.
The finally
will be invoked, but the autoCommit
being false prevents it from stopping the unit of work.
This means that if you're calling these methods from a Thread Pool (as many REST services do - for example) you might eventually re-use this thread. The thread will already have a unit of work that is still running!
There are many ways this could be solved, but I think it makes sense to rollback on any Throwable
and not just Exception
.
What do you think?
If you agree, I can provide a PR.
I see what you mean about the inconsistent state. It looks like catch (Exception e)
should be catch (Throwable t)
at the very least. This same issue exists in the JPA version of guice-persist, and a quick look at their bug tracker shows that some aspect of this has come up before, although I think this issue is slightly different but related to what you're facing: google/guice#692.
Given that @Transactional
is coming from Guice, any change must implement the semantics documented there. The explicit rollbackOn
and ignore
lists only allow for Exception
s. The doc on that annotation says that "By default, only unchecked exceptions trigger a rollback." which should include Error
, but should not include any other Throwable
which isn't an Error
or a RuntimeException
.
Without changing the contract of @Transactional
you'll have to commit-by-default on other "checked" Throwable
s without really providing any way for those to be explicitly ignored via the annotation. This seems like an acceptable compromise given the constraints...anyone who has an issue with this will have to provide exception handling beyond what can be achieved declaratively with the annotation: specifically, if you want to rollback on a checked Throwable
which is not also an Exception
, you'll have to catch and wrap it with an Exception
which can then be included in the rollbackOn
list.
As for the semantics of ignore
, I'll leave you with this gem from the javadoc:
A list of exceptions to not rollback on. A caveat to the rollbackOn clause. The
disjunction of rollbackOn and ignore represents the list of exceptions that will trigger a rollback. The complement of rollbackOn and the universal set plus any exceptions in the ignore set represents the list of exceptions that will trigger a commit. Note that ignore exceptions take precedence over rollbackOn, but with subtype granularity.
PR welcome!
Hi @GuiSim , is this issue still causing issues for you? I just noticed it had been awhile since you offered to raise a PR.
Hey!
I've since changed job and don't use this library anymore. Sorry for not providing a PR!