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}