The read path — design
Class:
design· Function:read-path· Uses:store. Only the shape that runs; the why (why three layers, why no TTL, why foldhash) is inread-path-reasoning.md.
f0 — the layered resolve
A read resolves left to right, falling through only on a miss. The invariant that makes it coherence-free: every in-memory layer is keyed by an immutable, content-addressed identifier (a metadata_location or a snapshot id), so an entry can only be evicted, never stale.
table_exists / resolve_metadata / load_table
│
L1 pointer mirror ──miss──▶ L2 redb `tables` (source of truth)
│ (metadata_location)
L1.5 Table handle cache ──miss──▶ L0 metadata cache ──miss──▶ FileIO read+parse
The warm-read entry points on RedbCatalog:
| method | source | warm cost |
table_exists |
catalog.rs:827 |
L1 hit, ~lock-free |
resolve_location |
catalog.rs:107 |
L1 hit, no redb |
resolve_metadata |
catalog.rs:99 |
L1 → L0, no Table build |
load_table |
catalog.rs:790 |
L1 → L1.5 clone |
f1 — L1: the pointer mirror (pointer_cache.rs)
PointerCache (pointer_cache.rs:60) is an ArcSwap<PointerSnapshot> wrapping an imbl::HashMap<String, Arc<str>, foldhash::fast::RandomState> (pointer_cache.rs:42). It maps table_key → metadata_location for every live table.
- Built once from a full scan of the
tableskeyspace at open (store.rs:187). - Maintained write-through: each commit path calls
insert/remove(pointer_cache.rs:94) after its redb commit, still under the redb write lock. - Reads (
get,pointer_cache.rs:80) load the snapshot lock-free. - Every published
PointerSnapshotcarries a monotonicgeneration(pointer_cache.rs:46);mutateclones the map (imblstructural sharing, O(1)) and stores a new snapshot, so readers observe a whole snapshot atomically.
A key absent from the mirror means the table does not exist; the redb fallback in resolve_location / table_exists is a safety net that only runs on a genuine miss.
f2 — L0: the parsed-metadata cache (meta_cache.rs)
MetadataCache (meta_cache.rs:34) is a byte-bounded moka::future::Cache<String, Arc<TableMetadata>> keyed by metadata_location. get_or_load (meta_cache.rs:62) returns a cached Arc on a hit, else reads + parses the metadata JSON via FileIO, coalescing concurrent misses on the same location into one read_from (moka single-flight). Entry weight is a structural estimate (weight_of, meta_cache.rs:117); budget defaults to DEFAULT_METADATA_CACHE_BYTES = 128 MiB (meta_cache.rs:27). A budget of 0 disables it.
f3 — L1.5: the built-Table handle cache (table_cache.rs)
TableHandleCache (table_cache.rs:42) is a capacity-bounded moka::future::Cache<String, Table> keyed by metadata_location. get (table_cache.rs:67) returns a clone of a built Table (which shares the underlying Arc<ObjectCache>), short-circuiting before L0 — but only if the cached handle's identifier() matches, so two entries aliasing the same metadata file (via register_table) don't cross. build_and_insert (table_cache.rs:79) builds on a miss and caches. Default capacity DEFAULT_TABLE_HANDLE_CACHE_BYTES = 256 MiB (table_cache.rs); 0 disables. It is a BYTE budget, not a handle count: one handle pins its parsed TableMetadata plus iceberg's per-Table ObjectCache, so a count bounded nothing (512 handles x iceberg's 32 MiB default was ~16 GiB). Each handle's object cache is now pinned to TABLE_OBJECT_CACHE_BYTES (4 MiB) and charged at that ceiling.
Correctness
All three caches share one property: the key is the immutable metadata location (or an immutable snapshot id). A new commit writes a new file at a new location and flips the pointer, so a stale value is unrepresentable — no TTL, no invalidation, no coherence protocol. The full argument is in read-path-reasoning.md.