Table of Contents

Durability (WAL / BulkLoad / Commit) Exceptions

Typed, fail-fast failures from the WAL writer, the commit pipeline's durability wait, and BulkLoad session lifecycle.

Status: โœ… Implemented ยท Visibility: Public ยท Level: ๐ŸŸฃ Advanced ยท Category: Errors

๐ŸŽฏ What it solves

WAL and commit failures are not interchangeable: a disk write that fails outright must stop the engine from accepting further durable commits, a WAL claim that's larger than the whole ring buffer can never succeed no matter how many times it's retried, and an Immediate-mode commit whose post-publish fsync wait times out has already committed โ€” it is not a failure to retry. Treating all of these as one generic I/O exception would force callers to parse messages to tell "restart the engine" apart from "this commit is fine, just unconfirmed." BulkLoad sessions add their own lifecycle failures (concurrent session, closed session, stuck checkpoint) that don't map to any standard transaction exception either. This feature gives each distinct outcome its own typed, catchable exception.

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

All seven types inherit DurabilityException : TyphonException, so catch (DurabilityException) is the subsystem-wide umbrella. WalWriteException wraps a fatal WAL writer I/O failure (ADR-020's dedicated writer thread) โ€” not transient; the engine stops accepting durable commits and needs a restart. WalClaimTooLargeException and WalSegmentException are buffer-sizing and segment-file errors, also not transient โ€” they're configuration problems, not contention. CommitDurabilityUncertainException is the AP-02 point-of-no-return signal: by the time it's thrown, Append and Publish already succeeded and the transaction is committed and visible โ€” only the durability confirmation (the post-publish FUA wait) didn't land in time. The three BulkLoadSession exceptions cover its own exclusivity gate, post-close calls, and the CompleteBulkLoad checkpoint barrier (BulkLoad Write Path covers the session mechanics; this entry covers only its exceptions).

๐Ÿ’ป Usage

using var uow = db.CreateUnitOfWork(DurabilityMode.Immediate);
using var tx = uow.CreateTransaction();
UpdateAccountBalance(tx, accountId, newBalance);

try
{
    tx.Commit();
}
catch (CommitDurabilityUncertainException ex)
{
    // Committed and visible โ€” APPEND + PUBLISH already ran. Do NOT retry the transaction.
    // Poll ex.HighLsn against DurabilityLog.DurableLsn to learn when it becomes durable.
    LogDurabilityUncertain(ex.HighLsn);
}
catch (WalWriteException)
{
    // Fatal WAL I/O โ€” the engine will reject further durable commits. Escalate to restart.
    InitiateEngineRestart();
}

try
{
    using var bulk = db.BeginBulkLoad();
    // ... bulk.Spawn / bulk.Update ...
    bulk.CompleteBulkLoad();
}
catch (BulkLoadCheckpointTimeoutException ex)
{
    // Session is still open โ€” caller may retry CompleteBulkLoad() or Dispose() to discard.
}
Type Error code IsTransient Notable properties
WalWriteException WalWriteFailure (7003) false inner exception only
WalClaimTooLargeException WalClaimTooLarge (7002) false RequestedBytes, BufferCapacity
WalSegmentException WalSegmentError (7004) false SegmentPath
CommitDurabilityUncertainException CommitDurabilityUncertain (7008) false HighLsn
BulkSessionAlreadyActiveException BulkSessionAlreadyActive (7005) false ActiveBulkSessionId
BulkSessionClosedException BulkSessionClosed (7006) false BulkSessionId
BulkLoadCheckpointTimeoutException BulkLoadCheckpointTimeout (7007) false BulkSessionId, Timeout

โš ๏ธ Guarantees & limits

  • WalWriteException is unconditionally fatal โ€” there is no retry path; it signals the dedicated WAL writer thread itself failed (ADR-020), and the engine stops accepting durable commits until restarted.
  • CommitDurabilityUncertainException means "committed, durability unconfirmed," never a rollback โ€” IsTransient is false specifically to stop callers from auto-retrying a transaction that already happened.
  • WalClaimTooLargeException / WalSegmentException are not transient: a larger buffer or a fixed segment path is needed before the same operation can succeed.
  • The three BulkLoadSession exceptions are scoped to that session's own state machine โ€” BulkSessionAlreadyActiveException from a second BeginBulkLoad, BulkSessionClosedException from any call after CompleteBulkLoad/Dispose, BulkLoadCheckpointTimeoutException only from CompleteBulkLoad (the session stays open afterward โ€” retry or dispose).
  • All seven inherit DurabilityException, so catch (DurabilityException) catches the whole family uniformly for logging/alerting, while specific catches still get typed properties (HighLsn, SegmentPath, etc.).
  • None of these are part of the TyphonTimeoutException family, even though BulkLoadCheckpointTimeoutException is deadline-driven โ€” it stays under DurabilityException because the BulkLoad session, unlike a lock wait, remains alive and resumable after the timeout.

๐Ÿงช Tests

  • WalCommitBufferTests โ€” an over-capacity claim throws WalClaimTooLargeException; a ring-buffer claim that outlives its deadline throws WalBackPressureTimeoutException.
  • BulkLoadApiSurfaceTests โ€” a second concurrent BeginBulkLoad throws BulkSessionAlreadyActiveException (ActiveBulkSessionId); any call after Dispose/CompleteBulkLoad throws BulkSessionClosedException (CompleteBulkLoad, Destroy).