cairn
Design Decisions

Seqno recovery

A property test found a real bug — sequence numbers reset on reopen unless every SSTable is scanned too.

The bug

Engine::open derived next_seqno by replaying the WAL and taking the max seqno seen, plus one. That's correct until a flush happens: flush() empties the WAL once its contents are safely inside a new SSTable, because durability now lives in the SSTable, not the log. But that also means the seqnos of everything that was ever flushed are no longer visible to WAL-only replay.

Concretely: open the engine, put a key, flush(), close, reopen. The WAL is empty, so next_seqno resets to 0 — even though the flushed SSTable on disk holds an entry at, say, seqno = 1. The next write after reopen reuses seqno = 0. Because InternalKey ordering treats a lower seqno as older, that reused-seqno write silently loses to the stale pre-reopen value once compaction merges them — a write that should have won instead vanishes, and old data resurrects.

How it was found

Not by inspection — by the differential property test. tests/model.rs runs randomized sequences of Put/Delete/Get/Flush/Compact against both the real Engine and a reference BTreeMap, asserting every Get agrees. Adding a Reopen operation to that operation set (drop the engine, Engine::open the same directory again, keep going) turned this into a reliably-reproducible failure: random sequences that happened to flush then write again after a reopen would diverge from the model. This is exactly what the architecture testing strategy means by property tests catching what inspection misses.

The fix

Engine::open now scans every loaded SSTable's entries in addition to WAL replay, and folds the max seqno found — from either source — into next_seqno:

for (_, sst) in &sstables {
    for (ik, _) in sst.iter()? {
        next_seqno = next_seqno.max(ik.seqno + 1);
    }
}

A dedicated regression test encodes the exact repro (flush a key, reopen, write the same key again, compact, assert the new write wins) so this can't silently regress, and the Reopen proptest operation stays in the random op set permanently — reopen-then-get consistency is now something every property-test run exercises, not a one-off check.

Why this matters beyond Phase 1

Sequence numbers are the ordering primitive the MVCC layer (Phase 3) will build multi-version keys on top of, and the ordering Raft's replicated log will thread through the state machine. A seqno-reuse bug here wouldn't have stayed contained to the storage engine — it would have surfaced as silent data resurrection anywhere downstream that trusts "higher seqno always means newer."

On this page