B4.1 · dive 3 — the jittered clock
Rows filled together must not expire together.
If a cohort of rows expires at one instant, the TTL itself schedules the next herd. Every
insert draws its expiry from ttl ± ttl·jitter, and the committed drill shows the
spread doing its work: 400 rows filled in 24 ms expire 138 ms apart at jitter 0.2 — no
synchronized re-herd — while the zero-jitter cohort spreads only its own fill walltime, the control
that proves the treatment. The sweeper then reclaims on its tick, and the size bound degrades to
pass-through, never to failure.
Source: content/bcs4.1.md, quoting bcs_rung_4_1_check.out; the module is
committed at runtimes/elixir/lib/echo_cache/table.ex.
The transcript
bcs_rung_4_1_check.out · verbatim · this dive reads the jitter and bound lines… derive (jitter): ttl 300 ms at jitter 0.2 spreads expiry uniformly across plus-minus 60 ms -- 400 rows filled in one fast pass should spread at least 70 ms beyond their fill walltime, approaching 120; a jitter 0.0 cohort's spread can never exceed its own fill walltime -- jitter adds nothing; the sweeper on a 100 ms tick then reclaims the whole cohort without a single read E5 jitter ok -- 400 rows filled in 24 ms expire 138 ms apart at jitter 0.2 -- no synchronized re-herd -- while the jitter 0.0 cohort spreads 5 ms across a 4 ms fill: jitter added nothing there; the sweeper then reclaimed the whole cohort on its tick (swept 400, table size 0) with not one read paying the cleanup derive (bound): refdata declares max_size 100 with a 60 s ttl, so nothing expires to reclaim -- 49 more fills fit beside the 51 live rows, then every further fill must serve its caller and skip the insert E6 bound ok -- the declaration holds: size capped at 100 of 100, 101 fills served their callers and skipped the insert -- a full cache is a stat, never an error -- and the writer path lands one value in both layers PASS 6/6
The elided lines hold the header, E1–E2, and the herd and speed drills — dive 1 and dive 2 read them, and the module hub freezes the record whole. The zero-jitter cohort is the record's own control: its spread can never exceed its own fill walltime, so the 138 ms spread of the treated cohort is the jitter's work and nothing else's.
The sweeper, and two clocks
content/bcs4.1.md · What · Decisions · WhenDead rows leave whether or not anyone reads them again. The sweeper reclaims on its tick
— the committed line: the sweeper then reclaimed the whole cohort on its tick (swept 400, table
size 0) with not one read paying the cleanup — a one-pass select_delete over the
table. Valkey does the same on its side with on-access and active expiration — two layers each keeping
their own house.
The decision is the series' two-clocks law, now in the cache: L1 expiry runs on the
BEAM's monotonic clock; L2 expiry runs on the server's clock via PX. Each layer is sovereign
over its own staleness, and neither trusts the other's watch. From the chapter's When: size the jitter to
the herd you fear — 0.1–0.2 spreads a cohort across a fifth of its TTL, which is enough to turn a
synchronized re-fill into a trickle.
The bound
content/bcs4.1.md · The bound · BoundariesA full cache is a stat, never an error. With max_size 100 declared and
nothing expired to reclaim, the record shows size capped at 100 of 100, 101 fills served their
callers and skipped the insert — every caller got its value, the table held its declaration, and
the overflow is visible in full_skips rather than in anyone's latency alarm. No eviction
cleverness in this chapter: "an LRU would tax every read with touch-tracking", and the read path's purity
is the product. Smarter eviction, if ever wanted, is a follow-up knob under its own gate.
The boundaries travel with the gate: this chapter's staleness bound is the TTL and nothing sharper — a write elsewhere becomes visible here only when the clock says so, and surfaces that cannot accept that wait for the versioned invalidation of B4.2 · Coherence by Mint Time. The L1 is per-node by design — two nodes warm independently, and cross-node agreement is exactly the coherence problem this chapter does not claim to solve.
Static record — the walk in the committed order: refdata declares max_size
100 with a 60 s ttl · 49 more fills fit beside the 51 live rows · size capped
at 100 of 100 · 101 fills served their callers and skipped the insert ·
the writer path lands one value in both layers.
References
sources · relatedSources
- Erlang/OTP — etsselect_delete match-specification deletion: the sweeper's one-pass reclaim
- Valkey — EXPIREexpiration semantics and the server's two reclamation paths, on-access and active cycles
- Valkey — SETthe PX option: the L2 row written with its expiry in one command
Related
- B4.1 — Cache-Aside at ETS Speedthe module hub; the full rung in context
- B4 — EchoCachethe chapter landing
- B3 — The Busthe volatile substrate beside the cache
- B2.2 — Property Storesthe owners the TTL shields
- Redis Patterns Appliedthe caching substrate patterns
- EchoMQ — the coursethe bus protocol in rung-level depth