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, Lazy, 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    /// Whether the storage is still loading its content asynchronously.
143    /// Entries become visible through [`get`](Storage::get) and
144    /// [`scan`](Storage::scan) once the load completes.
145    fn loading(&self) -> bool {
146        false
147    }
148
149    /// Human-readable location for log messages.
150    fn describe(&self) -> String;
151}
152
153/// The entries of every memory-backed namespace in this process.
154///
155/// Shared globally so that a namespace opened twice, or imported and then
156/// read, sees the same entries. Without a file system that is the only way an
157/// import can outlive the call that performed it.
158static MEMORY: Lazy<Mutex<HashMap<String, Arc<Mutex<Entries>>>>> =
159    Lazy::new(|| Mutex::new(HashMap::new()));
160
161pub(crate) type Entries = HashMap<Vec<u8>, (Bytes, Origin)>;
162
163/// The [`Storage`] contract applied to an in-memory namespace.
164///
165/// Every backend that keeps entries in memory shares these, so the insert
166/// arbitration exists once and the backends cannot drift on the contract
167/// documented on [`Storage`]. They take the map rather than owning it because
168/// the backends disagree on the lock around it and on what they do after a
169/// write lands.
170pub(crate) mod entries {
171    use super::{Bytes, Entries, Insertion, Origin, replaces};
172
173    pub(crate) fn get(entries: &Entries, key: &[u8]) -> Option<Bytes> {
174        entries.get(key).map(|(value, _)| value.clone())
175    }
176
177    pub(crate) fn insert(
178        entries: &mut Entries,
179        key: &[u8],
180        value: Bytes,
181        origin: Origin,
182    ) -> Insertion {
183        if let Some((existing, existing_origin)) = entries.get(key)
184            && !replaces(origin, *existing_origin)
185        {
186            return Insertion::Conflict(existing.clone());
187        }
188
189        entries.insert(key.to_vec(), (value, origin));
190
191        Insertion::Stored
192    }
193
194    pub(crate) fn replace(
195        entries: &mut Entries,
196        key: &[u8],
197        value: Bytes,
198        origin: Origin,
199    ) -> Insertion {
200        entries.insert(key.to_vec(), (value, origin));
201
202        Insertion::Stored
203    }
204
205    pub(crate) fn scan(entries: &Entries, visit: &mut dyn FnMut(&[u8], &[u8])) {
206        for (key, (value, _)) in entries.iter() {
207            visit(key, value);
208        }
209    }
210}
211
212/// A storage that keeps entries in memory for the lifetime of the process.
213///
214/// Used where nothing durable is available (no-std, or a cache root that can't
215/// be opened). Entries do not survive a restart, so an imported bundle has to
216/// be imported again on the next run, which costs nothing but time.
217#[derive(Debug, Clone)]
218pub struct MemoryStorage {
219    namespace: String,
220    entries: Arc<Mutex<Entries>>,
221}
222
223impl MemoryStorage {
224    /// The in-memory storage for `namespace`, shared process-wide across every
225    /// environment.
226    ///
227    /// The environment-bound path uses [`in_environment`](Self::in_environment)
228    /// instead, which isolates the entries per environment so a switch doesn't
229    /// serve the previous one's data. This unscoped constructor is for explicit
230    /// storages that aren't tied to an environment (tests, benches).
231    pub fn new(namespace: &str) -> Self {
232        Self::with_key(namespace.to_string(), namespace)
233    }
234
235    /// Like [`new`](Self::new) but isolated per active environment.
236    ///
237    /// The database backend keys its entries by the environment's file path, so
238    /// a switch reopens a different file; the memory fallback has no file, so it
239    /// folds the environment [`scope`](crate::environment::scope) into its
240    /// global key to get the same isolation. Without this, a bound store that
241    /// resets after a switch would reopen the memory storage and immediately
242    /// re-ingest the previous environment's entries.
243    pub(crate) fn in_environment(namespace: &str) -> Self {
244        // `\u{1f}` (unit separator) can appear in neither a `/`-separated
245        // namespace nor a file-system path, so the split back out is
246        // unambiguous.
247        let key = alloc::format!("{}\u{1f}{namespace}", crate::environment::scope());
248        Self::with_key(key, namespace)
249    }
250
251    fn with_key(key: String, namespace: &str) -> Self {
252        let mut memory = MEMORY.lock();
253        let entries = match memory.get(&key) {
254            Some(entries) => entries.clone(),
255            None => {
256                let entries = Arc::new(Mutex::new(HashMap::new()));
257                memory.insert(key, entries.clone());
258                entries
259            }
260        };
261
262        Self {
263            namespace: namespace.to_string(),
264            entries,
265        }
266    }
267
268    /// Every namespace the active environment holds in memory.
269    ///
270    /// Only the active environment's entries are reported: the process-wide map
271    /// also holds other environments' entries and unscoped explicit storages,
272    /// but a summary is always about the environment in effect right now.
273    pub fn namespaces() -> Vec<NamespaceSummary> {
274        let prefix = alloc::format!("{}\u{1f}", crate::environment::scope());
275        let memory = MEMORY.lock();
276
277        memory
278            .iter()
279            .filter_map(|(key, entries)| {
280                let namespace = key.strip_prefix(&prefix)?;
281                let entries = entries.lock();
282                Some(NamespaceSummary {
283                    namespace: namespace.to_string(),
284                    entries: entries.len() as u64,
285                    bytes: entries
286                        .iter()
287                        .map(|(key, (value, _))| (key.len() + value.len()) as u64)
288                        .sum(),
289                })
290            })
291            .collect()
292    }
293}
294
295impl Storage for MemoryStorage {
296    fn get(&self, key: &[u8]) -> Option<Bytes> {
297        entries::get(&self.entries.lock(), key)
298    }
299
300    fn insert(&self, key: &[u8], value: Bytes, origin: Origin) -> Insertion {
301        entries::insert(&mut self.entries.lock(), key, value, origin)
302    }
303
304    fn replace(&self, key: &[u8], value: Bytes, origin: Origin) -> Insertion {
305        entries::replace(&mut self.entries.lock(), key, value, origin)
306    }
307
308    fn scan(&self, visit: &mut dyn FnMut(&[u8], &[u8])) {
309        entries::scan(&self.entries.lock(), visit)
310    }
311
312    fn purge(&self) {
313        self.entries.lock().clear();
314    }
315
316    fn purge_key(&self, key: &[u8]) {
317        self.entries.lock().remove(key);
318    }
319
320    fn describe(&self) -> String {
321        alloc::format!("memory ({})", self.namespace)
322    }
323}
324
325/// Whether a write of `incoming` may overwrite an entry of `existing`.
326///
327/// Only one case overwrites: a locally computed value replacing an imported
328/// one. That is what keeps a stale bundle entry from wedging the application,
329/// now that imported entries live in the storage like any other.
330pub(crate) fn replaces(incoming: Origin, existing: Origin) -> bool {
331    matches!((incoming, existing), (Origin::Local, Origin::Imported))
332}
333
334/// One namespace's contribution to a storage or a bundle, for reporting.
335#[derive(Debug, Clone, PartialEq, Eq)]
336pub struct NamespaceSummary {
337    /// The namespace.
338    pub namespace: String,
339    /// Number of entries.
340    pub entries: u64,
341    /// Total size of the keys and values, in bytes.
342    pub bytes: u64,
343}
344
345/// The storage serving `namespace` in the active environment.
346///
347/// The location is not a parameter: an environment is the store, so a cache
348/// can't be opened somewhere else without making "a single active
349/// environment" false. See [`crate::environment`].
350pub fn open(namespace: &str) -> Box<dyn Storage> {
351    cfg_if::cfg_if! {
352        if #[cfg(native_cache)] {
353            super::open_database_storage(namespace)
354        } else if #[cfg(browser_cache)] {
355            super::browser::open_storage(namespace)
356        } else {
357            Box::new(MemoryStorage::in_environment(namespace))
358        }
359    }
360}