Commit / Rollback Pipeline (ACID Commit Path)
The two methods that end a transaction —
Commitmakes writes durable and visible as one atomic unit,Rollbackalways unwinds them completely.
Status: ✅ Implemented · Visibility: Public · Level: 🔵 Core · Category: Transactions
🎯 What it solves
Ending a transaction has to be all-or-nothing from the caller's point of view: either every component you touched becomes visible and crash-durable, or none of it does. A commit that can be interrupted partway — by a timeout, a lock failure, or a crash — and leave some components written and others not is worse than no transaction support at all. Symmetrically, an aborted transaction must always finish unwinding its work; a rollback that itself times out and leaves orphaned revisions behind turns every failure into a slow leak. Commit/Rollback are the two calls that give those guarantees without the caller having to reason about internal ordering.
⚙️ How it works (in brief)
Commit processes each touched component in two halves: PREPARE (conflict detection/resolution, index and spatial-index maintenance — fallible work, nothing made visible yet) and PUBLISH (clear isolation flags, bump revision sequence numbers, copy into cluster slots — plain field writes). Between the two phases sits one WAL append, the commit's point of no return: once it succeeds the transaction is Committed, even if a later Immediate-mode durability wait can't confirm the fsync in time (see Commit Pipeline for the full append-before-publish mechanics). Rollback has no such split — it runs entirely inside a holdoff (see Deadline & Cooperative Cancellation) so cleanup always reaches completion regardless of the caller's deadline.
💻 Usage
using var uow = dbe.CreateUnitOfWork(DurabilityMode.GroupCommit);
using var tx = uow.CreateTransaction();
tx.OpenMut(accountId).Write(Account.Balance).Amount -= 10m;
var ctx = UnitOfWorkContext.FromTimeout(TimeSpan.FromSeconds(2));
try
{
bool committed = tx.Commit(ref ctx); // null handler => last-writer-wins on conflicts
}
catch (TyphonTimeoutException)
{
// Deadline expired before any work was touched — state is still InProgress.
var rbCtx = UnitOfWorkContext.None; // rollback should never itself time out
tx.Rollback(ref rbCtx);
}
catch (CommitDurabilityUncertainException)
{
// Already committed and visible — do not roll back, do not retry.
}
// Backward-compatible wrappers — no explicit context needed:
tx.Commit(); // ConcurrencyConflictHandler optional; default 30s timeout
tx.Rollback(); // infinite deadline (UnitOfWorkContext.None)
| Overload | Default | Effect |
|---|---|---|
Commit(ref UnitOfWorkContext, handler = null) |
— | Full control over the deadline; pass a ConcurrencyConflictHandler to resolve write-write conflicts instead of last-writer-wins |
Commit(handler = null) |
TimeoutOptions.Current.DefaultCommitTimeout (30s) |
Wrapper for call sites that don't need custom deadlines |
Rollback(ref UnitOfWorkContext) / Rollback() |
infinite deadline | Rollback ignores the deadline for its own cleanup work regardless of which overload is used |
⚠️ Guarantees & limits
- Atomic commit — the only cancellation check in
Commitis at entry, before any component is touched; once the holdoff opens, every component's PREPARE/PUBLISH runs to completion, so a deadline can never produce a partially-committed transaction. - Append before publish — no change (isolation flag, index, entity map, cluster slot) becomes visible before its WAL record is appended; see Commit Pipeline for the full ordering and the residual spawn-publish throw case (#396).
- Rollback always completes — runs entirely inside holdoff with no yield point; an expired deadline or cancelled token cannot abort cleanup partway through, by design (
Rollback()'s wrapper usesUnitOfWorkContext.None). boolreturn, not an exception, for already-finished transactions — both methods returnfalseon an already-Committed/Rollbackedtransaction andtrueon an empty (Created-state) one; only CRUD calls on a finished transaction throwInvalidOperationException(ADR-038).- Conflict resolution is opt-in — pass a
ConcurrencyConflictHandlertoCommitfor anything other than last-writer-wins; see Optimistic Conflict Resolution for the handler API. - A failed durability wait is not a failed commit —
Immediate-mode commits that append successfully but can't confirm the fsync raiseCommitDurabilityUncertainException, never roll back; treat it as "committed, durability unconfirmed." - A lock timeout mid-commit is still possible — holdoff suppresses the cooperative cancellation check, not lock-acquisition failure; a
LockTimeoutExceptioncan surface before the WAL append, leaving the transactionInProgressand requiring an explicitRollback().
🧪 Tests
- TransactionTests —
DoubleCommit_ReturnsFalse,DoubleRollback_ReturnsFalse,CrudAfterCommitOrRollback_ThrowsInvalidOperation,Rollback_*series (created/ updated/deleted/multi-component),Commit_AfterRollback_ReturnsFalse,Dispose_UncommittedTransaction_AutoRollbacks - TransactionUnitOfWorkContextTests
—
Commit/Rollbackoverloads takingref UnitOfWorkContext: expired-deadline-at-entry throw, already-committed/rolled-back returningfalse, holdoff correctness mid-commit
🔗 Related
- Related features: Commit Pipeline (the append-before-publish mechanics in depth), Deadline & Cooperative Cancellation, Optimistic Conflict Resolution, Unit of Work