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}