1use 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
30pub const CELL_STATE_KIND: u8 = 233;
32pub 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(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))]
47pub(crate) fn waits_supported() -> bool {
48 orbit_core::sync::supported()
49}
50
51#[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "macos")))]
52pub(crate) fn waits_supported() -> bool {
53 false
54}
55
56#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))]
60pub(crate) fn wait_on(word: &AtomicU32, expected: u32) -> Result<()> {
61 orbit_core::sync::wait_word(word, expected).map_err(Error::Io)
62}
63
64#[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "macos")))]
65pub(crate) fn wait_on(_word: &AtomicU32, _expected: u32) -> Result<()> {
66 Err(Error::Io(std::io::Error::new(std::io::ErrorKind::Unsupported, "orbit-cell needs a platform that can wait on a shared word")))
67}
68
69pub(crate) fn wake_on(word: &AtomicU32) {
71 #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))]
72 let _ = orbit_core::sync::wake_word(word);
73 #[cfg(not(any(target_os = "linux", target_os = "freebsd", target_os = "macos")))]
74 let _ = word;
75}
76
77#[cfg(unix)]
78const STATE_MAGIC: u32 = 0x43_45_4C_4C; #[cfg(unix)]
80const STATE_VERSION: u16 = 2;
81
82pub type Result<T, E = Error> = std::result::Result<T, E>;
83
84#[non_exhaustive]
88#[derive(Debug)]
89pub enum Error {
90 Stale(CellId),
93 StaleText(TextId),
95 TooLong {
97 len: usize,
98 max: usize,
99 },
100 Corrupt(String),
103 TypeMismatch {
105 id: CellId,
106 expected: &'static str,
107 found: &'static str,
108 },
109 Overflow,
111 Full {
113 capacity: usize,
114 },
115 Malformed(String),
117 Io(std::io::Error),
118}
119
120impl fmt::Display for Error {
121 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
122 match self {
123 Self::Stale(id) => write!(f, "cell {id} has been released"),
124 Self::StaleText(id) => write!(f, "text cell {id} has been released"),
125 Self::TooLong { len, max } => {
126 write!(f, "text does not fit the cell: len={len} max={max}")
127 }
128 Self::Corrupt(id) => write!(f, "text cell {id} holds bytes that are not UTF-8"),
129 Self::TypeMismatch {
130 id,
131 expected,
132 found,
133 } => write!(f, "cell {id} holds {found}, not {expected}"),
134 Self::Overflow => f.write_str("cell value is outside the integer range"),
135 Self::Full { capacity } => write!(f, "cell table is full: capacity={capacity}"),
136 Self::Malformed(text) => write!(f, "not a cell id: {text:?}"),
137 Self::Io(error) => write!(f, "Orbit cell io error: {error}"),
138 }
139 }
140}
141
142impl std::error::Error for Error {
143 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
144 match self {
145 Self::Io(error) => Some(error),
146 _ => None,
147 }
148 }
149}
150
151impl From<std::io::Error> for Error {
152 fn from(value: std::io::Error) -> Self {
153 Self::Io(value)
154 }
155}
156
157#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
162pub struct CellId {
163 index: u32,
164 generation: u32,
165}
166
167impl CellId {
168 pub const fn index(self) -> u32 {
169 self.index
170 }
171
172 pub const fn generation(self) -> u32 {
173 self.generation
174 }
175
176 pub const fn to_bits(self) -> u64 {
178 ((self.index as u64) << 32) | self.generation as u64
179 }
180
181 pub const fn from_bits(bits: u64) -> Self {
182 Self {
183 index: (bits >> 32) as u32,
184 generation: bits as u32,
185 }
186 }
187}
188
189impl fmt::Display for CellId {
190 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191 write!(f, "cell:{}:{}", self.index, self.generation)
192 }
193}
194
195impl FromStr for CellId {
196 type Err = Error;
197
198 fn from_str(text: &str) -> Result<Self> {
199 let malformed = || Error::Malformed(text.to_owned());
200 let rest = text.strip_prefix("cell:").ok_or_else(malformed)?;
201 let (index, generation) = rest.split_once(':').ok_or_else(malformed)?;
202 Ok(Self {
203 index: index.parse().map_err(|_| malformed())?,
204 generation: generation.parse().map_err(|_| malformed())?,
205 })
206 }
207}
208
209pub trait CellType: Copy + Send + Sync + 'static {
212 const TAG: u8;
213 const NAME: &'static str;
214
215 fn to_bits(self) -> u64;
216 fn from_bits(bits: u64) -> Self;
217}
218
219impl CellType for i64 {
220 const TAG: u8 = 1;
221 const NAME: &'static str = "int";
222
223 fn to_bits(self) -> u64 {
224 self as u64
225 }
226
227 fn from_bits(bits: u64) -> Self {
228 bits as i64
229 }
230}
231
232impl CellType for u64 {
233 const TAG: u8 = 2;
234 const NAME: &'static str = "unsigned";
235
236 fn to_bits(self) -> u64 {
237 self
238 }
239
240 fn from_bits(bits: u64) -> Self {
241 bits
242 }
243}
244
245impl CellType for f64 {
246 const TAG: u8 = 3;
247 const NAME: &'static str = "float";
248
249 fn to_bits(self) -> u64 {
250 self.to_bits()
251 }
252
253 fn from_bits(bits: u64) -> Self {
254 f64::from_bits(bits)
255 }
256}
257
258impl CellType for bool {
259 const TAG: u8 = 4;
260 const NAME: &'static str = "bool";
261
262 fn to_bits(self) -> u64 {
263 u64::from(self)
264 }
265
266 fn from_bits(bits: u64) -> Self {
267 bits != 0
268 }
269}
270
271fn type_name(tag: u8) -> &'static str {
272 match tag {
273 1 => i64::NAME,
274 2 => u64::NAME,
275 3 => f64::NAME,
276 4 => bool::NAME,
277 _ => "unknown",
278 }
279}
280
281#[derive(Clone)]
283pub struct Cells {
284 backend: CellBackend,
285 text: text::TextBackend,
286}
287
288#[derive(Clone)]
289enum CellBackend {
290 InMemory(Arc<MemoryCellTable>),
291 #[cfg(unix)]
292 Shm(Arc<ShmCellTable>),
293}
294
295impl Cells {
296 pub fn new(fleet: Arc<Fleet>) -> Result<Self> {
297 if !crate::waits_supported() {
298 return Err(Error::Io(std::io::Error::new(
299 std::io::ErrorKind::Unsupported,
300 "orbit-cell needs a platform that can wait on a shared word: Linux, FreeBSD, or macOS 14.4 or later",
301 )));
302 }
303 let backend = if fleet.is_shm() {
304 #[cfg(unix)]
305 {
306 CellBackend::Shm(Arc::new(ShmCellTable::open_or_create(&ring_segment_name(
307 fleet.name(),
308 CELL_STATE_KIND,
309 ))?))
310 }
311 #[cfg(not(unix))]
312 unreachable!("non-Unix fleets cannot use POSIX SHM")
313 } else {
314 CellBackend::InMemory(memory_table(&fleet))
315 };
316 let text = text::TextBackend::new(&fleet)?;
317 Ok(Self { backend, text })
318 }
319
320 pub fn allocate_text(&self, initial: &str) -> Result<Text> {
322 self.text.allocate(initial)
323 }
324
325 pub fn open_text(&self, id: TextId) -> Result<Text> {
327 self.text.open(id)
328 }
329
330 pub fn release_text(&self, id: TextId) -> Result<()> {
331 self.text.release(id)
332 }
333
334 pub fn is_text_live(&self, id: TextId) -> bool {
335 self.text.is_live(id)
336 }
337
338 pub fn allocate<T: CellType>(&self, initial: T) -> Result<Orbital<T>> {
341 let id = self.with_structure(|slots, hint| {
342 let capacity = slots.len();
343 let start = hint.load(Ordering::Relaxed) as usize;
344 for offset in 0..capacity {
345 let index = (start + offset) & (capacity - 1);
346 let slot = &slots[index];
347 if slot.state.load(Ordering::Acquire) == SLOT_EMPTY {
348 let generation = slot.install(T::TAG, initial.to_bits());
349 hint.store(((index + 1) & (capacity - 1)) as u32, Ordering::Relaxed);
350 return Ok(CellId {
351 index: index as u32,
352 generation,
353 });
354 }
355 }
356 Err(Error::Full { capacity })
357 })?;
358 Ok(Orbital {
359 cells: self.clone(),
360 id,
361 _type: PhantomData,
362 })
363 }
364
365 pub fn open<T: CellType>(&self, id: CellId) -> Result<Orbital<T>> {
367 let slot = self.slot(id)?;
368 let found = slot.tag.load(Ordering::Relaxed);
369 if found != T::TAG {
370 return Err(Error::TypeMismatch {
371 id,
372 expected: T::NAME,
373 found: type_name(found),
374 });
375 }
376 Ok(Orbital {
377 cells: self.clone(),
378 id,
379 _type: PhantomData,
380 })
381 }
382
383 pub fn release(&self, id: CellId) -> Result<()> {
385 self.with_structure(|slots, _| {
386 let slot = slots
387 .get(id.index as usize)
388 .filter(|slot| slot.is(id))
389 .ok_or(Error::Stale(id))?;
390 slot.state.store(SLOT_EMPTY, Ordering::Release);
391 slot.changed();
393 Ok(())
394 })
395 }
396
397 pub fn is_live(&self, id: CellId) -> bool {
399 self.slot(id).is_ok()
400 }
401
402 pub fn reset_all(&self) -> Result<()> {
404 self.with_structure(|slots, hint| {
405 for slot in slots {
406 slot.state.store(SLOT_EMPTY, Ordering::Release);
407 slot.changed();
408 }
409 hint.store(0, Ordering::Relaxed);
410 Ok(())
411 })?;
412 self.text.reset_all()
413 }
414
415 #[cfg(unix)]
418 pub fn unlink(&self) -> Result<()> {
419 match &self.backend {
420 CellBackend::InMemory(_) => self.reset_all()?,
421 CellBackend::Shm(table) => table.region.unlink().map_err(Error::Io)?,
422 }
423 self.text.unlink()
424 }
425
426 fn slots(&self) -> &[CellSlot] {
427 match &self.backend {
428 CellBackend::InMemory(table) => &table.slots,
429 #[cfg(unix)]
430 CellBackend::Shm(table) => table.slots(),
431 }
432 }
433
434 fn slot(&self, id: CellId) -> Result<&CellSlot> {
435 self.slots()
436 .get(id.index as usize)
437 .filter(|slot| slot.is(id))
438 .ok_or(Error::Stale(id))
439 }
440
441 fn with_structure<T>(
442 &self,
443 operation: impl FnOnce(&[CellSlot], &AtomicU32) -> Result<T>,
444 ) -> Result<T> {
445 match &self.backend {
446 CellBackend::InMemory(table) => {
447 let _local = lock_unpoisoned(&table.structural_lock);
448 operation(&table.slots, &table.hint)
449 }
450 #[cfg(unix)]
451 CellBackend::Shm(table) => table.with_structure(operation),
452 }
453 }
454}
455
456#[derive(Clone, Copy, Debug, Eq, PartialEq)]
460pub enum Claim {
461 Took {
464 taken: i64,
465 after: i64,
466 },
467 Exhausted,
469}
470
471#[derive(Clone)]
473pub struct Orbital<T: CellType> {
474 cells: Cells,
475 id: CellId,
476 _type: PhantomData<T>,
477}
478
479impl<T: CellType> Orbital<T> {
480 pub fn id(&self) -> CellId {
481 self.id
482 }
483
484 pub fn load(&self) -> Result<T> {
485 Ok(T::from_bits(self.slot()?.value.load(Ordering::Acquire)))
486 }
487
488 pub fn store(&self, value: T) -> Result<()> {
489 let slot = self.slot()?;
490 slot.value.store(value.to_bits(), Ordering::Release);
491 slot.changed();
492 Ok(())
493 }
494
495 pub fn swap(&self, value: T) -> Result<T> {
497 let slot = self.slot()?;
498 let previous = slot.value.swap(value.to_bits(), Ordering::AcqRel);
499 slot.changed();
500 Ok(T::from_bits(previous))
501 }
502
503 pub fn version(&self) -> Result<u32> {
506 Ok(self.slot()?.changes.load(Ordering::Acquire))
507 }
508
509 pub fn wait_changed(&self, since: u32) -> Result<u32> {
514 loop {
515 let slot = self.slot()?;
516 let now = slot.changes.load(Ordering::SeqCst);
517 if now != since {
518 return Ok(now);
519 }
520 slot.waiters.fetch_add(1, Ordering::SeqCst);
521 let outcome = if slot.changes.load(Ordering::SeqCst) == since {
522 wait_on(&slot.changes, since)
523 } else {
524 Ok(())
525 };
526 slot.waiters.fetch_sub(1, Ordering::SeqCst);
527 outcome?;
528 }
529 }
530
531 pub fn compare_exchange(&self, current: T, new: T) -> Result<std::result::Result<T, T>> {
534 let slot = self.slot()?;
535 let exchanged = slot.value.compare_exchange(
536 current.to_bits(),
537 new.to_bits(),
538 Ordering::AcqRel,
539 Ordering::Acquire,
540 );
541 if exchanged.is_ok() {
542 slot.changed();
543 }
544 Ok(exchanged.map(T::from_bits).map_err(T::from_bits))
545 }
546
547 pub fn release(self) -> Result<()> {
549 self.cells.release(self.id)
550 }
551
552 fn slot(&self) -> Result<&CellSlot> {
553 self.cells.slot(self.id)
554 }
555}
556
557impl Orbital<i64> {
558 pub fn fetch_add(&self, by: i64) -> Result<i64> {
560 let slot = self.slot()?;
561 slot.value
562 .try_update(Ordering::AcqRel, Ordering::Acquire, |bits| {
563 (bits as i64).checked_add(by).map(|value| value as u64)
564 })
565 .map(|previous| {
566 slot.changed();
567 (previous as i64) + by
568 })
569 .map_err(|_| Error::Overflow)
570 }
571
572 pub fn fetch_sub(&self, by: i64) -> Result<i64> {
573 self.fetch_add(by.checked_neg().ok_or(Error::Overflow)?)
574 }
575
576 pub fn add_until(&self, by: i64, limit: i64) -> Result<Claim> {
604 if by <= 0 {
605 return Err(Error::Overflow);
606 }
607 let slot = self.slot()?;
608 loop {
609 let current = slot.value.load(Ordering::Acquire) as i64;
610 if current >= limit {
611 return Ok(Claim::Exhausted);
612 }
613 let taken = by.min(limit - current);
614 let after = current + taken;
615 if slot
616 .value
617 .compare_exchange(
618 current as u64,
619 after as u64,
620 Ordering::AcqRel,
621 Ordering::Acquire,
622 )
623 .is_ok()
624 {
625 slot.changed();
626 return Ok(Claim::Took { taken, after });
627 }
628 }
632 }
633}
634
635impl Orbital<u64> {
636 pub fn fetch_add(&self, by: u64) -> Result<u64> {
637 let slot = self.slot()?;
638 slot.value
639 .try_update(Ordering::AcqRel, Ordering::Acquire, |bits| {
640 bits.checked_add(by)
641 })
642 .map(|previous| {
643 slot.changed();
644 previous + by
645 })
646 .map_err(|_| Error::Overflow)
647 }
648
649 pub fn fetch_sub(&self, by: u64) -> Result<u64> {
650 let slot = self.slot()?;
651 slot.value
652 .try_update(Ordering::AcqRel, Ordering::Acquire, |bits| {
653 bits.checked_sub(by)
654 })
655 .map(|previous| {
656 slot.changed();
657 previous - by
658 })
659 .map_err(|_| Error::Overflow)
660 }
661}
662
663impl Orbital<f64> {
664 pub fn fetch_add(&self, by: f64) -> Result<f64> {
667 let slot = self.slot()?;
668 let previous = slot
669 .value
670 .try_update(Ordering::AcqRel, Ordering::Acquire, |bits| {
671 Some((f64::from_bits(bits) + by).to_bits())
672 })
673 .unwrap_or_else(|bits| bits);
674 slot.changed();
675 Ok(f64::from_bits(previous) + by)
676 }
677}
678
679impl Orbital<bool> {
680 pub fn set(&self) -> Result<bool> {
682 self.swap(true)
683 }
684
685 pub fn clear(&self) -> Result<bool> {
687 self.swap(false)
688 }
689}
690
691#[repr(C, align(64))]
692struct CellStateHeader {
693 #[cfg(unix)]
694 magic: u32,
695 #[cfg(not(unix))]
696 _magic: u32,
697 #[cfg(unix)]
698 version: u16,
699 #[cfg(not(unix))]
700 _version: u16,
701 header_size: u16,
702 capacity: u32,
703 slot_size: u32,
704 hint: AtomicU32,
706 _reserved: [u8; 44],
707}
708
709impl CellStateHeader {
710 fn new() -> Self {
711 Self {
712 #[cfg(unix)]
713 magic: STATE_MAGIC,
714 #[cfg(not(unix))]
715 _magic: 0,
716 #[cfg(unix)]
717 version: STATE_VERSION,
718 #[cfg(not(unix))]
719 _version: 0,
720 header_size: size_of::<Self>() as u16,
721 capacity: CELL_CAPACITY as u32,
722 slot_size: size_of::<CellSlot>() as u32,
723 hint: AtomicU32::new(0),
724 _reserved: [0; 44],
725 }
726 }
727}
728
729#[repr(C, align(64))]
733struct CellSlot {
734 state: AtomicU8,
735 tag: AtomicU8,
736 _reserved: [u8; 2],
737 generation: AtomicU32,
738 value: AtomicU64,
739 changes: AtomicU32,
742 waiters: AtomicU32,
745 _padding: [u8; 40],
746}
747
748impl CellSlot {
749 fn empty() -> Self {
750 Self {
751 state: AtomicU8::new(SLOT_EMPTY),
752 tag: AtomicU8::new(0),
753 _reserved: [0; 2],
754 generation: AtomicU32::new(0),
755 value: AtomicU64::new(0),
756 changes: AtomicU32::new(0),
757 waiters: AtomicU32::new(0),
758 _padding: [0; 40],
759 }
760 }
761
762 fn changed(&self) {
767 self.changes.fetch_add(1, Ordering::SeqCst);
768 if self.waiters.load(Ordering::SeqCst) > 0 {
769 wake_on(&self.changes);
770 }
771 }
772
773 fn is(&self, id: CellId) -> bool {
774 self.state.load(Ordering::Acquire) == SLOT_OCCUPIED
776 && self.generation.load(Ordering::Relaxed) == id.generation
777 }
778
779 fn install(&self, tag: u8, value: u64) -> u32 {
782 let generation = self
783 .generation
784 .load(Ordering::Relaxed)
785 .wrapping_add(1)
786 .max(1);
787 self.tag.store(tag, Ordering::Relaxed);
788 self.value.store(value, Ordering::Relaxed);
789 self.changes.store(0, Ordering::Relaxed);
790 self.waiters.store(0, Ordering::Relaxed);
791 self.generation.store(generation, Ordering::Relaxed);
792 self.state.store(SLOT_OCCUPIED, Ordering::Release);
793 generation
794 }
795}
796
797pub(crate) fn lock_unpoisoned<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
798 mutex.lock().unwrap_or_else(|error| error.into_inner())
799}
800
801struct MemoryCellTable {
802 _fleet: Arc<Fleet>,
805 slots: Vec<CellSlot>,
806 hint: AtomicU32,
807 structural_lock: Mutex<()>,
808}
809
810impl MemoryCellTable {
811 fn new(fleet: Arc<Fleet>) -> Self {
812 Self {
813 _fleet: fleet,
814 slots: (0..CELL_CAPACITY).map(|_| CellSlot::empty()).collect(),
815 hint: AtomicU32::new(0),
816 structural_lock: Mutex::new(()),
817 }
818 }
819}
820
821type MemoryRegistry = HashMap<usize, Weak<MemoryCellTable>>;
822
823static MEMORY_TABLES: LazyLock<Mutex<MemoryRegistry>> =
824 LazyLock::new(|| Mutex::new(HashMap::new()));
825
826fn memory_table(fleet: &Arc<Fleet>) -> Arc<MemoryCellTable> {
827 let fleet_identity = Arc::as_ptr(fleet) as usize;
828 let mut tables = lock_unpoisoned(&MEMORY_TABLES);
829 tables.retain(|_, table| table.strong_count() > 0);
830 if let Some(table) = tables.get(&fleet_identity).and_then(Weak::upgrade) {
831 return table;
832 }
833 let table = Arc::new(MemoryCellTable::new(Arc::clone(fleet)));
834 tables.insert(fleet_identity, Arc::downgrade(&table));
835 table
836}
837
838#[cfg(unix)]
839struct ShmCellTable {
840 region: ShmRegion,
841 structural_lock: Mutex<()>,
842}
843
844#[cfg(unix)]
845impl ShmCellTable {
846 fn open_or_create(name: &str) -> Result<Self> {
847 use std::{io, ptr};
848
849 let (region, _initialization_lock) =
850 ShmRegion::open_or_create_locked(name, shm_segment_size())?;
851 if region.created() {
852 unsafe {
853 ptr::write(
854 region.as_ptr().cast::<CellStateHeader>(),
855 CellStateHeader::new(),
856 );
857 let slots = region.as_ptr().add(size_of::<CellStateHeader>());
858 ptr::write_bytes(slots, 0, CELL_CAPACITY * size_of::<CellSlot>());
859 }
860 } else {
861 let header = unsafe { &*region.as_ptr().cast::<CellStateHeader>() };
862 if header.magic != STATE_MAGIC {
863 return Err(Error::Io(io::Error::new(
864 io::ErrorKind::InvalidData,
865 format!(
866 "SHM segment {name} has wrong magic 0x{:08X} (expected 0x{STATE_MAGIC:08X})",
867 header.magic
868 ),
869 )));
870 }
871 if header.version != STATE_VERSION
872 || usize::from(header.header_size) != size_of::<CellStateHeader>()
873 || header.capacity as usize != CELL_CAPACITY
874 || header.slot_size as usize != size_of::<CellSlot>()
875 {
876 return Err(Error::Io(io::Error::new(
877 io::ErrorKind::InvalidData,
878 format!("SHM segment {name} has an incompatible cell-state layout"),
879 )));
880 }
881 }
882 Ok(Self {
883 region,
884 structural_lock: Mutex::new(()),
885 })
886 }
887
888 fn header(&self) -> &CellStateHeader {
889 unsafe { &*self.region.as_ptr().cast::<CellStateHeader>() }
890 }
891
892 fn slots(&self) -> &[CellSlot] {
893 unsafe {
894 std::slice::from_raw_parts(
895 self.region
896 .as_ptr()
897 .add(size_of::<CellStateHeader>())
898 .cast::<CellSlot>(),
899 CELL_CAPACITY,
900 )
901 }
902 }
903
904 fn with_structure<T>(
905 &self,
906 operation: impl FnOnce(&[CellSlot], &AtomicU32) -> Result<T>,
907 ) -> Result<T> {
908 let _local = lock_unpoisoned(&self.structural_lock);
909 let _process = self.region.lock_exclusive()?;
910 operation(self.slots(), &self.header().hint)
911 }
912}
913
914#[cfg(unix)]
915fn shm_segment_size() -> usize {
916 size_of::<CellStateHeader>() + CELL_CAPACITY * size_of::<CellSlot>()
917}
918
919const _: () = assert!(CELL_CAPACITY.is_power_of_two());
920const _: () = assert!(CELL_CAPACITY <= u32::MAX as usize);
921const _: () = assert!(size_of::<CellStateHeader>() == 64);
922const _: () = assert!(size_of::<CellSlot>() == 64);
923
924#[cfg(test)]
925mod registry_tests {
926 use std::sync::Arc;
927
928 use orbit_core::Fleet;
929
930 use super::Cells;
931
932 #[test]
937 fn a_memory_table_keeps_its_fleet_alive() {
938 let fleet = Arc::new(Fleet::join("cell-registry-test", 1).unwrap());
939 let cells = Cells::new(Arc::clone(&fleet)).unwrap();
940 assert!(Arc::strong_count(&fleet) > 1);
941 drop(cells);
942 assert_eq!(Arc::strong_count(&fleet), 1);
943 }
944}
945
946#[cfg(test)]
947mod wait_tests {
948 use std::sync::Arc;
949 use std::time::Duration;
950
951 use orbit_core::Fleet;
952
953 use super::{Cells, Error};
954
955 fn cells() -> Cells {
956 Cells::new(Arc::new(Fleet::join("cell-wait-test", 1).unwrap())).unwrap()
957 }
958
959 #[test]
963 fn a_write_wakes_a_waiter_once() {
964 let cells = cells();
965 let cell = cells.allocate(0_i64).unwrap();
966 let since = cell.version().unwrap();
967 let watcher = cell.clone();
968 let waiter = std::thread::spawn(move || watcher.wait_changed(since));
969
970 std::thread::sleep(Duration::from_millis(30));
971 for _ in 0..10 {
972 cell.fetch_add(1).unwrap();
973 }
974 let now = waiter.join().unwrap().unwrap();
975 assert!(now > since);
976 assert_eq!(cell.load().unwrap(), 10);
977 assert_eq!(cell.version().unwrap(), cell.version().unwrap());
979 }
980
981 #[test]
984 fn release_wakes_a_waiter_stale() {
985 let cells = cells();
986 let cell = cells.allocate(7_i64).unwrap();
987 let since = cell.version().unwrap();
988 let watcher = cell.clone();
989 let waiter = std::thread::spawn(move || watcher.wait_changed(since));
990
991 std::thread::sleep(Duration::from_millis(30));
992 cell.release().unwrap();
993 assert!(matches!(waiter.join().unwrap(), Err(Error::Stale(_))));
994 }
995}