1use std::sync::Arc;
5use std::sync::atomic::{AtomicU64, Ordering};
6
7use bytes::Bytes;
8use dashmap::DashMap;
9
10use crate::OrbitTyped;
11use crate::error::{Error, Result};
12use crate::id::NetId64;
13#[cfg(unix)]
14use crate::ring::shm::{ShmRing, ShmRingRegistry};
15use crate::ring::{Frame, Ring, RingRegistry, RingTopology};
16#[cfg(any(
17 target_os = "linux",
18 target_os = "freebsd",
19 target_os = "macos"
20))]
21use crate::ring::{ParkedRingEventFd, RingEventFd};
22
23mod cursor;
24pub use cursor::{FleetLaneCursor, FleetLanePoll};
25mod name;
26pub use name::{FleetName, InvalidFleetName};
27
28#[cfg(unix)]
35#[derive(Clone, Debug, PartialEq, Eq)]
36pub struct FleetObserver {
37 name: String,
38 uid: u32,
39}
40
41#[cfg(unix)]
42impl FleetObserver {
43 pub fn attach_existing(name: impl Into<String>) -> std::io::Result<Self> {
49 let uid = unsafe { libc::geteuid() };
51 Self::attach_existing_for_uid(name, uid)
52 }
53
54 pub fn attach_existing_for_uid(
59 name: impl Into<String>,
60 uid: u32,
61 ) -> std::io::Result<Self> {
62 let name = name.into();
63 if name.is_empty() {
64 return Err(std::io::Error::new(
65 std::io::ErrorKind::InvalidInput,
66 "fleet name must not be empty",
67 ));
68 }
69 if name.contains('/') || name.contains('\0') {
70 return Err(std::io::Error::new(
71 std::io::ErrorKind::InvalidInput,
72 "fleet name must not contain '/' or a NUL byte",
73 ));
74 }
75 Ok(Self { name, uid })
76 }
77
78 pub fn name(&self) -> &str {
79 &self.name
80 }
81
82 pub fn uid(&self) -> u32 {
83 self.uid
84 }
85
86 pub fn ring(
88 &self,
89 kind: u8,
90 ) -> std::io::Result<crate::ring::shm::ShmRingView> {
91 crate::ring::shm::ShmRingView::attach_existing_for_uid(
92 &self.name, kind, self.uid,
93 )
94 }
95
96 pub fn ring_with_policy(
98 &self,
99 kind: u8,
100 policy: crate::shm::ShmAccessPolicy,
101 ) -> std::io::Result<crate::ring::shm::ShmRingView> {
102 crate::ring::shm::ShmRingView::attach_existing_for_uid_with_policy(
103 &self.name, kind, self.uid, policy,
104 )
105 }
106
107 pub fn typed_ring<T: OrbitTyped>(
109 &self
110 ) -> std::io::Result<crate::ring::shm::ShmRingView> {
111 self.typed_ring_with_policy::<T>(crate::shm::ShmAccessPolicy::default())
112 }
113
114 pub fn typed_ring_with_policy<T: OrbitTyped>(
116 &self,
117 policy: crate::shm::ShmAccessPolicy,
118 ) -> std::io::Result<crate::ring::shm::ShmRingView> {
119 let view = self.ring_with_policy(T::KIND, policy)?;
120 if view.metadata().spec != T::RING_SPEC {
121 return Err(std::io::Error::new(
122 std::io::ErrorKind::InvalidData,
123 format!(
124 "OrbitTyped KIND {} declares {:?}; existing spec is {:?}",
125 T::KIND,
126 T::RING_SPEC,
127 view.metadata().spec
128 ),
129 ));
130 }
131 Ok(view)
132 }
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
142#[repr(transparent)]
143pub struct NodeId(pub u16);
144
145impl NodeId {
146 pub const ZERO: Self = Self(0);
147
148 pub const fn new(value: u16) -> Self {
149 Self(value)
150 }
151
152 pub const fn get(self) -> u16 {
153 self.0
154 }
155}
156
157impl std::fmt::Display for NodeId {
158 fn fmt(
159 &self,
160 f: &mut std::fmt::Formatter<'_>,
161 ) -> std::fmt::Result {
162 write!(f, "node:{}", self.0)
163 }
164}
165
166#[derive(Clone)]
169pub struct Fleet {
170 inner: Arc<FleetInner>,
171}
172
173struct FleetInner {
174 name: Arc<str>,
175 fleet_capacity: u16,
176 node_id: NodeId,
177 id_counters: DashMap<u8, Arc<AtomicU64>>,
182 backing: RingBacking,
186 #[cfg(unix)]
190 #[allow(dead_code)]
191 membership: Option<crate::shm::FleetMembership>,
192}
193
194enum RingBacking {
198 InMemory(RingRegistry),
202 #[cfg(unix)]
206 Shm(ShmRingRegistry),
207}
208
209impl Fleet {
210 pub fn join(
213 name: &str,
214 fleet_capacity: u16,
215 ) -> Result<Self> {
216 Self::join_as(name, fleet_capacity, NodeId::ZERO)
217 }
218
219 pub fn join_as(
221 name: &str,
222 fleet_capacity: u16,
223 node_id: NodeId,
224 ) -> Result<Self> {
225 if fleet_capacity == 0 {
226 return Err(Error::EmptyFleet);
227 }
228 if node_id.get() >= fleet_capacity {
229 return Err(Error::NodeOutsideFleet {
230 node_id: node_id.get(),
231 fleet_capacity,
232 });
233 }
234 Ok(Self {
235 inner: Arc::new(FleetInner {
236 name: Arc::from(name),
237 fleet_capacity,
238 node_id,
239 id_counters: DashMap::new(),
240 backing: RingBacking::InMemory(RingRegistry::new(
241 fleet_capacity,
242 )),
243 #[cfg(unix)]
244 membership: None,
245 }),
246 })
247 }
248
249 #[cfg(unix)]
261 pub fn join_shm(
262 name: &str,
263 fleet_capacity: u16,
264 ) -> Result<Self> {
265 Self::join_shm_as(name, fleet_capacity, NodeId::ZERO)
266 }
267
268 #[cfg(unix)]
274 pub fn join_shm_as(
275 name: &str,
276 fleet_capacity: u16,
277 node_id: NodeId,
278 ) -> Result<Self> {
279 Self::join_shm_as_with_policies(name, fleet_capacity, node_id, [])
280 }
281
282 #[cfg(unix)]
286 pub fn join_shm_as_with_policies(
287 name: &str,
288 fleet_capacity: u16,
289 node_id: NodeId,
290 policies: impl IntoIterator<Item = (u8, crate::shm::ShmAccessPolicy)>,
291 ) -> Result<Self> {
292 if fleet_capacity == 0 {
293 return Err(Error::EmptyFleet);
294 }
295 if node_id.get() >= fleet_capacity {
296 return Err(Error::NodeOutsideFleet {
297 node_id: node_id.get(),
298 fleet_capacity,
299 });
300 }
301 let name: Arc<str> = Arc::from(name);
302 let membership =
303 crate::shm::join_fleet_membership(&name).map_err(Error::Io)?;
304 Ok(Self {
305 inner: Arc::new(FleetInner {
306 name: Arc::clone(&name),
307 fleet_capacity,
308 node_id,
309 id_counters: DashMap::new(),
310 backing: RingBacking::Shm(ShmRingRegistry::with_policies(
311 name.as_ref(),
312 fleet_capacity,
313 policies,
314 )),
315 membership: Some(membership),
316 }),
317 })
318 }
319
320 pub fn name(&self) -> &str {
321 &self.inner.name
322 }
323
324 pub fn ptr_eq(
328 &self,
329 other: &Self,
330 ) -> bool {
331 Arc::ptr_eq(&self.inner, &other.inner)
332 }
333
334 pub fn fleet_capacity(&self) -> u16 {
336 self.inner.fleet_capacity
337 }
338
339 pub fn node_id(&self) -> NodeId {
340 self.inner.node_id
341 }
342
343 pub fn next_id<T: OrbitTyped>(&self) -> NetId64 {
351 let counter_arc = self
352 .inner
353 .id_counters
354 .entry(T::KIND)
355 .or_insert_with(|| Arc::new(AtomicU64::new(0)))
356 .clone();
357 let counter = counter_arc.fetch_add(1, Ordering::Relaxed);
358 NetId64::make(T::KIND, self.node_id().get(), counter)
359 }
360
361 pub fn is_shm(&self) -> bool {
364 #[cfg(unix)]
365 {
366 matches!(self.inner.backing, RingBacking::Shm(_))
367 }
368 #[cfg(not(unix))]
369 {
370 false
371 }
372 }
373
374 pub fn ring<T: OrbitTyped>(&self) -> Arc<Ring> {
382 match &self.inner.backing {
383 RingBacking::InMemory(r) => r.get_or_create::<T>(),
384 #[cfg(unix)]
385 RingBacking::Shm(_) => {
386 panic!(
387 "Fleet::ring called on SHM-backed fleet — use Fleet::shm_ring instead"
388 );
389 }
390 }
391 }
392
393 #[cfg(unix)]
405 pub fn shm_ring<T: OrbitTyped>(&self) -> std::io::Result<Arc<ShmRing>> {
406 match &self.inner.backing {
407 RingBacking::Shm(r) => r.get_or_create_for::<T>(),
408 RingBacking::InMemory(_) => {
409 panic!(
410 "Fleet::shm_ring called on in-memory fleet — use Fleet::ring instead"
411 );
412 }
413 }
414 }
415
416 pub fn publish<T: OrbitTyped>(
426 &self,
427 frame_kind: u8,
428 ver: u64,
429 payload: Bytes,
430 ) -> NetId64 {
431 match &self.inner.backing {
432 RingBacking::InMemory(r) => {
433 let ring = r.get_or_create::<T>();
434 ring.write(self.node_id(), frame_kind, ver, payload)
435 }
436 #[cfg(unix)]
437 RingBacking::Shm(r) => {
438 let ring = r
439 .get_or_create_for::<T>()
440 .expect("SHM ring open failed — fleet unusable");
441 ring.write(self.node_id(), frame_kind, ver, payload)
442 .expect("SHM ring write failed")
443 }
444 }
445 }
446
447 pub fn publish_batch<T: OrbitTyped>(
460 &self,
461 frame_kind: u8,
462 ver: u64,
463 payloads: Vec<Bytes>,
464 ) -> Vec<NetId64> {
465 match &self.inner.backing {
466 RingBacking::InMemory(r) => {
467 let ring = r.get_or_create::<T>();
468 ring.write_batch(self.node_id(), frame_kind, ver, payloads)
469 }
470 #[cfg(unix)]
471 RingBacking::Shm(r) => {
472 let ring = r
473 .get_or_create_for::<T>()
474 .expect("SHM ring open failed — fleet unusable");
475 ring.write_batch(self.node_id(), frame_kind, ver, payloads)
476 .expect("SHM ring batch write failed")
477 }
478 }
479 }
480
481 pub fn read(
485 &self,
486 id: NetId64,
487 ) -> Option<Frame> {
488 match &self.inner.backing {
489 RingBacking::InMemory(r) => r.lookup(id.kind())?.read(id),
490 #[cfg(unix)]
491 RingBacking::Shm(r) => r.lookup(id.kind())?.read(id),
492 }
493 }
494
495 pub fn read_head<T: OrbitTyped>(&self) -> Option<Frame> {
498 if T::RING_SPEC.topology == RingTopology::PerNode {
499 let head = self.lane_head::<T>(self.node_id());
500 return (head > 0)
501 .then(|| self.read_lane_at::<T>(self.node_id(), head - 1))
502 .flatten();
503 }
504 match &self.inner.backing {
505 RingBacking::InMemory(r) => {
506 let ring = r.get_or_create::<T>();
507 ring.read_head()
508 }
509 #[cfg(unix)]
510 RingBacking::Shm(r) => {
511 let ring = r.get_or_create_for::<T>().ok()?;
512 ring.read_head()
513 }
514 }
515 }
516
517 pub fn head<T: OrbitTyped>(&self) -> u64 {
526 if T::RING_SPEC.topology == RingTopology::PerNode {
527 return self.lane_head::<T>(self.node_id());
528 }
529 match &self.inner.backing {
530 RingBacking::InMemory(r) => r.get_or_create::<T>().head(),
531 #[cfg(unix)]
532 RingBacking::Shm(r) => {
533 r.get_or_create_for::<T>().map(|ring| ring.head()).unwrap_or(0)
534 }
535 }
536 }
537
538 pub fn read_at<T: OrbitTyped>(
546 &self,
547 counter: u64,
548 ) -> Option<Frame> {
549 if T::RING_SPEC.topology == RingTopology::PerNode {
550 return self.read_lane_at::<T>(self.node_id(), counter);
551 }
552 match &self.inner.backing {
553 RingBacking::InMemory(r) => r.get_or_create::<T>().read_at(counter),
554 #[cfg(unix)]
555 RingBacking::Shm(r) => {
556 r.get_or_create_for::<T>().ok()?.read_at(counter)
557 }
558 }
559 }
560
561 pub(crate) fn read_state_at<T: OrbitTyped>(
562 &self,
563 counter: u64,
564 ) -> crate::ring::cursor::RingRead {
565 if T::RING_SPEC.topology == RingTopology::PerNode {
566 return self.read_lane_state_at::<T>(self.node_id(), counter);
567 }
568 match &self.inner.backing {
569 RingBacking::InMemory(r) => {
570 r.get_or_create::<T>().read_state_at(counter)
571 }
572 #[cfg(unix)]
573 RingBacking::Shm(r) => r
574 .get_or_create_for::<T>()
575 .map(|ring| ring.read_state_at(counter))
576 .unwrap_or(crate::ring::cursor::RingRead::Unavailable),
577 }
578 }
579
580 pub fn lane_head<T: OrbitTyped>(
584 &self,
585 node_id: NodeId,
586 ) -> u64 {
587 match &self.inner.backing {
588 RingBacking::InMemory(r) => {
589 r.get_or_create::<T>().lane_head(node_id)
590 }
591 #[cfg(unix)]
592 RingBacking::Shm(r) => r
593 .get_or_create_for::<T>()
594 .map(|ring| ring.lane_head(node_id))
595 .unwrap_or(0),
596 }
597 }
598
599 pub fn read_lane_at<T: OrbitTyped>(
601 &self,
602 node_id: NodeId,
603 counter: u64,
604 ) -> Option<Frame> {
605 match &self.inner.backing {
606 RingBacking::InMemory(r) => {
607 r.get_or_create::<T>().read_lane_at(node_id, counter)
608 }
609 #[cfg(unix)]
610 RingBacking::Shm(r) => {
611 r.get_or_create_for::<T>().ok()?.read_lane_at(node_id, counter)
612 }
613 }
614 }
615
616 pub(crate) fn read_lane_state_at<T: OrbitTyped>(
617 &self,
618 node_id: NodeId,
619 counter: u64,
620 ) -> crate::ring::cursor::RingRead {
621 match &self.inner.backing {
622 RingBacking::InMemory(r) => {
623 r.get_or_create::<T>().read_lane_state_at(node_id, counter)
624 }
625 #[cfg(unix)]
626 RingBacking::Shm(r) => r
627 .get_or_create_for::<T>()
628 .map(|ring| ring.read_lane_state_at(node_id, counter))
629 .unwrap_or(crate::ring::cursor::RingRead::Unavailable),
630 }
631 }
632
633 pub fn ring_capacity<T: OrbitTyped>(&self) -> usize {
637 match &self.inner.backing {
638 RingBacking::InMemory(r) => r.get_or_create::<T>().capacity(),
639 #[cfg(unix)]
640 RingBacking::Shm(r) => r
641 .get_or_create_for::<T>()
642 .map(|ring| ring.capacity())
643 .unwrap_or(T::RING_SPEC.capacity),
644 }
645 }
646
647 pub fn next_ring_version<T: OrbitTyped>(&self) -> u64 {
653 match &self.inner.backing {
654 RingBacking::InMemory(r) => r.get_or_create::<T>().next_version(),
655 #[cfg(unix)]
656 RingBacking::Shm(r) => r
657 .get_or_create_for::<T>()
658 .expect("SHM ring open failed — fleet unusable")
659 .next_version(),
660 }
661 }
662
663 pub fn current_ring_version<T: OrbitTyped>(&self) -> u64 {
665 match &self.inner.backing {
666 RingBacking::InMemory(r) => {
667 r.get_or_create::<T>().current_version()
668 }
669 #[cfg(unix)]
670 RingBacking::Shm(r) => r
671 .get_or_create_for::<T>()
672 .expect("SHM ring open failed — fleet unusable")
673 .current_version(),
674 }
675 }
676
677 pub fn reset_ring<T: OrbitTyped>(&self) -> std::io::Result<()> {
685 match &self.inner.backing {
686 RingBacking::InMemory(r) => {
687 r.get_or_create::<T>().reset();
688 Ok(())
689 }
690 #[cfg(unix)]
691 RingBacking::Shm(r) => {
692 r.get_or_create_for::<T>()?.reset();
693 Ok(())
694 }
695 }
696 }
697
698 #[cfg(any(
699 target_os = "linux",
700 target_os = "freebsd",
701 target_os = "macos"
702 ))]
703 pub fn ring_event_fd<T: OrbitTyped>(&self) -> std::io::Result<RingEventFd> {
709 match &self.inner.backing {
710 RingBacking::Shm(rings) => {
711 RingEventFd::new(rings.get_or_create_for::<T>()?)
712 }
713 RingBacking::InMemory(_) => Err(std::io::Error::new(
714 std::io::ErrorKind::Unsupported,
715 "Orbit eventfd requires a shared-memory fleet",
716 )),
717 }
718 }
719
720 #[cfg(any(
721 target_os = "linux",
722 target_os = "freebsd",
723 target_os = "macos"
724 ))]
725 pub fn publish_notified<T: OrbitTyped>(
727 &self,
728 frame_kind: u8,
729 ver: u64,
730 payload: Bytes,
731 ) -> std::io::Result<NetId64> {
732 match &self.inner.backing {
733 RingBacking::Shm(rings) => {
734 let ring = rings.get_or_create_for::<T>()?;
735 let id =
736 ring.write(self.node_id(), frame_kind, ver, payload)?;
737 RingEventFd::notify(&ring)?;
738 Ok(id)
739 }
740 RingBacking::InMemory(rings) => Ok(rings
742 .get_or_create::<T>()
743 .write(self.node_id(), frame_kind, ver, payload)),
744 }
745 }
746
747 #[cfg(any(
748 target_os = "linux",
749 target_os = "freebsd",
750 target_os = "macos"
751 ))]
752 pub fn ring_event_fd_parked<T: OrbitTyped>(
757 &self
758 ) -> std::io::Result<ParkedRingEventFd> {
759 match &self.inner.backing {
760 RingBacking::Shm(rings) => {
761 ParkedRingEventFd::new(rings.get_or_create_for::<T>()?)
762 }
763 RingBacking::InMemory(_) => Err(std::io::Error::new(
764 std::io::ErrorKind::Unsupported,
765 "Orbit parked readiness requires a shared-memory fleet",
766 )),
767 }
768 }
769
770 #[cfg(any(
771 target_os = "linux",
772 target_os = "freebsd",
773 target_os = "macos"
774 ))]
775 pub fn publish_notified_parked<T: OrbitTyped>(
782 &self,
783 frame_kind: u8,
784 ver: u64,
785 payload: Bytes,
786 ) -> std::io::Result<NetId64> {
787 match &self.inner.backing {
788 RingBacking::Shm(rings) => {
789 let ring = rings.get_or_create_for::<T>()?;
790 let id =
791 ring.write(self.node_id(), frame_kind, ver, payload)?;
792 ParkedRingEventFd::notify(&ring)?;
793 Ok(id)
794 }
795 RingBacking::InMemory(rings) => Ok(rings
796 .get_or_create::<T>()
797 .write(self.node_id(), frame_kind, ver, payload)),
798 }
799 }
800
801 #[cfg(any(
802 target_os = "linux",
803 target_os = "freebsd",
804 target_os = "macos"
805 ))]
806 pub fn publish_batch_notified_parked<T: OrbitTyped>(
811 &self,
812 frame_kind: u8,
813 ver: u64,
814 payloads: Vec<Bytes>,
815 ) -> std::io::Result<Vec<NetId64>> {
816 match &self.inner.backing {
817 RingBacking::Shm(rings) => {
818 let ring = rings.get_or_create_for::<T>()?;
819 let ids = ring.write_batch(
820 self.node_id(),
821 frame_kind,
822 ver,
823 payloads,
824 )?;
825 if !ids.is_empty() {
826 ParkedRingEventFd::notify(&ring)?;
827 }
828 Ok(ids)
829 }
830 RingBacking::InMemory(rings) => Ok(rings
831 .get_or_create::<T>()
832 .write_batch(self.node_id(), frame_kind, ver, payloads)),
833 }
834 }
835
836 #[cfg(any(
837 target_os = "linux",
838 target_os = "freebsd",
839 target_os = "macos"
840 ))]
841 pub fn publish_batch_notified<T: OrbitTyped>(
844 &self,
845 frame_kind: u8,
846 ver: u64,
847 payloads: Vec<Bytes>,
848 ) -> std::io::Result<Vec<NetId64>> {
849 match &self.inner.backing {
850 RingBacking::Shm(rings) => {
851 let ring = rings.get_or_create_for::<T>()?;
852 let ids = ring.write_batch(
853 self.node_id(),
854 frame_kind,
855 ver,
856 payloads,
857 )?;
858 if !ids.is_empty() {
859 RingEventFd::notify(&ring)?;
860 }
861 Ok(ids)
862 }
863 RingBacking::InMemory(rings) => Ok(rings
864 .get_or_create::<T>()
865 .write_batch(self.node_id(), frame_kind, ver, payloads)),
866 }
867 }
868}
869
870impl std::fmt::Debug for Fleet {
871 fn fmt(
872 &self,
873 f: &mut std::fmt::Formatter<'_>,
874 ) -> std::fmt::Result {
875 f.debug_struct("Fleet")
876 .field("name", &self.inner.name)
877 .field("fleet_capacity", &self.inner.fleet_capacity)
878 .field("node_id", &self.inner.node_id)
879 .field("id_counters", &self.inner.id_counters.len())
880 .finish()
881 }
882}