1use std::collections::HashMap;
7use std::mem::size_of;
8use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
9use std::sync::{Arc, LazyLock, Mutex, Weak};
10use std::task::Waker;
11use std::thread::JoinHandle;
12
13#[cfg(unix)]
14use orbit_core::shm::{ShmRegion, ring_segment_name};
15use orbit_core::{Fleet, OrbitEpoch};
16
17use crate::layout::{
18 Doorbell, Geometry, Header, KEY_EMPTY, KEY_LIVE, KeySlot, RESOURCE_EMPTY, ResourceSlot,
19};
20use orbit_core::readiness::{Readiness, Signal};
21
22use crate::{Error, Incarnation, PoolSpec, Result, lock_unpoisoned};
23
24enum Backing {
25 Memory(AlignedBytes),
26 #[cfg(unix)]
27 Shm(ShmRegion),
28}
29
30struct AlignedBytes {
31 ptr: *mut u8,
32 layout: std::alloc::Layout,
33}
34
35impl AlignedBytes {
36 fn zeroed(size: usize) -> Self {
37 let layout = std::alloc::Layout::from_size_align(size, 64).expect("segment layout");
38 let ptr = unsafe { std::alloc::alloc_zeroed(layout) };
40 assert!(!ptr.is_null(), "pool table allocation failed");
41 Self { ptr, layout }
42 }
43}
44
45impl Drop for AlignedBytes {
46 fn drop(&mut self) {
47 unsafe { std::alloc::dealloc(self.ptr, self.layout) }
49 }
50}
51
52unsafe impl Send for AlignedBytes {}
55unsafe impl Sync for AlignedBytes {}
56
57struct Driver {
60 stop: Arc<std::sync::atomic::AtomicBool>,
61 thread: Option<JoinHandle<()>>,
62 pid: u32,
63}
64
65pub(crate) struct Table {
66 _fleet: Arc<Fleet>,
69 backing: Backing,
70 geometry: Geometry,
71 kind: u8,
73 node: u16,
74 incarnation: u64,
75 structural: Mutex<usize>,
78 wakers: Box<[Mutex<Vec<Waker>>]>,
79 readiness: std::sync::OnceLock<Signal>,
82 driver: Mutex<Option<Driver>>,
83}
84
85impl Table {
86 fn base(&self) -> *mut u8 {
87 match &self.backing {
88 Backing::Memory(bytes) => bytes.ptr,
89 #[cfg(unix)]
90 Backing::Shm(region) => region.as_ptr(),
91 }
92 }
93
94 pub(crate) fn is_shared(&self) -> bool {
95 match &self.backing {
96 Backing::Memory(_) => false,
97 #[cfg(unix)]
98 Backing::Shm(_) => true,
99 }
100 }
101
102 pub(crate) fn geometry(&self) -> &Geometry {
103 &self.geometry
104 }
105
106 pub(crate) fn kind(&self) -> u8 {
108 self.kind
109 }
110
111 pub(crate) fn node(&self) -> u16 {
112 self.node
113 }
114
115 pub(crate) fn incarnation(&self) -> u64 {
116 self.incarnation
117 }
118
119 fn header(&self) -> &Header {
120 unsafe { &*self.base().cast::<Header>() }
122 }
123
124 pub(crate) fn epoch(&self) -> u64 {
125 self.header().epoch.load(Ordering::Acquire)
126 }
127
128 fn doorbell(&self, node: usize) -> &Doorbell {
129 debug_assert!(node < self.geometry.fleet_capacity);
130 unsafe {
132 &*self
133 .base()
134 .add(self.geometry.doorbells_offset + node * size_of::<Doorbell>())
135 .cast::<Doorbell>()
136 }
137 }
138
139 fn key_word(&self, offset: usize, node: usize, word: usize) -> &AtomicU64 {
140 debug_assert!(node < self.geometry.fleet_capacity && word < self.geometry.key_words);
141 unsafe {
143 &*self
144 .base()
145 .add(offset + (node * self.geometry.key_words + word) * size_of::<AtomicU64>())
146 .cast::<AtomicU64>()
147 }
148 }
149
150 fn pending_word(&self, node: usize, word: usize) -> &AtomicU64 {
151 self.key_word(self.geometry.pending_offset, node, word)
152 }
153
154 fn interest_word(&self, node: usize, word: usize) -> &AtomicU64 {
155 self.key_word(self.geometry.interest_offset, node, word)
156 }
157
158 pub(crate) fn claims(&self, node: usize, key_index: usize) -> &AtomicU32 {
160 debug_assert!(node < self.geometry.fleet_capacity && key_index < self.geometry.key_capacity);
161 unsafe {
163 &*self
164 .base()
165 .add(
166 self.geometry.claims_offset
167 + (node * self.geometry.key_capacity + key_index) * size_of::<AtomicU32>(),
168 )
169 .cast::<AtomicU32>()
170 }
171 }
172
173 pub(crate) fn key(&self, index: usize) -> &KeySlot {
174 debug_assert!(index < self.geometry.key_capacity);
175 unsafe {
178 &*self
179 .base()
180 .add(self.geometry.keys_offset + index * self.geometry.key_stride)
181 .cast::<KeySlot>()
182 }
183 }
184
185 pub(crate) fn members(&self, index: usize) -> &[AtomicU64] {
187 unsafe {
189 std::slice::from_raw_parts(
190 self.base()
191 .add(self.geometry.keys_offset + index * self.geometry.key_stride)
192 .add(size_of::<KeySlot>())
193 .cast::<AtomicU64>(),
194 self.geometry.member_words,
195 )
196 }
197 }
198
199 pub(crate) fn resources(&self) -> &[ResourceSlot] {
200 unsafe {
202 std::slice::from_raw_parts(
203 self.base()
204 .add(self.geometry.resources_offset)
205 .cast::<ResourceSlot>(),
206 self.geometry.total_resources,
207 )
208 }
209 }
210
211 pub(crate) fn key_index(&self, lo: u64, hi: u64) -> Result<usize> {
214 if let Some(index) = self.find_key(lo, hi) {
215 return Ok(index);
216 }
217 let _local = lock_unpoisoned(&self.structural);
218 #[cfg(unix)]
219 let _shared = match &self.backing {
220 Backing::Shm(region) => Some(region.lock_exclusive()?),
221 Backing::Memory(_) => None,
222 };
223 let hash = mix(lo, hi);
224 for offset in 0..self.geometry.key_capacity {
225 let index = (hash as usize).wrapping_add(offset) & (self.geometry.key_capacity - 1);
226 let slot = self.key(index);
227 match slot.state.load(Ordering::Acquire) {
228 KEY_LIVE if slot.holds(lo, hi) => return Ok(index),
229 KEY_EMPTY => {
230 slot.key_lo.store(lo, Ordering::Relaxed);
231 slot.key_hi.store(hi, Ordering::Relaxed);
232 slot.counts.store(0, Ordering::Relaxed);
233 slot.changes.store(0, Ordering::Relaxed);
234 slot.waiters.store(0, Ordering::Relaxed);
235 for word in self.members(index) {
236 word.store(0, Ordering::Relaxed);
237 }
238 slot.state.store(KEY_LIVE, Ordering::Release);
239 return Ok(index);
240 }
241 _ => {}
242 }
243 }
244 Err(Error::KeyFull {
245 capacity: self.geometry.key_capacity,
246 })
247 }
248
249 fn find_key(&self, lo: u64, hi: u64) -> Option<usize> {
250 let hash = mix(lo, hi);
251 for offset in 0..self.geometry.key_capacity {
252 let index = (hash as usize).wrapping_add(offset) & (self.geometry.key_capacity - 1);
253 let slot = self.key(index);
254 match slot.state.load(Ordering::Acquire) {
255 KEY_LIVE if slot.holds(lo, hi) => return Some(index),
256 KEY_EMPTY => return None,
257 _ => {}
258 }
259 }
260 None
261 }
262
263 pub(crate) fn allocate(
266 &self,
267 key: (u64, u64),
268 key_index: usize,
269 capacity: u32,
270 ) -> Result<(usize, u32)> {
271 let mut hint = lock_unpoisoned(&self.structural);
272 let lane_start = usize::from(self.node) * self.geometry.lane_capacity;
273 let slots = self.resources();
274 let now = OrbitEpoch::now().as_unix_ms();
275 for offset in 0..self.geometry.lane_capacity {
276 let index = lane_start + ((*hint + offset) & (self.geometry.lane_capacity - 1));
277 let slot = &slots[index];
278 let state = slot.state.load(Ordering::Acquire);
279 if (state == RESOURCE_EMPTY || state == crate::layout::RESOURCE_CLOSED)
280 && let Some(generation) = slot.install(
281 self.node,
282 self.incarnation,
283 key,
284 key_index as u32,
285 capacity,
286 now,
287 )
288 {
289 *hint = (index - lane_start + 1) & (self.geometry.lane_capacity - 1);
290 return Ok((index, generation));
291 }
292 }
293 Err(Error::Full {
294 capacity: self.geometry.lane_capacity,
295 })
296 }
297
298 pub(crate) fn key_changed(&self, key_index: usize) {
301 let key = self.key(key_index);
302 key.changes.fetch_add(1, Ordering::SeqCst);
303 if key.waiters.load(Ordering::SeqCst) > 0 {
304 crate::wake_on(&key.changes);
305 }
306 let word = key_index / 64;
307 let bit = 1_u64 << (key_index % 64);
308 if !self.is_shared() {
309 self.wake(key_index);
310 self.signal_readiness();
311 return;
312 }
313 for node in 0..self.geometry.fleet_capacity {
314 if self.interest_word(node, word).load(Ordering::SeqCst) & bit != 0 {
315 self.pending_word(node, word)
316 .fetch_or(bit, Ordering::SeqCst);
317 let doorbell = self.doorbell(node);
318 doorbell.generation.fetch_add(1, Ordering::SeqCst);
319 if doorbell.listening.load(Ordering::SeqCst) > 0 {
320 crate::wake_on(&doorbell.generation);
321 }
322 }
323 }
324 }
325
326 fn wake(&self, key_index: usize) {
327 let taken = std::mem::take(&mut *lock_unpoisoned(&self.wakers[key_index]));
328 for waker in taken {
329 waker.wake();
330 }
331 }
332
333 pub(crate) fn take_readiness(self: &Arc<Self>) -> Result<Readiness> {
336 let (readiness, signal) = orbit_core::readiness::pair()?;
337 self.readiness.set(signal).map_err(|_| {
338 Error::Malformed(
339 "this process already took the pool table's readiness descriptor".to_owned(),
340 )
341 })?;
342 if self.is_shared() {
343 let mut driver = lock_unpoisoned(&self.driver);
344 if driver.is_none() {
345 *driver = Some(self.start_driver()?);
346 }
347 }
348 Ok(readiness)
349 }
350
351 pub(crate) fn watch(self: &Arc<Self>, key_index: usize) -> Result<()> {
355 if !self.is_shared() {
356 return Ok(());
357 }
358 self.interest_word(usize::from(self.node), key_index / 64)
359 .fetch_or(1 << (key_index % 64), Ordering::SeqCst);
360 let mut driver = lock_unpoisoned(&self.driver);
361 if driver.is_none() {
362 *driver = Some(self.start_driver()?);
363 }
364 Ok(())
365 }
366
367 fn signal_readiness(&self) {
368 if let Some(signal) = self.readiness.get() {
369 let _ = signal.signal();
371 }
372 }
373
374 pub(crate) fn register(self: &Arc<Self>, key_index: usize, waker: &Waker) -> Result<()> {
377 {
378 let mut wakers = lock_unpoisoned(&self.wakers[key_index]);
379 if !wakers.iter().any(|existing| existing.will_wake(waker)) {
380 wakers.push(waker.clone());
381 }
382 }
383 if !self.is_shared() {
384 return Ok(());
385 }
386 self.interest_word(usize::from(self.node), key_index / 64)
387 .fetch_or(1 << (key_index % 64), Ordering::SeqCst);
388 let mut driver = lock_unpoisoned(&self.driver);
389 if driver.is_none() {
390 *driver = Some(self.start_driver()?);
391 }
392 Ok(())
393 }
394
395 fn start_driver(self: &Arc<Self>) -> std::io::Result<Driver> {
396 let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
397 let thread_stop = Arc::clone(&stop);
398 let target = SendPtr(Arc::as_ptr(self));
399 let thread = std::thread::Builder::new()
400 .name(format!("orbit-pool-{}-driver", self.node))
401 .spawn(move || {
402 let target = target;
403 let table = unsafe { &*target.0 };
406 table.run_driver(&thread_stop);
407 })?;
408 Ok(Driver {
409 stop,
410 thread: Some(thread),
411 pid: std::process::id(),
412 })
413 }
414
415 fn run_driver(&self, stop: &std::sync::atomic::AtomicBool) {
416 let node = usize::from(self.node);
417 let doorbell = self.doorbell(node);
418 doorbell
419 .incarnation
420 .store(self.incarnation, Ordering::SeqCst);
421 doorbell.listening.fetch_add(1, Ordering::SeqCst);
422 let mut seen = doorbell.generation.load(Ordering::SeqCst);
423 while !stop.load(Ordering::Acquire) {
424 let mut drained = false;
425 for word in 0..self.geometry.key_words {
426 let mut bits = self.pending_word(node, word).swap(0, Ordering::SeqCst);
427 if bits != 0 {
428 drained = true;
429 self.interest_word(node, word)
431 .fetch_and(!bits, Ordering::SeqCst);
432 }
433 while bits != 0 {
434 let bit = bits.trailing_zeros() as usize;
435 bits &= bits - 1;
436 let key_index = word * 64 + bit;
437 if key_index < self.geometry.key_capacity {
438 self.wake(key_index);
439 }
440 }
441 }
442 if drained {
443 self.signal_readiness();
447 }
448 let now = doorbell.generation.load(Ordering::SeqCst);
449 if now != seen {
450 seen = now;
451 continue;
452 }
453 if crate::wait_on(&doorbell.generation, seen).is_err() {
457 break;
458 }
459 }
460 doorbell.listening.fetch_sub(1, Ordering::SeqCst);
461 }
462
463 pub(crate) fn node_dead(&self, node: u16, incarnation: u64) {
466 for (index, slot) in self.resources().iter().enumerate() {
467 let state = slot.state.load(Ordering::Acquire);
468 if (state != crate::layout::RESOURCE_LIVE && state != crate::layout::RESOURCE_DRAINING)
469 || slot.owner_node.load(Ordering::Acquire) != node
470 || slot.owner_incarnation.load(Ordering::Acquire) != incarnation
471 {
472 continue;
473 }
474 self.close_resource(index, slot);
475 }
476 for key_index in 0..self.geometry.key_capacity {
477 let held = self
478 .claims(usize::from(node), key_index)
479 .swap(0, Ordering::SeqCst);
480 if held > 0 {
481 let key = self.key(key_index);
482 let _ = key
483 .counts
484 .try_update(Ordering::SeqCst, Ordering::SeqCst, |counts| {
485 let (live, creating) = crate::layout::unpack_counts(counts);
486 Some(crate::layout::pack_counts(
487 live,
488 creating.saturating_sub(held),
489 ))
490 });
491 self.key_changed(key_index);
492 }
493 }
494 let doorbell = self.doorbell(usize::from(node));
495 if doorbell.incarnation.load(Ordering::SeqCst) == incarnation {
496 doorbell.listening.store(0, Ordering::SeqCst);
497 doorbell.incarnation.store(0, Ordering::SeqCst);
498 }
499 }
500
501 pub(crate) fn close_resource(&self, index: usize, slot: &ResourceSlot) {
504 if slot
505 .state
506 .try_update(Ordering::SeqCst, Ordering::SeqCst, |state| {
507 (state == crate::layout::RESOURCE_LIVE || state == crate::layout::RESOURCE_DRAINING)
508 .then_some(crate::layout::RESOURCE_CLOSED)
509 })
510 .is_err()
511 {
512 return;
513 }
514 let key_index = slot.key_index.load(Ordering::Acquire) as usize;
515 self.members(key_index)[index / 64].fetch_and(!(1_u64 << (index % 64)), Ordering::SeqCst);
516 let key = self.key(key_index);
517 let _ = key
518 .counts
519 .try_update(Ordering::SeqCst, Ordering::SeqCst, |counts| {
520 let (live, creating) = crate::layout::unpack_counts(counts);
521 Some(crate::layout::pack_counts(live.saturating_sub(1), creating))
522 });
523 self.key_changed(key_index);
524 }
525
526 pub(crate) fn reset_all(&self) {
527 let mut hint = lock_unpoisoned(&self.structural);
528 *hint = 0;
529 let header = self.header();
530 let epoch = header.epoch.load(Ordering::Acquire);
531 header.epoch.store(next_epoch(epoch), Ordering::Release);
532 for slot in self.resources() {
533 slot.state.store(RESOURCE_EMPTY, Ordering::Release);
534 slot.generation.store(0, Ordering::Relaxed);
535 }
536 for index in 0..self.geometry.key_capacity {
537 let key = self.key(index);
538 key.state.store(KEY_EMPTY, Ordering::Release);
539 key.changes.fetch_add(1, Ordering::SeqCst);
540 crate::wake_on(&key.changes);
541 }
542 for node in 0..self.geometry.fleet_capacity {
543 for word in 0..self.geometry.key_words {
544 self.pending_word(node, word).store(0, Ordering::Relaxed);
545 self.interest_word(node, word).store(0, Ordering::Relaxed);
546 }
547 for key_index in 0..self.geometry.key_capacity {
548 self.claims(node, key_index).store(0, Ordering::Relaxed);
549 }
550 }
551 for wakers in &self.wakers {
552 lock_unpoisoned(wakers).clear();
553 }
554 }
555
556 #[cfg(unix)]
557 pub(crate) fn unlink(&self) -> Result<()> {
558 match &self.backing {
559 Backing::Memory(_) => {
560 self.reset_all();
561 Ok(())
562 }
563 Backing::Shm(region) => region.unlink().map_err(Error::Io),
564 }
565 }
566}
567
568impl Drop for Table {
569 fn drop(&mut self) {
570 if let Some(mut driver) = lock_unpoisoned(&self.driver).take() {
571 driver.stop.store(true, Ordering::Release);
572 let generation = &self.doorbell(usize::from(self.node)).generation;
573 generation.fetch_add(1, Ordering::SeqCst);
574 crate::wake_on(generation);
575 if let Some(thread) = driver.thread.take()
576 && driver.pid == std::process::id()
577 {
578 let _ = thread.join();
579 }
580 }
581 }
582}
583
584struct SendPtr<T>(*const T);
585
586unsafe impl<T: Sync> Send for SendPtr<T> {}
588
589fn mix(lo: u64, hi: u64) -> u64 {
590 (lo ^ hi.rotate_left(32)).wrapping_mul(0x9E37_79B9_7F4A_7C15)
592}
593
594fn next_epoch(previous: u64) -> u64 {
595 OrbitEpoch::now().as_unix_ms().max(previous + 1)
596}
597
598#[derive(Hash, PartialEq, Eq)]
599enum Key {
600 Memory(usize, u8),
601 #[cfg(unix)]
602 Shm(String, u16),
603}
604
605static TABLES: LazyLock<Mutex<HashMap<Key, Weak<Table>>>> =
606 LazyLock::new(|| Mutex::new(HashMap::new()));
607
608pub(crate) fn open(
609 fleet: &Arc<Fleet>,
610 incarnation: Incarnation,
611 spec: PoolSpec,
612) -> Result<Arc<Table>> {
613 if !crate::waits_supported() {
614 return Err(Error::Io(std::io::Error::new(
615 std::io::ErrorKind::Unsupported,
616 "orbit-pool needs a platform that can wait on a shared word: Linux, FreeBSD, or macOS 14.4 or later",
617 )));
618 }
619 let key = if fleet.is_shm() {
622 #[cfg(unix)]
623 {
624 Key::Shm(
625 ring_segment_name(fleet.name(), spec.kind),
626 fleet.node_id().get(),
627 )
628 }
629 #[cfg(not(unix))]
630 unreachable!("non-Unix fleets cannot use POSIX SHM")
631 } else {
632 Key::Memory(Arc::as_ptr(fleet) as usize, spec.kind)
633 };
634 let mut tables = lock_unpoisoned(&TABLES);
635 tables.retain(|_, table| table.strong_count() > 0);
636 if let Some(table) = tables.get(&key).and_then(Weak::upgrade) {
637 if table.incarnation != incarnation.get() {
638 return Err(Error::Malformed(format!(
639 "this process already opened the pool table as incarnation {}",
640 table.incarnation
641 )));
642 }
643 if table.geometry.key_capacity != spec.key_capacity
646 || table.geometry.lane_capacity != spec.lane_capacity
647 {
648 return Err(Error::Malformed(format!(
649 "this process already opened kind {} with key_capacity={} lane_capacity={}",
650 spec.kind, table.geometry.key_capacity, table.geometry.lane_capacity
651 )));
652 }
653 return Ok(table);
654 }
655 let geometry = Geometry::new(fleet.fleet_capacity(), spec);
656 let backing = match &key {
657 Key::Memory(..) => {
658 let bytes = AlignedBytes::zeroed(geometry.segment_size);
659 unsafe {
661 std::ptr::write(
662 bytes.ptr.cast::<Header>(),
663 Header::new(fleet.fleet_capacity(), &geometry, next_epoch(0)),
664 )
665 };
666 Backing::Memory(bytes)
667 }
668 #[cfg(unix)]
669 Key::Shm(name, _) => Backing::Shm(open_shm(name, fleet.fleet_capacity(), &geometry)?),
670 };
671 let table = Arc::new(Table {
672 _fleet: Arc::clone(fleet),
673 backing,
674 geometry,
675 kind: spec.kind,
676 node: fleet.node_id().get(),
677 incarnation: incarnation.get(),
678 structural: Mutex::new(0),
679 wakers: (0..geometry.key_capacity)
680 .map(|_| Mutex::new(Vec::new()))
681 .collect(),
682 readiness: std::sync::OnceLock::new(),
683 driver: Mutex::new(None),
684 });
685 tables.insert(key, Arc::downgrade(&table));
686 Ok(table)
687}
688
689#[cfg(unix)]
690fn open_shm(name: &str, fleet_capacity: u16, geometry: &Geometry) -> Result<ShmRegion> {
691 use std::io;
692
693 let (region, _initialization_lock) =
694 ShmRegion::open_or_create_locked(name, geometry.segment_size)?;
695 if region.created() {
696 unsafe {
698 std::ptr::write(
699 region.as_ptr().cast::<Header>(),
700 Header::new(fleet_capacity, geometry, next_epoch(0)),
701 );
702 }
703 } else {
704 if region.len() < geometry.segment_size {
705 return Err(Error::Io(io::Error::new(
706 io::ErrorKind::InvalidData,
707 format!("SHM segment {name} is smaller than the pool table"),
708 )));
709 }
710 let header = unsafe { &*region.as_ptr().cast::<Header>() };
712 if header.magic != crate::layout::MAGIC {
713 return Err(Error::Io(io::Error::new(
714 io::ErrorKind::InvalidData,
715 format!(
716 "SHM segment {name} has wrong magic 0x{:08X} (expected 0x{:08X})",
717 header.magic,
718 crate::layout::MAGIC
719 ),
720 )));
721 }
722 if !header.compatible(fleet_capacity, geometry) {
723 return Err(Error::Io(io::Error::new(
724 io::ErrorKind::InvalidData,
725 format!("SHM segment {name} has an incompatible pool-table layout"),
726 )));
727 }
728 }
729 Ok(region)
730}
731
732pub fn segment_size(fleet_capacity: u16) -> usize {
734 segment_size_for(fleet_capacity, PoolSpec::DEFAULT)
735}
736
737pub fn segment_size_for(fleet_capacity: u16, spec: PoolSpec) -> usize {
739 Geometry::new(fleet_capacity, spec).segment_size
740}