Table of Contents

Deadline & Timeout Propagation

Monotonic absolute deadlines bundled with cooperative cancellation, shared by reference through every nested call so timeouts never accumulate.

Status: โœ… Implemented ยท Visibility: Public ยท Level: ๐Ÿ”ต Core ยท Category: Foundation

๐ŸŽฏ What it solves

Relative timeouts compound: a 5-second budget handed to three nested calls, each restarting its own 5-second clock, can take 15 seconds in the worst case โ€” the original "limit" was meaningless. Wall-clock timeouts (DateTime.UtcNow) compound the problem further: NTP sync or DST can jump the clock backward or forward, making a timeout expire instantly or never. Typhon's lock primitives and transactions operate at microsecond granularity, where this drift is the difference between a sub-millisecond commit and a thread that hangs indefinitely. Deadline propagation converts a caller's relative timeout into an absolute, monotonic endpoint exactly once, then threads it โ€” together with a cooperative cancellation signal โ€” through every nested operation without re-deriving it.

โš™๏ธ How it works (in brief)

Deadline is an 8-byte readonly struct wrapping a Stopwatch.GetTimestamp() value; Deadline.FromTimeout(timeout) converts a relative TimeSpan to an absolute deadline once, at the operation's entry point. WaitContext (16 bytes) bundles a Deadline with a CancellationToken and is passed ref to every blocking Enter* call on Typhon's lock primitives; its ShouldStop property is the single check performed once per spin iteration. UnitOfWorkContext (24 bytes) embeds a WaitContext plus a Unit-of-Work identifier and a holdoff counter, and flows by ref through an entire Unit of Work or transaction; ThrowIfCancelled() is the cooperative yield-point check, and the holdoff counter โ€” incremented/decremented around critical sections โ€” suppresses cancellation mid-section without disabling the deadline itself.

๐Ÿ’ป Usage

using var uow = db.CreateUnitOfWork();
using var tx = uow.CreateTransaction();

// One conversion, at the top: a 5s relative timeout becomes an absolute monotonic deadline.
var ctx = UnitOfWorkContext.FromTimeout(TimeSpan.FromSeconds(5));

UpdateAccountBalance(tx, accountId, newBalance);

try
{
    tx.Commit(ref ctx);   // every lock acquired during commit shares ctx's single deadline
}
catch (TyphonTimeoutException)
{
    // ctx's deadline expired before commit finished โ€” no partial work was left behind.
}
Option Default Effect
Deadline.FromTimeout(Timeout.InfiniteTimeSpan) โ€” Returns Deadline.Infinite โ€” never expires
Deadline.FromTimeout(TimeSpan.Zero) / negative โ€” Returns Deadline.Zero โ€” already expired, fails immediately
UnitOfWorkContext.None โ€” Infinite deadline, no cancellation โ€” for internal cleanup/rollback paths only

โš ๏ธ Guarantees & limits

  • No accumulation โ€” the relative-to-absolute conversion happens once at the entry point; every nested call checks the same absolute endpoint, so total elapsed time is bounded by the original timeout regardless of call depth.
  • Monotonic, not wall-clock โ€” built on Stopwatch.GetTimestamp(); immune to NTP adjustments, DST transitions, and manual clock changes that would make a DateTime.UtcNow-based timeout expire early or never.
  • Fail-safe default โ€” default(Deadline), default(WaitContext), and default(UnitOfWorkContext) are all already-expired; a forgotten initialization fails fast instead of hanging forever. Unbounded waits require explicitly opting in via Deadline.Infinite, WaitContext.Null, or UnitOfWorkContext.None.
  • ~10-25ns per expiry check โ€” one Stopwatch.GetTimestamp() (effectively a single RDTSC on modern x64) plus a comparison; the cancellation check short-circuits to near-free when no token is attached.
  • Holdoff defers, never disables โ€” BeginHoldoff()/EndHoldoff() make ThrowIfCancelled() a no-op for the duration of a critical section so a timeout can't abort work partway through; the deadline keeps running underneath and is re-checked the instant the holdoff exits.
  • Deadline.ToCancellationToken() allocates โ€” bridges to a CancellationTokenSource + timer for interop with non-Typhon async APIs (e.g. HttpClient); not for use in spin loops or per-iteration hot paths.
  • A background watchdog polls registered deadlines at 200Hz (~5ms resolution) to fire CancellationTokens for code paths that need to observe expiry without spinning โ€” an internal mechanism, not directly exposed.

๐Ÿงช Tests

  • DeadlineTests โ€” monotonic expiry: default/zero/infinite states, FromTimeout conversion, Min() composition.
  • WaitContextTests โ€” deadline+cancellation composition, ShouldStop short-circuiting, FromTimeout/FromToken construction.
  • UnitOfWorkContextTests โ€” 24-byte struct layout, ThrowIfCancelled, holdoff begin/end nesting that suppresses cancellation without disabling the deadline.
  • TransactionUnitOfWorkContextTests โ€” Commit(ref ctx)/Rollback(ref ctx) propagation end-to-end, expired-deadline and cancelled-token paths matching the usage sample above.