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    /// [`Self::wait_changed`], but a second word can end it. The waiter
548    /// still parks until something wakes it; `interrupt` is what tells it
549    /// which kind of wake arrived. `Ok(Some(count))` is a write,
550    /// `Ok(None)` is the interrupt.
551    ///
552    /// This is the shape a caller needs when the wait may outlive its
553    /// reason — a process that has to shut down while a script waits on a
554    /// cell nothing will write again. It is not a timeout: nothing here
555    /// measures time, and a wait nobody interrupts is
556    /// [`Self::wait_changed`] exactly.
557    ///
558    /// The canceller sets `interrupt` and then calls
559    /// [`Self::wake_waiters`], in that order. Set-then-wake is what closes
560    /// the window: a waiter that had already parked is released by the
561    /// wake, and one that had not yet parked sees the flag instead.
562    pub fn wait_changed_until(
563        &self,
564        since: u32,
565        interrupt: &core::sync::atomic::AtomicBool,
566    ) -> Result<Option<u32>> {
567        loop {
568            if interrupt.load(Ordering::SeqCst) {
569                return Ok(None);
570            }
571
572            let slot = self.slot()?;
573            let now = slot.changes.load(Ordering::SeqCst);
574            if now != since {
575                return Ok(Some(now));
576            }
577
578            slot.waiters.fetch_add(1, Ordering::SeqCst);
579            let outcome = if slot.changes.load(Ordering::SeqCst) == since
580                && !interrupt.load(Ordering::SeqCst)
581            {
582                wait_on(&slot.changes, since)
583            } else {
584                Ok(())
585            };
586            slot.waiters.fetch_sub(1, Ordering::SeqCst);
587            outcome?;
588        }
589    }
590
591    /// Wake everyone parked on this cell without writing to it. On its own
592    /// this changes nothing — a waiter re-checks the count, finds it where
593    /// it was and parks again — so it is only useful after the word the
594    /// waiter also watches has been set. See [`Self::wait_changed_until`].
595    pub fn wake_waiters(&self) -> Result<()> {
596        wake_on(&self.slot()?.changes);
597        Ok(())
598    }
599
600    /// Give the cell back; this and every other handle to it go stale.
601    pub fn release(self) -> Result<()> {
602        self.cells.release(self.id)
603    }
604
605    fn slot(&self) -> Result<&CellSlot> {
606        self.cells.slot(self.id)
607    }
608}
609
610impl Orbital<i64> {
611    /// Add `by` (negative to subtract) and return the value after.
612    pub fn fetch_add(&self, by: i64) -> Result<i64> {
613        let slot = self.slot()?;
614        slot.value
615            .try_update(Ordering::AcqRel, Ordering::Acquire, |bits| {
616                (bits as i64).checked_add(by).map(|value| value as u64)
617            })
618            .map(|previous| {
619                slot.changed();
620                (previous as i64) + by
621            })
622            .map_err(|_| Error::Overflow)
623    }
624
625    pub fn fetch_sub(&self, by: i64) -> Result<i64> {
626        self.fetch_add(by.checked_neg().ok_or(Error::Overflow)?)
627    }
628
629    /// Add up to `by` while the value stays at or under `limit`, in one
630    /// compare-and-swap, and say what was taken.
631    ///
632    /// This is the operation a fleet needs to divide a fixed amount of work
633    /// between processes that cannot see each other. `fetch_add` followed by
634    /// a test cannot do it: the add lands before the caller learns it went
635    /// too far, so N callers overshoot by up to N-1 between them. Here the
636    /// bound is decided inside the swap, so the value **never** passes
637    /// `limit` however many callers race, and the one that finds less than
638    /// `by` left takes the remainder instead of being turned away.
639    ///
640    /// Taking more than one at a time is how the cost comes down: the
641    /// contended word is touched once per claim rather than once per unit.
642    /// `limit` is the ceiling for the total, never for a single call.
643    ///
644    /// Overflow needs no separate guard: a result that cannot pass `limit`
645    /// cannot pass `i64::MAX` either.
646    ///
647    /// **A claim is not a lease.** What this hands back is gone from the
648    /// total the moment the swap lands, and nothing gives it back: a caller
649    /// that takes a hundred, does forty and then dies leaves sixty that
650    /// nobody will ever do, while the count says all hundred were taken. So
651    /// `by` is not only a speed dial, it is the exposure — the most one
652    /// death can lose. It is the same trade a database sequence makes with
653    /// its cache size, and it leaves the same kind of gap. Work that must
654    /// not be lost wants a lease whose owner notices a death, which is
655    /// `orbit-pool`, not a counter.
656    pub fn add_until(&self, by: i64, limit: i64) -> Result<Claim> {
657        if by <= 0 {
658            return Err(Error::Overflow);
659        }
660        let slot = self.slot()?;
661        loop {
662            let current = slot.value.load(Ordering::Acquire) as i64;
663            if current >= limit {
664                return Ok(Claim::Exhausted);
665            }
666            let taken = by.min(limit - current);
667            let after = current + taken;
668            if slot
669                .value
670                .compare_exchange(
671                    current as u64,
672                    after as u64,
673                    Ordering::AcqRel,
674                    Ordering::Acquire,
675                )
676                .is_ok()
677            {
678                slot.changed();
679                return Ok(Claim::Took { taken, after });
680            }
681            // Lost the race; somebody else moved the value. Read it again
682            // rather than retry blind: what is left may now be less, or
683            // nothing.
684        }
685    }
686}
687
688impl Orbital<u64> {
689    pub fn fetch_add(&self, by: u64) -> Result<u64> {
690        let slot = self.slot()?;
691        slot.value
692            .try_update(Ordering::AcqRel, Ordering::Acquire, |bits| {
693                bits.checked_add(by)
694            })
695            .map(|previous| {
696                slot.changed();
697                previous + by
698            })
699            .map_err(|_| Error::Overflow)
700    }
701
702    pub fn fetch_sub(&self, by: u64) -> Result<u64> {
703        let slot = self.slot()?;
704        slot.value
705            .try_update(Ordering::AcqRel, Ordering::Acquire, |bits| {
706                bits.checked_sub(by)
707            })
708            .map(|previous| {
709                slot.changed();
710                previous - by
711            })
712            .map_err(|_| Error::Overflow)
713    }
714}
715
716impl Orbital<f64> {
717    /// Add `by` and return the value after. A compare-and-swap loop: floats
718    /// have no fetch-add of their own.
719    pub fn fetch_add(&self, by: f64) -> Result<f64> {
720        let slot = self.slot()?;
721        let previous = slot
722            .value
723            .try_update(Ordering::AcqRel, Ordering::Acquire, |bits| {
724                Some((f64::from_bits(bits) + by).to_bits())
725            })
726            .unwrap_or_else(|bits| bits);
727        slot.changed();
728        Ok(f64::from_bits(previous) + by)
729    }
730}
731
732impl Orbital<bool> {
733    /// Set the flag and return whether it was already set.
734    pub fn set(&self) -> Result<bool> {
735        self.swap(true)
736    }
737
738    /// Clear the flag and return whether it was set.
739    pub fn clear(&self) -> Result<bool> {
740        self.swap(false)
741    }
742}
743
744#[repr(C, align(64))]
745struct CellStateHeader {
746    #[cfg(unix)]
747    magic: u32,
748    #[cfg(not(unix))]
749    _magic: u32,
750    #[cfg(unix)]
751    version: u16,
752    #[cfg(not(unix))]
753    _version: u16,
754    header_size: u16,
755    capacity: u32,
756    slot_size: u32,
757    /// Where the next allocation starts looking; a hint, never a promise.
758    hint: AtomicU32,
759    _reserved: [u8; 44],
760}
761
762impl CellStateHeader {
763    fn new() -> Self {
764        Self {
765            #[cfg(unix)]
766            magic: STATE_MAGIC,
767            #[cfg(not(unix))]
768            _magic: 0,
769            #[cfg(unix)]
770            version: STATE_VERSION,
771            #[cfg(not(unix))]
772            _version: 0,
773            header_size: size_of::<Self>() as u16,
774            capacity: CELL_CAPACITY as u32,
775            slot_size: size_of::<CellSlot>() as u32,
776            hint: AtomicU32::new(0),
777            _reserved: [0; 44],
778        }
779    }
780}
781
782/// One cell, laid out for shared memory: a cache line of its own, so two hot
783/// cells never contend, and every field atomic, so the mapping is never
784/// borrowed `&mut` while another process reads it.
785#[repr(C, align(64))]
786struct CellSlot {
787    state: AtomicU8,
788    tag: AtomicU8,
789    _reserved: [u8; 2],
790    generation: AtomicU32,
791    value: AtomicU64,
792    /// Bumped by every write and by release: the word a waiter parks on.
793    /// Linux futex wants 32 bits, so the value itself cannot be that word.
794    changes: AtomicU32,
795    /// Waiters parked on `changes`; a writer wakes only when this is nonzero,
796    /// so the write path costs one extra load when nobody is listening.
797    waiters: AtomicU32,
798    _padding: [u8; 40],
799}
800
801impl CellSlot {
802    fn empty() -> Self {
803        Self {
804            state: AtomicU8::new(SLOT_EMPTY),
805            tag: AtomicU8::new(0),
806            _reserved: [0; 2],
807            generation: AtomicU32::new(0),
808            value: AtomicU64::new(0),
809            changes: AtomicU32::new(0),
810            waiters: AtomicU32::new(0),
811            _padding: [0; 40],
812        }
813    }
814
815    /// After a write: count it, and wake whoever is waiting for one. `SeqCst`
816    /// on both sides of the exchange with [`Orbital::wait_changed`], so a
817    /// waiter that announced itself just before this write is either seen
818    /// here or sees the new count itself; never neither.
819    fn changed(&self) {
820        self.changes.fetch_add(1, Ordering::SeqCst);
821        if self.waiters.load(Ordering::SeqCst) > 0 {
822            wake_on(&self.changes);
823        }
824    }
825
826    fn is(&self, id: CellId) -> bool {
827        // `Acquire` on `state` publishes the generation written by `install`.
828        self.state.load(Ordering::Acquire) == SLOT_OCCUPIED
829            && self.generation.load(Ordering::Relaxed) == id.generation
830    }
831
832    /// Under the structural lock. Returns the generation the new cell lives in;
833    /// it is bumped on every install, so no released id ever matches again.
834    fn install(&self, tag: u8, value: u64) -> u32 {
835        let generation = self
836            .generation
837            .load(Ordering::Relaxed)
838            .wrapping_add(1)
839            .max(1);
840        self.tag.store(tag, Ordering::Relaxed);
841        self.value.store(value, Ordering::Relaxed);
842        self.changes.store(0, Ordering::Relaxed);
843        self.waiters.store(0, Ordering::Relaxed);
844        self.generation.store(generation, Ordering::Relaxed);
845        self.state.store(SLOT_OCCUPIED, Ordering::Release);
846        generation
847    }
848}
849
850pub(crate) fn lock_unpoisoned<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
851    mutex.lock().unwrap_or_else(|error| error.into_inner())
852}
853
854struct MemoryCellTable {
855    /// Held so the fleet's address, which keys the registry, cannot be
856    /// reused by another fleet while this table lives.
857    _fleet: Arc<Fleet>,
858    slots: Vec<CellSlot>,
859    hint: AtomicU32,
860    structural_lock: Mutex<()>,
861}
862
863impl MemoryCellTable {
864    fn new(fleet: Arc<Fleet>) -> Self {
865        Self {
866            _fleet: fleet,
867            slots: (0..CELL_CAPACITY).map(|_| CellSlot::empty()).collect(),
868            hint: AtomicU32::new(0),
869            structural_lock: Mutex::new(()),
870        }
871    }
872}
873
874type MemoryRegistry = HashMap<usize, Weak<MemoryCellTable>>;
875
876static MEMORY_TABLES: LazyLock<Mutex<MemoryRegistry>> =
877    LazyLock::new(|| Mutex::new(HashMap::new()));
878
879fn memory_table(fleet: &Arc<Fleet>) -> Arc<MemoryCellTable> {
880    let fleet_identity = Arc::as_ptr(fleet) as usize;
881    let mut tables = lock_unpoisoned(&MEMORY_TABLES);
882    tables.retain(|_, table| table.strong_count() > 0);
883    if let Some(table) = tables.get(&fleet_identity).and_then(Weak::upgrade) {
884        return table;
885    }
886    let table = Arc::new(MemoryCellTable::new(Arc::clone(fleet)));
887    tables.insert(fleet_identity, Arc::downgrade(&table));
888    table
889}
890
891#[cfg(unix)]
892struct ShmCellTable {
893    region: ShmRegion,
894    structural_lock: Mutex<()>,
895}
896
897#[cfg(unix)]
898impl ShmCellTable {
899    fn open_or_create(name: &str) -> Result<Self> {
900        use std::{io, ptr};
901
902        let (region, _initialization_lock) =
903            ShmRegion::open_or_create_locked(name, shm_segment_size())?;
904        if region.created() {
905            unsafe {
906                ptr::write(
907                    region.as_ptr().cast::<CellStateHeader>(),
908                    CellStateHeader::new(),
909                );
910                let slots = region.as_ptr().add(size_of::<CellStateHeader>());
911                ptr::write_bytes(slots, 0, CELL_CAPACITY * size_of::<CellSlot>());
912            }
913        } else {
914            let header = unsafe { &*region.as_ptr().cast::<CellStateHeader>() };
915            if header.magic != STATE_MAGIC {
916                return Err(Error::Io(io::Error::new(
917                    io::ErrorKind::InvalidData,
918                    format!(
919                        "SHM segment {name} has wrong magic 0x{:08X} (expected 0x{STATE_MAGIC:08X})",
920                        header.magic
921                    ),
922                )));
923            }
924            if header.version != STATE_VERSION
925                || usize::from(header.header_size) != size_of::<CellStateHeader>()
926                || header.capacity as usize != CELL_CAPACITY
927                || header.slot_size as usize != size_of::<CellSlot>()
928            {
929                return Err(Error::Io(io::Error::new(
930                    io::ErrorKind::InvalidData,
931                    format!("SHM segment {name} has an incompatible cell-state layout"),
932                )));
933            }
934        }
935        Ok(Self {
936            region,
937            structural_lock: Mutex::new(()),
938        })
939    }
940
941    fn header(&self) -> &CellStateHeader {
942        unsafe { &*self.region.as_ptr().cast::<CellStateHeader>() }
943    }
944
945    fn slots(&self) -> &[CellSlot] {
946        unsafe {
947            std::slice::from_raw_parts(
948                self.region
949                    .as_ptr()
950                    .add(size_of::<CellStateHeader>())
951                    .cast::<CellSlot>(),
952                CELL_CAPACITY,
953            )
954        }
955    }
956
957    fn with_structure<T>(
958        &self,
959        operation: impl FnOnce(&[CellSlot], &AtomicU32) -> Result<T>,
960    ) -> Result<T> {
961        let _local = lock_unpoisoned(&self.structural_lock);
962        let _process = self.region.lock_exclusive()?;
963        operation(self.slots(), &self.header().hint)
964    }
965}
966
967#[cfg(unix)]
968fn shm_segment_size() -> usize {
969    size_of::<CellStateHeader>() + CELL_CAPACITY * size_of::<CellSlot>()
970}
971
972const _: () = assert!(CELL_CAPACITY.is_power_of_two());
973const _: () = assert!(CELL_CAPACITY <= u32::MAX as usize);
974const _: () = assert!(size_of::<CellStateHeader>() == 64);
975const _: () = assert!(size_of::<CellSlot>() == 64);
976
977#[cfg(test)]
978mod registry_tests {
979    use std::sync::Arc;
980
981    use orbit_core::Fleet;
982
983    use super::Cells;
984
985    /// The in-memory registry keys tables by the fleet's address. The
986    /// table therefore keeps the fleet alive: otherwise a dropped fleet's
987    /// address could be reused by a new one, which would then find and
988    /// share the old table.
989    #[test]
990    fn a_memory_table_keeps_its_fleet_alive() {
991        let fleet = Arc::new(Fleet::join("cell-registry-test", 1).unwrap());
992        let cells = Cells::new(Arc::clone(&fleet)).unwrap();
993        assert!(Arc::strong_count(&fleet) > 1);
994        drop(cells);
995        assert_eq!(Arc::strong_count(&fleet), 1);
996    }
997}
998
999#[cfg(test)]
1000mod wait_tests {
1001    use std::sync::Arc;
1002    use std::time::Duration;
1003
1004    use orbit_core::Fleet;
1005
1006    use super::{Cells, Error};
1007
1008    fn cells() -> Cells {
1009        Cells::new(Arc::new(Fleet::join("cell-wait-test", 1).unwrap())).unwrap()
1010    }
1011
1012    /// A waiter parks on the change count and a write from another thread
1013    /// wakes it; it then reads the value the write left. Ten writes while
1014    /// parked are one wake.
1015    #[test]
1016    fn a_write_wakes_a_waiter_once() {
1017        let cells = cells();
1018        let cell = cells.allocate(0_i64).unwrap();
1019        let since = cell.version().unwrap();
1020        let watcher = cell.clone();
1021        let waiter = std::thread::spawn(move || watcher.wait_changed(since));
1022
1023        std::thread::sleep(Duration::from_millis(30));
1024        for _ in 0..10 {
1025            cell.fetch_add(1).unwrap();
1026        }
1027        let now = waiter.join().unwrap().unwrap();
1028        assert!(now > since);
1029        assert_eq!(cell.load().unwrap(), 10);
1030        // Nothing new since: a wait would park; the count says so.
1031        assert_eq!(cell.version().unwrap(), cell.version().unwrap());
1032    }
1033
1034    /// Releasing the cell is a change too: a parked waiter comes back Stale
1035    /// instead of sleeping on a slot somebody else may take.
1036    #[test]
1037    fn release_wakes_a_waiter_stale() {
1038        let cells = cells();
1039        let cell = cells.allocate(7_i64).unwrap();
1040        let since = cell.version().unwrap();
1041        let watcher = cell.clone();
1042        let waiter = std::thread::spawn(move || watcher.wait_changed(since));
1043
1044        std::thread::sleep(Duration::from_millis(30));
1045        cell.release().unwrap();
1046        assert!(matches!(waiter.join().unwrap(), Err(Error::Stale(_))));
1047    }
1048}