subetha_cxc/
shared_once_cell.rs1use 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 pub fn is_initialized(&self) -> bool {
132 self.header().state.load(Ordering::Acquire) == STATE_INITIALIZED
133 }
134
135 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); 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 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); 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 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 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 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 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 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)] 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}