Skip to main content

cubecl_environment/persistence/
store.rs

1use core::{fmt::Display, hash::Hash};
2
3use alloc::boxed::Box;
4use alloc::string::String;
5use alloc::vec::Vec;
6
7use hashbrown::{HashMap, HashSet};
8use serde::{Serialize, de::DeserializeOwned};
9
10use super::namespace::Namespace;
11use super::storage::{Insertion, Origin, Storage};
12use crate::bytes::Bytes;
13
14/// Error related to a [`Store`].
15#[derive(Debug)]
16pub enum StoreError<K, V> {
17    /// This process already stored a different value under that key: the same
18    /// function was computed twice with disagreeing results.
19    #[allow(missing_docs)]
20    DuplicatedKey {
21        key: K,
22        value_previous: V,
23        value_updated: V,
24    },
25    /// The durable entry was written by someone else — another process sharing
26    /// the cache root, or a bundle import — before this insert reached it.
27    /// Benign: the two values are equally valid and the stored one stays.
28    #[allow(missing_docs)]
29    KeyOutOfSync {
30        key: K,
31        value_previous: V,
32        value_updated: V,
33    },
34    /// The storage backend refused the write, so the entry is not durable and
35    /// will be recomputed on the next run.
36    #[allow(missing_docs)]
37    Backend { key: K, error: String },
38}
39
40impl<K, V> StoreError<K, V> {
41    /// Why the write failed, without the key or the value.
42    ///
43    /// [`Display`] renders both, which is right for small entries and wrong
44    /// for a compiled kernel: those values are megabytes of binary, and a log
45    /// line must not carry one. Callers storing large values report this
46    /// instead.
47    pub fn reason(&self) -> &str {
48        match self {
49            Self::DuplicatedKey { .. } => "the key was already stored with a different value",
50            Self::KeyOutOfSync { .. } => "another process stored the key first",
51            Self::Backend { error, .. } => error,
52        }
53    }
54}
55
56impl<K: core::fmt::Debug, V: core::fmt::Debug> Display for StoreError<K, V> {
57    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
58        match self {
59            Self::DuplicatedKey {
60                key,
61                value_previous,
62                value_updated,
63            } => write!(
64                f,
65                "key {key:?} was already stored with a different value: \
66                 kept {value_previous:?}, dropped {value_updated:?}"
67            ),
68            Self::KeyOutOfSync {
69                key,
70                value_previous,
71                value_updated,
72            } => write!(
73                f,
74                "key {key:?} was stored concurrently: kept {value_previous:?}, \
75                 dropped {value_updated:?}"
76            ),
77            Self::Backend { key, error } => {
78                write!(f, "storing key {key:?} failed: {error}")
79            }
80        }
81    }
82}
83
84impl<K: core::fmt::Debug, V: core::fmt::Debug> core::error::Error for StoreError<K, V> {}
85
86/// Trait to be implemented for store keys.
87pub trait StoreKey: Serialize + DeserializeOwned + PartialEq + Eq + Hash + Clone {}
88/// Trait to be implemented for store values.
89pub trait StoreValue: Serialize + DeserializeOwned + PartialEq + Eq + Clone {}
90
91impl<T: Serialize + DeserializeOwned + PartialEq + Eq + Clone + Hash> StoreKey for T {}
92impl<T: Serialize + DeserializeOwned + PartialEq + Eq + Clone> StoreValue for T {}
93
94/// How a [`Store`] populates its in-memory map from its storage.
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
96pub enum CacheOption {
97    /// Scan everything at open. Reads are complete: a miss is a miss and
98    /// never queries the storage.
99    #[default]
100    Eager,
101    /// Fault entries in from the storage on first access. Right when the
102    /// values are large enough that loading all of them eagerly is wasteful,
103    /// typically compilation artifacts.
104    Lazy,
105}
106
107/// Where a [`Store`] persists its entries.
108#[derive(Debug, Default)]
109enum StorageOption {
110    /// Nowhere: the in-memory map is all there is.
111    #[default]
112    InMemory,
113    /// The active environment's storage for this namespace.
114    Environment(Namespace),
115    /// An explicitly provided storage, addressing this namespace.
116    Explicit(Box<dyn Storage>, Namespace),
117}
118
119/// Defines how to create a [`Store`].
120#[derive(Debug, Default)]
121pub struct StoreOptions {
122    storage: StorageOption,
123    cache: CacheOption,
124}
125
126impl StoreOptions {
127    /// Options for a store with no persistence and everything in memory.
128    pub fn new() -> Self {
129        Self::default()
130    }
131
132    /// Persist entries under `namespace` in the active environment.
133    pub fn storage<N: Into<Namespace>>(mut self, namespace: N) -> Self {
134        self.storage = StorageOption::Environment(namespace.into());
135        self
136    }
137
138    /// Persist entries to an explicit storage addressing `namespace`,
139    /// bypassing the active environment. Mostly for tests and benches.
140    pub fn storage_with<N: Into<Namespace>>(
141        mut self,
142        storage: Box<dyn Storage>,
143        namespace: N,
144    ) -> Self {
145        self.storage = StorageOption::Explicit(storage, namespace.into());
146        self
147    }
148
149    /// How the in-memory map is populated from the storage. Meaningless
150    /// without one: an in-memory store keeps everything regardless.
151    pub fn cache(mut self, cache: CacheOption) -> Self {
152        self.cache = cache;
153        self
154    }
155}
156
157/// A typed key-value store over an optional persistence [`Storage`]: a Turso
158/// database on native targets and in the browser (feature `persistence`), or
159/// nothing at all.
160///
161/// Reads follow `HashMap`'s shape and mutation rules, with no interior
162/// mutability: [`get`](Store::get) serves shared references from memory,
163/// while everything that may change the store — [`insert`](Store::insert),
164/// [`get_mut`](Store::get_mut), [`remove`](Store::remove),
165/// [`sync`](Store::sync) — requires `&mut self`. Share a store by wrapping it
166/// in a lock, not by cloning it.
167///
168/// [`remove`](Store::remove) and [`clear`](Store::clear) act on memory alone;
169/// their durable mirrors [`purge_key`](Store::purge_key) and
170/// [`purge`](Store::purge) also delete from the storage.
171///
172/// [`CacheOption`] decides how the map is populated: [`Eager`
173/// ](CacheOption::Eager) ingests the whole namespace at open and serves every
174/// read from memory, [`Lazy`](CacheOption::Lazy) reads one key at a time on
175/// demand.
176///
177/// # No Edits
178///
179/// A stored value never changes: there is no update, and reinserting a key
180/// with a different value is an error. The one exception is imported entries:
181/// a locally computed value replaces a value that came from a bundle, because
182/// a shipped bundle must never be able to wedge the application that imported
183/// it. See [`Store::insert`]. Mutating a value through
184/// [`get_mut`](Store::get_mut) changes only the in-memory copy, never the
185/// storage.
186///
187/// # Environment switches
188///
189/// A store opened on the active environment stays bound to *the environment*,
190/// not to the storage it opened: when [`crate::environment`] switches
191/// ([`activate`](crate::environment::activate),
192/// [`load`](crate::environment::load), ...), the store detects it and resets —
193/// reads miss instead of serving the old environment's entries, and the next
194/// `&mut` operation drops the in-memory state and reopens the storage.
195/// Detection is one relaxed atomic load, so it costs nothing while the
196/// environment stays put. Stores on an explicit or absent storage are not
197/// bound and never reset.
198pub struct Store<K, V> {
199    entries: HashMap<K, MemoryEntry<V>>,
200    /// Keys this process interacted with without the map holding their value:
201    /// [`CacheOption::Lazy`] inserts (which drop the value rather than retain
202    /// megabytes of compiled artifacts never read again) and
203    /// [`remove`](Store::remove)d entries. What tells a reinsert collision
204    /// [`StoreError::DuplicatedKey`] apart from [`StoreError::KeyOutOfSync`].
205    known: HashSet<K>,
206    storage: Option<Box<dyn Storage>>,
207    namespace: Option<Namespace>,
208    cache: CacheOption,
209    /// The environment generation the state belongs to, for stores bound to
210    /// the active environment; `None` for unbound ones (explicit storage, or
211    /// none). A mismatch with the current generation means everything here
212    /// describes an environment that is no longer active.
213    generation: Option<u32>,
214}
215
216impl<K: StoreKey, V: StoreValue> Store<K, V> {
217    /// Create a new store from the options.
218    ///
219    /// With an [`Eager`](CacheOption::Eager) cache over a storage, everything
220    /// the storage holds is ingested before returning.
221    #[cfg_attr(
222        feature = "tracing",
223        tracing::instrument(level = "trace", skip_all, fields(options = ?options))
224    )]
225    pub fn new(options: StoreOptions) -> Self {
226        let (storage, namespace, generation) = match options.storage {
227            StorageOption::InMemory => (None, None, None),
228            StorageOption::Environment(namespace) => {
229                // Sampled before the storage opens: a switch landing in
230                // between leaves a stale generation, which reads as "reset",
231                // never as "this storage belongs to the new environment".
232                let generation = crate::environment::generation();
233                (
234                    Some(super::storage::open(namespace.as_str())),
235                    Some(namespace),
236                    Some(generation),
237                )
238            }
239            StorageOption::Explicit(storage, namespace) => (Some(storage), Some(namespace), None),
240        };
241
242        let mut store = Self {
243            entries: HashMap::new(),
244            known: HashSet::new(),
245            storage,
246            namespace,
247            cache: options.cache,
248            generation,
249        };
250
251        if matches!(store.cache, CacheOption::Eager) && store.storage.is_some() {
252            store.ingest();
253        }
254
255        store
256    }
257
258    /// The namespace this store addresses, if it persists anywhere.
259    pub fn namespace(&self) -> Option<&Namespace> {
260        self.namespace.as_ref()
261    }
262
263    /// Fetch an item from memory.
264    ///
265    /// Never touches the storage: on an eager store the map is complete, so a
266    /// miss is a miss. On a lazy store this only serves entries a previous
267    /// [`get_mut`](Store::get_mut) faulted in; use `get_mut` to read through.
268    ///
269    /// After an environment switch everything in memory belongs to the old
270    /// environment, so this misses rather than serve it — a miss costs a
271    /// recompute, a stale hit would be wrong.
272    pub fn get(&self, key: &K) -> Option<&V> {
273        if self.stale() {
274            return None;
275        }
276
277        self.entries.get(key).map(MemoryEntry::value)
278    }
279
280    /// Fetch an item, reading it from the storage on the first lookup of a
281    /// lazy store and memoizing it afterwards.
282    ///
283    /// Mutating the value changes only the in-memory copy, never the storage.
284    pub fn get_mut(&mut self, key: &K) -> Option<&mut V> {
285        self.reset_if_stale();
286
287        if matches!(self.cache, CacheOption::Lazy)
288            && !self.entries.contains_key(key)
289            && let Some(value) = self.fetch(key)
290        {
291            self.entries.insert(key.clone(), MemoryEntry::Stored(value));
292        }
293
294        self.entries.get_mut(key).map(MemoryEntry::value_mut)
295    }
296
297    /// Take an item out of memory, reading through to the storage on a lazy
298    /// store, and hand it out owned.
299    ///
300    /// The storage is untouched — deleting durably is
301    /// [`purge_key`](Store::purge_key). The key stays
302    /// [`known`](StoreError::DuplicatedKey) to the store, unless the storage
303    /// refused its value: nothing durable backs that key, so it is forgotten
304    /// and a later insert is a fresh one. This is the read
305    /// for a value consumed once per process — a compiled kernel about to be
306    /// loaded — because nothing is cloned and nothing stays memoized.
307    pub fn remove(&mut self, key: &K) -> Option<V> {
308        self.reset_if_stale();
309
310        let entry = self.take(key);
311
312        // Only when durable: for a purely in-memory store, or an entry the
313        // storage refused, the entry is simply gone, and reinserting the key
314        // is a fresh insert.
315        if self.storage.is_some() && entry.as_ref().is_some_and(MemoryEntry::is_stored) {
316            self.known.insert(key.clone());
317        }
318
319        entry.map(MemoryEntry::into_value)
320    }
321
322    /// Insert a new item into the store.
323    ///
324    /// - Key absent: the entry is written to the storage.
325    /// - Present with the same value: `Ok`, nothing written.
326    /// - Present with a different value: an error, and the stored value is
327    ///   left untouched — [`StoreError::DuplicatedKey`] when this process
328    ///   wrote or read it, [`StoreError::KeyOutOfSync`] when another process
329    ///   sharing the environment got there first. Both are routine, not bugs.
330    /// - The storage refuses the write: [`StoreError::Backend`]. An eager
331    ///   store still serves the value for the rest of the run, and reinserting
332    ///   it retries the write. A lazy store drops it, as it drops every value
333    ///   it writes.
334    ///
335    /// The exception is an entry that came from a bundle: the storage lets a
336    /// locally computed value replace it, so a stale bundle can never wedge
337    /// the application that imported it.
338    pub fn insert(&mut self, key: K, value: V) -> Result<(), StoreError<K, V>> {
339        self.reset_if_stale();
340
341        let known = match self.entries.get(&key) {
342            Some(MemoryEntry::Stored(existing)) if existing == &value => return Ok(()),
343            Some(MemoryEntry::Stored(_)) => true,
344            // Only memory holds the value, so only memory arbitrates.
345            Some(MemoryEntry::Unwritten(existing)) if existing != &value => {
346                return Err(StoreError::DuplicatedKey {
347                    value_previous: existing.clone(),
348                    value_updated: value,
349                    key,
350                });
351            }
352            // The same value again retries the write. The storage never took
353            // it, so the key is as fresh as it was before the refusal.
354            Some(MemoryEntry::Unwritten(_)) | None => self.known.contains(&key),
355        };
356
357        let Some(storage) = self.storage.as_deref() else {
358            return match self.entries.get(&key) {
359                Some(existing) => Err(StoreError::DuplicatedKey {
360                    value_previous: existing.value().clone(),
361                    value_updated: value,
362                    key,
363                }),
364                None => {
365                    self.entries.insert(key, MemoryEntry::Stored(value));
366                    Ok(())
367                }
368            };
369        };
370
371        // Only memory is consulted above: `write_through` asks the storage
372        // atomically, so reading it first would cost a second round trip and
373        // still not know whether the existing entry is imported.
374        match write_through(storage, &key, &value) {
375            Written::Stored => {
376                self.record(key, value);
377                Ok(())
378            }
379            Written::Failed(error) => {
380                // An eager store is read from memory alone, so dropping the
381                // value would make every later lookup miss and recompute it.
382                // A lazy store drops what it writes anyway.
383                if matches!(self.cache, CacheOption::Eager) {
384                    self.entries
385                        .insert(key.clone(), MemoryEntry::Unwritten(value));
386                }
387                Err(StoreError::Backend { key, error })
388            }
389            Written::Conflict(existing) => {
390                // A later read must serve the durable value, so the eager map
391                // memoizes it; the clone only happens on this cold path.
392                if matches!(self.cache, CacheOption::Eager) {
393                    self.entries
394                        .insert(key.clone(), MemoryEntry::Stored(existing.clone()));
395                } else {
396                    self.known.insert(key.clone());
397                }
398
399                // `known` is what tells the two conflicts apart: this process
400                // saw the key before, or someone else stored it first. The
401                // second is a routine multi-process race, not a bug.
402                let (value_previous, value_updated) = (existing, value);
403                Err(if known {
404                    StoreError::DuplicatedKey {
405                        key,
406                        value_previous,
407                        value_updated,
408                    }
409                } else {
410                    StoreError::KeyOutOfSync {
411                        key,
412                        value_previous,
413                        value_updated,
414                    }
415                })
416            }
417        }
418    }
419
420    /// Deletes one entry, in memory and durably, and hands it out owned.
421    ///
422    /// [`remove`](Store::remove)'s durable mirror, with the same signature:
423    /// where `remove` only evicts the in-memory copy, this also deletes the
424    /// storage entry, so the key is gone for every process sharing the
425    /// environment and free to reinsert.
426    pub fn purge_key(&mut self, key: &K) -> Option<V> {
427        self.reset_if_stale();
428
429        let value = self.take(key).map(MemoryEntry::into_value);
430
431        // A purged key is a fresh key, and the storage is asked regardless of
432        // what memory held: another process may have written it since.
433        self.known.remove(key);
434        if let Some(storage) = self.storage.as_deref() {
435            storage.purge_key(&encode(key));
436        }
437
438        value
439    }
440
441    /// Evicts every in-memory entry, leaving the storage untouched.
442    ///
443    /// `clear` is to [`purge`](Store::purge) what [`remove`](Store::remove)
444    /// is to [`purge_key`](Store::purge_key): the in-memory half only. On an
445    /// eager store the entries come back on the next [`sync`](Store::sync);
446    /// the keys stay known, exactly as with `remove`. Entries the storage
447    /// refused are forgotten instead, since no sync can bring them back.
448    pub fn clear(&mut self) {
449        self.reset_if_stale();
450
451        if self.storage.is_some() {
452            self.known.extend(
453                self.entries
454                    .drain()
455                    .filter(|(_, entry)| entry.is_stored())
456                    .map(|(key, _)| key),
457            );
458        } else {
459            self.entries.clear();
460        }
461    }
462
463    /// Deletes everything this store addresses, in memory and durably.
464    ///
465    /// [`clear`](Store::clear)'s durable mirror: after a purge the namespace
466    /// is empty for every process sharing the environment, and every key is
467    /// free to reinsert. Only this namespace is touched, never the rest of
468    /// the environment. Other stores on the same namespace keep what they
469    /// already ingested until they sync.
470    pub fn purge(&mut self) {
471        self.reset_if_stale();
472
473        self.entries.clear();
474        self.known.clear();
475
476        if let Some(storage) = self.storage.as_deref() {
477            storage.purge();
478        }
479    }
480
481    /// Ingest everything the storage holds into memory.
482    ///
483    /// This is what makes an eager store complete, and [`new`](Store::new)
484    /// performs it; call it again to ingest content another store or process
485    /// wrote to the storage since.
486    #[cfg_attr(
487        feature = "tracing",
488        tracing::instrument(level = "trace", skip_all, fields(namespace = ?self.namespace))
489    )]
490    pub fn sync(&mut self) {
491        // A reset re-ingests an eager store on its own.
492        if self.reset_if_stale() && matches!(self.cache, CacheOption::Eager) {
493            return;
494        }
495        self.ingest();
496    }
497
498    /// Reads every entry of the storage into memory.
499    fn ingest(&mut self) {
500        let Some(storage) = self.storage.as_deref() else {
501            return;
502        };
503        let entries = &mut self.entries;
504
505        storage.scan(&mut |key, value| {
506            if let Some((key, value)) = decode_entry::<K, V>(key, value) {
507                // A durable value replaces one the storage refused earlier.
508                entries.insert(key, MemoryEntry::Stored(value));
509            }
510        });
511    }
512
513    /// Visits every entry the storage holds, decoded and handed out owned,
514    /// retaining none of them in memory.
515    ///
516    /// The read-through counterpart of [`for_each`](Store::for_each): where
517    /// `for_each` walks the in-memory map, this walks the storage. It is the
518    /// hydration read for a caller keeping its own index over a
519    /// [`Lazy`](CacheOption::Lazy) store — everything is visited once, and
520    /// nothing stays resident afterwards.
521    pub fn scan<F: FnMut(K, V)>(&mut self, mut func: F) {
522        self.reset_if_stale();
523
524        let Some(storage) = self.storage.as_deref() else {
525            for (key, entry) in self.entries.iter() {
526                func(key.clone(), entry.value().clone());
527            }
528            return;
529        };
530
531        storage.scan(&mut |key, value| {
532            if let Some((key, value)) = decode_entry::<K, V>(key, value) {
533                func(key, value);
534            }
535        });
536    }
537
538    /// Iterate over all in-memory entries of the store.
539    pub fn for_each<F: FnMut(&K, &V)>(&self, mut func: F) {
540        if self.stale() {
541            return;
542        }
543
544        for (key, entry) in self.entries.iter() {
545            func(key, entry.value());
546        }
547    }
548
549    /// How many entries are in memory.
550    pub fn len(&self) -> usize {
551        if self.stale() {
552            return 0;
553        }
554
555        self.entries.len()
556    }
557
558    /// If nothing is in memory.
559    pub fn is_empty(&self) -> bool {
560        self.len() == 0
561    }
562
563    /// Takes an entry out of memory, reading through to the storage on a
564    /// lazy store.
565    fn take(&mut self, key: &K) -> Option<MemoryEntry<V>> {
566        match self.entries.remove(key) {
567            Some(entry) => Some(entry),
568            None => match self.cache {
569                CacheOption::Eager => None,
570                CacheOption::Lazy => self.fetch(key).map(MemoryEntry::Stored),
571            },
572        }
573    }
574
575    /// One entry read straight from the storage.
576    fn fetch(&self, key: &K) -> Option<V> {
577        let bytes = self.storage.as_deref()?.get(&encode(key))?;
578        decode::<V>(&bytes)
579    }
580
581    /// Records a value the storage accepted, according to the cache option.
582    fn record(&mut self, key: K, value: V) {
583        match self.cache {
584            CacheOption::Eager => {
585                self.entries.insert(key, MemoryEntry::Stored(value));
586            }
587            // The value is dropped rather than memoized: freshly compiled
588            // artifacts are typically never read again by this process.
589            CacheOption::Lazy => {
590                self.known.insert(key);
591            }
592        }
593    }
594
595    /// Whether the in-memory state belongs to an environment that is no
596    /// longer active. One relaxed atomic load for bound stores; unbound ones
597    /// are never stale.
598    fn stale(&self) -> bool {
599        match self.generation {
600            Some(generation) => generation != crate::environment::generation(),
601            None => false,
602        }
603    }
604
605    /// Drops everything belonging to the previous environment and reopens the
606    /// storage against the active one, re-ingesting it when eager. Reports
607    /// whether a reset happened.
608    fn reset_if_stale(&mut self) -> bool {
609        if !self.stale() {
610            return false;
611        }
612
613        // `stale` implies `generation` and an environment-bound namespace.
614        let (Some(namespace), Some(_)) = (&self.namespace, self.generation) else {
615            return false;
616        };
617
618        log::debug!("Environment switched, resetting the store for {namespace}");
619
620        // Generation first, storage second, mirroring `new`: a switch landing
621        // in between reads as stale again, never as up to date.
622        self.generation = Some(crate::environment::generation());
623        self.storage = Some(super::storage::open(namespace.as_str()));
624        self.entries.clear();
625        self.known.clear();
626        if matches!(self.cache, CacheOption::Eager) {
627            self.ingest();
628        }
629
630        true
631    }
632}
633
634impl<K, V> core::fmt::Debug for Store<K, V> {
635    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
636        f.debug_struct("Store")
637            .field("namespace", &self.namespace)
638            .field("cache", &self.cache)
639            .field("entries", &self.entries.len())
640            .field("known", &self.known.len())
641            .field("storage", &self.storage)
642            .finish()
643    }
644}
645
646impl<K: StoreKey, V: StoreValue> Display for Store<K, V> {
647    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
648        match (&self.namespace, &self.storage) {
649            (Some(namespace), Some(storage)) => write!(
650                f,
651                "{namespace} ({} entries in {})",
652                self.len(),
653                storage.describe()
654            ),
655            _ => write!(f, "in-memory ({} entries)", self.len()),
656        }
657    }
658}
659
660/// A value a [`Store`] holds in memory, and whether its storage holds it too.
661enum MemoryEntry<V> {
662    /// The store accepted the value: its storage holds it, or it has none.
663    Stored(V),
664    /// The storage refused the value, so only memory holds it. An eager store
665    /// keeps serving it rather than recompute it on every lookup, and a
666    /// reinsert retries the write.
667    Unwritten(V),
668}
669
670impl<V> MemoryEntry<V> {
671    fn value(&self) -> &V {
672        match self {
673            Self::Stored(value) | Self::Unwritten(value) => value,
674        }
675    }
676
677    fn value_mut(&mut self) -> &mut V {
678        match self {
679            Self::Stored(value) | Self::Unwritten(value) => value,
680        }
681    }
682
683    fn into_value(self) -> V {
684        match self {
685            Self::Stored(value) | Self::Unwritten(value) => value,
686        }
687    }
688
689    fn is_stored(&self) -> bool {
690        matches!(self, Self::Stored(_))
691    }
692}
693
694/// The outcome of writing an entry through to the storage.
695pub(crate) enum Written<V> {
696    /// The storage now holds this value, or already held an identical one.
697    Stored,
698    /// The storage kept a different value, which is the durable one.
699    Conflict(V),
700    /// The backend refused the write; nothing is durable.
701    Failed(String),
702}
703
704/// Writes `value` through to `storage` and lets it arbitrate.
705///
706/// The storage decides what happens on a collision: it lets a local value
707/// replace an imported one, and otherwise refuses to overwrite. Both stores go
708/// through here, so the rule is identical for eager and lazy caches rather
709/// than reimplemented per store.
710pub(crate) fn write_through<K: StoreKey, V: StoreValue>(
711    storage: &dyn Storage,
712    key: &K,
713    value: &V,
714) -> Written<V> {
715    let key_bytes = encode(key);
716
717    match storage.insert(&key_bytes, encode(value), Origin::Local) {
718        Insertion::Stored => Written::Stored,
719        Insertion::Failed(error) => Written::Failed(error),
720        Insertion::Conflict(existing) => match decode::<V>(&existing) {
721            Some(existing) if &existing != value => Written::Conflict(existing),
722            Some(_) => Written::Stored,
723            // Bytes that don't decode are bytes no later insert could ever
724            // agree with, so leaving them in place would refuse every write
725            // for this key forever — a permanent recompile for a lazily read
726            // key. Repair the row instead.
727            None => match storage.replace(&key_bytes, encode(value), Origin::Local) {
728                Insertion::Failed(error) => Written::Failed(error),
729                _ => Written::Stored,
730            },
731        },
732    }
733}
734
735/// Serializes a key or a value to its stored representation.
736///
737/// Unlike [`decode`], this panics on failure rather than degrading. The two
738/// are not symmetric: `decode` reads bytes from outside this process (a shared
739/// database, an imported bundle) that a version skew or corruption can make
740/// unreadable, which is a routine recompute. `encode` serializes a value the
741/// caller just constructed with a derived `Serialize`, and writing CBOR into a
742/// `Vec` has no I/O to fail on: a failure here is a broken `StoreKey`/
743/// `StoreValue` impl, a bug to surface loudly, not a cache miss to swallow.
744pub(crate) fn encode<T: Serialize>(value: &T) -> Bytes {
745    let mut bytes = Vec::new();
746    ciborium::ser::into_writer(value, &mut bytes).expect("Can serialize data");
747    Bytes::from_bytes_vec(bytes)
748}
749
750/// Deserializes a key or a value, reporting corrupted content instead of
751/// failing: a cache entry we can't read is one we recompute.
752pub(crate) fn decode<T: DeserializeOwned>(bytes: &[u8]) -> Option<T> {
753    match ciborium::de::from_reader(bytes) {
754        Ok(value) => Some(value),
755        Err(err) => {
756            log::warn!("Corrupted cache entry, ignoring it: {err}");
757            None
758        }
759    }
760}
761
762fn decode_entry<K: StoreKey, V: StoreValue>(key: &[u8], value: &[u8]) -> Option<(K, V)> {
763    Some((decode::<K>(key)?, decode::<V>(value)?))
764}
765
766#[cfg(all(test, native_cache))]
767mod tests {
768    use std::string::ToString;
769    use std::vec;
770
771    use super::*;
772
773    fn eager(path: &str) -> StoreOptions {
774        StoreOptions::new().storage(Namespace::new(path))
775    }
776
777    fn lazy(path: &str) -> StoreOptions {
778        eager(path).cache(CacheOption::Lazy)
779    }
780
781    #[test_log::test]
782    #[serial_test::serial]
783    #[cfg_attr(miri, ignore)]
784    fn test_cache_simple() {
785        let dir = tempfile::tempdir().unwrap();
786        crate::environment::set_root(dir.path());
787
788        let key1 = || "key1".to_string();
789        let key2 = || "key2".to_string();
790
791        let value1 = || "value1".to_string();
792        let value2 = || "value2".to_string();
793
794        let mut cache = Store::<String, String>::new(eager("test"));
795        cache.insert(key1(), value1()).unwrap();
796        cache.insert(key2(), value2()).unwrap();
797
798        let result = cache.insert(key1(), value2());
799        assert!(
800            result.is_err(),
801            "Can't reinsert the same key with a different value."
802        );
803
804        assert_eq!(cache.len(), 2);
805
806        let value1_actual = cache.get(&key1()).unwrap();
807        assert_eq!(value1_actual, &value1());
808
809        let value2_actual = cache.get(&key2()).unwrap();
810        assert_eq!(value2_actual, &value2());
811    }
812
813    /// Guards the on-disk contract: the database file location and the exact
814    /// namespace a given set of options resolves to. Breaking either
815    /// invalidates every existing cache on users' machines.
816    #[test_log::test]
817    #[serial_test::serial]
818    #[cfg_attr(miri, ignore)]
819    fn test_on_disk_format_is_stable() {
820        let dir = tempfile::tempdir().unwrap();
821        crate::environment::set_root(dir.path());
822        let namespace = Namespace::scoped("golden", "device0/matmul");
823
824        let mut cache = Store::<String, u32>::new(StoreOptions::new().storage(namespace));
825        cache.insert("shape=2x2".to_string(), 42).unwrap();
826
827        let expected_namespace =
828            std::format!("golden/{}/device0/matmul", env!("CARGO_PKG_VERSION"));
829        assert_eq!(cache.namespace().unwrap().as_str(), expected_namespace);
830
831        let path = crate::environment::path();
832        assert!(path.exists(), "Database missing at {path:?}");
833
834        // Read it back through a fresh store: the entry must be addressable
835        // by namespace and encoded key alone.
836        let reopened = Store::<String, u32>::new(
837            StoreOptions::new().storage(Namespace::scoped("golden", "device0/matmul")),
838        );
839        assert_eq!(reopened.get(&"shape=2x2".to_string()), Some(&42));
840    }
841
842    /// A store reopened over the same root must see what the previous one
843    /// wrote, without any bundle involved.
844    #[test_log::test]
845    #[serial_test::serial]
846    #[cfg_attr(miri, ignore)]
847    fn test_entries_survive_reopen() {
848        let dir = tempfile::tempdir().unwrap();
849        crate::environment::set_root(dir.path());
850
851        let mut cache = Store::<String, u32>::new(eager("reopen"));
852        cache.insert("key".to_string(), 7).unwrap();
853        drop(cache);
854
855        let cache = Store::<String, u32>::new(eager("reopen"));
856        assert_eq!(cache.get(&"key".to_string()), Some(&7));
857    }
858
859    /// Two namespaces in the same root must not see each other's entries.
860    #[test_log::test]
861    #[serial_test::serial]
862    #[cfg_attr(miri, ignore)]
863    fn test_stores_are_isolated() {
864        let dir = tempfile::tempdir().unwrap();
865        crate::environment::set_root(dir.path());
866
867        let mut first = Store::<String, u32>::new(eager("device0/matmul"));
868        first.insert("key".to_string(), 1).unwrap();
869
870        let mut second = Store::<String, u32>::new(eager("device1/matmul"));
871        assert_eq!(second.get(&"key".to_string()), None);
872        second.insert("key".to_string(), 2).unwrap();
873
874        assert_eq!(first.get(&"key".to_string()), Some(&1));
875        assert_eq!(second.get(&"key".to_string()), Some(&2));
876    }
877
878    #[test_log::test]
879    #[serial_test::serial]
880    #[cfg_attr(miri, ignore)]
881    fn lazy_values_survive_reopen_and_load_lazily() {
882        let dir = tempfile::tempdir().unwrap();
883        crate::environment::set_root(dir.path());
884
885        let mut cache = Store::<String, Bytes>::new(lazy("ptx_sm90"));
886        cache
887            .insert(
888                "kernel_a".to_string(),
889                Bytes::from_bytes_vec(std::vec![1, 2, 3]),
890            )
891            .unwrap();
892        cache
893            .insert(
894                "kernel_b".to_string(),
895                Bytes::from_bytes_vec(std::vec![4, 5]),
896            )
897            .unwrap();
898        // A lazy insert records the key but never retains the artifact.
899        assert!(cache.is_empty());
900        drop(cache);
901
902        let mut cache = Store::<String, Bytes>::new(lazy("ptx_sm90"));
903        // Nothing is read until a key is asked for.
904        assert!(cache.is_empty());
905
906        assert_eq!(
907            cache.get_mut(&"kernel_a".to_string()).map(|v| v.to_vec()),
908            Some(std::vec![1, 2, 3])
909        );
910        assert_eq!(cache.len(), 1, "get_mut memoizes");
911        assert_eq!(
912            cache.remove(&"kernel_b".to_string()).map(|v| v.to_vec()),
913            Some(std::vec![4, 5])
914        );
915        assert_eq!(cache.len(), 1, "remove reads through without memoizing");
916        assert_eq!(cache.get_mut(&"missing".to_string()), None);
917    }
918
919    #[test_log::test]
920    #[serial_test::serial]
921    #[cfg_attr(miri, ignore)]
922    fn lazy_reinserting_a_different_value_errors() {
923        let dir = tempfile::tempdir().unwrap();
924        crate::environment::set_root(dir.path());
925
926        let mut cache = Store::<String, Bytes>::new(lazy("ptx_sm90"));
927        let kernel = |byte: u8| Bytes::from_bytes_vec(std::vec![byte]);
928        cache.insert("kernel".to_string(), kernel(1)).unwrap();
929
930        assert!(cache.insert("kernel".to_string(), kernel(1)).is_ok());
931        let error = cache.insert("kernel".to_string(), kernel(2));
932        assert!(matches!(error, Err(StoreError::DuplicatedKey { .. })));
933
934        // Taking the value out doesn't forget the key: the entry is still
935        // durable, so a disagreeing reinsert stays a duplicate.
936        assert!(cache.remove(&"kernel".to_string()).is_some());
937        let error = cache.insert("kernel".to_string(), kernel(2));
938        assert!(matches!(error, Err(StoreError::DuplicatedKey { .. })));
939    }
940
941    /// A bound store follows the environment: a switch makes reads miss
942    /// instead of serving the old environment, and the next `&mut` access
943    /// reopens against the new one.
944    #[test_log::test]
945    #[serial_test::serial]
946    #[cfg_attr(miri, ignore)]
947    fn switching_environments_resets_bound_stores() {
948        let first = tempfile::tempdir().unwrap();
949        let second = tempfile::tempdir().unwrap();
950
951        crate::environment::set_root(first.path());
952        let mut store = Store::<String, u32>::new(eager("reset"));
953        store.insert("key".to_string(), 1).unwrap();
954        assert_eq!(store.get(&"key".to_string()), Some(&1));
955
956        // The old environment's entries are never served after the switch.
957        crate::environment::set_root(second.path());
958        assert_eq!(store.get(&"key".to_string()), None);
959        assert_eq!(store.len(), 0);
960
961        // The next write lands in the new environment, with no conflict
962        // against the value the old one holds.
963        store.insert("key".to_string(), 2).unwrap();
964        assert_eq!(store.get(&"key".to_string()), Some(&2));
965
966        // Switching back serves the first environment's value again.
967        crate::environment::set_root(first.path());
968        store.sync();
969        assert_eq!(store.get(&"key".to_string()), Some(&1));
970    }
971
972    /// Stores on an explicit or absent storage are not bound to the
973    /// environment and must not reset on a switch.
974    #[test_log::test]
975    #[serial_test::serial]
976    #[cfg_attr(miri, ignore)]
977    fn unbound_stores_survive_environment_switches() {
978        let root = tempfile::tempdir().unwrap();
979
980        let mut store = Store::<String, u32>::new(StoreOptions::new());
981        store.insert("key".to_string(), 1).unwrap();
982
983        crate::environment::set_root(root.path());
984        assert_eq!(store.get(&"key".to_string()), Some(&1));
985    }
986
987    /// `purge_key` is `remove` with a durable delete: the entry is handed out
988    /// owned, gone from the storage, and the key is fresh again.
989    #[test_log::test]
990    #[serial_test::serial]
991    #[cfg_attr(miri, ignore)]
992    fn purge_key_deletes_one_entry_durably() {
993        let dir = tempfile::tempdir().unwrap();
994        crate::environment::set_root(dir.path());
995
996        let mut store = Store::<String, u32>::new(eager("purge_key"));
997        store.insert("gone".to_string(), 1).unwrap();
998        store.insert("kept".to_string(), 2).unwrap();
999
1000        assert_eq!(store.purge_key(&"gone".to_string()), Some(1));
1001        // Fresh key: a different value is a plain insert, not a duplicate.
1002        store.insert("gone".to_string(), 3).unwrap();
1003        assert_eq!(store.purge_key(&"gone".to_string()), Some(3));
1004        drop(store);
1005
1006        let store = Store::<String, u32>::new(eager("purge_key"));
1007        assert_eq!(store.get(&"gone".to_string()), None);
1008        assert_eq!(store.get(&"kept".to_string()), Some(&2));
1009    }
1010
1011    /// `clear` evicts memory only: the storage keeps everything, the keys
1012    /// stay known, and a sync brings the entries back.
1013    #[test_log::test]
1014    #[serial_test::serial]
1015    #[cfg_attr(miri, ignore)]
1016    fn clear_evicts_memory_but_not_the_storage() {
1017        let dir = tempfile::tempdir().unwrap();
1018        crate::environment::set_root(dir.path());
1019
1020        let mut store = Store::<String, u32>::new(eager("clear"));
1021        store.insert("key".to_string(), 1).unwrap();
1022
1023        store.clear();
1024        assert!(store.is_empty());
1025        // Still durable and still known: a disagreeing reinsert is a
1026        // duplicate, not a fresh insert.
1027        assert!(matches!(
1028            store.insert("key".to_string(), 2),
1029            Err(StoreError::DuplicatedKey { .. })
1030        ));
1031
1032        store.sync();
1033        assert_eq!(store.get(&"key".to_string()), Some(&1));
1034    }
1035
1036    /// Unlike `remove`, which only evicts the in-memory copy, `purge` deletes
1037    /// the whole namespace durably and frees every key for reinsertion.
1038    #[test_log::test]
1039    #[serial_test::serial]
1040    #[cfg_attr(miri, ignore)]
1041    fn purge_deletes_durably_and_frees_the_keys() {
1042        let dir = tempfile::tempdir().unwrap();
1043        crate::environment::set_root(dir.path());
1044
1045        let mut store = Store::<String, u32>::new(eager("purge"));
1046        store.insert("kept".to_string(), 1).unwrap();
1047        store.insert("gone".to_string(), 2).unwrap();
1048
1049        // An isolated namespace must survive its neighbor's purge.
1050        let mut other = Store::<String, u32>::new(eager("other"));
1051        other.insert("kept".to_string(), 9).unwrap();
1052
1053        store.purge();
1054        assert!(store.is_empty());
1055
1056        // A purged key is a fresh key, even with a different value.
1057        store.insert("kept".to_string(), 3).unwrap();
1058        drop(store);
1059
1060        let store = Store::<String, u32>::new(eager("purge"));
1061        assert_eq!(store.get(&"kept".to_string()), Some(&3));
1062        assert_eq!(store.get(&"gone".to_string()), None);
1063        assert_eq!(
1064            Store::<String, u32>::new(eager("other")).get(&"kept".to_string()),
1065            Some(&9)
1066        );
1067    }
1068
1069    /// `scan` visits the whole storage owned, without retaining anything —
1070    /// the hydration read for consumers keeping their own index.
1071    #[test_log::test]
1072    #[serial_test::serial]
1073    #[cfg_attr(miri, ignore)]
1074    fn scan_visits_the_storage_without_retaining() {
1075        let dir = tempfile::tempdir().unwrap();
1076        crate::environment::set_root(dir.path());
1077
1078        let mut store = Store::<String, u32>::new(lazy("scan"));
1079        store.insert("a".to_string(), 1).unwrap();
1080        store.insert("b".to_string(), 2).unwrap();
1081
1082        let mut seen = std::vec::Vec::new();
1083        store.scan(|key, value| seen.push((key, value)));
1084        seen.sort();
1085
1086        assert_eq!(seen, std::vec![("a".to_string(), 1), ("b".to_string(), 2)]);
1087        assert!(store.is_empty(), "nothing stays resident after a scan");
1088    }
1089
1090    #[test]
1091    fn in_memory_store_needs_no_storage() {
1092        let mut store = Store::<String, u32>::new(StoreOptions::new());
1093
1094        store.insert("key".to_string(), 1).unwrap();
1095        assert_eq!(store.get(&"key".to_string()), Some(&1));
1096        assert!(store.insert("key".to_string(), 1).is_ok());
1097        assert!(matches!(
1098            store.insert("key".to_string(), 2),
1099            Err(StoreError::DuplicatedKey { .. })
1100        ));
1101
1102        // Nothing durable behind the map: a removed entry is simply gone and
1103        // the key is free again.
1104        assert_eq!(store.remove(&"key".to_string()), Some(1));
1105        store.insert("key".to_string(), 2).unwrap();
1106        assert_eq!(store.get(&"key".to_string()), Some(&2));
1107    }
1108}