enhancement
major
minor
defect
critical
major
minor
minor
#30259
KB: Committable.rollback() is called after a successful database commit if the changes or caches fail to be published
Problem
In DefaultDBContext.commitTransaction(TransactionImpl), everything after the database commit is still within the same try block:
commitCommitables();
dbCommit(commitConnection); // the changes are now in the database
checkLongRunningSuccess();
link = new UpdateChainLink(updateEvent);
dropContext(true);
localCommit(removedObjects, changedObjects, newObjects, link);
// insertNew, publishChanges, dropDeleted, publishKBCaches, publishAssociationCaches, commitPublished
journal(journalLine);
kb.updateCaches(link.getUpdateEvent());
If any of these steps throws an exception after ` dbCommit ` (e.g., publishing a cache), the exception is caught in the outer ` catch ` block of ` commitTransaction`:
} catch (... ex) {
doRollback(ex); // rollbackComplete(false) -> localRollback() -> rollbackCommitables()
throw ex;
}
As a result, the engine calls ` Committable.rollback(CommitContext) ` for a transaction whose changes are already in the database. A ` Committable ` cannot determine from this whether the commit took place:
- According to Javadoc,rollback() means “the transaction is rolled back.”
- Committables that undo external effects during a rollback (compensation) then destroy data corresponding to a committed state.
- In addition, the new, modified, and deleted objects are rolled back in memory, even though the database already contains them; ` commit()` is thrown, and the transaction’s ` AfterCommitActions` are skipped.
The same applies to an exception thrown by a synchronous UpdateListener: It is notified after the database commit (kbEndCommit), and its exception originated from ` commit() `.
(The originally reported trigger—an exception in ` Committable.complete()`—no longer exists since #30218: `complete()` has been removed; post-commit actions are now `AfterCommitActions`, whose errors are logged. The remaining steps following `dbCommit ` continue to trigger the issue.)
Version: 8.0.0-alpha12, module tl-core (com.top_logic.knowledge.service.db2.DefaultDBContext).
Reproduction
- Register a Committable using ` DBKnowledgeBase.addCommittable ` within a transaction that contains a change.
- A step after the database commit (e.g., publishing a cache) throws a RuntimeException.
- tx.commit(): The change is in the database, commit() is thrown, and rollback(CommitContext) of the committable is called.
Impact
A DMS add-on for an external document repository with corresponding version control in the application database deletes the document versions stored in the DMS during the transaction when rollback() is called. If the scenario described above occurs, it would delete the content of a committed version. Workaround: ComittableImpl.commit(CommitContext) (immediately before dbCommit) sets a flag. A subsequent rollback() does not delete anything, but instead logs the stored objects as potential orphans (WARN).
Solution
- `commitTransaction ` has two phases, separated by the database commit. If any step up to and including `dbCommit` fails, the transaction is fully rolled back as before (including ` Committable.rollback()`—even after ` Committable.commit()` has already been called—if `dbCommit` itself fails).
- After a successful ` dbCommit`, the transaction is considered committed: There is no longer any rollback of the `Committables` or of the objects in memory. The publication steps (publishCommit: checking the commit duration, closing the transaction context, publishing the objects and caches, journal, updateCaches) as well as the announcement of the commit (kbEndCommit, including notification of the synchronous UpdateListeners) are individually safeguarded; an error is logged as an ERROR with the revision number; the remaining steps continue to run.
- In this case,commit() returns normally with the revision, and the AfterCommitActionsrun. This also applies to an exception thrown by a synchronous UpdateListener: it is logged and does not exit commit().
- The Javadoc for Committable, Committable.commit(), and Committable.rollback() clarifies: rollback() is called exactly when the database transaction has not been committed, never after a successful database commit. The flag workaround is therefore no longer necessary.
Test
- TestCommitPublicationFailure: An error during publication after the database commit does not trigger a rollback() of a Committable; commit() returns the revision, the error is logged once with the revision number, the object remains active and visible from another thread, the AfterCommitAction runs, and the subsequent transaction succeeds. Counterexample: If Committable.commit() fails before the database commit, rollback() is called and the change is not visible.
- TestDBKnowledgeBase: testFailureInListener and testErrorInListener expect the commit to succeed despite a failed synchronous UpdateListener.