Table of Contents

A/B Protected-Page Slot-Pairing

Doublewrite-free torn-write protection for the meta page and segment-directory pages that crash recovery can't re-derive.

Status: โœ… Implemented ยท Visibility: Internal ยท Category: Durability

๐ŸŽฏ What it solves

Most data pages survive a torn write because they're either rebuilt from primary data on the next crash recovery, or detected as corrupt and the open fails loudly. Two kinds of page are neither: the database's root metadata and each segment's page directory describe where everything else lives โ€” there's no "primary data" to rebuild them from. A power cut mid-write to the only copy of such a page would corrupt structure recovery depends on, with nothing to recover it from. The earlier fix (a before-image written on every checkpoint cycle) protected these pages but cost an extra write per protected page on every single cycle, whether or not the page had changed.

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

Every protected page โ€” the meta page and each segment's directory pages โ€” is backed by two physical slots. A write always lands on whichever slot isn't the currently-trusted one, tagged with a higher generation number and its own checksum; only after that write is confirmed durable does Typhon start treating it as current. The previously-current slot is never touched by the write, so a crash can tear at most the other copy. At open, or whenever a torn write is suspected, both slots are read and the highest-generation one that passes its checksum is trusted; if neither does, the open fails loudly rather than risk silently serving corrupt structure โ€” with one exception on the crash path: a segment root page whose both slots are invalid and whose first sector records PairGeneration 1 means the pair's first persist tore before it completed, so the root never had a durable copy. The engine replaces the segment from the WAL window instead of failing (see Guarantees & limits below for the full carve-out).

๐Ÿ’ป Usage

Entirely automatic โ€” there is no API to opt into. It engages whenever the engine writes the meta page (schema/bootstrap changes) or grows a segment's directory (an archetype acquiring more storage), both ordinary side effects of normal writes and of every checkpoint cycle:

services
    .AddScopedManagedPagedMemoryMappedFile(o =>
    {
        o.DatabaseName = "skirmish";
        o.DatabaseDirectory = ".";
    })
    .AddScopedDatabaseEngine();

// Nothing extra to call โ€” spawning enough entities to grow an archetype's segment, or any
// schema mutation, is already protected the moment the engine persists it.
using var t = dbe.CreateQuickTransaction();
var id = t.Spawn<Soldier>(Unit.Health.Set(new Health { Current = 100 }));
t.Commit();

โš ๏ธ Guarantees & limits

  • At least one valid copy always exists โ€” the trusted slot is never overwritten by a protected-page write, so torn-write exposure is limited to the alternate slot only.
  • Both-slots-corrupt is a loud failure, never a silent fallback โ€” with one carve-out: on the crash path only, a segment root page with both slots invalid at PairGeneration 1 is treated as a root whose first persist tore before completing (not as corruption). The engine replaces the segment from the WAL window and continues; no backup is required. A root at any other generation, a map-extension page, and any page on the clean-open path still fail loudly โ€” those have no WAL window to recover from.
  • Doublewrite-free โ€” this costs one extra page write only when a protected page actually changes (segment creation/growth, schema mutation), not on every checkpoint cycle regardless of change.
  • Cold path only โ€” segment-directory writes happen at segment create/grow time, not on steady-state entity reads/writes, so normal-operation hot paths pay zero extra cost from this mechanism.
  • Generation is monotonic โ€” selection at open/recovery is unambiguous: the highest valid generation number wins, never a heuristic or timestamp comparison.
  • Tied to the on-disk format version โ€” a database file whose format version does not carry paired protected pages is refused outright, not auto-migrated.

๐Ÿงช Tests

  • MetaPairTests โ€” meta-page A/B slot alternation, torn-slot recovery, generation monotonicity
  • DirectoryPairTests โ€” segment-directory twin slot-pairing, same corruption/reopen style for directory pages
  • ProtectedPairTests โ€” falsifiable proof via a write-log that the current-valid slot is never physically written