Skip to main content

orbit_cell/
lib.rs

1//! Typed atomic cells in one fleet-shared SHM table, addressed by id.
2//!
3//! A cell is a place, not a name. [`Cells::allocate`] takes a slot, stamps
4//! its type and hands back an [`Orbital`] handle whose [`CellId`] is the
5//! only thing that ever needs to travel: any process in the fleet that
6//! [`Cells::open`]s the id reads and updates the same 64 bits, atomically,
7//! without a lock and without publishing a frame. `orbit-counter` is the
8//! keyed sibling; this is memory rather than a dictionary.
9//!
10//! Every id carries the slot's generation. [`Cells::release`] bumps it, so a
11//! handle kept past a release answers [`Error::Stale`] instead of touching
12//! whoever took the slot next. Cells have no lifetime of their own: what
13//! allocates one is expected to release it.
14
15use std::collections::HashMap;
16use std::fmt;
17use std::marker::PhantomData;
18use std::str::FromStr;
19use std::sync::atomic::{AtomicU8, AtomicU32, AtomicU64, Ordering};
20use std::sync::{Arc, LazyLock, Mutex, MutexGuard, Weak};
21
22use orbit_core::Fleet;
23#[cfg(unix)]
24use orbit_core::shm::{ShmRegion, ring_segment_name};
25
26mod text;
27
28pub use text::{CELL_TEXT_CAPACITY, CELL_TEXT_KIND, CELL_TEXT_MAX, Text, TextId};
29
30/// Reserved Orbit SHM kind for the cell table.
31pub const CELL_STATE_KIND: u8 = 233;
32/// Number of cells one fleet generation can hold at once.
33///
34/// Compile-time geometry: `ORBIT_CELL_CAPACITY` in the application's
35/// `.cargo/config.toml` overrides the default. Must be a power of two. Peers
36/// built with a different value are refused when they open the table.
37pub const CELL_CAPACITY: usize =
38    orbit_core::compile::usize_from_env(option_env!("ORBIT_CELL_CAPACITY"), 4_096);
39
40pub(crate) const SLOT_EMPTY: u8 = 0;
41pub(crate) const SLOT_OCCUPIED: u8 = 1;
42
43/// Park until `word` no longer holds `expected`, through the platform's
44/// shared address wait; where there is none, a short sleep and a re-check.
45#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))]
46pub(crate) fn wait_on(word: &AtomicU32, expected: u32) -> Result<()> {
47    match orbit_core::sync::wait_word(word, expected) {
48        Ok(()) => Ok(()),
49        Err(error) if error.kind() == std::io::ErrorKind::Unsupported => {
50            std::thread::sleep(std::time::Duration::from_millis(1));
51            Ok(())
52        }
53        Err(error) => Err(Error::Io(error)),
54    }
55}
56
57#[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "macos")))]
58pub(crate) fn wait_on(_word: &AtomicU32, _expected: u32) -> Result<()> {
59    std::thread::sleep(std::time::Duration::from_millis(1));
60    Ok(())
61}
62
63/// Wake everyone parked on `word`; nothing to do where nobody can park.
64pub(crate) fn wake_on(word: &AtomicU32) {
65    #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))]
66    let _ = orbit_core::sync::wake_word(word);
67    #[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "macos")))]
68    let _ = word;
69}
70
71#[cfg(unix)]
72const STATE_MAGIC: u32 = 0x43_45_4C_4C; // "CELL"
73#[cfg(unix)]
74const STATE_VERSION: u16 = 2;
75
76pub type Result<T, E = Error> = std::result::Result<T, E>;
77
78#[derive(Debug)]
79pub enum Error {
80    /// The id names a slot nothing occupies, or a generation that has been
81    /// released since the id was minted.
82    Stale(CellId),
83    /// The same, for a text cell.
84    StaleText(TextId),
85    /// The text would not fit the cell; nothing was written.
86    TooLong {
87        len: usize,
88        max: usize,
89    },
90    /// A text cell held bytes that are not UTF-8: something wrote past the
91    /// contract.
92    Corrupt(String),
93    /// The id names a live cell of another type.
94    TypeMismatch {
95        id: CellId,
96        expected: &'static str,
97        found: &'static str,
98    },
99    /// The update would leave the integer range.
100    Overflow,
101    /// Every slot is occupied.
102    Full {
103        capacity: usize,
104    },
105    /// The text is not a cell id.
106    Malformed(String),
107    Io(std::io::Error),
108}
109
110impl fmt::Display for Error {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        match self {
113            Self::Stale(id) => write!(f, "cell {id} has been released"),
114            Self::StaleText(id) => write!(f, "text cell {id} has been released"),
115            Self::TooLong { len, max } => {
116                write!(f, "text does not fit the cell: len={len} max={max}")
117            }
118            Self::Corrupt(id) => write!(f, "text cell {id} holds bytes that are not UTF-8"),
119            Self::TypeMismatch {
120                id,
121                expected,
122                found,
123            } => write!(f, "cell {id} holds {found}, not {expected}"),
124            Self::Overflow => f.write_str("cell value is outside the integer range"),
125            Self::Full { capacity } => write!(f, "cell table is full: capacity={capacity}"),
126            Self::Malformed(text) => write!(f, "not a cell id: {text:?}"),
127            Self::Io(error) => write!(f, "Orbit cell io error: {error}"),
128        }
129    }
130}
131
132impl std::error::Error for Error {
133    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
134        match self {
135            Self::Io(error) => Some(error),
136            _ => None,
137        }
138    }
139}
140
141impl From<std::io::Error> for Error {
142    fn from(value: std::io::Error) -> Self {
143        Self::Io(value)
144    }
145}
146
147/// The address of one cell: its slot and the generation it was allocated in.
148///
149/// Prints as `cell:<slot>:<generation>` and parses back, which is the form
150/// meant to cross a serialization boundary.
151#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
152pub struct CellId {
153    index: u32,
154    generation: u32,
155}
156
157impl CellId {
158    pub const fn index(self) -> u32 {
159        self.index
160    }
161
162    pub const fn generation(self) -> u32 {
163        self.generation
164    }
165
166    /// The two halves in one integer, slot high, generation low.
167    pub const fn to_bits(self) -> u64 {
168        ((self.index as u64) << 32) | self.generation as u64
169    }
170
171    pub const fn from_bits(bits: u64) -> Self {
172        Self {
173            index: (bits >> 32) as u32,
174            generation: bits as u32,
175        }
176    }
177}
178
179impl fmt::Display for CellId {
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        write!(f, "cell:{}:{}", self.index, self.generation)
182    }
183}
184
185impl FromStr for CellId {
186    type Err = Error;
187
188    fn from_str(text: &str) -> Result<Self> {
189        let malformed = || Error::Malformed(text.to_owned());
190        let rest = text.strip_prefix("cell:").ok_or_else(malformed)?;
191        let (index, generation) = rest.split_once(':').ok_or_else(malformed)?;
192        Ok(Self {
193            index: index.parse().map_err(|_| malformed())?,
194            generation: generation.parse().map_err(|_| malformed())?,
195        })
196    }
197}
198
199/// A value that fits one cell: 64 bits, and a tag that says which type they
200/// spell.
201pub trait CellType: Copy + Send + Sync + 'static {
202    const TAG: u8;
203    const NAME: &'static str;
204
205    fn to_bits(self) -> u64;
206    fn from_bits(bits: u64) -> Self;
207}
208
209impl CellType for i64 {
210    const TAG: u8 = 1;
211    const NAME: &'static str = "int";
212
213    fn to_bits(self) -> u64 {
214        self as u64
215    }
216
217    fn from_bits(bits: u64) -> Self {
218        bits as i64
219    }
220}
221
222impl CellType for u64 {
223    const TAG: u8 = 2;
224    const NAME: &'static str = "unsigned";
225
226    fn to_bits(self) -> u64 {
227        self
228    }
229
230    fn from_bits(bits: u64) -> Self {
231        bits
232    }
233}
234
235impl CellType for f64 {
236    const TAG: u8 = 3;
237    const NAME: &'static str = "float";
238
239    fn to_bits(self) -> u64 {
240        self.to_bits()
241    }
242
243    fn from_bits(bits: u64) -> Self {
244        f64::from_bits(bits)
245    }
246}
247
248impl CellType for bool {
249    const TAG: u8 = 4;
250    const NAME: &'static str = "bool";
251
252    fn to_bits(self) -> u64 {
253        u64::from(self)
254    }
255
256    fn from_bits(bits: u64) -> Self {
257        bits != 0
258    }
259}
260
261fn type_name(tag: u8) -> &'static str {
262    match tag {
263        1 => i64::NAME,
264        2 => u64::NAME,
265        3 => f64::NAME,
266        4 => bool::NAME,
267        _ => "unknown",
268    }
269}
270
271/// The fleet's cell tables, scalar and text. Cheap to clone.
272#[derive(Clone)]
273pub struct Cells {
274    backend: CellBackend,
275    text: text::TextBackend,
276}
277
278#[derive(Clone)]
279enum CellBackend {
280    InMemory(Arc<MemoryCellTable>),
281    #[cfg(unix)]
282    Shm(Arc<ShmCellTable>),
283}
284
285impl Cells {
286    pub fn new(fleet: Arc<Fleet>) -> Result<Self> {
287        let backend = if fleet.is_shm() {
288            #[cfg(unix)]
289            {
290                CellBackend::Shm(Arc::new(ShmCellTable::open_or_create(&ring_segment_name(
291                    fleet.name(),
292                    CELL_STATE_KIND,
293                ))?))
294            }
295            #[cfg(not(unix))]
296            unreachable!("non-Unix fleets cannot use POSIX SHM")
297        } else {
298            CellBackend::InMemory(memory_table(&fleet))
299        };
300        let text = text::TextBackend::new(&fleet)?;
301        Ok(Self { backend, text })
302    }
303
304    /// Take a text cell holding `initial`; up to [`CELL_TEXT_MAX`] bytes.
305    pub fn allocate_text(&self, initial: &str) -> Result<Text> {
306        self.text.allocate(initial)
307    }
308
309    /// A handle to a live text cell.
310    pub fn open_text(&self, id: TextId) -> Result<Text> {
311        self.text.open(id)
312    }
313
314    pub fn release_text(&self, id: TextId) -> Result<()> {
315        self.text.release(id)
316    }
317
318    pub fn is_text_live(&self, id: TextId) -> bool {
319        self.text.is_live(id)
320    }
321
322    /// Take a free slot, stamp it with `T` and `initial`, and return the
323    /// handle. The id inside it is what other processes open.
324    pub fn allocate<T: CellType>(&self, initial: T) -> Result<Orbital<T>> {
325        let id = self.with_structure(|slots, hint| {
326            let capacity = slots.len();
327            let start = hint.load(Ordering::Relaxed) as usize;
328            for offset in 0..capacity {
329                let index = (start + offset) & (capacity - 1);
330                let slot = &slots[index];
331                if slot.state.load(Ordering::Acquire) == SLOT_EMPTY {
332                    let generation = slot.install(T::TAG, initial.to_bits());
333                    hint.store(((index + 1) & (capacity - 1)) as u32, Ordering::Relaxed);
334                    return Ok(CellId {
335                        index: index as u32,
336                        generation,
337                    });
338                }
339            }
340            Err(Error::Full { capacity })
341        })?;
342        Ok(Orbital {
343            cells: self.clone(),
344            id,
345            _type: PhantomData,
346        })
347    }
348
349    /// A handle to a live cell of type `T`.
350    pub fn open<T: CellType>(&self, id: CellId) -> Result<Orbital<T>> {
351        let slot = self.slot(id)?;
352        let found = slot.tag.load(Ordering::Relaxed);
353        if found != T::TAG {
354            return Err(Error::TypeMismatch {
355                id,
356                expected: T::NAME,
357                found: type_name(found),
358            });
359        }
360        Ok(Orbital {
361            cells: self.clone(),
362            id,
363            _type: PhantomData,
364        })
365    }
366
367    /// Give the slot back. Every handle to this generation goes stale.
368    pub fn release(&self, id: CellId) -> Result<()> {
369        self.with_structure(|slots, _| {
370            let slot = slots
371                .get(id.index as usize)
372                .filter(|slot| slot.is(id))
373                .ok_or(Error::Stale(id))?;
374            slot.state.store(SLOT_EMPTY, Ordering::Release);
375            // Whoever is parked on it finds the slot gone and answers Stale.
376            slot.changed();
377            Ok(())
378        })
379    }
380
381    /// Whether `id` names a live cell right now.
382    pub fn is_live(&self, id: CellId) -> bool {
383        self.slot(id).is_ok()
384    }
385
386    /// Clear both tables during quiescent owner boot.
387    pub fn reset_all(&self) -> Result<()> {
388        self.with_structure(|slots, hint| {
389            for slot in slots {
390                slot.state.store(SLOT_EMPTY, Ordering::Release);
391                slot.changed();
392            }
393            hint.store(0, Ordering::Relaxed);
394            Ok(())
395        })?;
396        self.text.reset_all()
397    }
398
399    /// Remove the cell SHM object. Existing mappings remain valid until their
400    /// processes release them.
401    #[cfg(unix)]
402    pub fn unlink(&self) -> Result<()> {
403        match &self.backend {
404            CellBackend::InMemory(_) => self.reset_all()?,
405            CellBackend::Shm(table) => table.region.unlink().map_err(Error::Io)?,
406        }
407        self.text.unlink()
408    }
409
410    fn slots(&self) -> &[CellSlot] {
411        match &self.backend {
412            CellBackend::InMemory(table) => &table.slots,
413            #[cfg(unix)]
414            CellBackend::Shm(table) => table.slots(),
415        }
416    }
417
418    fn slot(&self, id: CellId) -> Result<&CellSlot> {
419        self.slots()
420            .get(id.index as usize)
421            .filter(|slot| slot.is(id))
422            .ok_or(Error::Stale(id))
423    }
424
425    fn with_structure<T>(
426        &self,
427        operation: impl FnOnce(&[CellSlot], &AtomicU32) -> Result<T>,
428    ) -> Result<T> {
429        match &self.backend {
430            CellBackend::InMemory(table) => {
431                let _local = lock_unpoisoned(&table.structural_lock);
432                operation(&table.slots, &table.hint)
433            }
434            #[cfg(unix)]
435            CellBackend::Shm(table) => table.with_structure(operation),
436        }
437    }
438}
439
440/// One cell, typed. Cheap to clone; every clone is the same place.
441#[derive(Clone)]
442pub struct Orbital<T: CellType> {
443    cells: Cells,
444    id: CellId,
445    _type: PhantomData<T>,
446}
447
448impl<T: CellType> Orbital<T> {
449    pub fn id(&self) -> CellId {
450        self.id
451    }
452
453    pub fn load(&self) -> Result<T> {
454        Ok(T::from_bits(self.slot()?.value.load(Ordering::Acquire)))
455    }
456
457    pub fn store(&self, value: T) -> Result<()> {
458        let slot = self.slot()?;
459        slot.value.store(value.to_bits(), Ordering::Release);
460        slot.changed();
461        Ok(())
462    }
463
464    /// Replace the value and return what it was.
465    pub fn swap(&self, value: T) -> Result<T> {
466        let slot = self.slot()?;
467        let previous = slot.value.swap(value.to_bits(), Ordering::AcqRel);
468        slot.changed();
469        Ok(T::from_bits(previous))
470    }
471
472    /// The write count, the thing [`Self::wait_changed`] waits on. Take it
473    /// before reading the value, and the wait misses nothing in between.
474    pub fn version(&self) -> Result<u32> {
475        Ok(self.slot()?.changes.load(Ordering::Acquire))
476    }
477
478    /// Park until the cell has been written since `since`, then return the
479    /// count now. Coalescing: ten writes while parked wake the caller once,
480    /// and [`Self::load`] gives the latest. A released cell wakes every waiter
481    /// with [`Error::Stale`]. Blocking; an async runtime wraps it.
482    pub fn wait_changed(&self, since: u32) -> Result<u32> {
483        loop {
484            let slot = self.slot()?;
485            let now = slot.changes.load(Ordering::SeqCst);
486            if now != since {
487                return Ok(now);
488            }
489            slot.waiters.fetch_add(1, Ordering::SeqCst);
490            let outcome = if slot.changes.load(Ordering::SeqCst) == since {
491                wait_on(&slot.changes, since)
492            } else {
493                Ok(())
494            };
495            slot.waiters.fetch_sub(1, Ordering::SeqCst);
496            outcome?;
497        }
498    }
499
500    /// Store `new` only if the cell still holds `current`. `Ok(Ok(previous))`
501    /// on success, `Ok(Err(actual))` when it held something else.
502    pub fn compare_exchange(&self, current: T, new: T) -> Result<std::result::Result<T, T>> {
503        let slot = self.slot()?;
504        let exchanged = slot.value.compare_exchange(
505            current.to_bits(),
506            new.to_bits(),
507            Ordering::AcqRel,
508            Ordering::Acquire,
509        );
510        if exchanged.is_ok() {
511            slot.changed();
512        }
513        Ok(exchanged.map(T::from_bits).map_err(T::from_bits))
514    }
515
516    /// Give the cell back; this and every other handle to it go stale.
517    pub fn release(self) -> Result<()> {
518        self.cells.release(self.id)
519    }
520
521    fn slot(&self) -> Result<&CellSlot> {
522        self.cells.slot(self.id)
523    }
524}
525
526impl Orbital<i64> {
527    /// Add `by` (negative to subtract) and return the value after.
528    pub fn fetch_add(&self, by: i64) -> Result<i64> {
529        let slot = self.slot()?;
530        slot.value
531            .try_update(Ordering::AcqRel, Ordering::Acquire, |bits| {
532                (bits as i64).checked_add(by).map(|value| value as u64)
533            })
534            .map(|previous| {
535                slot.changed();
536                (previous as i64) + by
537            })
538            .map_err(|_| Error::Overflow)
539    }
540
541    pub fn fetch_sub(&self, by: i64) -> Result<i64> {
542        self.fetch_add(by.checked_neg().ok_or(Error::Overflow)?)
543    }
544}
545
546impl Orbital<u64> {
547    pub fn fetch_add(&self, by: u64) -> Result<u64> {
548        let slot = self.slot()?;
549        slot.value
550            .try_update(Ordering::AcqRel, Ordering::Acquire, |bits| {
551                bits.checked_add(by)
552            })
553            .map(|previous| {
554                slot.changed();
555                previous + by
556            })
557            .map_err(|_| Error::Overflow)
558    }
559
560    pub fn fetch_sub(&self, by: u64) -> Result<u64> {
561        let slot = self.slot()?;
562        slot.value
563            .try_update(Ordering::AcqRel, Ordering::Acquire, |bits| {
564                bits.checked_sub(by)
565            })
566            .map(|previous| {
567                slot.changed();
568                previous - by
569            })
570            .map_err(|_| Error::Overflow)
571    }
572}
573
574impl Orbital<f64> {
575    /// Add `by` and return the value after. A compare-and-swap loop: floats
576    /// have no fetch-add of their own.
577    pub fn fetch_add(&self, by: f64) -> Result<f64> {
578        let slot = self.slot()?;
579        let previous = slot
580            .value
581            .fetch_update(Ordering::AcqRel, Ordering::Acquire, |bits| {
582                Some((f64::from_bits(bits) + by).to_bits())
583            })
584            .unwrap_or_else(|bits| bits);
585        slot.changed();
586        Ok(f64::from_bits(previous) + by)
587    }
588}
589
590impl Orbital<bool> {
591    /// Set the flag and return whether it was already set.
592    pub fn set(&self) -> Result<bool> {
593        self.swap(true)
594    }
595
596    /// Clear the flag and return whether it was set.
597    pub fn clear(&self) -> Result<bool> {
598        self.swap(false)
599    }
600}
601
602#[repr(C, align(64))]
603struct CellStateHeader {
604    #[cfg(unix)]
605    magic: u32,
606    #[cfg(not(unix))]
607    _magic: u32,
608    #[cfg(unix)]
609    version: u16,
610    #[cfg(not(unix))]
611    _version: u16,
612    header_size: u16,
613    capacity: u32,
614    slot_size: u32,
615    /// Where the next allocation starts looking; a hint, never a promise.
616    hint: AtomicU32,
617    _reserved: [u8; 44],
618}
619
620impl CellStateHeader {
621    fn new() -> Self {
622        Self {
623            #[cfg(unix)]
624            magic: STATE_MAGIC,
625            #[cfg(not(unix))]
626            _magic: 0,
627            #[cfg(unix)]
628            version: STATE_VERSION,
629            #[cfg(not(unix))]
630            _version: 0,
631            header_size: size_of::<Self>() as u16,
632            capacity: CELL_CAPACITY as u32,
633            slot_size: size_of::<CellSlot>() as u32,
634            hint: AtomicU32::new(0),
635            _reserved: [0; 44],
636        }
637    }
638}
639
640/// One cell, laid out for shared memory: a cache line of its own, so two hot
641/// cells never contend, and every field atomic, so the mapping is never
642/// borrowed `&mut` while another process reads it.
643#[repr(C, align(64))]
644struct CellSlot {
645    state: AtomicU8,
646    tag: AtomicU8,
647    _reserved: [u8; 2],
648    generation: AtomicU32,
649    value: AtomicU64,
650    /// Bumped by every write and by release: the word a waiter parks on.
651    /// Linux futex wants 32 bits, so the value itself cannot be that word.
652    changes: AtomicU32,
653    /// Waiters parked on `changes`; a writer wakes only when this is nonzero,
654    /// so the write path costs one extra load when nobody is listening.
655    waiters: AtomicU32,
656    _padding: [u8; 40],
657}
658
659impl CellSlot {
660    fn empty() -> Self {
661        Self {
662            state: AtomicU8::new(SLOT_EMPTY),
663            tag: AtomicU8::new(0),
664            _reserved: [0; 2],
665            generation: AtomicU32::new(0),
666            value: AtomicU64::new(0),
667            changes: AtomicU32::new(0),
668            waiters: AtomicU32::new(0),
669            _padding: [0; 40],
670        }
671    }
672
673    /// After a write: count it, and wake whoever is waiting for one. `SeqCst`
674    /// on both sides of the exchange with [`Orbital::wait_changed`], so a
675    /// waiter that announced itself just before this write is either seen
676    /// here or sees the new count itself; never neither.
677    fn changed(&self) {
678        self.changes.fetch_add(1, Ordering::SeqCst);
679        if self.waiters.load(Ordering::SeqCst) > 0 {
680            wake_on(&self.changes);
681        }
682    }
683
684    fn is(&self, id: CellId) -> bool {
685        // `Acquire` on `state` publishes the generation written by `install`.
686        self.state.load(Ordering::Acquire) == SLOT_OCCUPIED
687            && self.generation.load(Ordering::Relaxed) == id.generation
688    }
689
690    /// Under the structural lock. Returns the generation the new cell lives in;
691    /// it is bumped on every install, so no released id ever matches again.
692    fn install(&self, tag: u8, value: u64) -> u32 {
693        let generation = self
694            .generation
695            .load(Ordering::Relaxed)
696            .wrapping_add(1)
697            .max(1);
698        self.tag.store(tag, Ordering::Relaxed);
699        self.value.store(value, Ordering::Relaxed);
700        self.changes.store(0, Ordering::Relaxed);
701        self.waiters.store(0, Ordering::Relaxed);
702        self.generation.store(generation, Ordering::Relaxed);
703        self.state.store(SLOT_OCCUPIED, Ordering::Release);
704        generation
705    }
706}
707
708pub(crate) fn lock_unpoisoned<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
709    mutex.lock().unwrap_or_else(|error| error.into_inner())
710}
711
712struct MemoryCellTable {
713    slots: Vec<CellSlot>,
714    hint: AtomicU32,
715    structural_lock: Mutex<()>,
716}
717
718impl MemoryCellTable {
719    fn new() -> Self {
720        Self {
721            slots: (0..CELL_CAPACITY).map(|_| CellSlot::empty()).collect(),
722            hint: AtomicU32::new(0),
723            structural_lock: Mutex::new(()),
724        }
725    }
726}
727
728type MemoryRegistry = HashMap<usize, Weak<MemoryCellTable>>;
729
730static MEMORY_TABLES: LazyLock<Mutex<MemoryRegistry>> =
731    LazyLock::new(|| Mutex::new(HashMap::new()));
732
733fn memory_table(fleet: &Arc<Fleet>) -> Arc<MemoryCellTable> {
734    let fleet_identity = Arc::as_ptr(fleet) as usize;
735    let mut tables = lock_unpoisoned(&MEMORY_TABLES);
736    tables.retain(|_, table| table.strong_count() > 0);
737    if let Some(table) = tables.get(&fleet_identity).and_then(Weak::upgrade) {
738        return table;
739    }
740    let table = Arc::new(MemoryCellTable::new());
741    tables.insert(fleet_identity, Arc::downgrade(&table));
742    table
743}
744
745#[cfg(unix)]
746struct ShmCellTable {
747    region: ShmRegion,
748    structural_lock: Mutex<()>,
749}
750
751#[cfg(unix)]
752impl ShmCellTable {
753    fn open_or_create(name: &str) -> Result<Self> {
754        use std::{io, ptr};
755
756        let (region, _initialization_lock) =
757            ShmRegion::open_or_create_locked(name, shm_segment_size())?;
758        if region.created() {
759            unsafe {
760                ptr::write(
761                    region.as_ptr().cast::<CellStateHeader>(),
762                    CellStateHeader::new(),
763                );
764                let slots = region.as_ptr().add(size_of::<CellStateHeader>());
765                ptr::write_bytes(slots, 0, CELL_CAPACITY * size_of::<CellSlot>());
766            }
767        } else {
768            let header = unsafe { &*region.as_ptr().cast::<CellStateHeader>() };
769            if header.magic != STATE_MAGIC {
770                return Err(Error::Io(io::Error::new(
771                    io::ErrorKind::InvalidData,
772                    format!(
773                        "SHM segment {name} has wrong magic 0x{:08X} (expected 0x{STATE_MAGIC:08X})",
774                        header.magic
775                    ),
776                )));
777            }
778            if header.version != STATE_VERSION
779                || usize::from(header.header_size) != size_of::<CellStateHeader>()
780                || header.capacity as usize != CELL_CAPACITY
781                || header.slot_size as usize != size_of::<CellSlot>()
782            {
783                return Err(Error::Io(io::Error::new(
784                    io::ErrorKind::InvalidData,
785                    format!("SHM segment {name} has an incompatible cell-state layout"),
786                )));
787            }
788        }
789        Ok(Self {
790            region,
791            structural_lock: Mutex::new(()),
792        })
793    }
794
795    fn header(&self) -> &CellStateHeader {
796        unsafe { &*self.region.as_ptr().cast::<CellStateHeader>() }
797    }
798
799    fn slots(&self) -> &[CellSlot] {
800        unsafe {
801            std::slice::from_raw_parts(
802                self.region
803                    .as_ptr()
804                    .add(size_of::<CellStateHeader>())
805                    .cast::<CellSlot>(),
806                CELL_CAPACITY,
807            )
808        }
809    }
810
811    fn with_structure<T>(
812        &self,
813        operation: impl FnOnce(&[CellSlot], &AtomicU32) -> Result<T>,
814    ) -> Result<T> {
815        let _local = lock_unpoisoned(&self.structural_lock);
816        let _process = self.region.lock_exclusive()?;
817        operation(self.slots(), &self.header().hint)
818    }
819}
820
821#[cfg(unix)]
822fn shm_segment_size() -> usize {
823    size_of::<CellStateHeader>() + CELL_CAPACITY * size_of::<CellSlot>()
824}
825
826const _: () = assert!(CELL_CAPACITY.is_power_of_two());
827const _: () = assert!(CELL_CAPACITY <= u32::MAX as usize);
828const _: () = assert!(size_of::<CellStateHeader>() == 64);
829const _: () = assert!(size_of::<CellSlot>() == 64);
830
831#[cfg(test)]
832mod wait_tests {
833    use std::sync::Arc;
834    use std::time::Duration;
835
836    use orbit_core::Fleet;
837
838    use super::{Cells, Error};
839
840    fn cells() -> Cells {
841        Cells::new(Arc::new(Fleet::join("cell-wait-test", 1).unwrap())).unwrap()
842    }
843
844    /// A waiter parks on the change count and a write from another thread
845    /// wakes it; it then reads the value the write left. Ten writes while
846    /// parked are one wake.
847    #[test]
848    fn a_write_wakes_a_waiter_once() {
849        let cells = cells();
850        let cell = cells.allocate(0_i64).unwrap();
851        let since = cell.version().unwrap();
852        let watcher = cell.clone();
853        let waiter = std::thread::spawn(move || watcher.wait_changed(since));
854
855        std::thread::sleep(Duration::from_millis(30));
856        for _ in 0..10 {
857            cell.fetch_add(1).unwrap();
858        }
859        let now = waiter.join().unwrap().unwrap();
860        assert!(now > since);
861        assert_eq!(cell.load().unwrap(), 10);
862        // Nothing new since: a wait would park; the count says so.
863        assert_eq!(cell.version().unwrap(), cell.version().unwrap());
864    }
865
866    /// Releasing the cell is a change too: a parked waiter comes back Stale
867    /// instead of sleeping on a slot somebody else may take.
868    #[test]
869    fn release_wakes_a_waiter_stale() {
870        let cells = cells();
871        let cell = cells.allocate(7_i64).unwrap();
872        let since = cell.version().unwrap();
873        let watcher = cell.clone();
874        let waiter = std::thread::spawn(move || watcher.wait_changed(since));
875
876        std::thread::sleep(Duration::from_millis(30));
877        cell.release().unwrap();
878        assert!(matches!(waiter.join().unwrap(), Err(Error::Stale(_))));
879    }
880}