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.

๐Ÿ’ป 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 โ€” unlike ordinary data pages, these have no primary data to rebuild from; a confirmed double failure means restoring from backup.
  • Doublewrite-free โ€” unlike the retired Full-Page-Image mechanism, 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 written before this mechanism cannot be opened by an engine that expects it; older files are 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