Expand description
Race-free per-key once-init cache.
Generalizes the “cached shared resource” pattern that Polydat node
functions repeatedly need: a global, expensive-to-construct
handle (a vectordata TestDataGroup, a typed reader, an HTTP
client, a parsed schema, a compiled regex, …) that should be
built at most once per unique key, regardless of how many
fibers race to the access point.
§Why a dedicated type
The naïve form — Mutex<HashMap<K, V>> with
lock → check missing → unlock → load → relock → insert —
exhibits a TOCTOU bug under concurrency: N fibers all see
“missing” simultaneously, each runs the (expensive) loader,
and the last writer wins. nb-rs hit exactly this in
polydat::library::vectors: 20 fibers all opened their
own vectordata::Storage per facet, each constructing a
fresh reqwest::blocking::Client (≈ load-native-certs +
TLS-bootstrap), driving the per-cycle reqwest cost into
observable flamegraph dominance.
OnceCache caches an Arc<OnceLock<Result<V, String>>>
per key, holds the outer Mutex only long enough to insert
the slot, then dispatches the actual loader through
OnceLock::get_or_init. Concurrent callers for the same
key block on the OnceLock and reuse the cached Result —
exactly one loader run per (key, lifetime).
§Failure semantics
Failed loads are sticky: every concurrent caller for the
same key sees the same Err rather than triggering a retry
storm. This is deliberate — the caller in nb-rs treats a load
failure as a workload-config issue (missing dataset, bad URL,
permission), and re-attempting per fiber wouldn’t change the
diagnostic. If a future caller wants retry semantics, it
should clear the slot via a purge method (not yet
exposed; add when needed).
§Hot-path cost
After the first successful load, every subsequent
get_or_init is: one outer Mutex::lock (for the
HashMap::entry lookup), one Arc::clone, one
OnceLock::get_or_init (returns immediately because the
slot is initialized), and one Result::clone (the V is
typically Arc<…> — a refcount bump). No load runs, no I/O
happens. This is comparable to a plain Mutex<HashMap> read
and not on any hot path nb-rs cares about (cycle-time reads
go through pre-resolved handles, not through the cache).
Structs§
- Once
Cache - Per-key once-init cache. See module docs for the rationale and pattern.