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`] retires 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 lifetime;
27mod text;
28mod wake;
29
30use lifetime::{Lease, Owners};
31use wake::Wake;
32
33pub use text::{
34    CELL_TEXT_CAPACITY, CELL_TEXT_KIND, CELL_TEXT_MAX, Text, TextId,
35};
36
37/// Reserved Orbit SHM kind for the cell table.
38pub const CELL_STATE_KIND: u8 = 187;
39/// Number of cells one fleet generation can hold at once.
40///
41/// Compile-time geometry: `ORBIT_CELL_CAPACITY` in the application's
42/// `.cargo/config.toml` overrides the default. Must be a power of two. Peers
43/// built with a different value are refused when they open the table.
44pub const CELL_CAPACITY: usize = orbit_core::compile::usize_from_env(
45    option_env!("ORBIT_CELL_CAPACITY"),
46    4_096,
47);
48
49pub(crate) const SLOT_EMPTY: u8 = 0;
50pub(crate) const SLOT_OCCUPIED: u8 = 1;
51pub(crate) const SLOT_RETIRED: u8 = 2;
52
53/// Whether this build can park on a shared word. A table refuses to open
54/// where it cannot: a sleep loop wearing the shape of a wait is worse
55/// than a clear no, and nothing above here should have to ask again.
56#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))]
57pub(crate) fn waits_supported() -> bool {
58    orbit_core::sync::supported()
59}
60
61#[cfg(not(any(
62    target_os = "linux",
63    target_os = "freebsd",
64    target_os = "macos"
65)))]
66pub(crate) fn waits_supported() -> bool {
67    false
68}
69
70/// Park until `word` no longer holds `expected`, through the platform's
71/// shared address wait. There is no polling fallback: a table refuses to
72/// open where the platform cannot wait, so by here it can.
73#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))]
74pub(crate) fn wait_on(
75    word: &AtomicU32,
76    expected: u32,
77) -> Result<()> {
78    orbit_core::sync::wait_word(word, expected).map_err(Error::Io)
79}
80
81#[cfg(not(any(
82    target_os = "linux",
83    target_os = "freebsd",
84    target_os = "macos"
85)))]
86pub(crate) fn wait_on(
87    _word: &AtomicU32,
88    _expected: u32,
89) -> Result<()> {
90    Err(Error::Io(std::io::Error::new(
91        std::io::ErrorKind::Unsupported,
92        "orbit-cell needs a platform that can wait on a shared word",
93    )))
94}
95
96/// Wake everyone parked on `word`; nothing to do where nobody can park.
97pub(crate) fn wake_on(word: &AtomicU32) {
98    #[cfg(any(
99        target_os = "linux",
100        target_os = "freebsd",
101        target_os = "macos"
102    ))]
103    let _ = orbit_core::sync::wake_word(word);
104    #[cfg(not(any(
105        target_os = "linux",
106        target_os = "freebsd",
107        target_os = "macos"
108    )))]
109    let _ = word;
110}
111
112#[cfg(unix)]
113const STATE_MAGIC: u32 = 0x43_45_4c_4c; // "CELL"
114#[cfg(unix)]
115const STATE_VERSION: u16 = 4;
116
117pub type Result<T, E = Error> = std::result::Result<T, E>;
118
119/// Additions are expected: a cause discovered later lands here rather
120/// than in a new major version, so a caller matches what it handles and
121/// leaves the rest to a catch-all.
122#[non_exhaustive]
123#[derive(Debug)]
124pub enum Error {
125    /// The id names a slot nothing occupies, or a generation that has been
126    /// released since the id was minted.
127    Stale(CellId),
128    /// The same, for a text cell.
129    StaleText(TextId),
130    /// The text would not fit the cell; nothing was written.
131    TooLong {
132        len: usize,
133        max: usize,
134    },
135    /// The caller's read buffer cannot hold the validated text snapshot.
136    BufferTooSmall {
137        required: usize,
138        available: usize,
139    },
140    /// A text cell held bytes that are not UTF-8: something wrote past the
141    /// contract.
142    Corrupt(String),
143    /// The id names a live cell of another type.
144    TypeMismatch {
145        id: CellId,
146        expected: &'static str,
147        found: &'static str,
148    },
149    /// The update would leave the integer range.
150    Overflow,
151    /// The native parking generation reached its limit. Value access still
152    /// works; release and reallocate to obtain a new parking generation.
153    WakeExhausted,
154    /// A bounded text read raced bank reuse, or another live writer holds
155    /// the cell. The operation does not spin or wait for that writer.
156    Contended,
157    /// The full-width text publication sequence cannot advance without ABA.
158    RevisionExhausted,
159    /// A text owner row cannot mint another distinct incarnation.
160    OwnerExhausted,
161    /// No slot is reusable: slots may be live, pinned after release, or have
162    /// exhausted their generation range.
163    Full {
164        capacity: usize,
165    },
166    /// The text is not a cell id.
167    Malformed(String),
168    Io(std::io::Error),
169}
170
171impl fmt::Display for Error {
172    fn fmt(
173        &self,
174        f: &mut fmt::Formatter<'_>,
175    ) -> fmt::Result {
176        match self {
177            Self::Stale(id) => write!(f, "cell {id} has been released"),
178            Self::StaleText(id) => {
179                write!(f, "text cell {id} has been released")
180            }
181            Self::TooLong { len, max } => {
182                write!(f, "text does not fit the cell: len={len} max={max}")
183            }
184            Self::BufferTooSmall { required, available } => write!(
185                f,
186                "text read buffer is too small: required={required} available={available}"
187            ),
188            Self::Corrupt(id) => {
189                write!(f, "text cell {id} holds bytes that are not UTF-8")
190            }
191            Self::TypeMismatch { id, expected, found } => {
192                write!(f, "cell {id} holds {found}, not {expected}")
193            }
194            Self::Overflow => {
195                f.write_str("cell value is outside the integer range")
196            }
197            Self::WakeExhausted => {
198                f.write_str("cell parking generation is exhausted")
199            }
200            Self::Contended => f.write_str("text cell is contended"),
201            Self::RevisionExhausted => {
202                f.write_str("text publication sequence is exhausted")
203            }
204            Self::OwnerExhausted => {
205                f.write_str("text owner incarnation is exhausted")
206            }
207            Self::Full { capacity } => {
208                write!(
209                    f,
210                    "cell table has no reusable slot: capacity={capacity}"
211                )
212            }
213            Self::Malformed(text) => write!(f, "not a cell id: {text:?}"),
214            Self::Io(error) => write!(f, "Orbit cell io error: {error}"),
215        }
216    }
217}
218
219impl std::error::Error for Error {
220    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
221        match self {
222            Self::Io(error) => Some(error),
223            _ => None,
224        }
225    }
226}
227
228impl From<std::io::Error> for Error {
229    fn from(value: std::io::Error) -> Self {
230        Self::Io(value)
231    }
232}
233
234/// The address of one cell: its slot and the generation it was allocated in.
235///
236/// Prints as `cell:<slot>:<generation>` and parses back, which is the form
237/// meant to cross a serialization boundary.
238#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
239pub struct CellId {
240    index: u32,
241    generation: u32,
242}
243
244impl CellId {
245    pub const fn index(self) -> u32 {
246        self.index
247    }
248
249    pub const fn generation(self) -> u32 {
250        self.generation
251    }
252
253    /// The two halves in one integer, slot high, generation low.
254    pub const fn to_bits(self) -> u64 {
255        ((self.index as u64) << 32) | self.generation as u64
256    }
257
258    pub const fn from_bits(bits: u64) -> Self {
259        Self { index: (bits >> 32) as u32, generation: bits as u32 }
260    }
261}
262
263impl fmt::Display for CellId {
264    fn fmt(
265        &self,
266        f: &mut fmt::Formatter<'_>,
267    ) -> fmt::Result {
268        write!(f, "cell:{}:{}", self.index, self.generation)
269    }
270}
271
272impl FromStr for CellId {
273    type Err = Error;
274
275    fn from_str(text: &str) -> Result<Self> {
276        let malformed = || Error::Malformed(text.to_owned());
277        let rest = text.strip_prefix("cell:").ok_or_else(malformed)?;
278        let (index, generation) = rest.split_once(':').ok_or_else(malformed)?;
279        Ok(Self {
280            index: index.parse().map_err(|_| malformed())?,
281            generation: generation.parse().map_err(|_| malformed())?,
282        })
283    }
284}
285
286/// A value that fits one cell: 64 bits, and a tag that says which type they
287/// spell.
288pub trait CellType: Copy + Send + Sync + 'static {
289    const TAG: u8;
290    const NAME: &'static str;
291
292    fn to_bits(self) -> u64;
293    fn from_bits(bits: u64) -> Self;
294}
295
296impl CellType for i64 {
297    const TAG: u8 = 1;
298    const NAME: &'static str = "int";
299
300    fn to_bits(self) -> u64 {
301        self as u64
302    }
303
304    fn from_bits(bits: u64) -> Self {
305        bits as i64
306    }
307}
308
309impl CellType for u64 {
310    const TAG: u8 = 2;
311    const NAME: &'static str = "unsigned";
312
313    fn to_bits(self) -> u64 {
314        self
315    }
316
317    fn from_bits(bits: u64) -> Self {
318        bits
319    }
320}
321
322impl CellType for f64 {
323    const TAG: u8 = 3;
324    const NAME: &'static str = "float";
325
326    fn to_bits(self) -> u64 {
327        self.to_bits()
328    }
329
330    fn from_bits(bits: u64) -> Self {
331        f64::from_bits(bits)
332    }
333}
334
335impl CellType for bool {
336    const TAG: u8 = 4;
337    const NAME: &'static str = "bool";
338
339    fn to_bits(self) -> u64 {
340        u64::from(self)
341    }
342
343    fn from_bits(bits: u64) -> Self {
344        bits != 0
345    }
346}
347
348fn type_name(tag: u8) -> &'static str {
349    match tag {
350        1 => i64::NAME,
351        2 => u64::NAME,
352        3 => f64::NAME,
353        4 => bool::NAME,
354        _ => "unknown",
355    }
356}
357
358/// The fleet's cell tables, scalar and text. Cheap to clone.
359#[derive(Clone)]
360pub struct Cells {
361    backend: CellBackend,
362    text: text::TextBackend,
363}
364
365#[derive(Clone)]
366enum CellBackend {
367    InMemory(Arc<MemoryCellTable>),
368    #[cfg(unix)]
369    Shm(Arc<ShmCellTable>),
370}
371
372impl Cells {
373    pub fn new(fleet: Arc<Fleet>) -> Result<Self> {
374        if !crate::waits_supported() {
375            return Err(Error::Io(std::io::Error::new(
376                std::io::ErrorKind::Unsupported,
377                "orbit-cell needs a platform that can wait on a shared word: Linux, FreeBSD, or macOS 14.4 or later",
378            )));
379        }
380        let backend = if fleet.is_shm() {
381            #[cfg(unix)]
382            {
383                CellBackend::Shm(shm_table(
384                    &ring_segment_name(fleet.name(), CELL_STATE_KIND),
385                    lifetime::owner_capacity(&fleet),
386                )?)
387            }
388            #[cfg(not(unix))]
389            unreachable!("non-Unix fleets cannot use POSIX SHM")
390        } else {
391            CellBackend::InMemory(memory_table(&fleet))
392        };
393        let text = text::TextBackend::new(&fleet)?;
394        Ok(Self { backend, text })
395    }
396
397    /// Take a text cell holding `initial`; up to [`CELL_TEXT_MAX`] bytes.
398    pub fn allocate_text(
399        &self,
400        initial: &str,
401    ) -> Result<Text> {
402        self.text.allocate(initial)
403    }
404
405    /// Recover abandoned text writers after an owner-exit notification.
406    ///
407    /// Takes only kernel holds whose owners have gone, completes their wake
408    /// before clearing ownership, and preserves the last committed text.
409    /// This cold-path hook does not infer death from a PID or elapsed time.
410    /// Call it from the runtime's process-exit handler; no polling is installed.
411    /// Returns the number of recovered writer descriptors. In-memory tables
412    /// use RAII for unwinding and have no dead process owners to recover.
413    pub fn recover_text_writers(&self) -> Result<usize> {
414        self.text.recover_writers()
415    }
416
417    /// A handle to a live text cell.
418    pub fn open_text(
419        &self,
420        id: TextId,
421    ) -> Result<Text> {
422        self.text.open(id)
423    }
424
425    pub fn release_text(
426        &self,
427        id: TextId,
428    ) -> Result<()> {
429        self.text.release(id)
430    }
431
432    pub fn is_text_live(
433        &self,
434        id: TextId,
435    ) -> bool {
436        self.text.is_live(id)
437    }
438
439    /// Take a free slot, stamp it with `T` and `initial`, and return the
440    /// handle. The id inside it is what other processes open.
441    pub fn allocate<T: CellType>(
442        &self,
443        initial: T,
444    ) -> Result<Orbital<T>> {
445        let bits = initial.to_bits();
446        let (id, lease) = self.with_structure(|slots, hint, owners| {
447            let capacity = slots.len();
448            let start = hint.load(Ordering::Relaxed) as usize;
449            let allocate = || -> Result<Option<(CellId, Arc<Lease>)>> {
450                for offset in 0..capacity {
451                    let index = (start + offset) & (capacity - 1);
452                    let slot = &slots[index];
453                    if matches!(
454                        slot.state.load(Ordering::Acquire),
455                        SLOT_EMPTY | SLOT_RETIRED
456                    ) && slot.generation.load(Ordering::Relaxed) != u32::MAX
457                        && !owners.pinned(index)
458                    {
459                        let lease = owners.lease(index)?;
460                        let generation = slot.install(T::TAG, bits);
461                        hint.store(
462                            ((index + 1) & (capacity - 1)) as u32,
463                            Ordering::Relaxed,
464                        );
465                        return Ok(Some((
466                            CellId { index: index as u32, generation },
467                            lease,
468                        )));
469                    }
470                }
471                Ok(None)
472            };
473            if let Some(allocated) = allocate()? {
474                return Ok(allocated);
475            }
476            if owners.reclaim_dead()?
477                && let Some(allocated) = allocate()?
478            {
479                return Ok(allocated);
480            }
481            Err(Error::Full { capacity })
482        })?;
483        Ok(Orbital {
484            cells: self.clone(),
485            id,
486            _type: PhantomData,
487            _lease: lease,
488        })
489    }
490
491    /// A handle to a live cell of type `T`.
492    pub fn open<T: CellType>(
493        &self,
494        id: CellId,
495    ) -> Result<Orbital<T>> {
496        let lease = self.with_structure(|slots, _, owners| {
497            let slot = slots
498                .get(id.index as usize)
499                .filter(|slot| slot.is(id))
500                .ok_or(Error::Stale(id))?;
501            let found = slot.tag.load(Ordering::Relaxed);
502            if found != T::TAG {
503                return Err(Error::TypeMismatch {
504                    id,
505                    expected: T::NAME,
506                    found: type_name(found),
507                });
508            }
509            owners.lease(id.index as usize)
510        })?;
511        Ok(Orbital {
512            cells: self.clone(),
513            id,
514            _type: PhantomData,
515            _lease: lease,
516        })
517    }
518
519    /// Retire this generation immediately. Existing handles go stale, but
520    /// keep its physical slot pinned until their last clone is dropped.
521    pub fn release(
522        &self,
523        id: CellId,
524    ) -> Result<()> {
525        self.with_structure(|slots, _, _| {
526            let slot = slots
527                .get(id.index as usize)
528                .filter(|slot| slot.is(id))
529                .ok_or(Error::Stale(id))?;
530            slot.state.store(SLOT_RETIRED, Ordering::Release);
531            // Whoever is parked on it finds the slot gone and answers Stale.
532            slot.changed();
533            Ok(())
534        })
535    }
536
537    /// Whether `id` names a live cell right now.
538    pub fn is_live(
539        &self,
540        id: CellId,
541    ) -> bool {
542        self.slot(id).is_ok()
543    }
544
545    /// Clear both tables during quiescent owner boot.
546    pub fn reset_all(&self) -> Result<()> {
547        self.with_structure(|slots, hint, _| {
548            for slot in slots {
549                slot.state.store(SLOT_RETIRED, Ordering::Release);
550                slot.changed();
551            }
552            hint.store(0, Ordering::Relaxed);
553            Ok(())
554        })?;
555        self.text.reset_all()
556    }
557
558    /// Remove the cell SHM object. Existing mappings remain valid until their
559    /// processes release them.
560    #[cfg(unix)]
561    pub fn unlink(&self) -> Result<()> {
562        match &self.backend {
563            CellBackend::InMemory(_) => self.reset_all()?,
564            CellBackend::Shm(table) => {
565                table.shared.region.unlink().map_err(Error::Io)?
566            }
567        }
568        self.text.unlink()
569    }
570
571    fn slots(&self) -> &[CellSlot] {
572        match &self.backend {
573            CellBackend::InMemory(table) => &table.slots,
574            #[cfg(unix)]
575            CellBackend::Shm(table) => table.slots(),
576        }
577    }
578
579    fn slot(
580        &self,
581        id: CellId,
582    ) -> Result<&CellSlot> {
583        self.slots()
584            .get(id.index as usize)
585            .filter(|slot| slot.is(id))
586            .ok_or(Error::Stale(id))
587    }
588
589    fn with_structure<T>(
590        &self,
591        operation: impl FnOnce(&[CellSlot], &AtomicU32, &Owners) -> Result<T>,
592    ) -> Result<T> {
593        match &self.backend {
594            CellBackend::InMemory(table) => {
595                let _local = lock_unpoisoned(&table.structural_lock);
596                operation(&table.slots, &table.hint, &table.owners)
597            }
598            #[cfg(unix)]
599            CellBackend::Shm(table) => table.with_structure(operation),
600        }
601    }
602}
603
604/// What [`Orbital::add_until`] did. A bounded add either takes some of what
605/// is left or finds nothing left; there is no third answer, and no value
606/// past the limit is ever written.
607#[derive(Clone, Copy, Debug, Eq, PartialEq)]
608pub enum Claim {
609    /// `taken` can be less than asked for: the last caller gets the
610    /// remainder rather than being refused, so no unit goes unclaimed.
611    Took { taken: i64, after: i64 },
612    /// The limit was already reached. Nothing was added, nothing is left.
613    Exhausted,
614}
615
616/// One cell, typed. Cheap to clone; every clone is the same place.
617#[derive(Clone)]
618pub struct Orbital<T: CellType> {
619    cells: Cells,
620    id: CellId,
621    _type: PhantomData<T>,
622    _lease: Arc<Lease>,
623}
624
625impl<T: CellType> Orbital<T> {
626    pub fn id(&self) -> CellId {
627        self.id
628    }
629
630    pub fn load(&self) -> Result<T> {
631        Ok(T::from_bits(self.slot()?.value.load(Ordering::Acquire)))
632    }
633
634    pub fn store(
635        &self,
636        value: T,
637    ) -> Result<()> {
638        let slot = self.slot()?;
639        slot.value.store(value.to_bits(), Ordering::Release);
640        slot.changed();
641        Ok(())
642    }
643
644    /// Replace the value and return what it was.
645    pub fn swap(
646        &self,
647        value: T,
648    ) -> Result<T> {
649        let slot = self.slot()?;
650        let previous = slot.value.swap(value.to_bits(), Ordering::AcqRel);
651        slot.changed();
652        Ok(T::from_bits(previous))
653    }
654
655    /// The write count, the thing [`Self::wait_changed`] waits on. Take it
656    /// before reading the value, and the wait misses nothing in between.
657    pub fn version(&self) -> Result<u32> {
658        Ok(self.slot()?.changes.load(Ordering::Acquire))
659    }
660
661    /// Park until the cell has been written since `since`, then return the
662    /// count now. Coalescing: ten writes while parked wake the caller once,
663    /// and [`Self::load`] gives the latest. A released cell wakes every waiter
664    /// with [`Error::Stale`]. Blocking; an async runtime wraps it.
665    pub fn wait_changed(
666        &self,
667        since: u32,
668    ) -> Result<u32> {
669        loop {
670            let slot = self.slot()?;
671            let now = slot.changes.load(Ordering::SeqCst);
672            if now != since {
673                return Ok(now);
674            }
675            slot.wake
676                .wait_if(|| slot.changes.load(Ordering::SeqCst) == since)?;
677        }
678    }
679
680    /// Store `new` only if the cell still holds `current`. `Ok(Ok(previous))`
681    /// on success, `Ok(Err(actual))` when it held something else.
682    pub fn compare_exchange(
683        &self,
684        current: T,
685        new: T,
686    ) -> Result<std::result::Result<T, T>> {
687        let slot = self.slot()?;
688        let exchanged = slot.value.compare_exchange(
689            current.to_bits(),
690            new.to_bits(),
691            Ordering::AcqRel,
692            Ordering::Acquire,
693        );
694        if exchanged.is_ok() {
695            slot.changed();
696        }
697        Ok(exchanged.map(T::from_bits).map_err(T::from_bits))
698    }
699
700    /// [`Self::wait_changed`], but a second word can end it. The waiter
701    /// still parks until something wakes it; `interrupt` is what tells it
702    /// which kind of wake arrived. `Ok(Some(count))` is a write,
703    /// `Ok(None)` is the interrupt.
704    ///
705    /// This is the shape a caller needs when the wait may outlive its
706    /// reason — a process that has to shut down while a script waits on a
707    /// cell nothing will write again. It is not a timeout: nothing here
708    /// measures time, and a wait nobody interrupts is
709    /// [`Self::wait_changed`] exactly.
710    ///
711    /// The canceller sets `interrupt` and then calls
712    /// [`Self::wake_waiters`], in that order. Set-then-wake is what closes
713    /// the window: the wake advances a separate parking generation before
714    /// waking. A waiter either sees the flag or cannot park on the old
715    /// generation. Neither the value nor its version is changed by cancellation.
716    pub fn wait_changed_until(
717        &self,
718        since: u32,
719        interrupt: &core::sync::atomic::AtomicBool,
720    ) -> Result<Option<u32>> {
721        loop {
722            if interrupt.load(Ordering::SeqCst) {
723                return Ok(None);
724            }
725
726            let slot = self.slot()?;
727            let now = slot.changes.load(Ordering::SeqCst);
728            if now != since {
729                return Ok(Some(now));
730            }
731
732            slot.wake.wait_if(|| {
733                slot.changes.load(Ordering::SeqCst) == since
734                    && !interrupt.load(Ordering::SeqCst)
735            })?;
736        }
737    }
738
739    /// Wake everyone parked on this cell without writing to it. On its own
740    /// this does not change the value version — a waiter re-checks it, finds
741    /// it where it was and parks again — so it is only useful after the word the
742    /// waiter also watches has been set. See [`Self::wait_changed_until`].
743    pub fn wake_waiters(&self) -> Result<()> {
744        self.slot()?.wake.notify();
745        Ok(())
746    }
747
748    /// Retire the generation. Other handles go stale immediately; the slot
749    /// becomes reusable only when the last old handle is dropped.
750    pub fn release(self) -> Result<()> {
751        self.cells.release(self.id)
752    }
753
754    fn slot(&self) -> Result<&CellSlot> {
755        self.cells.slot(self.id)
756    }
757}
758
759impl Orbital<i64> {
760    /// Add `by` (negative to subtract) and return the value after.
761    pub fn fetch_add(
762        &self,
763        by: i64,
764    ) -> Result<i64> {
765        let slot = self.slot()?;
766        slot.value
767            .try_update(Ordering::AcqRel, Ordering::Acquire, |bits| {
768                (bits as i64).checked_add(by).map(|value| value as u64)
769            })
770            .map(|previous| {
771                slot.changed();
772                (previous as i64) + by
773            })
774            .map_err(|_| Error::Overflow)
775    }
776
777    pub fn fetch_sub(
778        &self,
779        by: i64,
780    ) -> Result<i64> {
781        let slot = self.slot()?;
782        slot.value
783            .try_update(Ordering::AcqRel, Ordering::Acquire, |bits| {
784                (bits as i64).checked_sub(by).map(|value| value as u64)
785            })
786            .map(|previous| {
787                slot.changed();
788                (previous as i64) - by
789            })
790            .map_err(|_| Error::Overflow)
791    }
792
793    /// Add up to `by` while the value stays at or under `limit`, in one
794    /// compare-and-swap, and say what was taken.
795    ///
796    /// This is the operation a fleet needs to divide a fixed amount of work
797    /// between processes that cannot see each other. `fetch_add` followed by
798    /// a test cannot do it: the add lands before the caller learns it went
799    /// too far, so N callers overshoot by up to N-1 between them. Here the
800    /// bound is decided inside the swap, so the value **never** passes
801    /// `limit` however many callers race, and the one that finds less than
802    /// `by` left takes the remainder instead of being turned away.
803    ///
804    /// Taking more than one at a time is how the cost comes down: the
805    /// contended word is touched once per claim rather than once per unit.
806    /// `limit` is the ceiling for the total, never for a single call.
807    ///
808    /// The positive distance to the ceiling may exceed `i64::MAX`. Clamping
809    /// that distance is safe because `by` itself cannot exceed `i64::MAX`;
810    /// the chosen increment and result remain representable.
811    ///
812    /// **A claim is not a lease.** What this hands back is gone from the
813    /// total the moment the swap lands, and nothing gives it back: a caller
814    /// that takes a hundred, does forty and then dies leaves sixty that
815    /// nobody will ever do, while the count says all hundred were taken. So
816    /// `by` is not only a speed dial, it is the exposure — the most one
817    /// death can lose. It is the same trade a database sequence makes with
818    /// its cache size, and it leaves the same kind of gap. Work that must
819    /// not be lost wants a lease whose owner notices a death, which is
820    /// `orbit-pool`, not a counter.
821    pub fn add_until(
822        &self,
823        by: i64,
824        limit: i64,
825    ) -> Result<Claim> {
826        if by <= 0 {
827            return Err(Error::Overflow);
828        }
829        let slot = self.slot()?;
830        loop {
831            let current = slot.value.load(Ordering::Acquire) as i64;
832            if current >= limit {
833                return Ok(Claim::Exhausted);
834            }
835            let taken = by.min(limit.saturating_sub(current));
836            let after = current + taken;
837            if slot
838                .value
839                .compare_exchange(
840                    current as u64,
841                    after as u64,
842                    Ordering::AcqRel,
843                    Ordering::Acquire,
844                )
845                .is_ok()
846            {
847                slot.changed();
848                return Ok(Claim::Took { taken, after });
849            }
850            // Lost the race; somebody else moved the value. Read it again
851            // rather than retry blind: what is left may now be less, or
852            // nothing.
853        }
854    }
855}
856
857impl Orbital<u64> {
858    pub fn fetch_add(
859        &self,
860        by: u64,
861    ) -> Result<u64> {
862        let slot = self.slot()?;
863        slot.value
864            .try_update(Ordering::AcqRel, Ordering::Acquire, |bits| {
865                bits.checked_add(by)
866            })
867            .map(|previous| {
868                slot.changed();
869                previous + by
870            })
871            .map_err(|_| Error::Overflow)
872    }
873
874    pub fn fetch_sub(
875        &self,
876        by: u64,
877    ) -> Result<u64> {
878        let slot = self.slot()?;
879        slot.value
880            .try_update(Ordering::AcqRel, Ordering::Acquire, |bits| {
881                bits.checked_sub(by)
882            })
883            .map(|previous| {
884                slot.changed();
885                previous - by
886            })
887            .map_err(|_| Error::Overflow)
888    }
889}
890
891impl Orbital<f64> {
892    /// Add `by` and return the value after. A compare-and-swap loop: floats
893    /// have no fetch-add of their own.
894    pub fn fetch_add(
895        &self,
896        by: f64,
897    ) -> Result<f64> {
898        let slot = self.slot()?;
899        let previous = slot
900            .value
901            .try_update(Ordering::AcqRel, Ordering::Acquire, |bits| {
902                Some((f64::from_bits(bits) + by).to_bits())
903            })
904            .unwrap_or_else(|bits| bits);
905        slot.changed();
906        Ok(f64::from_bits(previous) + by)
907    }
908}
909
910impl Orbital<bool> {
911    /// Set the flag and return whether it was already set.
912    pub fn set(&self) -> Result<bool> {
913        self.swap(true)
914    }
915
916    /// Clear the flag and return whether it was set.
917    pub fn clear(&self) -> Result<bool> {
918        self.swap(false)
919    }
920}
921
922#[repr(C, align(64))]
923struct CellStateHeader {
924    #[cfg(unix)]
925    magic: u32,
926    #[cfg(not(unix))]
927    _magic: u32,
928    #[cfg(unix)]
929    version: u16,
930    #[cfg(not(unix))]
931    _version: u16,
932    header_size: u16,
933    capacity: u32,
934    slot_size: u32,
935    /// Where the next allocation starts looking; a hint, never a promise.
936    hint: AtomicU32,
937    owners: u32,
938    _reserved: [u8; 40],
939}
940
941impl CellStateHeader {
942    fn new(owners: usize) -> Self {
943        Self {
944            #[cfg(unix)]
945            magic: STATE_MAGIC,
946            #[cfg(not(unix))]
947            _magic: 0,
948            #[cfg(unix)]
949            version: STATE_VERSION,
950            #[cfg(not(unix))]
951            _version: 0,
952            header_size: size_of::<Self>() as u16,
953            capacity: CELL_CAPACITY as u32,
954            slot_size: size_of::<CellSlot>() as u32,
955            hint: AtomicU32::new(0),
956            owners: owners as u32,
957            _reserved: [0; 40],
958        }
959    }
960}
961
962/// One cell, laid out for shared memory: a cache line of its own, so two hot
963/// cells never contend, and every field atomic, so the mapping is never
964/// borrowed `&mut` while another process reads it.
965#[repr(C, align(64))]
966struct CellSlot {
967    state: AtomicU8,
968    tag: AtomicU8,
969    _reserved: [u8; 2],
970    generation: AtomicU32,
971    value: AtomicU64,
972    /// Public value version, bumped by every write and release. Native
973    /// parking uses the separate sequence in `wake`.
974    changes: AtomicU32,
975    /// Native parking sequence and waiter count, separate from `changes`.
976    wake: Wake,
977    _padding: [u8; 36],
978}
979
980impl CellSlot {
981    fn empty() -> Self {
982        Self {
983            state: AtomicU8::new(SLOT_EMPTY),
984            tag: AtomicU8::new(0),
985            _reserved: [0; 2],
986            generation: AtomicU32::new(0),
987            value: AtomicU64::new(0),
988            changes: AtomicU32::new(0),
989            wake: Wake::new(),
990            _padding: [0; 36],
991        }
992    }
993
994    /// After a write: count it, and wake whoever is waiting for one. `SeqCst`
995    /// on both sides of the exchange with [`Orbital::wait_changed`], so a
996    /// waiter that announced itself just before this write is either seen
997    /// here or sees the new count itself; never neither.
998    fn changed(&self) {
999        self.changes.fetch_add(1, Ordering::SeqCst);
1000        self.wake.changed();
1001    }
1002
1003    fn is(
1004        &self,
1005        id: CellId,
1006    ) -> bool {
1007        // `Acquire` on `state` publishes the generation written by `install`.
1008        self.state.load(Ordering::Acquire) == SLOT_OCCUPIED
1009            && self.generation.load(Ordering::Relaxed) == id.generation
1010    }
1011
1012    /// Under the structural lock. Returns the generation the new cell lives in;
1013    /// it is bumped on every install, so no released id ever matches again.
1014    fn install(
1015        &self,
1016        tag: u8,
1017        value: u64,
1018    ) -> u32 {
1019        let generation = self
1020            .generation
1021            .load(Ordering::Relaxed)
1022            .checked_add(1)
1023            .expect("exhausted generations are never reused");
1024        self.tag.store(tag, Ordering::Relaxed);
1025        self.value.store(value, Ordering::Relaxed);
1026        self.changes.store(0, Ordering::Relaxed);
1027        self.wake.reset();
1028        self.generation.store(generation, Ordering::Relaxed);
1029        self.state.store(SLOT_OCCUPIED, Ordering::Release);
1030        generation
1031    }
1032}
1033
1034pub(crate) fn lock_unpoisoned<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
1035    mutex.lock().unwrap_or_else(|error| error.into_inner())
1036}
1037
1038struct MemoryCellTable {
1039    /// Held so the fleet's address, which keys the registry, cannot be
1040    /// reused by another fleet while this table lives.
1041    _fleet: Arc<Fleet>,
1042    slots: Vec<CellSlot>,
1043    hint: AtomicU32,
1044    structural_lock: Mutex<()>,
1045    owners: Owners,
1046}
1047
1048impl MemoryCellTable {
1049    fn new(fleet: Arc<Fleet>) -> Self {
1050        Self {
1051            _fleet: fleet,
1052            slots: (0..CELL_CAPACITY).map(|_| CellSlot::empty()).collect(),
1053            hint: AtomicU32::new(0),
1054            structural_lock: Mutex::new(()),
1055            owners: Owners::memory(CELL_CAPACITY),
1056        }
1057    }
1058}
1059
1060type MemoryRegistry = HashMap<usize, Weak<MemoryCellTable>>;
1061
1062static MEMORY_TABLES: LazyLock<Mutex<MemoryRegistry>> =
1063    LazyLock::new(|| Mutex::new(HashMap::new()));
1064
1065fn memory_table(fleet: &Arc<Fleet>) -> Arc<MemoryCellTable> {
1066    let fleet_identity = Arc::as_ptr(fleet) as usize;
1067    let mut tables = lock_unpoisoned(&MEMORY_TABLES);
1068    tables.retain(|_, table| table.strong_count() > 0);
1069    if let Some(table) = tables.get(&fleet_identity).and_then(Weak::upgrade) {
1070        return table;
1071    }
1072    let table = Arc::new(MemoryCellTable::new(Arc::clone(fleet)));
1073    tables.insert(fleet_identity, Arc::downgrade(&table));
1074    table
1075}
1076
1077#[cfg(unix)]
1078struct ShmCellTable {
1079    shared: Arc<lifetime::Shared>,
1080    structural_lock: Mutex<()>,
1081}
1082
1083#[cfg(unix)]
1084impl ShmCellTable {
1085    fn open_or_create(
1086        name: &str,
1087        owners: usize,
1088    ) -> Result<Self> {
1089        use std::{io, ptr};
1090
1091        let (region, _initialization_lock) =
1092            ShmRegion::open_or_create_locked(name, shm_segment_size(owners)?)?;
1093        if region.created() {
1094            unsafe {
1095                ptr::write(
1096                    region.as_ptr().cast::<CellStateHeader>(),
1097                    CellStateHeader::new(owners),
1098                );
1099                let slots = region.as_ptr().add(size_of::<CellStateHeader>());
1100                ptr::write_bytes(
1101                    slots,
1102                    0,
1103                    shm_segment_size(owners)? - size_of::<CellStateHeader>(),
1104                );
1105            }
1106        } else {
1107            let header = unsafe { &*region.as_ptr().cast::<CellStateHeader>() };
1108            if header.magic != STATE_MAGIC {
1109                return Err(Error::Io(io::Error::new(
1110                    io::ErrorKind::InvalidData,
1111                    format!(
1112                        "SHM segment {name} has wrong magic 0x{:08X} (expected 0x{STATE_MAGIC:08X})",
1113                        header.magic
1114                    ),
1115                )));
1116            }
1117            if header.version != STATE_VERSION
1118                || usize::from(header.header_size)
1119                    != size_of::<CellStateHeader>()
1120                || header.capacity as usize != CELL_CAPACITY
1121                || header.slot_size as usize != size_of::<CellSlot>()
1122                || header.owners as usize != owners
1123            {
1124                return Err(Error::Io(io::Error::new(
1125                    io::ErrorKind::InvalidData,
1126                    format!(
1127                        "SHM segment {name} has an incompatible cell-state layout"
1128                    ),
1129                )));
1130            }
1131        }
1132        let shared = lifetime::Shared::claim_locked(
1133            region,
1134            name,
1135            owners,
1136            CELL_CAPACITY,
1137            owner_offset(),
1138        )?;
1139        Ok(Self { shared, structural_lock: Mutex::new(()) })
1140    }
1141
1142    fn header(&self) -> &CellStateHeader {
1143        unsafe { &*self.shared.region.as_ptr().cast::<CellStateHeader>() }
1144    }
1145
1146    fn slots(&self) -> &[CellSlot] {
1147        unsafe {
1148            std::slice::from_raw_parts(
1149                self.shared
1150                    .region
1151                    .as_ptr()
1152                    .add(size_of::<CellStateHeader>())
1153                    .cast::<CellSlot>(),
1154                CELL_CAPACITY,
1155            )
1156        }
1157    }
1158
1159    fn with_structure<T>(
1160        &self,
1161        operation: impl FnOnce(&[CellSlot], &AtomicU32, &Owners) -> Result<T>,
1162    ) -> Result<T> {
1163        let owners = Owners::Shared(self.shared.clone());
1164        owners.check_process()?;
1165        let _local = lock_unpoisoned(&self.structural_lock);
1166        let _process = self.shared.region.lock_exclusive()?;
1167        operation(self.slots(), &self.header().hint, &owners)
1168    }
1169}
1170
1171#[cfg(unix)]
1172fn owner_offset() -> usize {
1173    size_of::<CellStateHeader>() + CELL_CAPACITY * size_of::<CellSlot>()
1174}
1175
1176#[cfg(unix)]
1177fn shm_segment_size(owners: usize) -> Result<usize> {
1178    lifetime::segment_size(owner_offset(), CELL_CAPACITY, owners)
1179}
1180
1181#[cfg(unix)]
1182fn shm_table(
1183    name: &str,
1184    owners: usize,
1185) -> Result<Arc<ShmCellTable>> {
1186    static TABLES: LazyLock<Mutex<HashMap<String, Weak<ShmCellTable>>>> =
1187        LazyLock::new(|| Mutex::new(HashMap::new()));
1188    let mut tables = lock_unpoisoned(&TABLES);
1189    tables.retain(|_, table| table.strong_count() != 0);
1190    if let Some(table) = tables.get(name).and_then(Weak::upgrade)
1191        && table.shared.is_current(owners)
1192    {
1193        return Ok(table);
1194    }
1195    let table = Arc::new(ShmCellTable::open_or_create(name, owners)?);
1196    tables.insert(name.to_owned(), Arc::downgrade(&table));
1197    Ok(table)
1198}
1199
1200const _: () = assert!(CELL_CAPACITY.is_power_of_two());
1201const _: () = assert!(CELL_CAPACITY <= u32::MAX as usize);
1202const _: () = assert!(size_of::<CellStateHeader>() == 64);
1203const _: () = assert!(size_of::<CellSlot>() == 64);
1204
1205#[cfg(test)]
1206mod registry_tests {
1207    use std::sync::Arc;
1208
1209    use orbit_core::Fleet;
1210
1211    use super::Cells;
1212
1213    /// The in-memory registry keys tables by the fleet's address. The
1214    /// table therefore keeps the fleet alive: otherwise a dropped fleet's
1215    /// address could be reused by a new one, which would then find and
1216    /// share the old table.
1217    #[test]
1218    fn a_memory_table_keeps_its_fleet_alive() {
1219        let fleet = Arc::new(Fleet::join("cell-registry-test", 1).unwrap());
1220        let cells = Cells::new(Arc::clone(&fleet)).unwrap();
1221        assert!(Arc::strong_count(&fleet) > 1);
1222        drop(cells);
1223        assert_eq!(Arc::strong_count(&fleet), 1);
1224    }
1225}
1226
1227#[cfg(test)]
1228mod lifetime_tests {
1229    use super::*;
1230
1231    #[test]
1232    fn a_retired_cell_keeps_old_waiter_accounting_until_its_handles_drop() {
1233        let cells =
1234            Cells::new(Arc::new(Fleet::join("example", 1).unwrap())).unwrap();
1235        let old = cells.allocate(7_u64).unwrap();
1236        let id = old.id();
1237        let held: Vec<_> = (1..CELL_CAPACITY)
1238            .map(|_| cells.allocate(0_u64).unwrap())
1239            .collect();
1240        let slot = old.slot().unwrap();
1241        slot.wake.waiters.fetch_add(1, Ordering::SeqCst);
1242        cells.release(id).unwrap();
1243        assert!(matches!(cells.allocate(42_u64), Err(Error::Full { .. })));
1244        assert_eq!(slot.wake.waiters.fetch_sub(1, Ordering::SeqCst), 1);
1245        drop(old);
1246        let fresh = cells.allocate(42_u64).unwrap();
1247        assert_eq!(fresh.id().index(), id.index());
1248        assert_eq!(
1249            fresh.slot().unwrap().wake.waiters.load(Ordering::Acquire),
1250            0
1251        );
1252        assert_eq!(fresh.load().unwrap(), 42);
1253        drop(held);
1254    }
1255
1256    #[test]
1257    fn exhausted_cell_generations_are_never_reissued() {
1258        let cells =
1259            Cells::new(Arc::new(Fleet::join("example", 1).unwrap())).unwrap();
1260        cells
1261            .with_structure(|slots, _, _| {
1262                for slot in slots {
1263                    slot.generation.store(u32::MAX, Ordering::Relaxed);
1264                }
1265                Ok(())
1266            })
1267            .unwrap();
1268        assert!(matches!(cells.allocate(1_u64), Err(Error::Full { .. })));
1269    }
1270}
1271
1272#[cfg(test)]
1273mod wait_tests {
1274    use std::sync::Arc;
1275    use std::time::Duration;
1276
1277    use orbit_core::Fleet;
1278
1279    use super::{Cells, Error};
1280
1281    fn cells() -> Cells {
1282        Cells::new(Arc::new(Fleet::join("cell-wait-test", 1).unwrap())).unwrap()
1283    }
1284
1285    /// A waiter parks on the change count and a write from another thread
1286    /// wakes it; it then reads the value the write left. Ten writes while
1287    /// parked are one wake.
1288    #[test]
1289    fn a_write_wakes_a_waiter_once() {
1290        let cells = cells();
1291        let cell = cells.allocate(0_i64).unwrap();
1292        let since = cell.version().unwrap();
1293        let watcher = cell.clone();
1294        let waiter = std::thread::spawn(move || watcher.wait_changed(since));
1295
1296        std::thread::sleep(Duration::from_millis(30));
1297        for _ in 0..10 {
1298            cell.fetch_add(1).unwrap();
1299        }
1300        let now = waiter.join().unwrap().unwrap();
1301        assert!(now > since);
1302        assert_eq!(cell.load().unwrap(), 10);
1303        // Nothing new since: a wait would park; the count says so.
1304        assert_eq!(cell.version().unwrap(), cell.version().unwrap());
1305    }
1306
1307    /// Releasing the cell is a change too: a parked waiter comes back Stale
1308    /// instead of sleeping on a slot somebody else may take.
1309    #[test]
1310    fn release_wakes_a_waiter_stale() {
1311        let cells = cells();
1312        let cell = cells.allocate(7_i64).unwrap();
1313        let since = cell.version().unwrap();
1314        let watcher = cell.clone();
1315        let waiter = std::thread::spawn(move || watcher.wait_changed(since));
1316
1317        std::thread::sleep(Duration::from_millis(30));
1318        cell.release().unwrap();
1319        assert!(matches!(waiter.join().unwrap(), Err(Error::Stale(_))));
1320    }
1321}