Timeout Exceptions & Deadline Propagation
Configurable, finite deadlines replace infinite waits, turning every contention hang into a typed, catchable timeout exception.
Status: โ Implemented ยท Visibility: Public ยท Level: ๐ต Core ยท Category: Errors Assumes: Deadline & Timeout Propagation
๐ฏ What it solves
Every lock acquisition and buffer claim in Typhon used to wait forever โ ref WaitContext.Null meant any
contention, stuck writer, or resource exhaustion hung the caller indefinitely, indistinguishable from a deadlock.
There was no way to bound how long an operation could block, and no structured signal when it did. This feature
gives every wait a finite, per-subsystem-configurable deadline and converts an expired wait into a specific
exception type carrying what was being waited for and how long โ never a silent hang, never a generic
System.TimeoutException.
โ๏ธ How it works (in brief)
TimeoutOptions (on DatabaseEngineOptions.Timeouts) holds one TimeSpan per subsystem โ page cache, B+Tree,
transaction chain, revision chain, segment allocation โ plus back-pressure and bulk-load checkpoint budgets.
Each call site builds a WaitContext.FromTimeout(...) fresh (an absolute monotonic deadline, not a wall-clock
one) and passes it ref into the lock primitive. Lock primitives never throw โ they return false on expiry โ
so the subsystem checks the result and throws the appropriate TyphonTimeoutException subclass. The throw
always happens at the acquisition point, before any mutation begins, so a timeout never leaves a structural
operation half-done.
๐ป Usage
var options = new DatabaseEngineOptions
{
Timeouts = new TimeoutOptions
{
BTreeLockTimeout = TimeSpan.FromSeconds(2),
TransactionChainLockTimeout = TimeSpan.FromSeconds(10),
},
};
try
{
using var tx = dbe.CreateQuickTransaction();
var e = tx.OpenMut(playerId);
e.Write(Player.Wallet).Gold -= price;
tx.Commit();
}
catch (TyphonTimeoutException ex)
{
// Catches LockTimeoutException, TransactionTimeoutException,
// PageCacheBackpressureTimeoutException, and WalBackPressureTimeoutException alike.
_logger.LogWarning("Operation timed out after {Wait}ms", ex.WaitDuration.TotalMilliseconds);
// ex.IsTransient is always true here โ caller decides whether/how to retry.
}
TimeoutOptions property |
Default | Governs |
|---|---|---|
PageCacheLockTimeout |
5s | Page cache state-transition locks |
BTreeLockTimeout |
5s | B+Tree insert/delete/lookup locks |
TransactionChainLockTimeout |
10s | Transaction chain create/remove/walk |
RevisionChainLockTimeout |
5s | MVCC revision read/add/cleanup |
SegmentAllocationLockTimeout |
10s | Chained-block allocator / segment growth |
PageCacheBackpressureTimeout |
5s | Waiting for dirty pages to flush so allocation can proceed |
WalBackPressureTimeout |
5s | Waiting for the WAL ring buffer to drain |
DefaultCommitTimeout / DefaultUowTimeout |
30s | Outer bound when Transaction.Commit() / a UoW is created without an explicit timeout |
BulkLoadOptions.CheckpointTimeout |
5 min | BulkLoadSession.CompleteBulkLoad's forced synchronous checkpoint |
โ ๏ธ Guarantees & limits
TyphonTimeoutExceptionis the common catchable base;IsTransientis alwaystruefor the whole family โ the resource is presumed available later, but the engine never retries automatically.- Four concrete leaves:
LockTimeoutException(carriesResourceName),TransactionTimeoutException(carriesTransactionId),PageCacheBackpressureTimeoutException(carriesDirtyPageCount/EpochProtectedCount),WalBackPressureTimeoutException(carriesRequestedBytes) โ each also carriesWaitDuration. BulkLoadCheckpointTimeoutExceptionis a deadline-driven timeout in spirit but inheritsDurabilityException, notTyphonTimeoutExceptionโ it does not surface in acatch (TyphonTimeoutException)block, andIsTransientdefaults tofalse. Unlike the other four, the underlyingBulkLoadSessionremains open after this exception; the caller may retryCompleteBulkLoador dispose the session.- Deadlines are monotonic (
Stopwatch-based), not wall-clock โ immune to NTP/DST jumps โ and are computed fresh at each call site rather than inherited, so nested lock acquisitions each get their own full window (not yet a single shared transaction-wide budget โ that is a future Execution Context tier). - Checking
WaitContext.ShouldStopcosts ~10-25ns per spin iteration โ paid only while contended; the uncontended fast path skips the check entirely. - Test code should use
TestWaitContext.Default(a fresh 10s deadline) instead ofWaitContext.Nullin multi-threaded contention tests, to avoid genuinely hanging the test runner on a regression.
๐งช Tests
- DeadlinePropagationTests โ
TimeoutOptionsdefaults/overrides,AccessControl/ResourceAccessControllock contention throwingLockTimeoutExceptiononce the deadline expires, andTestWaitContextexpiry semantics. - TyphonExceptionTests โ
LockTimeoutException/TransactionTimeoutExceptiontyped properties (ResourceName/TransactionId/WaitDuration) and thecatch (TyphonTimeoutException)mid-granularity roundtrip.
๐ Related
- Related catalog entries: Deadline & Timeout Propagation (the underlying
Deadline/WaitContextmechanism), TyphonException Hierarchy & Catalog