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 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
37pub const CELL_STATE_KIND: u8 = 187;
39pub 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#[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#[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
96pub(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; #[cfg(unix)]
115const STATE_VERSION: u16 = 4;
116
117pub type Result<T, E = Error> = std::result::Result<T, E>;
118
119#[non_exhaustive]
123#[derive(Debug)]
124pub enum Error {
125 Stale(CellId),
128 StaleText(TextId),
130 TooLong {
132 len: usize,
133 max: usize,
134 },
135 Corrupt(String),
138 TypeMismatch {
140 id: CellId,
141 expected: &'static str,
142 found: &'static str,
143 },
144 Overflow,
146 WakeExhausted,
149 Contended,
152 RevisionExhausted,
154 OwnerExhausted,
156 Full {
159 capacity: usize,
160 },
161 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#[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 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
277pub 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#[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 pub fn allocate_text(
390 &self,
391 initial: &str,
392 ) -> Result<Text> {
393 self.text.allocate(initial)
394 }
395
396 pub fn recover_text_writers(&self) -> Result<usize> {
405 self.text.recover_writers()
406 }
407
408 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 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 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 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 slot.changed();
524 Ok(())
525 })
526 }
527
528 pub fn is_live(
530 &self,
531 id: CellId,
532 ) -> bool {
533 self.slot(id).is_ok()
534 }
535
536 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 #[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
599pub enum Claim {
600 Took { taken: i64, after: i64 },
603 Exhausted,
605}
606
607#[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 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 pub fn version(&self) -> Result<u32> {
649 Ok(self.slot()?.changes.load(Ordering::Acquire))
650 }
651
652 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 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 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 pub fn wake_waiters(&self) -> Result<()> {
735 self.slot()?.wake.notify();
736 Ok(())
737 }
738
739 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 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 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 }
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 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 pub fn set(&self) -> Result<bool> {
904 self.swap(true)
905 }
906
907 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 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#[repr(C, align(64))]
957struct CellSlot {
958 state: AtomicU8,
959 tag: AtomicU8,
960 _reserved: [u8; 2],
961 generation: AtomicU32,
962 value: AtomicU64,
963 changes: AtomicU32,
966 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 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 self.state.load(Ordering::Acquire) == SLOT_OCCUPIED
1000 && self.generation.load(Ordering::Relaxed) == id.generation
1001 }
1002
1003 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 _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 #[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 #[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 assert_eq!(cell.version().unwrap(), cell.version().unwrap());
1296 }
1297
1298 #[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}