enhancement
major
minor
defect
critical
major
minor
minor
#30259
KB: Committable.rollback() wird nach erfolgreichem Datenbank-Commit aufgerufen, wenn das Veröffentlichen der Änderungen oder Caches scheitert
Problem
In DefaultDBContext.commitTransaction(TransactionImpl) liegt alles nach dem Datenbank-Commit noch im selben try-Block:
commitCommitables();
dbCommit(commitConnection); // die Änderungen sind jetzt in der Datenbank
checkLongRunningSuccess();
link = new UpdateChainLink(updateEvent);
dropContext(true);
localCommit(removedObjects, changedObjects, newObjects, link);
// insertNew, publishChanges, dropDeleted, publishKBCaches, publishAssociationCaches, commitPublished
journal(journalLine);
kb.updateCaches(link.getUpdateEvent());
Wirft einer dieser Schritte nach dbCommit (z. B. das Veröffentlichen eines Caches), landet die Ausnahme im äußeren catch von commitTransaction:
} catch (... ex) {
doRollback(ex); // rollbackComplete(false) -> localRollback() -> rollbackCommitables()
throw ex;
}
Damit ruft die Engine Committable.rollback(CommitContext) für eine Transaktion, deren Änderungen bereits in der Datenbank stehen. Ein Committable kann daraus nicht erkennen, ob der Commit stattgefunden hat:
- rollback() heißt laut Javadoc „the transaction is rolled back“.
- Committables, die beim Rollback externe Wirkungen zurücknehmen (Kompensation), zerstören dann Daten zu einem committeten Stand.
- Zusätzlich werden die neuen, geänderten und gelöschten Objekte im Speicher zurückgerollt, obwohl die Datenbank sie bereits enthält, commit() wirft, und die AfterCommitActions der Transaktion entfallen.
Dasselbe gilt für eine Ausnahme eines synchronen UpdateListeners: Er wird nach dem Datenbank-Commit benachrichtigt (kbEndCommit), seine Ausnahme kam aus commit() heraus.
(Der ursprünglich gemeldete Auslöser, eine Ausnahme in Committable.complete(), existiert seit #30218 nicht mehr: complete() ist entfallen, Aktionen nach dem Commit sind AfterCommitActions, deren Fehler geloggt werden. Die übrigen Schritte nach dbCommit lösen das Problem weiterhin aus.)
Version: 8.0.0-alpha12, Modul tl-core (com.top_logic.knowledge.service.db2.DefaultDBContext).
Reproduktion
- Ein Committable per DBKnowledgeBase.addCommittable in einer Transaktion registrieren, die eine Änderung enthält.
- Ein Schritt nach dem Datenbank-Commit (z. B. das Veröffentlichen eines Caches) wirft eine RuntimeException.
- tx.commit(): Die Änderung ist in der Datenbank, commit() wirft, und rollback(CommitContext) des Committables wird gerufen.
Auswirkung
Ein DMS-Add-on für eine externe Dokumentenablage mit korrespondierender Versionsverwaltung in der Anwendungsdatenbank löscht im rollback() die in der Transaktion im DMS abgelegten Dokumentversionen wieder. Tritt der oben beschriebene Fall ein, würde es den Inhalt einer committeten Version löschen. Workaround: ComittableImpl.commit(CommitContext) (direkt vor dbCommit) setzt ein Flag. Ein rollback() danach löscht nichts, sondern loggt die abgelegten Objekte als mögliche Waisen (WARN).
Lösung
- commitTransaction hat zwei Phasen, getrennt durch den Datenbank-Commit. Scheitert ein Schritt bis einschließlich dbCommit, wird die Transaktion wie bisher vollständig zurückgerollt (inkl. Committable.rollback(), auch nach einem bereits gerufenen Committable.commit(), wenn dbCommit selbst scheitert).
- Nach erfolgreichem dbCommit gilt die Transaktion als committet: Es gibt keinen Rollback der Committables und keinen Rollback der Objekte im Speicher mehr. Die Schritte der Veröffentlichung (publishCommit: Prüfung der Commit-Dauer, Schließen des Transaktionskontexts, Veröffentlichen der Objekte und Caches, Journal, updateCaches) sowie das Bekanntgeben des Commits (kbEndCommit inkl. Benachrichtigung der synchronen UpdateListener) sind einzeln abgesichert; ein Fehler wird mit der Revisionsnummer als ERROR geloggt, die übrigen Schritte laufen weiter.
- commit() kehrt in diesem Fall normal mit der Revision zurück, und die AfterCommitActions laufen. Das gilt auch für eine Ausnahme eines synchronen UpdateListeners: Sie wird geloggt und kommt nicht mehr aus commit() heraus.
- Javadoc von Committable, Committable.commit() und Committable.rollback() präzisiert: rollback() wird genau dann gerufen, wenn die Datenbank-Transaktion nicht committet wurde, nie nach einem erfolgreichen Datenbank-Commit. Der Flag-Workaround ist damit nicht mehr nötig.
Test
- TestCommitPublicationFailure: Ein Fehler beim Veröffentlichen nach dem Datenbank-Commit führt zu keinem rollback() eines Committables; commit() liefert die Revision, der Fehler wird einmal mit Revisionsnummer geloggt, das Objekt ist lebendig und aus einem anderen Thread sichtbar, die AfterCommitAction läuft, die Folgetransaktion gelingt. Gegenfall: Scheitert Committable.commit() vor dem Datenbank-Commit, wird rollback() gerufen und die Änderung ist nicht sichtbar.
- TestDBKnowledgeBase: testFailureInListener und testErrorInListener erwarten, dass der Commit trotz fehlschlagendem synchronen UpdateListener gelingt.