jonnify·bcs /bcs/cache/cache-aside/declared-not-discovered

B4.1 · dive 1 — declared, not discovered

A cache absent from the directory does not exist.

A cache comes into existence by declaring itself: name, kind, TTL, jitter, size bound, sweep interval, and coherence mode, registered in the node's directory the moment the table starts and removed the moment it stops or crashes — the directory monitors its tables, so the roster is true even after a failure. The negative is gated too: an undeclared name answers :error, and a wrong-kind id is refused at the door — zero loader runs, zero keys on the wire.

Source: content/bcs4.1.md, quoting bcs_rung_4_1_check.out; the modules are committed at runtimes/elixir/lib/echo_store/echo_store.ex, lib/echo_store/keyspace.ex, lib/echo_store/table.ex.

The directory, asked three questions. Two declared caches, an undeclared name, and the kind gate at the door. Select a question to read the directory's answer.
:quotes kind "AST" · ttl 300 ms jitter 0.2 · coherence :none :refdata max_size 100 · ttl 60 s from derive (bound) an undeclared name not on the roster answers :error the kind gate refused at the door zero loader runs
readout · Ask the directory a question. Two caches on the roster with their full declarations; the negative answers are gated by E1, frozen in §1 below.
§1

The transcript

bcs_rung_4_1_check.out · verbatim · this dive reads the header, E1, E2
content/echo_data/runtimes/elixir/bcs_rung_4_1_check.out · the slice this dive readsfrozen
header: Valkey on 6390 | Elixir 1.14.0 OTP 25 | schedulers 1
E1 declared ok -- two caches enumerable with their full declarations -- kind, ttl, coherence -- an undeclared name answers :error, and a wrong-kind id is refused at the door: zero loader runs, zero keys on the wire
E2 sources ok -- one name, three sources in order: a cold read fills (loader ran once), a warm read hits L1 without touching the owner, and an L1 drop falls back to L2 -- the loader still ran once; the L2 row carries the declared TTL (PTTL 300 ms of 300)
…
PASS 6/6

The elided middle holds the three derive lines and gates E3–E6 — the herd, the speed, the jitter, and the bound. Dive 2 and dive 3 read them, and the module hub freezes the record whole, header and derive lines included.

§2

The declaration

content/bcs4.1.md · What · How · Decisions

A quote cache, declared — verbatim from the chapter's How:

{:ok, _} =
  EchoStore.Table.start_link(
    name: :quotes,
    kind: "AST",
    ttl_ms: 300,
    jitter: 0.2,
    sweep_ms: 100,
    coherence: :none,          # wired by Chapter 4.2
    loader: &PriceFeed.load/1,
    connector: [port: 6390]
  )

The decision is "declared, not discovered": the directory is a monitored registry, not a convention — a table registers its full specification at start, and a crash removes it. Enumeration is the operator's right, and the negative is gated — an undeclared name answers :error. For operators, EchoStore.tables/0 is "the law made callable": every cache on the node, with its full declaration, and nothing else — a cache absent from the directory does not exist.

EchoStore.tables()
# [{:quotes, %{kind: "AST", ttl_ms: 300, jitter: 0.2, coherence: :none, ...}}, ...]

The declared coherence: :none is a socket, not a shrug: B4.2 · Coherence by Mint Time wires it, and inherits invalidate/2 as the hook its coherence messages will pull. The declaration's wire is the production connector, "reused untouched" — the manuscript plans the connector's prose appendix.

§3

Three sources of one answer

content/bcs4.1.md · What · Where

A read resolves from L1, L2, or the declared loader, in that order, and the record walks one name through all three: a cold read fills (loader ran once), a warm read hits L1 without touching the owner, and an L1 drop falls back to L2 -- the loader still ran once. The L2 row carries the declared TTL on the server's own clock — PTTL 300 ms of 300 — written with SET ... PX so the second layer expires itself even if every node forgets.

The surface is fetch/2 with source tags :hit | :l2 | :fill — observability, not ceremony:

{:ok, quote, :hit} = EchoStore.Table.fetch(:quotes, "AST0NuE6bV7FoH")

The keyspace is its own — ecc:{<table>}:<id>, "a fresh prefix beside emq:, never inside it", hashtagged on the table name for the clustered day.

The three-sources walk. One name walked through its three sources, in the record's order; the loader-runs counter rides the readout. Every figure is the committed E2 line's.
readout · Walk the read path. One name, three sources in order — and the loader runs once for the whole walk.

Static record — the walk in the committed order: cold read → :fill (loader ran once) · warm read → :hit (L1, without touching the owner) · L1 drop → :l2 (the loader still ran once; PTTL 300 ms of 300).

§4

References

sources · related

Sources

  • Erlang/OTP — etspublic read_concurrency tables: the L1's storage, read caller-side
  • Valkey — SETthe PX option: the L2 row written with its expiry in one command
  • Valkey — EXPIREexpiration semantics: the server expires the L2 row on its own clock

Related