ADR-0022: Opt-in persistent head catalog and restart recovery
Status: Accepted (2026-07-24).
Context
The default marker-gated wipe (ADR-0013) is safe and simple, but a multi-terabyte cache should be able to reuse valid bytes after an ordinary process restart. Goblin already hashes every client key to one SHA-256 digest and computes tier geometry from object length, so recovery does not require a second metadata database.
Decision
--heads-path DIRis explicit, opt-in, separately marker-gated, and distinct from every SSD/HDD pool. Without it, startup behavior is unchanged.- A published head is
DIR/<first-digest-byte>/<64-lowercase-hex-digest>and contains exactlymin(object_size, ram_head)bytes. SSD/HDD shards use the same two-hex-digit fan-out and generationless published filename. Recovery also accepts the original flat layout in place. - Private
<digest>.g<generation>files remain the concurrent-write format. Under the publication lock, replacement commit removes the old head, replaces/removes SSD, replaces/removes HDD, then renames the new head last. A first insert skips negative unlink probes and directly renames its private files. The head filename is therefore the restart commit marker. - Whole-object deletion and eviction unlink head, SSD, then HDD. A RAM-head-cache eviction of a disk-backed object does not remove the persistent head marker: the object remains live and its SSD write-through prefix can still serve byte zero.
- Startup scans heads, SSD drives, and HDD drives once. Valid shapes are head-only, head+SSD, and head+SSD+HDD. A lower-tier component without a head is reclaimed before serving traffic; an incompatible head-bearing shape remains untouched unless that digest is explicitly replaced.
- SSD continues to store
[0, ssd_prefix), including the write-through copy of the RAM head. Therefore SSD-only size is the aggregate exact SSD shard length; three-tier size is aggregate SSD plus HDD length. Recovery validates the head length and configured tier geometry before admission. ObjectMetacaches explicit persistent-head, SSD, and HDD presence bits. The GET path opens only known components; it never probes HDD for an SSD-only object or SSD for a head-only object.- Recovery loads every accepted head into the configured RAM arenas. Insufficient RAM is a startup error, not a silently partial cache.
- The format intentionally stores bytes and digest identity only. Flags, TTL, access score, HTTP response metadata, and the original unhashed key are reset or unavailable after restart.
Consequences
- Warm process restarts avoid repopulating valid object bodies and need no journal/database.
- Publishing several striped shards is not one filesystem-atomic transaction, but head-last publication makes every interrupted state conservatively invisible.
- Exact published shard lengths permit size inference; O_DIRECT may return a short completion for alignment padding at EOF, and readers validate the logical segment length rather than requiring padding to exist.
- Crash-left
.gNscratch files are ignored. Headless published lower-tier orphans are reclaimed during startup, eliminating per-insert speculative cleanup lookups.
