Skip to main content

subetha_cxc/
shared_once_cell.rs

1//! `SharedOnceCell<T>` - cross-process init-once cell.
2//!
3//! State machine:
4//! - EMPTY (0): no value; first writer to CAS to INITIALIZING wins.
5//! - INITIALIZING (1): a writer is filling the payload; other
6//!   writers spin until state advances.
7//! - INITIALIZED (2): payload is stable; readers may consume.
8//!
9//! The winner of the EMPTY -> INITIALIZING CAS performs the write
10//! and advances to INITIALIZED. Losers see INITIALIZED and read the
11//! winner's bytes.
12//!
13//! This is the cross-process analog of `once_cell::sync::OnceCell`,
14//! with cross-process safety guaranteed by the atomic CAS protocol
15//! over shared memory.
16
17use std::fs::{File, OpenOptions};
18use std::marker::PhantomData;
19use std::mem::{align_of, size_of};
20use std::path::Path;
21use std::sync::atomic::{AtomicU8, Ordering};
22
23use memmap2::{MmapMut, MmapOptions};
24
25pub const ONCE_MAGIC: u32 = 0x4F4E_4346;
26pub const ONCE_PAYLOAD_BYTES: usize = 56;
27
28pub const STATE_EMPTY: u8 = 0;
29pub const STATE_INITIALIZING: u8 = 1;
30pub const STATE_INITIALIZED: u8 = 2;
31
32#[repr(C, align(64))]
33pub struct OnceHeader {
34    pub magic: u32,
35    pub size: u32,
36    pub state: AtomicU8,
37    pub _pad_to_payload: [u8; 7],
38    pub payload: [u8; ONCE_PAYLOAD_BYTES],
39}
40
41pub const ONCE_FILE_SIZE: usize = size_of::<OnceHeader>();
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum SharedOnceError {
45    LayoutMismatch,
46    PayloadTooLarge,
47    IoError(std::io::ErrorKind),
48}
49
50impl From<std::io::Error> for SharedOnceError {
51    fn from(e: std::io::Error) -> Self { Self::IoError(e.kind()) }
52}
53
54pub struct SharedOnceCell<T: Copy + 'static> {
55    _file: File,
56    mmap: MmapMut,
57    _phantom: PhantomData<T>,
58    header_sidecar: subetha_core::HandshakeHeader,
59    ring_sidecar: Box<subetha_core::ObservationRing>,
60}
61
62unsafe impl<T: Copy + Send + 'static> Send for SharedOnceCell<T> {}
63unsafe impl<T: Copy + Sync + 'static> Sync for SharedOnceCell<T> {}
64
65impl<T: Copy + Send + Sync + 'static> subetha_sidecar::AdaptiveInstance for SharedOnceCell<T> {
66    fn header(&self) -> &subetha_core::HandshakeHeader { &self.header_sidecar }
67    fn ring(&self) -> &subetha_core::ObservationRing { &self.ring_sidecar }
68    fn make_policy(&self) -> Box<dyn subetha_sidecar::Policy> {
69        Box::new(subetha_sidecar::NoMigrationPolicy)
70    }
71}
72
73impl<T: Copy + 'static> SharedOnceCell<T> {
74    pub fn create(path: impl AsRef<Path>) -> Result<Self, SharedOnceError> {
75        Self::check_layout()?;
76        let file = OpenOptions::new()
77            .read(true).write(true).create(true).truncate(true)
78            .open(path.as_ref())?;
79        file.set_len(ONCE_FILE_SIZE as u64)?;
80        let mut mmap = unsafe { MmapOptions::new().len(ONCE_FILE_SIZE).map_mut(&file)? };
81        let ptr = mmap.as_mut_ptr() as *mut OnceHeader;
82        unsafe {
83            std::ptr::write(ptr, OnceHeader {
84                magic: ONCE_MAGIC,
85                size: size_of::<T>() as u32,
86                state: AtomicU8::new(STATE_EMPTY),
87                _pad_to_payload: [0; 7],
88                payload: [0; ONCE_PAYLOAD_BYTES],
89            });
90        }
91        Ok(Self {
92            _file: file, mmap, _phantom: PhantomData,
93            header_sidecar: subetha_core::HandshakeHeader::new(),
94            ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
95        })
96    }
97
98    pub fn open(path: impl AsRef<Path>) -> Result<Self, SharedOnceError> {
99        Self::check_layout()?;
100        let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
101        if file.metadata()?.len() < ONCE_FILE_SIZE as u64 {
102            return Err(SharedOnceError::LayoutMismatch);
103        }
104        let mmap = unsafe { MmapOptions::new().len(ONCE_FILE_SIZE).map_mut(&file)? };
105        let header = unsafe { &*(mmap.as_ptr() as *const OnceHeader) };
106        if header.magic != ONCE_MAGIC || header.size as usize != size_of::<T>() {
107            return Err(SharedOnceError::LayoutMismatch);
108        }
109        Ok(Self {
110            _file: file, mmap, _phantom: PhantomData,
111            header_sidecar: subetha_core::HandshakeHeader::new(),
112            ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
113        })
114    }
115
116    fn check_layout() -> Result<(), SharedOnceError> {
117        if size_of::<T>() > ONCE_PAYLOAD_BYTES {
118            return Err(SharedOnceError::PayloadTooLarge);
119        }
120        if align_of::<T>() > 8 {
121            return Err(SharedOnceError::PayloadTooLarge);
122        }
123        Ok(())
124    }
125
126    fn header(&self) -> &OnceHeader {
127        unsafe { &*(self.mmap.as_ptr() as *const OnceHeader) }
128    }
129
130    /// True when the cell has been initialised.
131    pub fn is_initialized(&self) -> bool {
132        self.header().state.load(Ordering::Acquire) == STATE_INITIALIZED
133    }
134
135    /// Get the value if initialised; otherwise return None.
136    /// Non-blocking; never invokes the initialiser.
137    pub fn get(&self) -> Option<T> {
138        let header = self.header();
139        if header.state.load(Ordering::Acquire) != STATE_INITIALIZED {
140            self.ring_sidecar
141                .push_op(crate::sidecar_ops::cell::OP_GET, 2); // empty / uninitialised
142            return None;
143        }
144        let value: T = unsafe {
145            let src = header.payload.as_ptr() as *const T;
146            std::ptr::read_unaligned(src)
147        };
148        self.ring_sidecar
149            .push_op(crate::sidecar_ops::cell::OP_GET, 0);
150        Some(value)
151    }
152
153    /// Try to write the value. Returns `true` if this caller won
154    /// the init race, `false` if the cell was already initialised
155    /// or another init is in progress.
156    pub fn set(&self, value: T) -> bool {
157        let header = self.header();
158        if header.state.compare_exchange(
159            STATE_EMPTY, STATE_INITIALIZING,
160            Ordering::AcqRel, Ordering::Acquire,
161        ).is_err() {
162            self.ring_sidecar
163                .push_op(crate::sidecar_ops::cell::OP_SET, 1); // lost the init race
164            return false;
165        }
166        unsafe {
167            let dst = header.payload.as_ptr() as *mut T;
168            std::ptr::write_unaligned(dst, value);
169        }
170        header.state.store(STATE_INITIALIZED, Ordering::Release);
171        self.ring_sidecar
172            .push_op(crate::sidecar_ops::cell::OP_SET, 0);
173        true
174    }
175
176    /// Get the cached value, or run `init` to produce it. The first
177    /// caller across all processes runs `init`; subsequent callers
178    /// spin until the value is published and return that value.
179    pub fn get_or_init<F: FnOnce() -> T>(&self, init: F) -> T {
180        if let Some(v) = self.get() { return v; }
181        let header = self.header();
182        match header.state.compare_exchange(
183            STATE_EMPTY, STATE_INITIALIZING,
184            Ordering::AcqRel, Ordering::Acquire,
185        ) {
186            Ok(_) => {
187                // We won; produce the value and publish.
188                let v = init();
189                unsafe {
190                    let dst = header.payload.as_ptr() as *mut T;
191                    std::ptr::write_unaligned(dst, v);
192                }
193                header.state.store(STATE_INITIALIZED, Ordering::Release);
194                self.ring_sidecar
195                    .push_op(crate::sidecar_ops::cell::OP_SET, 0);
196                v
197            }
198            Err(_) => {
199                // Spin until the winner publishes.
200                while header.state.load(Ordering::Acquire) != STATE_INITIALIZED {
201                    std::hint::spin_loop();
202                }
203                self.get().expect("INITIALIZED implies value present")
204            }
205        }
206    }
207
208    /// Non-blocking flush: schedules a writeback via the OS.
209    /// Note: Windows is only partially async (sync to page cache,
210    /// not to disk).
211    pub fn flush_async(&self) -> Result<(), SharedOnceError> {
212        self.mmap.flush_async()?;
213        Ok(())
214    }
215
216    pub fn flush(&self) -> Result<(), SharedOnceError> {
217        self.mmap.flush()?;
218        Ok(())
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    fn tmp(name: &str) -> std::path::PathBuf {
227        let mut p = std::env::temp_dir();
228        let pid = std::process::id();
229        p.push(format!("subetha-once-{name}-{pid}.bin"));
230        p
231    }
232
233    #[test]
234    fn fresh_cell_is_empty() {
235        let p = tmp("fresh");
236        let c: SharedOnceCell<u64> = SharedOnceCell::create(&p).unwrap();
237        assert!(!c.is_initialized());
238        assert_eq!(c.get(), None);
239        std::fs::remove_file(&p).ok();
240    }
241
242    #[test]
243    fn first_set_wins_subsequent_sets_lose() {
244        let p = tmp("first-wins");
245        let c: SharedOnceCell<u64> = SharedOnceCell::create(&p).unwrap();
246        assert!(c.set(42));
247        assert!(!c.set(99));
248        assert_eq!(c.get(), Some(42));
249        std::fs::remove_file(&p).ok();
250    }
251
252    #[test]
253    fn cross_handle_init_visible_to_other_handle() {
254        let p = tmp("cross-handle");
255        let writer: SharedOnceCell<u64> = SharedOnceCell::create(&p).unwrap();
256        let reader: SharedOnceCell<u64> = SharedOnceCell::open(&p).unwrap();
257        assert!(!reader.is_initialized());
258        writer.set(7777);
259        assert!(reader.is_initialized());
260        assert_eq!(reader.get(), Some(7777));
261        std::fs::remove_file(&p).ok();
262    }
263
264    #[test]
265    fn get_or_init_runs_closure_at_most_once() {
266        use std::sync::Arc;
267        use std::sync::atomic::AtomicU32;
268        use std::thread;
269        let p = tmp("get-or-init");
270        let c: Arc<SharedOnceCell<u64>> = Arc::new(SharedOnceCell::create(&p).unwrap());
271        let runs = Arc::new(AtomicU32::new(0));
272        let mut handles = vec![];
273        for _ in 0..8 {
274            let c = c.clone();
275            let runs = runs.clone();
276            handles.push(thread::spawn(move || {
277                c.get_or_init(|| {
278                    runs.fetch_add(1, Ordering::AcqRel);
279                    1234u64
280                })
281            }));
282        }
283        let results: Vec<u64> = handles.into_iter().map(|h| h.join().unwrap()).collect();
284        assert!(results.iter().all(|v| *v == 1234));
285        assert_eq!(runs.load(Ordering::Acquire), 1,
286                   "init closure must run exactly once across 8 threads");
287        std::fs::remove_file(&p).ok();
288    }
289
290    #[test]
291    fn disk_persistence_survives_reopen() {
292        let p = tmp("disk-persist");
293        {
294            let c: SharedOnceCell<u64> = SharedOnceCell::create(&p).unwrap();
295            c.set(8888);
296            c.flush().unwrap();
297        }
298        let c2: SharedOnceCell<u64> = SharedOnceCell::open(&p).unwrap();
299        assert_eq!(c2.get(), Some(8888));
300        assert!(c2.is_initialized());
301        // Set must fail on reopen because the cell is already init.
302        assert!(!c2.set(9999));
303        assert_eq!(c2.get(), Some(8888));
304        std::fs::remove_file(&p).ok();
305    }
306
307    #[test]
308    fn payload_too_large_at_create() {
309        #[allow(dead_code)] // size_of<Big> is the test signal, not the field
310        struct Big([u8; ONCE_PAYLOAD_BYTES + 1]);
311        impl Copy for Big {}
312        impl Clone for Big { fn clone(&self) -> Self { *self } }
313        let p = tmp("too-large");
314        match SharedOnceCell::<Big>::create(&p) {
315            Err(SharedOnceError::PayloadTooLarge) => {}
316            other => panic!("expected PayloadTooLarge, got {:?}", other.as_ref().err()),
317        }
318        std::fs::remove_file(&p).ok();
319    }
320
321    #[test]
322    fn open_rejects_wrong_payload_size() {
323        let p = tmp("wrong-size");
324        let _c: SharedOnceCell<u64> = SharedOnceCell::create(&p).unwrap();
325        match SharedOnceCell::<u32>::open(&p) {
326            Err(SharedOnceError::LayoutMismatch) => {}
327            other => panic!("expected LayoutMismatch, got {:?}", other.as_ref().err()),
328        }
329        std::fs::remove_file(&p).ok();
330    }
331}