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#[cfg(unix)]
44const STATE_MAGIC: u32 = 0x43_45_4C_4C; // "CELL"
45#[cfg(unix)]
46const STATE_VERSION: u16 = 1;
47
48pub type Result<T, E = Error> = std::result::Result<T, E>;
49
50#[derive(Debug)]
51pub enum Error {
52    /// The id names a slot nothing occupies, or a generation that has been
53    /// released since the id was minted.
54    Stale(CellId),
55    /// The same, for a text cell.
56    StaleText(TextId),
57    /// The text would not fit the cell; nothing was written.
58    TooLong {
59        len: usize,
60        max: usize,
61    },
62    /// A text cell held bytes that are not UTF-8: something wrote past the
63    /// contract.
64    Corrupt(String),
65    /// The id names a live cell of another type.
66    TypeMismatch {
67        id: CellId,
68        expected: &'static str,
69        found: &'static str,
70    },
71    /// The update would leave the integer range.
72    Overflow,
73    /// Every slot is occupied.
74    Full {
75        capacity: usize,
76    },
77    /// The text is not a cell id.
78    Malformed(String),
79    Io(std::io::Error),
80}
81
82impl fmt::Display for Error {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        match self {
85            Self::Stale(id) => write!(f, "cell {id} has been released"),
86            Self::StaleText(id) => write!(f, "text cell {id} has been released"),
87            Self::TooLong { len, max } => {
88                write!(f, "text does not fit the cell: len={len} max={max}")
89            }
90            Self::Corrupt(id) => write!(f, "text cell {id} holds bytes that are not UTF-8"),
91            Self::TypeMismatch {
92                id,
93                expected,
94                found,
95            } => write!(f, "cell {id} holds {found}, not {expected}"),
96            Self::Overflow => f.write_str("cell value is outside the integer range"),
97            Self::Full { capacity } => write!(f, "cell table is full: capacity={capacity}"),
98            Self::Malformed(text) => write!(f, "not a cell id: {text:?}"),
99            Self::Io(error) => write!(f, "Orbit cell io error: {error}"),
100        }
101    }
102}
103
104impl std::error::Error for Error {
105    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
106        match self {
107            Self::Io(error) => Some(error),
108            _ => None,
109        }
110    }
111}
112
113impl From<std::io::Error> for Error {
114    fn from(value: std::io::Error) -> Self {
115        Self::Io(value)
116    }
117}
118
119/// The address of one cell: its slot and the generation it was allocated in.
120///
121/// Prints as `cell:<slot>:<generation>` and parses back, which is the form
122/// meant to cross a serialization boundary.
123#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
124pub struct CellId {
125    index: u32,
126    generation: u32,
127}
128
129impl CellId {
130    pub const fn index(self) -> u32 {
131        self.index
132    }
133
134    pub const fn generation(self) -> u32 {
135        self.generation
136    }
137
138    /// The two halves in one integer, slot high, generation low.
139    pub const fn to_bits(self) -> u64 {
140        ((self.index as u64) << 32) | self.generation as u64
141    }
142
143    pub const fn from_bits(bits: u64) -> Self {
144        Self {
145            index: (bits >> 32) as u32,
146            generation: bits as u32,
147        }
148    }
149}
150
151impl fmt::Display for CellId {
152    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153        write!(f, "cell:{}:{}", self.index, self.generation)
154    }
155}
156
157impl FromStr for CellId {
158    type Err = Error;
159
160    fn from_str(text: &str) -> Result<Self> {
161        let malformed = || Error::Malformed(text.to_owned());
162        let rest = text.strip_prefix("cell:").ok_or_else(malformed)?;
163        let (index, generation) = rest.split_once(':').ok_or_else(malformed)?;
164        Ok(Self {
165            index: index.parse().map_err(|_| malformed())?,
166            generation: generation.parse().map_err(|_| malformed())?,
167        })
168    }
169}
170
171/// A value that fits one cell: 64 bits, and a tag that says which type they
172/// spell.
173pub trait CellType: Copy + Send + Sync + 'static {
174    const TAG: u8;
175    const NAME: &'static str;
176
177    fn to_bits(self) -> u64;
178    fn from_bits(bits: u64) -> Self;
179}
180
181impl CellType for i64 {
182    const TAG: u8 = 1;
183    const NAME: &'static str = "int";
184
185    fn to_bits(self) -> u64 {
186        self as u64
187    }
188
189    fn from_bits(bits: u64) -> Self {
190        bits as i64
191    }
192}
193
194impl CellType for u64 {
195    const TAG: u8 = 2;
196    const NAME: &'static str = "unsigned";
197
198    fn to_bits(self) -> u64 {
199        self
200    }
201
202    fn from_bits(bits: u64) -> Self {
203        bits
204    }
205}
206
207impl CellType for f64 {
208    const TAG: u8 = 3;
209    const NAME: &'static str = "float";
210
211    fn to_bits(self) -> u64 {
212        self.to_bits()
213    }
214
215    fn from_bits(bits: u64) -> Self {
216        f64::from_bits(bits)
217    }
218}
219
220impl CellType for bool {
221    const TAG: u8 = 4;
222    const NAME: &'static str = "bool";
223
224    fn to_bits(self) -> u64 {
225        u64::from(self)
226    }
227
228    fn from_bits(bits: u64) -> Self {
229        bits != 0
230    }
231}
232
233fn type_name(tag: u8) -> &'static str {
234    match tag {
235        1 => i64::NAME,
236        2 => u64::NAME,
237        3 => f64::NAME,
238        4 => bool::NAME,
239        _ => "unknown",
240    }
241}
242
243/// The fleet's cell tables, scalar and text. Cheap to clone.
244#[derive(Clone)]
245pub struct Cells {
246    backend: CellBackend,
247    text: text::TextBackend,
248}
249
250#[derive(Clone)]
251enum CellBackend {
252    InMemory(Arc<MemoryCellTable>),
253    #[cfg(unix)]
254    Shm(Arc<ShmCellTable>),
255}
256
257impl Cells {
258    pub fn new(fleet: Arc<Fleet>) -> Result<Self> {
259        let backend = if fleet.is_shm() {
260            #[cfg(unix)]
261            {
262                CellBackend::Shm(Arc::new(ShmCellTable::open_or_create(&ring_segment_name(
263                    fleet.name(),
264                    CELL_STATE_KIND,
265                ))?))
266            }
267            #[cfg(not(unix))]
268            unreachable!("non-Unix fleets cannot use POSIX SHM")
269        } else {
270            CellBackend::InMemory(memory_table(&fleet))
271        };
272        let text = text::TextBackend::new(&fleet)?;
273        Ok(Self { backend, text })
274    }
275
276    /// Take a text cell holding `initial`; up to [`CELL_TEXT_MAX`] bytes.
277    pub fn allocate_text(&self, initial: &str) -> Result<Text> {
278        self.text.allocate(initial)
279    }
280
281    /// A handle to a live text cell.
282    pub fn open_text(&self, id: TextId) -> Result<Text> {
283        self.text.open(id)
284    }
285
286    pub fn release_text(&self, id: TextId) -> Result<()> {
287        self.text.release(id)
288    }
289
290    pub fn is_text_live(&self, id: TextId) -> bool {
291        self.text.is_live(id)
292    }
293
294    /// Take a free slot, stamp it with `T` and `initial`, and return the
295    /// handle. The id inside it is what other processes open.
296    pub fn allocate<T: CellType>(&self, initial: T) -> Result<Orbital<T>> {
297        let id = self.with_structure(|slots, hint| {
298            let capacity = slots.len();
299            let start = hint.load(Ordering::Relaxed) as usize;
300            for offset in 0..capacity {
301                let index = (start + offset) & (capacity - 1);
302                let slot = &slots[index];
303                if slot.state.load(Ordering::Acquire) == SLOT_EMPTY {
304                    let generation = slot.install(T::TAG, initial.to_bits());
305                    hint.store(((index + 1) & (capacity - 1)) as u32, Ordering::Relaxed);
306                    return Ok(CellId {
307                        index: index as u32,
308                        generation,
309                    });
310                }
311            }
312            Err(Error::Full { capacity })
313        })?;
314        Ok(Orbital {
315            cells: self.clone(),
316            id,
317            _type: PhantomData,
318        })
319    }
320
321    /// A handle to a live cell of type `T`.
322    pub fn open<T: CellType>(&self, id: CellId) -> Result<Orbital<T>> {
323        let slot = self.slot(id)?;
324        let found = slot.tag.load(Ordering::Relaxed);
325        if found != T::TAG {
326            return Err(Error::TypeMismatch {
327                id,
328                expected: T::NAME,
329                found: type_name(found),
330            });
331        }
332        Ok(Orbital {
333            cells: self.clone(),
334            id,
335            _type: PhantomData,
336        })
337    }
338
339    /// Give the slot back. Every handle to this generation goes stale.
340    pub fn release(&self, id: CellId) -> Result<()> {
341        self.with_structure(|slots, _| {
342            let slot = slots
343                .get(id.index as usize)
344                .filter(|slot| slot.is(id))
345                .ok_or(Error::Stale(id))?;
346            slot.state.store(SLOT_EMPTY, Ordering::Release);
347            Ok(())
348        })
349    }
350
351    /// Whether `id` names a live cell right now.
352    pub fn is_live(&self, id: CellId) -> bool {
353        self.slot(id).is_ok()
354    }
355
356    /// Clear both tables during quiescent owner boot.
357    pub fn reset_all(&self) -> Result<()> {
358        self.with_structure(|slots, hint| {
359            for slot in slots {
360                slot.state.store(SLOT_EMPTY, Ordering::Release);
361            }
362            hint.store(0, Ordering::Relaxed);
363            Ok(())
364        })?;
365        self.text.reset_all()
366    }
367
368    /// Remove the cell SHM object. Existing mappings remain valid until their
369    /// processes release them.
370    #[cfg(unix)]
371    pub fn unlink(&self) -> Result<()> {
372        match &self.backend {
373            CellBackend::InMemory(_) => self.reset_all()?,
374            CellBackend::Shm(table) => table.region.unlink().map_err(Error::Io)?,
375        }
376        self.text.unlink()
377    }
378
379    fn slots(&self) -> &[CellSlot] {
380        match &self.backend {
381            CellBackend::InMemory(table) => &table.slots,
382            #[cfg(unix)]
383            CellBackend::Shm(table) => table.slots(),
384        }
385    }
386
387    fn slot(&self, id: CellId) -> Result<&CellSlot> {
388        self.slots()
389            .get(id.index as usize)
390            .filter(|slot| slot.is(id))
391            .ok_or(Error::Stale(id))
392    }
393
394    fn with_structure<T>(
395        &self,
396        operation: impl FnOnce(&[CellSlot], &AtomicU32) -> Result<T>,
397    ) -> Result<T> {
398        match &self.backend {
399            CellBackend::InMemory(table) => {
400                let _local = lock_unpoisoned(&table.structural_lock);
401                operation(&table.slots, &table.hint)
402            }
403            #[cfg(unix)]
404            CellBackend::Shm(table) => table.with_structure(operation),
405        }
406    }
407}
408
409/// One cell, typed. Cheap to clone; every clone is the same place.
410#[derive(Clone)]
411pub struct Orbital<T: CellType> {
412    cells: Cells,
413    id: CellId,
414    _type: PhantomData<T>,
415}
416
417impl<T: CellType> Orbital<T> {
418    pub fn id(&self) -> CellId {
419        self.id
420    }
421
422    pub fn load(&self) -> Result<T> {
423        Ok(T::from_bits(self.slot()?.value.load(Ordering::Acquire)))
424    }
425
426    pub fn store(&self, value: T) -> Result<()> {
427        self.slot()?.value.store(value.to_bits(), Ordering::Release);
428        Ok(())
429    }
430
431    /// Replace the value and return what it was.
432    pub fn swap(&self, value: T) -> Result<T> {
433        Ok(T::from_bits(
434            self.slot()?.value.swap(value.to_bits(), Ordering::AcqRel),
435        ))
436    }
437
438    /// Store `new` only if the cell still holds `current`. `Ok(Ok(previous))`
439    /// on success, `Ok(Err(actual))` when it held something else.
440    pub fn compare_exchange(&self, current: T, new: T) -> Result<std::result::Result<T, T>> {
441        Ok(self
442            .slot()?
443            .value
444            .compare_exchange(
445                current.to_bits(),
446                new.to_bits(),
447                Ordering::AcqRel,
448                Ordering::Acquire,
449            )
450            .map(T::from_bits)
451            .map_err(T::from_bits))
452    }
453
454    /// Give the cell back; this and every other handle to it go stale.
455    pub fn release(self) -> Result<()> {
456        self.cells.release(self.id)
457    }
458
459    fn slot(&self) -> Result<&CellSlot> {
460        self.cells.slot(self.id)
461    }
462}
463
464impl Orbital<i64> {
465    /// Add `by` (negative to subtract) and return the value after.
466    pub fn fetch_add(&self, by: i64) -> Result<i64> {
467        let slot = self.slot()?;
468        slot.value
469            .try_update(Ordering::AcqRel, Ordering::Acquire, |bits| {
470                (bits as i64).checked_add(by).map(|value| value as u64)
471            })
472            .map(|previous| (previous as i64) + by)
473            .map_err(|_| Error::Overflow)
474    }
475
476    pub fn fetch_sub(&self, by: i64) -> Result<i64> {
477        self.fetch_add(by.checked_neg().ok_or(Error::Overflow)?)
478    }
479}
480
481impl Orbital<u64> {
482    pub fn fetch_add(&self, by: u64) -> Result<u64> {
483        let slot = self.slot()?;
484        slot.value
485            .try_update(Ordering::AcqRel, Ordering::Acquire, |bits| {
486                bits.checked_add(by)
487            })
488            .map(|previous| previous + by)
489            .map_err(|_| Error::Overflow)
490    }
491
492    pub fn fetch_sub(&self, by: u64) -> Result<u64> {
493        let slot = self.slot()?;
494        slot.value
495            .try_update(Ordering::AcqRel, Ordering::Acquire, |bits| {
496                bits.checked_sub(by)
497            })
498            .map(|previous| previous - by)
499            .map_err(|_| Error::Overflow)
500    }
501}
502
503impl Orbital<f64> {
504    /// Add `by` and return the value after. A compare-and-swap loop: floats
505    /// have no fetch-add of their own.
506    pub fn fetch_add(&self, by: f64) -> Result<f64> {
507        let slot = self.slot()?;
508        let previous = slot
509            .value
510            .fetch_update(Ordering::AcqRel, Ordering::Acquire, |bits| {
511                Some((f64::from_bits(bits) + by).to_bits())
512            })
513            .unwrap_or_else(|bits| bits);
514        Ok(f64::from_bits(previous) + by)
515    }
516}
517
518impl Orbital<bool> {
519    /// Set the flag and return whether it was already set.
520    pub fn set(&self) -> Result<bool> {
521        self.swap(true)
522    }
523
524    /// Clear the flag and return whether it was set.
525    pub fn clear(&self) -> Result<bool> {
526        self.swap(false)
527    }
528}
529
530#[repr(C, align(64))]
531struct CellStateHeader {
532    #[cfg(unix)]
533    magic: u32,
534    #[cfg(not(unix))]
535    _magic: u32,
536    #[cfg(unix)]
537    version: u16,
538    #[cfg(not(unix))]
539    _version: u16,
540    header_size: u16,
541    capacity: u32,
542    slot_size: u32,
543    /// Where the next allocation starts looking; a hint, never a promise.
544    hint: AtomicU32,
545    _reserved: [u8; 44],
546}
547
548impl CellStateHeader {
549    fn new() -> Self {
550        Self {
551            #[cfg(unix)]
552            magic: STATE_MAGIC,
553            #[cfg(not(unix))]
554            _magic: 0,
555            #[cfg(unix)]
556            version: STATE_VERSION,
557            #[cfg(not(unix))]
558            _version: 0,
559            header_size: size_of::<Self>() as u16,
560            capacity: CELL_CAPACITY as u32,
561            slot_size: size_of::<CellSlot>() as u32,
562            hint: AtomicU32::new(0),
563            _reserved: [0; 44],
564        }
565    }
566}
567
568/// One cell, laid out for shared memory: a cache line of its own, so two hot
569/// cells never contend, and every field atomic, so the mapping is never
570/// borrowed `&mut` while another process reads it.
571#[repr(C, align(64))]
572struct CellSlot {
573    state: AtomicU8,
574    tag: AtomicU8,
575    _reserved: [u8; 2],
576    generation: AtomicU32,
577    value: AtomicU64,
578    _padding: [u8; 48],
579}
580
581impl CellSlot {
582    fn empty() -> Self {
583        Self {
584            state: AtomicU8::new(SLOT_EMPTY),
585            tag: AtomicU8::new(0),
586            _reserved: [0; 2],
587            generation: AtomicU32::new(0),
588            value: AtomicU64::new(0),
589            _padding: [0; 48],
590        }
591    }
592
593    fn is(&self, id: CellId) -> bool {
594        // `Acquire` on `state` publishes the generation written by `install`.
595        self.state.load(Ordering::Acquire) == SLOT_OCCUPIED
596            && self.generation.load(Ordering::Relaxed) == id.generation
597    }
598
599    /// Under the structural lock. Returns the generation the new cell lives in;
600    /// it is bumped on every install, so no released id ever matches again.
601    fn install(&self, tag: u8, value: u64) -> u32 {
602        let generation = self
603            .generation
604            .load(Ordering::Relaxed)
605            .wrapping_add(1)
606            .max(1);
607        self.tag.store(tag, Ordering::Relaxed);
608        self.value.store(value, Ordering::Relaxed);
609        self.generation.store(generation, Ordering::Relaxed);
610        self.state.store(SLOT_OCCUPIED, Ordering::Release);
611        generation
612    }
613}
614
615pub(crate) fn lock_unpoisoned<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
616    mutex.lock().unwrap_or_else(|error| error.into_inner())
617}
618
619struct MemoryCellTable {
620    slots: Vec<CellSlot>,
621    hint: AtomicU32,
622    structural_lock: Mutex<()>,
623}
624
625impl MemoryCellTable {
626    fn new() -> Self {
627        Self {
628            slots: (0..CELL_CAPACITY).map(|_| CellSlot::empty()).collect(),
629            hint: AtomicU32::new(0),
630            structural_lock: Mutex::new(()),
631        }
632    }
633}
634
635type MemoryRegistry = HashMap<usize, Weak<MemoryCellTable>>;
636
637static MEMORY_TABLES: LazyLock<Mutex<MemoryRegistry>> =
638    LazyLock::new(|| Mutex::new(HashMap::new()));
639
640fn memory_table(fleet: &Arc<Fleet>) -> Arc<MemoryCellTable> {
641    let fleet_identity = Arc::as_ptr(fleet) as usize;
642    let mut tables = lock_unpoisoned(&MEMORY_TABLES);
643    tables.retain(|_, table| table.strong_count() > 0);
644    if let Some(table) = tables.get(&fleet_identity).and_then(Weak::upgrade) {
645        return table;
646    }
647    let table = Arc::new(MemoryCellTable::new());
648    tables.insert(fleet_identity, Arc::downgrade(&table));
649    table
650}
651
652#[cfg(unix)]
653struct ShmCellTable {
654    region: ShmRegion,
655    structural_lock: Mutex<()>,
656}
657
658#[cfg(unix)]
659impl ShmCellTable {
660    fn open_or_create(name: &str) -> Result<Self> {
661        use std::{io, ptr};
662
663        let (region, _initialization_lock) =
664            ShmRegion::open_or_create_locked(name, shm_segment_size())?;
665        if region.created() {
666            unsafe {
667                ptr::write(
668                    region.as_ptr().cast::<CellStateHeader>(),
669                    CellStateHeader::new(),
670                );
671                let slots = region.as_ptr().add(size_of::<CellStateHeader>());
672                ptr::write_bytes(slots, 0, CELL_CAPACITY * size_of::<CellSlot>());
673            }
674        } else {
675            let header = unsafe { &*region.as_ptr().cast::<CellStateHeader>() };
676            if header.magic != STATE_MAGIC {
677                return Err(Error::Io(io::Error::new(
678                    io::ErrorKind::InvalidData,
679                    format!(
680                        "SHM segment {name} has wrong magic 0x{:08X} (expected 0x{STATE_MAGIC:08X})",
681                        header.magic
682                    ),
683                )));
684            }
685            if header.version != STATE_VERSION
686                || usize::from(header.header_size) != size_of::<CellStateHeader>()
687                || header.capacity as usize != CELL_CAPACITY
688                || header.slot_size as usize != size_of::<CellSlot>()
689            {
690                return Err(Error::Io(io::Error::new(
691                    io::ErrorKind::InvalidData,
692                    format!("SHM segment {name} has an incompatible cell-state layout"),
693                )));
694            }
695        }
696        Ok(Self {
697            region,
698            structural_lock: Mutex::new(()),
699        })
700    }
701
702    fn header(&self) -> &CellStateHeader {
703        unsafe { &*self.region.as_ptr().cast::<CellStateHeader>() }
704    }
705
706    fn slots(&self) -> &[CellSlot] {
707        unsafe {
708            std::slice::from_raw_parts(
709                self.region
710                    .as_ptr()
711                    .add(size_of::<CellStateHeader>())
712                    .cast::<CellSlot>(),
713                CELL_CAPACITY,
714            )
715        }
716    }
717
718    fn with_structure<T>(
719        &self,
720        operation: impl FnOnce(&[CellSlot], &AtomicU32) -> Result<T>,
721    ) -> Result<T> {
722        let _local = lock_unpoisoned(&self.structural_lock);
723        let _process = self.region.lock_exclusive()?;
724        operation(self.slots(), &self.header().hint)
725    }
726}
727
728#[cfg(unix)]
729fn shm_segment_size() -> usize {
730    size_of::<CellStateHeader>() + CELL_CAPACITY * size_of::<CellSlot>()
731}
732
733const _: () = assert!(CELL_CAPACITY.is_power_of_two());
734const _: () = assert!(CELL_CAPACITY <= u32::MAX as usize);
735const _: () = assert!(size_of::<CellStateHeader>() == 64);
736const _: () = assert!(size_of::<CellSlot>() == 64);