Skip to main content

cubecl_environment/persistence/
storage.rs

1use alloc::boxed::Box;
2use alloc::string::{String, ToString};
3use alloc::vec::Vec;
4
5use hashbrown::HashMap;
6
7use crate::bytes::Bytes;
8use crate::sync::{Arc, LazyLock, Mutex};
9
10/// Where an entry came from.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum Origin {
13    /// Computed on this machine.
14    Local,
15    /// Copied in from a bundle by [`crate::bundle::import`].
16    Imported,
17}
18
19/// What a [`Storage::insert`] did.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum Insertion {
22    /// The storage now holds the value that was passed in.
23    Stored,
24    /// The storage declined the write and kept a different value, which is
25    /// returned. Someone else — another process, or an earlier run — got
26    /// there first.
27    Conflict(Bytes),
28    /// The backend refused the write: a full disk, a lock held past the busy
29    /// timeout, a revoked permission. Nothing was stored, and the message
30    /// says why.
31    Failed(String),
32}
33
34/// What an [`insert_many`](Storage::insert_many) did, counted per outcome.
35#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
36pub struct InsertSummary {
37    /// Entries written.
38    pub stored: usize,
39    /// Entries the storage already held under a different value.
40    pub conflict: usize,
41    /// Entries the backend refused.
42    pub failed: usize,
43}
44
45impl InsertSummary {
46    /// Counts one outcome.
47    pub fn record(&mut self, insertion: &Insertion) {
48        match insertion {
49            Insertion::Stored => self.stored += 1,
50            Insertion::Conflict(_) => self.conflict += 1,
51            Insertion::Failed(_) => self.failed += 1,
52        }
53    }
54}
55
56/// Where the entries of a single namespace are kept, and written to.
57///
58/// A storage is bound to one namespace at construction (a `/`-separated name
59/// such as `autotune/0.11.0/cuda-0/matmul`) and addresses entries by their
60/// serialized key bytes.
61///
62/// This is the only thing read at runtime. Bundles are an import format: they
63/// fill a storage once through [`crate::bundle::import`] and are never
64/// consulted again.
65///
66/// # Contract
67///
68/// - [`insert`](Storage::insert) is insert-only between two [`Origin::Local`]
69///   values: it leaves the stored value untouched and reports
70///   [`Insertion::Conflict`] with it.
71/// - A [`Origin::Local`] value *replaces* an [`Origin::Imported`] one, so a
72///   stale bundle entry can never wedge the application that imported it.
73///   An [`Origin::Imported`] value never replaces anything.
74/// - [`replace`](Storage::replace) ignores both rules. It exists for one
75///   caller: repairing a row whose bytes no longer decode, which no `insert`
76///   could ever agree with.
77/// - [`purge`](Storage::purge) and [`purge_key`](Storage::purge_key) are the
78///   deletions: the whole namespace, or one entry. Everything else is
79///   insert-only.
80/// - The check and the write must be atomic with respect to other processes.
81/// - A read that fails reports a miss: a cache entry we can't read is one we
82///   recompute. A *write* that fails reports [`Insertion::Failed`] rather
83///   than passing for success, so a caller can tell "someone else got there
84///   first" from "the write did not happen". Implementations log failures but
85///   never panic on them.
86/// - [`scan`](Storage::scan) may run the visitor while holding the backend's
87///   lock, and several namespaces of one environment share that lock. The
88///   visitor must therefore not touch any other store of the same
89///   environment: doing so deadlocks.
90///
91/// Methods take `&self` because reads happen behind shared references on the
92/// hot path; implementations use interior mutability.
93pub trait Storage: Send + core::fmt::Debug {
94    /// The value stored under `key`, if any.
95    fn get(&self, key: &[u8]) -> Option<Bytes>;
96
97    /// Stores `value` under `key`. See the trait contract for when the write
98    /// is declined.
99    fn insert(&self, key: &[u8], value: Bytes, origin: Origin) -> Insertion;
100
101    /// Stores `value` under `key`, overwriting whatever is there.
102    ///
103    /// Bypasses the insert-only rule, so it never reports a conflict. Only for
104    /// repairing an entry that can't be decoded; ordinary writes go through
105    /// [`insert`](Storage::insert).
106    fn replace(&self, key: &[u8], value: Bytes, origin: Origin) -> Insertion;
107
108    /// Stores many entries under the same rules as
109    /// [`insert`](Storage::insert).
110    ///
111    /// Backends that can commit a batch atomically override this; the default
112    /// is one `insert` per entry.
113    fn insert_many(
114        &self,
115        entries: &mut dyn Iterator<Item = (Bytes, Bytes)>,
116        origin: Origin,
117    ) -> InsertSummary {
118        let mut summary = InsertSummary::default();
119        for (key, value) in entries {
120            summary.record(&self.insert(&key, value, origin));
121        }
122        summary
123    }
124
125    /// Visits every entry of the namespace.
126    ///
127    /// The visitor must not read or write another store of the same
128    /// environment; see the trait contract.
129    fn scan(&self, visit: &mut dyn FnMut(&[u8], &[u8]));
130
131    /// Deletes every entry of the namespace, durably.
132    ///
133    /// A failed delete is logged, not reported: the entries were expendable
134    /// cache content either way, and whatever survives is arbitrated like any
135    /// other pre-existing entry.
136    fn purge(&self);
137
138    /// Deletes the entry under `key`, durably. Same failure contract as
139    /// [`purge`](Storage::purge).
140    fn purge_key(&self, key: &[u8]);
141
142    /// Human-readable location for log messages.
143    fn describe(&self) -> String;
144}
145
146/// The entries of every memory-backed namespace in this process.
147///
148/// Shared globally so that a namespace opened twice, or imported and then
149/// read, sees the same entries. Without a file system that is the only way an
150/// import can outlive the call that performed it.
151static MEMORY: LazyLock<Mutex<HashMap<String, Arc<Mutex<Entries>>>>> =
152    LazyLock::new(|| Mutex::new(HashMap::new()));
153
154pub(crate) type Entries = HashMap<Vec<u8>, (Bytes, Origin)>;
155
156/// The [`Storage`] contract applied to an in-memory namespace.
157mod entries {
158    use super::{Bytes, Entries, Insertion, Origin, replaces};
159
160    pub(crate) fn get(entries: &Entries, key: &[u8]) -> Option<Bytes> {
161        entries.get(key).map(|(value, _)| value.clone())
162    }
163
164    pub(crate) fn insert(
165        entries: &mut Entries,
166        key: &[u8],
167        value: Bytes,
168        origin: Origin,
169    ) -> Insertion {
170        if let Some((existing, existing_origin)) = entries.get(key)
171            && !replaces(origin, *existing_origin)
172        {
173            return Insertion::Conflict(existing.clone());
174        }
175
176        entries.insert(key.to_vec(), (value, origin));
177
178        Insertion::Stored
179    }
180
181    pub(crate) fn replace(
182        entries: &mut Entries,
183        key: &[u8],
184        value: Bytes,
185        origin: Origin,
186    ) -> Insertion {
187        entries.insert(key.to_vec(), (value, origin));
188
189        Insertion::Stored
190    }
191
192    pub(crate) fn scan(entries: &Entries, visit: &mut dyn FnMut(&[u8], &[u8])) {
193        for (key, (value, _)) in entries.iter() {
194            visit(key, value);
195        }
196    }
197}
198
199/// A storage that keeps entries in memory for the lifetime of the process.
200///
201/// Used where nothing durable is available (no-std, or a cache root that can't
202/// be opened). Entries do not survive a restart, so an imported bundle has to
203/// be imported again on the next run, which costs nothing but time.
204#[derive(Debug, Clone)]
205pub struct MemoryStorage {
206    namespace: String,
207    entries: Arc<Mutex<Entries>>,
208}
209
210impl MemoryStorage {
211    /// The in-memory storage for `namespace`, shared process-wide across every
212    /// environment.
213    ///
214    /// The environment-bound path uses `in_environment` instead, which
215    /// isolates the entries per environment so a switch doesn't
216    /// serve the previous one's data. This unscoped constructor is for explicit
217    /// storages that aren't tied to an environment (tests, benches).
218    pub fn new(namespace: &str) -> Self {
219        Self::with_key(namespace.to_string(), namespace)
220    }
221
222    /// Like [`new`](Self::new) but isolated per active environment.
223    ///
224    /// The database backend keys its entries by the environment's file path, so
225    /// a switch reopens a different file; the memory fallback has no file, so it
226    /// folds the environment [`scope`](crate::environment::scope) into its
227    /// global key to get the same isolation. Without this, a bound store that
228    /// resets after a switch would reopen the memory storage and immediately
229    /// re-ingest the previous environment's entries.
230    fn in_environment(namespace: &str) -> Self {
231        // `\u{1f}` (unit separator) can appear in neither a `/`-separated
232        // namespace nor a file-system path, so the split back out is
233        // unambiguous.
234        let key = alloc::format!("{}\u{1f}{namespace}", crate::environment::scope());
235        Self::with_key(key, namespace)
236    }
237
238    fn with_key(key: String, namespace: &str) -> Self {
239        let mut memory = MEMORY.lock();
240        let entries = match memory.get(&key) {
241            Some(entries) => entries.clone(),
242            None => {
243                let entries = Arc::new(Mutex::new(HashMap::new()));
244                memory.insert(key, entries.clone());
245                entries
246            }
247        };
248
249        Self {
250            namespace: namespace.to_string(),
251            entries,
252        }
253    }
254
255    /// Every namespace the active environment holds in memory.
256    ///
257    /// Only the active environment's entries are reported: the process-wide map
258    /// also holds other environments' entries and unscoped explicit storages,
259    /// but a summary is always about the environment in effect right now.
260    pub fn namespaces() -> Vec<NamespaceSummary> {
261        let prefix = alloc::format!("{}\u{1f}", crate::environment::scope());
262        let memory = MEMORY.lock();
263
264        memory
265            .iter()
266            .filter_map(|(key, entries)| {
267                let namespace = key.strip_prefix(&prefix)?;
268                let entries = entries.lock();
269                Some(NamespaceSummary {
270                    namespace: namespace.to_string(),
271                    entries: entries.len() as u64,
272                    bytes: entries
273                        .iter()
274                        .map(|(key, (value, _))| (key.len() + value.len()) as u64)
275                        .sum(),
276                })
277            })
278            .collect()
279    }
280}
281
282impl Storage for MemoryStorage {
283    fn get(&self, key: &[u8]) -> Option<Bytes> {
284        entries::get(&self.entries.lock(), key)
285    }
286
287    fn insert(&self, key: &[u8], value: Bytes, origin: Origin) -> Insertion {
288        entries::insert(&mut self.entries.lock(), key, value, origin)
289    }
290
291    fn replace(&self, key: &[u8], value: Bytes, origin: Origin) -> Insertion {
292        entries::replace(&mut self.entries.lock(), key, value, origin)
293    }
294
295    fn scan(&self, visit: &mut dyn FnMut(&[u8], &[u8])) {
296        entries::scan(&self.entries.lock(), visit)
297    }
298
299    fn purge(&self) {
300        self.entries.lock().clear();
301    }
302
303    fn purge_key(&self, key: &[u8]) {
304        self.entries.lock().remove(key);
305    }
306
307    fn describe(&self) -> String {
308        alloc::format!("memory ({})", self.namespace)
309    }
310}
311
312/// Whether a write of `incoming` may overwrite an entry of `existing`.
313///
314/// Only one case overwrites: a locally computed value replacing an imported
315/// one. That is what keeps a stale bundle entry from wedging the application,
316/// now that imported entries live in the storage like any other.
317fn replaces(incoming: Origin, existing: Origin) -> bool {
318    matches!((incoming, existing), (Origin::Local, Origin::Imported))
319}
320
321/// One namespace's contribution to a storage or a bundle, for reporting.
322#[derive(Debug, Clone, PartialEq, Eq)]
323pub struct NamespaceSummary {
324    /// The namespace.
325    pub namespace: String,
326    /// Number of entries.
327    pub entries: u64,
328    /// Total size of the keys and values, in bytes.
329    pub bytes: u64,
330}
331
332/// Every namespace the active environment holds durably.
333///
334/// Empty without a durable backend: memory namespaces do not outlive the
335/// process that wrote them.
336pub fn namespaces() -> Vec<String> {
337    cfg_if::cfg_if! {
338        if #[cfg(any(native_cache, browser_cache))] {
339            super::turso::summary()
340                .into_iter()
341                .map(|summary| summary.namespace)
342                .collect()
343        } else {
344            Vec::new()
345        }
346    }
347}
348
349/// The storage serving `namespace` in the active environment, degrading to
350/// process-wide memory when the database can't be opened.
351///
352/// The location is not a parameter: an environment is the store, so a cache
353/// can't be opened somewhere else without making "a single active
354/// environment" false. See [`crate::environment`].
355pub fn open(namespace: &str) -> Box<dyn Storage> {
356    cfg_if::cfg_if! {
357        if #[cfg(any(native_cache, browser_cache))] {
358            match super::turso::open(namespace) {
359                Ok(storage) => storage,
360                // Isolate the memory fallback per environment, so a switch after
361                // the database failed to open doesn't serve the previous
362                // environment's entries.
363                Err(error) => {
364                    log::warn!("Unable to open Turso cache for '{namespace}', using memory: {error}");
365                    Box::new(MemoryStorage::in_environment(namespace))
366                }
367            }
368        } else {
369            Box::new(MemoryStorage::in_environment(namespace))
370        }
371    }
372}