Deadline & Cooperative Cancellation
A single absolute deadline rides every transaction commit, aborting cleanly before work starts and never leaving a partial commit behind.
Status: โ Implemented ยท Visibility: Public ยท Level: ๐ต Core ยท Category: Transactions
๐ฏ What it solves
A commit can block on contended locks, revision-chain growth, or a durability wait โ with no bound, a stuck commit holds resources indefinitely and stalls every transaction behind it. Per-call timeouts don't compose: a commit that re-derives a fresh timeout at each internal step (lock acquire, index update, WAL wait) can run far longer than the caller intended. And a timeout that fires during a commit is worse than no timeout at all if it aborts mid-write โ half the components persisted, half not. Typhon needs one deadline shared by the whole commit, cancellation that reaches code blocked on a lock or a wait (not just spinning code), and a hard guarantee that once a commit starts mutating data it cannot be cut off partway through.
โ๏ธ How it works (in brief)
UnitOfWorkContext is a 24-byte struct carrying an absolute Deadline, a CancellationToken, the UoW id, and a holdoff counter; it is passed ref into Transaction.Commit/Rollback and propagates unchanged into every lock acquisition along the commit path, so every internal wait shares the same endpoint. Commit checks ctx.ThrowIfCancelled() exactly once, at entry, before any data is touched โ that is the only place a timeout or cancellation can abort the operation. Immediately after, it enters a holdoff region (ctx.EnterHoldoff()) that wraps the entire commit loop and nested critical sections (B+Tree splits, revision-chain appends): while in holdoff, cancellation checks are a no-op, so the deadline keeps running underneath but cannot interrupt the commit โ it can only fail a lock acquisition, never abandon a half-written commit. A 200Hz DeadlineWatchdog fires the CancellationToken for any deadline within ~5ms of expiry, so threads parked on a wait (e.g. WAL durability) observe cancellation too, not just threads spinning on a lock.
๐ป Usage
using var dbe = ServiceProvider.GetRequiredService<DatabaseEngine>();
using var t = dbe.CreateQuickTransaction();
t.Spawn<CompAArch>(CompAArch.A.Set(in a));
// One relative timeout, converted to an absolute deadline once.
var ctx = UnitOfWorkContext.FromTimeout(TimeSpan.FromSeconds(5));
try
{
t.Commit(ref ctx); // every lock acquired during commit shares ctx's single deadline
}
catch (TyphonTimeoutException)
{
// Deadline expired before commit began โ transaction state is still InProgress, untouched.
t.Rollback();
}
| Option | Default | Effect |
|---|---|---|
Commit(handler) / Rollback() |
TimeoutOptions.Current.DefaultCommitTimeout (30s) / infinite |
Backward-compatible overloads โ no ref ctx needed, existing call sites are unaffected |
UnitOfWorkContext.FromTimeout(TimeSpan) |
โ | Converts a relative timeout to an absolute deadline, no cancellation token |
UnitOfWorkContext.None |
โ | Infinite deadline, no cancellation โ used internally for rollback/cleanup |
โ ๏ธ Guarantees & limits
- Commit atomicity โ the only yield point in
Commit()is before any mutation begins; once the holdoff opens, the commit loop runs to completion regardless of deadline expiry. A timeout can never leave some components committed and others not. - Rollback always completes โ
Rollback()runs entirely under holdoff with no yield point; cleanup is never abandoned, even with an expired deadline or cancelled token. - Deadline composition โ internal lock sites combine the UoW deadline with a subsystem timeout via
Deadline.Min, so a long UoW deadline never overrides a tighter internal lock-timeout ceiling. - ~5ms cancellation latency โ the
DeadlineWatchdogchecks registered deadlines at 200Hz; threads blocked on a wait (not spinning) observe cancellation within one tick, not instantly. - Zero allocation โ
UnitOfWorkContextis a stack-passed struct; no heap object, no pooling, no GC pressure on the commit hot path. - A lock timeout during holdoff still throws โ holdoff suppresses the cooperative
ThrowIfCancelled()check, not lock-acquisition failures; aLockTimeoutExceptioncan still surface mid-commit, leaving the transactionInProgressand requiring an explicitRollback(). - Backward compatible โ
Commit()/Rollback()without a context build one internally (30s default / infinite); all pre-existing call sites behave unchanged.
๐งช Tests
- TransactionUnitOfWorkContextTests
โ
Commit/Rollback(ref UnitOfWorkContext): expired-deadline and cancelled-token throw at entry, expired-during-holdoff still commits, holdoff nesting counter,ComposeWaitContext_*deadline-composition cases - UnitOfWorkContextTests โ the
UnitOfWorkContextstruct itself: 24-byte size,FromTimeout/None, holdoff enter/exit semantics (ThrowIfCancelledbecomes a no-op inside holdoff, throws again after exit) - DeadlineWatchdogTests โ the 200Hz
watchdog firing
CancellationTokens for deadlines near expiry, so parked (not spinning) waiters observe cancellation
๐ Related
- Related feature: Deadline & Timeout Propagation (the underlying
Deadline/WaitContextprimitives)