Storage & Durability
Work through examples, try the reasoning yourself, then put it into practice.
View SlidesBefore you start
- Trace a map through sets and deletes.
- Distinguish durable storage from volatile process state.
What you will practice
- Replay a durable prefix and separate persistence from acknowledgment.
- Trace newest-version read precedence without resurrecting deleted values.
- Justify tombstone reclamation from full coverage and safe-GC assumptions.
In this lesson
Lesson
7 min readStorage & Durability
A delete must remain a delete after a crash, a read, and compaction. Keeping the newest value is only half the problem: keeping enough evidence that an older value was deleted is the other half.
You should already be comfortable with maps, sorted arrays, and binary search. By the end, you should be able to replay a persisted history, trace a lookup, separate logical deletion from physical reclamation, and justify why compaction cannot expose a shadowed value.
Persisted history and crash recovery
A simplified write path is: append a WAL record, make the required log prefix durable, apply it to the memtable, and acknowledge the write. A successful buffered append alone does not establish crash durability. Later, a flush writes an immutable SSTable; compaction replaces several tables with merged tables. Recovery also needs durable metadata identifying which tables are installed. The exercises model records and entries, not filesystem synchronization or atomic installation of files.
Replay complete persisted records in append order:
| Record | Visible state after replay |
|---|---|
set a=1 |
{a:1} |
set b=2 |
{a:1,b:2} |
delete a |
{b:2} |
set a=3 |
{a:3,b:2} |
A crash after persisting record 3 but before applying it in memory still recovers
{b:2}. Record 4 is recovered only if it belongs to the durable prefix. An
unacknowledged operation can appear after recovery if it was already persisted.
ReplayWAL receives that complete prefix; torn records and transaction commits
are outside its contract. Sort final keys to make its output deterministic.
A tombstone is a stored fact, not an expiring delete
SSTables are immutable. Deleting k cannot simply remove its newest in-memory
value, because an older table may still hold k=old. Write a tombstone instead.
A read selects the winning version across all sources, then returns absent
when that winner is a tombstone.
gcBefore is a reclamation threshold. It never tells a read to ignore a stored
tombstone. Age alone cannot prove that no older copy remains. LevelDB retains
deletion markers to hide older values and drops markers only when overlap with
older levels permits it. LevelDB implementation notes.
A distributed store must also account for replicas that missed the delete: removing all evidence too early can let an old replica reintroduce the value. This module assumes that an externally established GC horizon accounts for those copies; computing that horizon is outside the exercises. Apache Cassandra tombstone documentation.
Trace the read path before implementing it
LSMGet and MultiWayCompaction share one rule: greatest timestamp wins;
on a tie, the earlier source wins. Read sources are memory followed by tables
in input order. Within memory, the first equal-timestamp occurrence wins. Each
table has at most one entry per key. Table creation order need not match every
entry's timestamp, so a read must compare candidates from every possible table.
Query k, with gcBefore=10:
| Location | Entries or metadata | Decision |
|---|---|---|
| memtable | k@4=old, x@11=x, k@8=delete |
Scan all entries; k@8 wins |
| first SSTable | k@6=stored |
Candidate is older than the current winner |
| second SSTable | k@1=ancient |
Candidate is older; keep the tombstone winner |
The result is absent even though 8 < 10. The marker still exists and still
covers those older values. Changing only the threshold cannot change a read.
If a later memtable write k@12=new is added, that write wins: recreating a key
with a newer write is legitimate, unlike exposing its pre-delete value.
With no matching memtable entry, a Bloom-negative table can be skipped. A
Bloom-positive table only might contain the key; binary search must confirm
an exact match. For example, a positive table containing [a,c] does not answer
query b, so continue. In valid inputs, a negative flag guarantees absence for
this query. The flag is supplied; building a Bloom filter is not required.
Compaction: choose the winner before reclaiming anything
The first merge exercise, CompactRuns, is a complete two-run materialization:
there are no other versions, replicas, or snapshots to preserve. Newer-run
entries override older-run entries and deleted keys are omitted. This makes its
value-only output safe under that narrow assumption. It is not a routine for
compacting an arbitrary pair while leaving overlapping older tables outside.
MultiWayCompaction returns a stored run, which may contain tombstones.
For each key, rank versions by timestamp descending, then run index ascending.
This is the same priority as LSMGet across its tables. All runs are sorted by
key, with at most one entry per key per run.
The exercises serve current reads only: no historical snapshot requires any
losing version. The caller guarantees full coverage of older local versions for
these keys and that stale replicas cannot reintroduce values shadowed by markers
below gcBefore. The cutoff certifies that external safety condition; time alone
is not evidence of it. Under those assumptions:
- Select exactly one winner, including tombstones in the ranking.
- A value winner is emitted unchanged.
- A tombstone winner at or above
gcBeforeis retained unchanged. - A tombstone winner below
gcBeforeis omitted together with all its older versions. Never fall back to a losing value.
Worked compaction trace
Inputs, newest run first:
run0: k@7=delete, x@5=v5
run1: k@6=v6, x@4=v4
run2: k@2=v2
| Cutoff | Winning versions | Stored output | Visible values |
|---|---|---|---|
gcBefore=5 |
k@7=delete, x@5=v5 |
k@7=delete, x@5=v5 |
x=v5 |
gcBefore=7 |
same | same; equality retains the marker | x=v5 |
gcBefore=8 |
same | x@5=v5; all versions of k removed |
x=v5 |
To integrate the components, query these runs as SSTables with an empty
memtable, compact them, then query the single output table. k must remain
absent and x must remain v5 at every cutoff. Repeat with a later table
holding a greater timestamp: both read and compaction must choose it. A reader
that stops at the first match would fail that test.
Physical storage changes, but the visible answer does not. If another older
run containing k@1 were outside this compaction, the full-coverage assumption
would fail; keep the marker instead. A production API needs that coverage
information rather than inferring it from a clock.
A map holding each key's current winner uses O(N) scanning, then sorting K
keys costs O(K log K), with O(K) extra storage. Grouping and sorting all
candidates is also acceptable, but sorting must never discard tombstones before
selecting the winner.
Predict, explain, then implement
- A table's winning delete is at timestamp 9 and the cutoff becomes 100. Can a read return an older value without any write or compaction?
- A compaction winner is
z@9=delete, cutoff 10, with all older versions and required GC guarantees covered. What is emitted forz? - At timestamp 12, run 0 has a value and run 1 a tombstone. Which wins? What if their positions are swapped?
- What information is missing if someone proposes dropping a marker simply because it is a week old?
Check your reasoning
- No. Reads honor stored tombstones regardless of cutoff.
- Nothing for
z; none of its older values survives either. - The run-0 entry wins each tie. A newer equal-timestamp tombstone deletes; a newer equal-timestamp value recreates the key under this exercise's tie rule.
- Whether overlapping older versions remain, historical snapshots are served, or replicas can return stale data. Elapsed time alone supplies none of these.
Implement the four exercises in module order, then use the checkpoint to test new crash and version histories. If a question about reclamation is missed, retrace both the stored output and the visible value columns above.
Practice sequence
Use the order below for a guided path, or choose an item to revisit. A pass records a successful attempt; try a fresh example later to check what you can reconstruct.
- Step 1
WAL Replay
Reconstruct visible state from a complete durable history, including deletes and later recreation.
- Step 2
LSM Compaction Merge
Materialize a complete two-run history while removing shadowed values and deleted keys.
- Step 3
LSM Read Path
- Step 4
LSM Multi-Way Compaction
- Step 5
Storage & Durability Checkpoint
Trace durable histories, distinguish stored markers from visible values, and diagnose unsafe reclamation. Select one answer per multiple-choice question; enter only the requested integer for numeric answers.