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