Skip to main content

ts_l3/
store.rs

1// Copyright (c) 2026 LightSeek Foundation
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in
11// all copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! The byte stores packed Host cache blocks go to, and the in-memory one.
22
23use std::{
24    collections::HashMap,
25    sync::{Arc, Mutex, OnceLock},
26};
27
28use crate::layout::Range;
29
30/// The Host pool's bytes, as the stores read and write them.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct HostBuffer {
33    /// The pool's address.
34    pub base: usize,
35    /// Its size.
36    pub len: usize,
37}
38
39impl HostBuffer {
40    /// The address of `range`, which must lie inside the pool.
41    pub fn address(&self, range: Range) -> usize {
42        assert!(
43            range
44                .offset
45                .checked_add(range.size)
46                .is_some_and(|end| end <= self.len),
47            "{range:?} is outside the {}-byte Host pool",
48            self.len
49        );
50        self.base + range.offset
51    }
52}
53
54/// What one batch did, per key: whether it succeeded, whether the store
55/// confirmed the object missing or invalid, and whether every native access
56/// to the Host bytes has ended.
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct IoResult {
59    /// Per key: a full read, or a published (or already present) object.
60    pub success: Vec<bool>,
61    /// Native retirement: nothing still touches the batch's Host bytes.
62    pub retired: bool,
63    /// Per key: the object is missing or invalid; never a transport or
64    /// policy failure.
65    pub object_failures: Vec<bool>,
66}
67
68/// A call that failed as a whole.
69#[derive(Debug, thiserror::Error)]
70#[error("{0}")]
71pub struct StoreError(pub String);
72
73/// A byte store for packed Host cache blocks. The executor's workers call
74/// it from several threads at once.
75pub trait Store: Send + Sync {
76    /// Whether a failed call proves its Host access ended. Valid results
77    /// carry their own `retired`.
78    fn failures_retire(&self) -> bool;
79
80    /// Per key: the object exists.
81    fn batch_exists(&self, keys: &[String]) -> Result<Vec<bool>, StoreError>;
82
83    /// Read objects into Host ranges.
84    fn batch_get_into(
85        &self,
86        keys: &[String],
87        host: HostBuffer,
88        ranges: &[Range],
89    ) -> Result<IoResult, StoreError>;
90
91    /// Write Host ranges as objects; an object already present succeeds.
92    fn batch_put_from(
93        &self,
94        keys: &[String],
95        host: HostBuffer,
96        ranges: &[Range],
97    ) -> Result<IoResult, StoreError>;
98
99    /// Remove every object whose key starts with `prefix`: true when they
100    /// are gone. False must not be followed by an irreversible cache clear.
101    fn remove_by_prefix(&self, prefix: &str) -> bool;
102
103    /// Release the client's resources once nothing touches the Host pool;
104    /// the objects stay in the store.
105    fn close(&self);
106}
107
108impl<S: Store + ?Sized> Store for Arc<S> {
109    fn failures_retire(&self) -> bool {
110        (**self).failures_retire()
111    }
112
113    fn batch_exists(&self, keys: &[String]) -> Result<Vec<bool>, StoreError> {
114        (**self).batch_exists(keys)
115    }
116
117    fn batch_get_into(
118        &self,
119        keys: &[String],
120        host: HostBuffer,
121        ranges: &[Range],
122    ) -> Result<IoResult, StoreError> {
123        (**self).batch_get_into(keys, host, ranges)
124    }
125
126    fn batch_put_from(
127        &self,
128        keys: &[String],
129        host: HostBuffer,
130        ranges: &[Range],
131    ) -> Result<IoResult, StoreError> {
132        (**self).batch_put_from(keys, host, ranges)
133    }
134
135    fn remove_by_prefix(&self, prefix: &str) -> bool {
136        (**self).remove_by_prefix(prefix)
137    }
138
139    fn close(&self) {
140        (**self).close();
141    }
142}
143
144/// An in-process store: for tests, and as a reference with no Mooncake.
145#[derive(Debug, Default)]
146pub struct MemoryStore {
147    objects: Mutex<HashMap<String, Vec<u8>>>,
148}
149
150/// The in-memory store every `memory` executor of the process shares: like
151/// a remote store, it keeps objects after their writer closes.
152pub fn process_memory_store() -> Arc<MemoryStore> {
153    static STORE: OnceLock<Arc<MemoryStore>> = OnceLock::new();
154    Arc::clone(STORE.get_or_init(Arc::default))
155}
156
157impl MemoryStore {
158    /// An empty store.
159    pub fn new() -> Self {
160        Self::default()
161    }
162
163    /// The keys stored, sorted.
164    pub fn keys(&self) -> Vec<String> {
165        let mut keys: Vec<String> = self
166            .objects
167            .lock()
168            .expect("memory store lock")
169            .keys()
170            .cloned()
171            .collect();
172        keys.sort();
173        keys
174    }
175
176    /// Drop the objects whose keys `forget` selects.
177    pub fn forget(&self, forget: impl Fn(&str) -> bool) {
178        self.objects
179            .lock()
180            .expect("memory store lock")
181            .retain(|key, _| !forget(key));
182    }
183}
184
185impl Store for MemoryStore {
186    fn failures_retire(&self) -> bool {
187        true
188    }
189
190    fn batch_exists(&self, keys: &[String]) -> Result<Vec<bool>, StoreError> {
191        let objects = self.objects.lock().expect("memory store lock");
192        Ok(keys.iter().map(|key| objects.contains_key(key)).collect())
193    }
194
195    fn batch_get_into(
196        &self,
197        keys: &[String],
198        host: HostBuffer,
199        ranges: &[Range],
200    ) -> Result<IoResult, StoreError> {
201        if keys.len() != ranges.len() {
202            return Err(StoreError("ragged L3 get".to_owned()));
203        }
204        let objects = self.objects.lock().expect("memory store lock");
205        let success: Vec<bool> = keys
206            .iter()
207            .zip(ranges)
208            .map(|(key, &range)| match objects.get(key) {
209                Some(payload) if payload.len() == range.size => {
210                    // SAFETY: the range lies inside the live Host pool (the
211                    // address check), which the scheduler holds for this
212                    // operation until its retirement, so nothing else writes
213                    // it meanwhile.
214                    #[allow(unsafe_code)]
215                    unsafe {
216                        std::ptr::copy_nonoverlapping(
217                            payload.as_ptr(),
218                            host.address(range) as *mut u8,
219                            range.size,
220                        );
221                    }
222                    true
223                }
224                _ => false,
225            })
226            .collect();
227        let object_failures = success.iter().map(|ok| !ok).collect();
228        Ok(IoResult {
229            success,
230            retired: true,
231            object_failures,
232        })
233    }
234
235    fn batch_put_from(
236        &self,
237        keys: &[String],
238        host: HostBuffer,
239        ranges: &[Range],
240    ) -> Result<IoResult, StoreError> {
241        if keys.len() != ranges.len() {
242            return Err(StoreError("ragged L3 put".to_owned()));
243        }
244        let mut objects = self.objects.lock().expect("memory store lock");
245        for (key, &range) in keys.iter().zip(ranges) {
246            if objects.contains_key(key) {
247                continue;
248            }
249            let mut payload = vec![0_u8; range.size];
250            // SAFETY: as in `batch_get_into`; the source blocks are pinned
251            // until the operation retires.
252            #[allow(unsafe_code)]
253            unsafe {
254                std::ptr::copy_nonoverlapping(
255                    host.address(range) as *const u8,
256                    payload.as_mut_ptr(),
257                    range.size,
258                );
259            }
260            objects.insert(key.clone(), payload);
261        }
262        Ok(IoResult {
263            success: vec![true; keys.len()],
264            retired: true,
265            object_failures: vec![false; keys.len()],
266        })
267    }
268
269    fn remove_by_prefix(&self, prefix: &str) -> bool {
270        self.forget(|key| key.starts_with(prefix));
271        true
272    }
273
274    fn close(&self) {}
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280
281    #[test]
282    fn objects_round_trip_through_the_host_pool() {
283        let mut pool = vec![0_u8; 16];
284        pool[0..4].copy_from_slice(b"abcd");
285        let host = HostBuffer {
286            base: pool.as_mut_ptr() as usize,
287            len: pool.len(),
288        };
289        let store = MemoryStore::new();
290        let keys = vec!["k1".to_owned(), "k2".to_owned()];
291        let first = Range { offset: 0, size: 4 };
292        let put = store.batch_put_from(&keys[..1], host, &[first]).unwrap();
293        assert_eq!(put.success, [true]);
294        assert_eq!(store.batch_exists(&keys).unwrap(), [true, false]);
295
296        let target = Range { offset: 8, size: 4 };
297        let short = Range {
298            offset: 12,
299            size: 3,
300        };
301        let got = store
302            .batch_get_into(&["k1".to_owned(), "k1".to_owned()], host, &[target, short])
303            .unwrap();
304        // A length mismatch is an invalid object, not a full read.
305        assert_eq!(got.success, [true, false]);
306        assert_eq!(got.object_failures, [false, true]);
307        assert!(got.retired);
308        assert_eq!(&pool[8..12], b"abcd");
309
310        assert!(store.remove_by_prefix("k"));
311        assert!(store.keys().is_empty());
312    }
313}