Gavin,
Quote:
This is correct behaviour. If the commit() fails, you are supposed to
call rollback(), as described in the Hibernate doco.
I took your quote from the developer list: I'm of course aware of that being in the docs. I don't think that you've understood my point, so I'll retry.
-----
Hibernate's Transaction.commit/rollback semantics are notably different from JDBC's Connection.commit/rollback and JTA's UserTransaction.commit/rollback: A failed commit has already *completed* the transaction with the latter, no need to call rollback anymore. With JTA, that's explicitly stated in the spec; with JDBC, it's rather de facto, as the spec is unclear in that respect.
In the JDBC case, an additional rollback call will silently roll back a new emtpy transaction (which doesn't really hurt). But in the JTA case, an additional rollback call will cause an IllegalStateException as there is no active transaction anymore (at least with proper JTA implementations that stick to the spec). Unfortunately, Hibernate's JTATransaction.commit can also fail due to flushing; in that case, rollback still needs to be called.
So from an application's point of view, it's not obvious what to do on commit failure with JTATransaction: in case of a flushing failure, need to call rollback to avoid dangling transaction; but in case of a JTA commit failure, must not call rollback to avoid IllegalStateException. As I've outlined, JDBCTransaction will just silently roll back a new empty transaction, so it's safe to call rollback in any case there.
Funnily enough, Hibernate's Session.afterTransactionCompletion gets called in a finally block in both JDBCTransaction's and JTATransaction's commit implementations. So even if commit fails due to flushing, afterTransactionCompletion is triggered; an additional rollback call by the aplication will then trigger *another* afterTransactionCompletion invocation. That also shows that there's some issue with API semantics.
-----
The only way I see to fix this is to redefine the semantics of Hibernate's Transaction interface: i.e. do not call rollback after a failed commit. Both JDBCTransaction and JTATransaction would have to trigger a rollback themselves then on a flushing failure before the actual commit. When commit returns, the transaction would have to be finished in any case, no matter if correctly or with an exception.
Admittedly, it's a bit hard to redefine semantics of such core API at that stage. But as far as I can consider, the current "call rollback after failed commit" idiom is broken with JTATransaction anyway. When used with JDBCTransaction, an additional rollback call wouldn't hurt, so existing code would not break after such a change in semantics. The new recommended way would be "no rollback after failed commit" though.
The only workaround for application code that I can think of with the current semantics is to explicitly call Session.flush before attempting a commit. In case of flushing failure (or any other data access failure), invoke Transaction.rollback; only call Transaction.commit when everything else succeeded. If commit itself fails, do nothing special -- just propagate the exception.
Of course, the latter workaround will cause double flushing of the Session at transaction commit time: once by application code, doing the actual work; once by the Transaction implementation, rechecking but not finding anything to flush (as intended). To avoid that inefficiency, one can set the Session to FlushMode.NEVER after having explicitly invoked flush; the Transaction implementation will then not even attempt to recheck.
-----
So there *is* an issue involved, although one can live with it if just using JDBCTransaction (accepting silent rollback of new empty transaction and double afterTransactionCompletion invocation), or by explicitly calling flush before commit. Please think it through again, I'm 100% sure that the semantics of the Transaction interface and the current implementations aren't clean -- particularly if you let them do the flushing of the Session.
Juergen