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