Tombstones — deletes that come back

A tombstone is a record stating absence; if merge garbage-collects the tombstone before every older segment containing the key has been merged away, restart will replay the older live record and resurrect the deleted key.

Previously

fsync chose how much of the tail to keep. Merge chose how much of the dead to reclaim. The collision between those two policies is where deleted keys come back from the dead.

Scene 07

Tombstones — deletes that come back

  1. Watch
  2. Try it
  3. Predict
  4. Capture
Bitcask tombstone — step 1/5: PUT k=v1Storyboard of the tombstone resurrection bug. Merge rule is naive; delete mode is immediate; gap between delete and merge is immediate.1PUT k=v1appends to active file …data 003k=v1KEYDIRk → 003@642DELETE ktombstone in 007 · k re…data 003k=v1data 005a=alpb=betdata 007a=alpk=⊥KEYDIRa → 007@0b → 005@323Merge 005-007naive: no live k in win…data 003 (untouched)k=v1data 005reclaimeda=alpb=betdata 007reclaimeda=alpk=⊥merged M-1a=alpb=betactive 008c=gamKEYDIRa → M-1@0b → M-1@24rule: naive (drops tomb)4Crashkeydir is RAM only · di…Step 4 introduces the resurrection bugUH OHdata 003k=v1merged M-1a=alpb=betKEYDIR(empty)5Restartscan replays k=v1 from …Step 5 introduces the resurrection bugUH OHdata 003 (replayed)k=v1merged M-1 (replayed)a=alpb=betKEYDIRk → 003@64a → M-1@0b → M-1@24MERGE BEHAVIOURrule: naiverulenaivetombstone2delete_mode: immediatedelete_modeimmediatekeepgap: immediategapimmediatedelayednaive merge drops tombstones; delete_mode=immediate forgets the tomb fast;…naive merge drops tombstones; delete_mode=immediate forgets the tomb fast; immediate merge runs before the tomb can land.FINAL GET k(run all 5 steps to see the result)Slot 0 — naive merge drops the tombstone and resurrects k · step 1/5 — PUT k=v1
What to watch for

Naive merge rule. Five steps animate left to right, ~2s each: PUT, DELETE, merge, crash, restart. Step 5 is the punchline — restart rebuilds the keydir from disk and the deleted key reappears with a red WRONG badge.

Continue unlocks when the animation finishes.
Implementation

Highlighted lines are the ones running in the diagram right now.

Bitcask.delete
delete is just another append — a tombstone record. Highlight indices are 0-based.
def delete(key):
tomb = Record(
key = key,
value = TOMBSTONE_MARKER, # vsz = 0
tstamp = now(),
)
active_file.append(tomb)
keydir.pop(key, None) # RAM only — disk still holds older PUT
Merge.naive
scan a window of immutable files, keep only what keydir points at. Highlight indices are 0-based.
def merge(window_files, keydir):
out = new_data_file()
for f in window_files:
for rec in f.scan():
if rec.is_tombstone:
# key already gone from keydir → assume redundant
if rec.key not in keydir:
continue # drop the tombstone
elif keydir[rec.key] != (f.id, rec.offset):
continue # stale PUT, shadowed by newer record
out.append(rec)
atomic_swap(window_files, out)
Merge.tombstone2
riak_kv #925 — re-append the tombstone into the oldest file still mentioning the key. Highlight indices are 0-based.
def merge(window_files, keydir, all_files):
out = new_data_file()
for f in window_files:
for rec in f.scan():
if rec.is_tombstone:
older = oldest_file_with(rec.key, all_files)
if older and older not in window_files:
older.append(rec) # carry tombstone backwards
continue
# safe to drop: no older file mentions this key
continue
elif keydir[rec.key] != (f.id, rec.offset):
continue
out.append(rec)
atomic_swap(window_files, out)
Bitcask.startup_replay
rebuild the keydir by scanning every immutable file in tstamp order. Highlight indices are 0-based.
def open(dir):
keydir = {}
for f in sorted(dir.data_files(), key=lambda f: f.tstamp):
for rec in f.scan():
if rec.is_tombstone:
keydir.pop(rec.key, None)
else:
keydir[rec.key] = (f.id, rec.offset, rec.tstamp)
return keydir # whichever record wins by tstamp wins on disk

Where this sits in Build a Bitcask-style KV store

Scene 07 of 9, in the Sharp edges act — Tombstone resurrection and the design ceiling.. If merge GCs a tombstone before every older segment containing the key has been merged away, restart resurrects the deleted key. The riak_kv #925 fix.

Up next. Tombstones must outlive every older segment that mentions the key — the rule is nontrivial because every log-structured store ships some version of this trap. Now zoom all the way out and design.

All 9 scenes in Build a Bitcask-style KV store · Every curriculum

Built with Arqly
Every scene in Build a Bitcask-style KV store builds on the one before it.All 9 Build a Bitcask-style KV store scenes