1use std::fs::{File, OpenOptions};
41use std::path::Path;
42use std::sync::atomic::{AtomicU32, AtomicU64, Ordering as AtomOrd};
43
44use memmap2::{MmapMut, MmapOptions};
45
46use crate::shared_ring::RingError;
47
48pub const PRODUCER_SLOT_CEILING: usize = 4096;
53
54pub const CONSUMER_SLOT_CEILING: usize = 256;
57
58pub const OWNER_NONE: u16 = u16::MAX;
60
61const SLOT_RELEASED: u64 = u64::MAX;
66
67const DIR_MAGIC: u32 = 0x5045_4552; const P_WORDS: usize = PRODUCER_SLOT_CEILING / 64;
70const C_WORDS: usize = CONSUMER_SLOT_CEILING / 64;
71
72#[repr(C)]
73struct DirHeader {
74 magic: AtomicU32,
75 _rsvd: u32,
76 epoch: AtomicU64,
78 published: AtomicU32,
80 active_producers: AtomicU32,
81 active_consumers: AtomicU32,
82 _pad: u32,
83}
84
85const OFF_P_BITMAP: usize = std::mem::size_of::<DirHeader>();
86const OFF_C_BITMAP: usize = OFF_P_BITMAP + P_WORDS * 8;
87const OFF_C_PIDS: usize = OFF_C_BITMAP + C_WORDS * 8;
88const OFF_P_PIDS: usize = OFF_C_PIDS + CONSUMER_SLOT_CEILING * 8;
89const OFF_OWNERS: usize = OFF_P_PIDS + PRODUCER_SLOT_CEILING * 8;
90
91pub const fn peer_directory_size() -> usize {
93 OFF_OWNERS + PRODUCER_SLOT_CEILING * 8
94}
95
96#[allow(dead_code)]
98enum DirBacking {
99 Anon(MmapMut),
100 File(File, MmapMut),
101 Shm(crate::shm_file::ShmFile),
102}
103
104pub struct PeerDirectory {
106 _backing: DirBacking,
107 raw_ptr: *mut u8,
108}
109
110unsafe impl Send for PeerDirectory {}
111unsafe impl Sync for PeerDirectory {}
112
113unsafe fn init_dir_layout(ptr: *mut u8) {
116 unsafe {
117 std::ptr::write_bytes(ptr, 0, peer_directory_size());
118 let owners = ptr.add(OFF_OWNERS) as *mut u64;
120 let none = pack_owner(OWNER_NONE, OWNER_NONE);
121 for i in 0..PRODUCER_SLOT_CEILING {
122 std::ptr::write(owners.add(i), none);
123 }
124 let header = &*(ptr as *const DirHeader);
125 header.magic.store(DIR_MAGIC, AtomOrd::Release);
126 }
127}
128
129#[inline]
130const fn pack_owner(owner: u16, pending: u16) -> u64 {
131 (owner as u64) | ((pending as u64) << 16)
132}
133
134#[inline]
135const fn unpack_owner(word: u64) -> (u16, u16) {
136 (word as u16, (word >> 16) as u16)
137}
138
139impl PeerDirectory {
140 pub fn create_anon() -> Result<Self, RingError> {
142 let mut mmap = MmapOptions::new().len(peer_directory_size()).map_anon()?;
143 unsafe { init_dir_layout(mmap.as_mut_ptr()) };
144 let raw_ptr = mmap.as_mut_ptr();
145 Ok(Self { _backing: DirBacking::Anon(mmap), raw_ptr })
146 }
147
148 pub fn create(path: impl AsRef<Path>) -> Result<Self, RingError> {
153 let total = peer_directory_size();
154 let (file, mut mmap) = crate::mmf_attach::create_or_attach(
155 path.as_ref(),
156 total,
157 |ptr| unsafe { init_dir_layout(ptr) },
158 |ptr| {
159 let header = unsafe { &*(ptr as *const DirHeader) };
160 header.magic.load(AtomOrd::Acquire) == DIR_MAGIC
161 },
162 )
163 .map_err(|e| RingError::IoError(e.kind()))?;
164 let raw_ptr = mmap.as_mut_ptr();
165 Ok(Self { _backing: DirBacking::File(file, mmap), raw_ptr })
166 }
167
168 pub fn reset(path: impl AsRef<Path>) -> Result<Self, RingError> {
171 let total = peer_directory_size();
172 let (file, mut mmap) =
173 crate::mmf_attach::reset(path.as_ref(), total, |ptr| unsafe { init_dir_layout(ptr) })
174 .map_err(|e| RingError::IoError(e.kind()))?;
175 let raw_ptr = mmap.as_mut_ptr();
176 Ok(Self { _backing: DirBacking::File(file, mmap), raw_ptr })
177 }
178
179 pub fn open(path: impl AsRef<Path>) -> Result<Self, RingError> {
182 let total = peer_directory_size();
183 let file = OpenOptions::new().read(true).write(true).open(path.as_ref())?;
184 if (file.metadata()?.len() as usize) < total {
185 return Err(RingError::LayoutMismatch);
186 }
187 let mmap = unsafe { MmapOptions::new().len(total).map_mut(&file)? };
188 let header = unsafe { &*(mmap.as_ptr() as *const DirHeader) };
189 if header.magic.load(AtomOrd::Acquire) != DIR_MAGIC {
190 return Err(RingError::LayoutMismatch);
191 }
192 let raw_ptr = mmap.as_ptr() as *mut u8;
193 Ok(Self { _backing: DirBacking::File(file, mmap), raw_ptr })
194 }
195
196 pub fn create_or_open_shm(name: &str) -> Result<Self, RingError> {
200 let mut shm = crate::shm_file::ShmFile::create_or_open_named(
201 name, peer_directory_size(),
202 ).map_err(|e| RingError::IoError(e.kind()))?;
203 if shm.len() < peer_directory_size() {
204 return Err(RingError::LayoutMismatch);
205 }
206 let raw_ptr = shm.as_mut_slice().as_mut_ptr();
207 let dir = Self { _backing: DirBacking::Shm(shm), raw_ptr };
208 let header = dir.header();
209 if header.magic
210 .compare_exchange(0, 1, AtomOrd::AcqRel, AtomOrd::Acquire)
211 .is_ok()
212 {
213 unsafe { init_dir_layout(raw_ptr) };
216 } else {
217 let mut spins = 0u32;
218 while dir.header().magic.load(AtomOrd::Acquire) != DIR_MAGIC {
219 std::hint::spin_loop();
220 spins += 1;
221 if spins > 100_000_000 {
222 return Err(RingError::LayoutMismatch);
223 }
224 }
225 }
226 Ok(dir)
227 }
228
229 #[inline]
230 fn header(&self) -> &DirHeader {
231 unsafe { &*(self.raw_ptr as *const DirHeader) }
232 }
233
234 #[inline]
235 fn bitmap_word(&self, base: usize, i: usize) -> &AtomicU64 {
236 unsafe { &*(self.raw_ptr.add(base + i * 8) as *const AtomicU64) }
237 }
238
239 #[inline]
240 fn owner_word(&self, ring: usize) -> &AtomicU64 {
241 debug_assert!(ring < PRODUCER_SLOT_CEILING);
242 unsafe { &*(self.raw_ptr.add(OFF_OWNERS + ring * 8) as *const AtomicU64) }
243 }
244
245 #[inline]
246 fn pid_word(&self, slot: usize) -> &AtomicU64 {
247 debug_assert!(slot < CONSUMER_SLOT_CEILING);
248 unsafe { &*(self.raw_ptr.add(OFF_C_PIDS + slot * 8) as *const AtomicU64) }
249 }
250
251 #[inline]
252 fn producer_pid_word(&self, slot: usize) -> &AtomicU64 {
253 debug_assert!(slot < PRODUCER_SLOT_CEILING);
254 unsafe { &*(self.raw_ptr.add(OFF_P_PIDS + slot * 8) as *const AtomicU64) }
255 }
256
257 #[inline]
260 pub fn epoch(&self) -> u64 {
261 self.header().epoch.load(AtomOrd::Acquire)
262 }
263
264 pub fn bump_epoch(&self) -> u64 {
266 self.header().epoch.fetch_add(1, AtomOrd::AcqRel) + 1
267 }
268
269 #[inline]
271 pub fn published(&self) -> usize {
272 self.header().published.load(AtomOrd::Acquire) as usize
273 }
274
275 pub fn publish_rings(&self, to: usize) {
279 self.header().published.fetch_max(to as u32, AtomOrd::AcqRel);
280 self.bump_epoch();
281 }
282
283 #[inline]
285 pub fn active_producers(&self) -> usize {
286 self.header().active_producers.load(AtomOrd::Acquire) as usize
287 }
288
289 #[inline]
291 pub fn active_consumers(&self) -> usize {
292 self.header().active_consumers.load(AtomOrd::Acquire) as usize
293 }
294
295 pub fn claim_producer_slot(&self) -> Option<usize> {
299 let slot = self.claim_bit(OFF_P_BITMAP, P_WORDS)?;
300 self.producer_pid_word(slot).store(std::process::id() as u64, AtomOrd::Release);
301 self.header().active_producers.fetch_add(1, AtomOrd::AcqRel);
302 self.bump_epoch();
303 Some(slot)
304 }
305
306 pub fn release_producer_slot(&self, slot: usize) {
309 if slot < PRODUCER_SLOT_CEILING {
310 self.producer_pid_word(slot).store(0, AtomOrd::Release);
311 }
312 if self.release_bit(OFF_P_BITMAP, P_WORDS, slot) {
313 self.header().active_producers.fetch_sub(1, AtomOrd::AcqRel);
314 self.bump_epoch();
315 }
316 }
317
318 pub fn reap_dead_peers(&self) {
324 for slot in self.claimed_slots(OFF_P_BITMAP, P_WORDS) {
325 let pid = self.producer_pid_word(slot).load(AtomOrd::Acquire);
326 if pid != 0 && pid != std::process::id() as u64
327 && !process_alive(pid as u32)
328 {
329 self.release_producer_slot(slot);
330 }
331 }
332 for slot in self.claimed_slots(OFF_C_BITMAP, C_WORDS) {
333 let pid = self.pid_word(slot).load(AtomOrd::Acquire);
334 if pid != 0 && pid != std::process::id() as u64
335 && !process_alive(pid as u32)
336 {
337 self.release_consumer_slot(slot);
338 }
339 }
340 }
341
342 fn claimed_slots(&self, base: usize, words: usize) -> Vec<usize> {
343 let mut slots = Vec::new();
344 for w in 0..words {
345 let mut word = self.bitmap_word(base, w).load(AtomOrd::Acquire);
346 while word != 0 {
347 let bit = word.trailing_zeros() as usize;
348 slots.push(w * 64 + bit);
349 word &= word - 1;
350 }
351 }
352 slots
353 }
354
355 pub fn claim_consumer_slot(&self) -> Option<usize> {
358 let slot = self.claim_bit(OFF_C_BITMAP, C_WORDS)?;
359 self.pid_word(slot).store(std::process::id() as u64, AtomOrd::Release);
360 self.header().active_consumers.fetch_add(1, AtomOrd::AcqRel);
361 self.bump_epoch();
362 Some(slot)
363 }
364
365 pub fn release_consumer_slot(&self, slot: usize) {
369 if slot >= CONSUMER_SLOT_CEILING {
370 return;
371 }
372 self.pid_word(slot).store(SLOT_RELEASED, AtomOrd::Release);
373 if self.release_bit(OFF_C_BITMAP, C_WORDS, slot) {
374 self.header().active_consumers.fetch_sub(1, AtomOrd::AcqRel);
375 self.bump_epoch();
376 }
377 }
378
379 pub fn consumer_slot_claimed(&self, slot: usize) -> bool {
381 if slot >= CONSUMER_SLOT_CEILING {
382 return false;
383 }
384 let word = self.bitmap_word(OFF_C_BITMAP, slot / 64).load(AtomOrd::Acquire);
385 word & (1u64 << (slot % 64)) != 0
386 }
387
388 pub fn claimed_consumer_slots(&self) -> Vec<u16> {
392 let mut slots = Vec::new();
393 for w in 0..C_WORDS {
394 let mut word = self.bitmap_word(OFF_C_BITMAP, w).load(AtomOrd::Acquire);
395 while word != 0 {
396 let bit = word.trailing_zeros() as usize;
397 slots.push((w * 64 + bit) as u16);
398 word &= word - 1;
399 }
400 }
401 slots
402 }
403
404 #[inline]
406 pub fn ring_owner(&self, ring: usize) -> (u16, u16) {
407 unpack_owner(self.owner_word(ring).load(AtomOrd::Acquire))
408 }
409
410 pub fn try_claim_ring(&self, ring: usize, me: u16) -> bool {
415 let cur = pack_owner(OWNER_NONE, OWNER_NONE);
416 self.owner_word(ring)
417 .compare_exchange(cur, pack_owner(me, OWNER_NONE),
418 AtomOrd::AcqRel, AtomOrd::Acquire)
419 .is_ok()
420 }
421
422 pub fn request_handoff(&self, ring: usize, target: u16) {
426 let word = self.owner_word(ring);
427 let mut cur = word.load(AtomOrd::Acquire);
428 loop {
429 let (owner, _) = unpack_owner(cur);
430 if owner == target || owner == OWNER_NONE {
431 return; }
433 match word.compare_exchange(cur, pack_owner(owner, target),
434 AtomOrd::AcqRel, AtomOrd::Acquire) {
435 Ok(_) => return,
436 Err(actual) => cur = actual,
437 }
438 }
439 }
440
441 pub fn apply_handoff(&self, ring: usize, me: u16) -> Option<u16> {
445 let word = self.owner_word(ring);
446 let cur = word.load(AtomOrd::Acquire);
447 let (owner, pending) = unpack_owner(cur);
448 if owner != me || pending == OWNER_NONE {
449 return None;
450 }
451 match word.compare_exchange(cur, pack_owner(pending, OWNER_NONE),
452 AtomOrd::AcqRel, AtomOrd::Acquire) {
453 Ok(_) => {
454 self.bump_epoch();
455 Some(pending)
456 }
457 Err(_) => None,
458 }
459 }
460
461 pub fn transfer_ring(&self, ring: usize, me: u16, to: u16) {
464 let word = self.owner_word(ring);
465 let mut cur = word.load(AtomOrd::Acquire);
466 loop {
467 let (owner, _) = unpack_owner(cur);
468 if owner != me {
469 return;
470 }
471 match word.compare_exchange(cur, pack_owner(to, OWNER_NONE),
472 AtomOrd::AcqRel, AtomOrd::Acquire) {
473 Ok(_) => return,
474 Err(actual) => cur = actual,
475 }
476 }
477 }
478
479 pub fn try_takeover(&self, ring: usize, dead_owner: u16, me: u16) -> bool {
485 let slot = dead_owner as usize;
486 let pid = self.pid_word(slot).load(AtomOrd::Acquire);
487 if pid != SLOT_RELEASED {
488 if !self.consumer_slot_claimed(slot) {
489 return false;
490 }
491 if pid == std::process::id() as u64 || pid == 0 || process_alive(pid as u32) {
492 return false;
493 }
494 }
495 let word = self.owner_word(ring);
496 let cur = word.load(AtomOrd::Acquire);
497 let (owner, _) = unpack_owner(cur);
498 if owner != dead_owner {
499 return false;
500 }
501 let swapped = word.compare_exchange(
502 cur, pack_owner(me, OWNER_NONE), AtomOrd::AcqRel, AtomOrd::Acquire,
503 ).is_ok();
504 if swapped {
505 self.bump_epoch();
506 }
507 swapped
508 }
509
510 fn claim_bit(&self, base: usize, words: usize) -> Option<usize> {
511 for w in 0..words {
512 let word = self.bitmap_word(base, w);
513 let mut cur = word.load(AtomOrd::Acquire);
514 loop {
515 if cur == u64::MAX {
516 break; }
518 let bit = (!cur).trailing_zeros() as usize;
519 match word.compare_exchange(cur, cur | (1u64 << bit),
520 AtomOrd::AcqRel, AtomOrd::Acquire) {
521 Ok(_) => return Some(w * 64 + bit),
522 Err(actual) => cur = actual,
523 }
524 }
525 }
526 None
527 }
528
529 fn release_bit(&self, base: usize, words: usize, slot: usize) -> bool {
530 if slot >= words * 64 {
531 return false;
532 }
533 let word = self.bitmap_word(base, slot / 64);
534 let mask = 1u64 << (slot % 64);
535 word.fetch_and(!mask, AtomOrd::AcqRel) & mask != 0
536 }
537}
538
539#[cfg(unix)]
543fn process_alive(pid: u32) -> bool {
544 let rc = unsafe { libc::kill(pid as libc::pid_t, 0) };
545 rc == 0
546 || std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
547}
548
549#[cfg(windows)]
550fn process_alive(pid: u32) -> bool {
551 use windows_sys::Win32::Foundation::{CloseHandle, STILL_ACTIVE};
552 use windows_sys::Win32::System::Threading::{
553 GetExitCodeProcess, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
554 };
555 unsafe {
556 let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
557 if handle.is_null() {
558 return std::io::Error::last_os_error().raw_os_error()
561 == Some(5 );
562 }
563 let mut code: u32 = 0;
564 let ok = GetExitCodeProcess(handle, &mut code);
565 CloseHandle(handle);
566 ok != 0 && code == STILL_ACTIVE as u32
567 }
568}
569
570#[cfg(test)]
571mod tests {
572 use super::*;
573
574 #[test]
575 fn producer_slots_claim_release_recycle() {
576 let dir = PeerDirectory::create_anon().unwrap();
577 assert_eq!(dir.claim_producer_slot(), Some(0));
578 assert_eq!(dir.claim_producer_slot(), Some(1));
579 assert_eq!(dir.claim_producer_slot(), Some(2));
580 assert_eq!(dir.active_producers(), 3);
581 dir.release_producer_slot(1);
582 assert_eq!(dir.active_producers(), 2);
583 assert_eq!(dir.claim_producer_slot(), Some(1));
585 assert_eq!(dir.active_producers(), 3);
586 }
587
588 #[test]
589 fn epoch_bumps_on_every_topology_change() {
590 let dir = PeerDirectory::create_anon().unwrap();
591 let e0 = dir.epoch();
592 let p = dir.claim_producer_slot().unwrap();
593 assert!(dir.epoch() > e0);
594 let e1 = dir.epoch();
595 dir.release_producer_slot(p);
596 assert!(dir.epoch() > e1);
597 let e2 = dir.epoch();
598 dir.publish_rings(4);
599 assert!(dir.epoch() > e2);
600 assert_eq!(dir.published(), 4);
601 dir.publish_rings(2);
603 assert_eq!(dir.published(), 4);
604 }
605
606 #[test]
607 fn ring_ownership_claim_handoff_transfer() {
608 let dir = PeerDirectory::create_anon().unwrap();
609 assert_eq!(dir.ring_owner(0), (OWNER_NONE, OWNER_NONE));
610 assert!(dir.try_claim_ring(0, 3));
611 assert!(!dir.try_claim_ring(0, 5), "claimed ring must reject CAS");
612 assert_eq!(dir.ring_owner(0), (3, OWNER_NONE));
613
614 dir.request_handoff(0, 7);
616 assert_eq!(dir.ring_owner(0), (3, 7));
617 assert_eq!(dir.apply_handoff(0, 5), None, "non-owner cannot apply");
618 assert_eq!(dir.apply_handoff(0, 3), Some(7));
619 assert_eq!(dir.ring_owner(0), (7, OWNER_NONE));
620
621 dir.transfer_ring(0, 7, 2);
623 assert_eq!(dir.ring_owner(0), (2, OWNER_NONE));
624 }
625
626 #[test]
627 fn takeover_requires_dead_or_released_owner() {
628 let dir = PeerDirectory::create_anon().unwrap();
629 let slot = dir.claim_consumer_slot().unwrap() as u16;
630 assert!(dir.try_claim_ring(0, slot));
631 assert!(!dir.try_takeover(0, slot, 9));
633 dir.release_consumer_slot(slot as usize);
635 assert!(dir.try_takeover(0, slot, 9));
636 assert_eq!(dir.ring_owner(0), (9, OWNER_NONE));
637 }
638
639 #[test]
645 fn an_owner_that_never_claimed_a_slot_is_not_taken_over() {
646 let dir = PeerDirectory::create_anon().unwrap();
647 let owner: u16 = 3;
648 assert!(!dir.consumer_slot_claimed(owner as usize));
649 assert!(dir.try_claim_ring(0, owner), "ring claim needs no slot");
650
651 assert!(
652 !dir.try_takeover(0, owner, 9),
653 "an owner with no slot claim is live, not departed"
654 );
655 assert_eq!(dir.ring_owner(0), (owner, OWNER_NONE));
656
657 let slot = dir.claim_consumer_slot().unwrap();
659 dir.release_consumer_slot(slot);
660 assert!(dir.try_claim_ring(1, slot as u16));
661 assert!(dir.try_takeover(1, slot as u16, 9));
662 assert_eq!(dir.ring_owner(1), (9, OWNER_NONE));
663 }
664
665 #[test]
666 fn consumer_slots_record_pid_and_enumerate_densely() {
667 let dir = PeerDirectory::create_anon().unwrap();
668 let a = dir.claim_consumer_slot().unwrap();
669 let b = dir.claim_consumer_slot().unwrap();
670 let c = dir.claim_consumer_slot().unwrap();
671 assert_eq!((a, b, c), (0, 1, 2));
672 dir.release_consumer_slot(b);
673 assert_eq!(dir.claimed_consumer_slots(), vec![0u16, 2]);
674 assert_eq!(dir.active_consumers(), 2);
675 }
676
677 #[test]
678 fn file_backed_directory_shares_claims_across_handles() {
679 let base = std::env::temp_dir()
680 .join(format!("subetha_peerdir_{}", std::process::id()));
681 let path = base.with_extension("peers.bin");
682 let dir_a = PeerDirectory::create(&path).unwrap();
683 let p = dir_a.claim_producer_slot().unwrap();
684 dir_a.publish_rings(p + 1);
685
686 let dir_b = PeerDirectory::open(&path).unwrap();
687 assert_eq!(dir_b.active_producers(), 1);
688 assert_eq!(dir_b.published(), p + 1);
689 assert_eq!(dir_b.claim_producer_slot(), Some(p + 1));
690 assert_eq!(dir_a.active_producers(), 2, "claim visible both ways");
691
692 drop(dir_a);
693 drop(dir_b);
694 std::fs::remove_file(&path).ok();
695 }
696}