1use std::sync::Arc;
5use std::sync::atomic::{AtomicU64, Ordering};
6
7use bytes::Bytes;
8
9use crate::OrbitTyped;
10use crate::error::{Error, Result};
11use crate::id::NetId64;
12#[cfg(any(
13 target_os = "linux",
14 target_os = "freebsd",
15 target_os = "macos"
16))]
17use crate::ring::RingEventFd;
18#[cfg(unix)]
19use crate::ring::shm::{ShmRing, ShmRingRegistry};
20use crate::ring::{Frame, Ring, RingRegistry, RingTopology};
21
22mod cursor;
23pub use cursor::{FleetLaneCursor, FleetLanePoll};
24mod name;
25pub use name::{FleetName, InvalidFleetName};
26
27#[cfg(unix)]
34#[derive(Clone, Debug, PartialEq, Eq)]
35pub struct FleetObserver {
36 name: String,
37 uid: u32,
38}
39
40#[cfg(unix)]
41impl FleetObserver {
42 pub fn attach_existing(name: impl Into<String>) -> std::io::Result<Self> {
48 let uid = unsafe { libc::geteuid() };
50 Self::attach_existing_for_uid(name, uid)
51 }
52
53 pub fn attach_existing_for_uid(
58 name: impl Into<String>,
59 uid: u32,
60 ) -> std::io::Result<Self> {
61 let name = name.into();
62 if name.is_empty() {
63 return Err(std::io::Error::new(
64 std::io::ErrorKind::InvalidInput,
65 "fleet name must not be empty",
66 ));
67 }
68 if name.contains('/') || name.contains('\0') {
69 return Err(std::io::Error::new(
70 std::io::ErrorKind::InvalidInput,
71 "fleet name must not contain '/' or a NUL byte",
72 ));
73 }
74 Ok(Self { name, uid })
75 }
76
77 pub fn name(&self) -> &str {
78 &self.name
79 }
80
81 pub fn uid(&self) -> u32 {
82 self.uid
83 }
84
85 pub fn ring(
87 &self,
88 kind: u8,
89 ) -> std::io::Result<crate::ring::shm::ShmRingView> {
90 crate::ring::shm::ShmRingView::attach_existing_for_uid(
91 &self.name, kind, self.uid,
92 )
93 }
94
95 pub fn ring_with_policy(
97 &self,
98 kind: u8,
99 policy: crate::shm::ShmAccessPolicy,
100 ) -> std::io::Result<crate::ring::shm::ShmRingView> {
101 crate::ring::shm::ShmRingView::attach_existing_for_uid_with_policy(
102 &self.name, kind, self.uid, policy,
103 )
104 }
105
106 pub fn typed_ring<T: OrbitTyped>(
108 &self
109 ) -> std::io::Result<crate::ring::shm::ShmRingView> {
110 self.typed_ring_with_policy::<T>(crate::shm::ShmAccessPolicy::default())
111 }
112
113 pub fn typed_ring_with_policy<T: OrbitTyped>(
115 &self,
116 policy: crate::shm::ShmAccessPolicy,
117 ) -> std::io::Result<crate::ring::shm::ShmRingView> {
118 let view = self.ring_with_policy(T::KIND, policy)?;
119 if view.metadata().spec != T::RING_SPEC {
120 return Err(std::io::Error::new(
121 std::io::ErrorKind::InvalidData,
122 format!(
123 "OrbitTyped KIND {} declares {:?}; existing spec is {:?}",
124 T::KIND,
125 T::RING_SPEC,
126 view.metadata().spec
127 ),
128 ));
129 }
130 Ok(view)
131 }
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
141#[repr(transparent)]
142pub struct NodeId(pub u16);
143
144impl NodeId {
145 pub const ZERO: Self = Self(0);
146
147 pub const fn new(value: u16) -> Self {
148 Self(value)
149 }
150
151 pub const fn get(self) -> u16 {
152 self.0
153 }
154}
155
156impl std::fmt::Display for NodeId {
157 fn fmt(
158 &self,
159 f: &mut std::fmt::Formatter<'_>,
160 ) -> std::fmt::Result {
161 write!(f, "node:{}", self.0)
162 }
163}
164
165#[derive(Clone)]
168pub struct Fleet {
169 inner: Arc<FleetInner>,
170}
171
172struct FleetInner {
173 name: Arc<str>,
174 fleet_capacity: u16,
175 node_id: NodeId,
176 id_counters: crate::ring::ByKind<AtomicU64>,
181 backing: RingBacking,
185 #[cfg(unix)]
189 #[allow(dead_code)]
190 membership: Option<crate::shm::FleetMembership>,
191}
192
193enum RingBacking {
197 InMemory(RingRegistry),
201 #[cfg(unix)]
205 Shm(ShmRingRegistry),
206}
207
208impl Fleet {
209 pub fn join(
212 name: &str,
213 fleet_capacity: u16,
214 ) -> Result<Self> {
215 Self::join_as(name, fleet_capacity, NodeId::ZERO)
216 }
217
218 pub fn join_as(
220 name: &str,
221 fleet_capacity: u16,
222 node_id: NodeId,
223 ) -> Result<Self> {
224 if fleet_capacity == 0 {
225 return Err(Error::EmptyFleet);
226 }
227 if node_id.get() >= fleet_capacity {
228 return Err(Error::NodeOutsideFleet {
229 node_id: node_id.get(),
230 fleet_capacity,
231 });
232 }
233 Ok(Self {
234 inner: Arc::new(FleetInner {
235 name: Arc::from(name),
236 fleet_capacity,
237 node_id,
238 id_counters: crate::ring::ByKind::new(),
239 backing: RingBacking::InMemory(RingRegistry::new(
240 fleet_capacity,
241 )),
242 #[cfg(unix)]
243 membership: None,
244 }),
245 })
246 }
247
248 #[cfg(unix)]
260 pub fn join_shm(
261 name: &str,
262 fleet_capacity: u16,
263 ) -> Result<Self> {
264 Self::join_shm_as(name, fleet_capacity, NodeId::ZERO)
265 }
266
267 #[cfg(unix)]
273 pub fn join_shm_as(
274 name: &str,
275 fleet_capacity: u16,
276 node_id: NodeId,
277 ) -> Result<Self> {
278 Self::join_shm_as_with_policies(name, fleet_capacity, node_id, [])
279 }
280
281 #[cfg(unix)]
285 pub fn join_shm_as_with_policies(
286 name: &str,
287 fleet_capacity: u16,
288 node_id: NodeId,
289 policies: impl IntoIterator<Item = (u8, crate::shm::ShmAccessPolicy)>,
290 ) -> Result<Self> {
291 if fleet_capacity == 0 {
292 return Err(Error::EmptyFleet);
293 }
294 if node_id.get() >= fleet_capacity {
295 return Err(Error::NodeOutsideFleet {
296 node_id: node_id.get(),
297 fleet_capacity,
298 });
299 }
300 let name: Arc<str> = Arc::from(name);
301 let membership =
302 crate::shm::join_fleet_membership(&name).map_err(Error::Io)?;
303 Ok(Self {
304 inner: Arc::new(FleetInner {
305 name: Arc::clone(&name),
306 fleet_capacity,
307 node_id,
308 id_counters: crate::ring::ByKind::new(),
309 backing: RingBacking::Shm(ShmRingRegistry::with_policies(
310 name.as_ref(),
311 fleet_capacity,
312 policies,
313 )),
314 membership: Some(membership),
315 }),
316 })
317 }
318
319 pub fn name(&self) -> &str {
320 &self.inner.name
321 }
322
323 pub fn ptr_eq(
327 &self,
328 other: &Self,
329 ) -> bool {
330 Arc::ptr_eq(&self.inner, &other.inner)
331 }
332
333 pub fn fleet_capacity(&self) -> u16 {
335 self.inner.fleet_capacity
336 }
337
338 pub fn node_id(&self) -> NodeId {
339 self.inner.node_id
340 }
341
342 pub fn next_id<T: OrbitTyped>(&self) -> NetId64 {
350 let counter = self
351 .inner
352 .id_counters
353 .get_or_init(T::KIND, || AtomicU64::new(0))
354 .fetch_add(1, Ordering::Relaxed);
355 NetId64::make(T::KIND, self.node_id().get(), counter)
356 }
357
358 pub fn is_shm(&self) -> bool {
361 #[cfg(unix)]
362 {
363 matches!(self.inner.backing, RingBacking::Shm(_))
364 }
365 #[cfg(not(unix))]
366 {
367 false
368 }
369 }
370
371 pub fn ring<T: OrbitTyped>(&self) -> Arc<Ring> {
379 match &self.inner.backing {
380 RingBacking::InMemory(r) => r.get_or_create::<T>(),
381 #[cfg(unix)]
382 RingBacking::Shm(_) => {
383 panic!(
384 "Fleet::ring called on SHM-backed fleet — use Fleet::shm_ring instead"
385 );
386 }
387 }
388 }
389
390 #[cfg(unix)]
402 pub fn shm_ring<T: OrbitTyped>(&self) -> std::io::Result<Arc<ShmRing>> {
403 match &self.inner.backing {
404 RingBacking::Shm(r) => r.get_or_create_for::<T>(),
405 RingBacking::InMemory(_) => {
406 panic!(
407 "Fleet::shm_ring called on in-memory fleet — use Fleet::ring instead"
408 );
409 }
410 }
411 }
412
413 pub fn publish<T: OrbitTyped>(
423 &self,
424 frame_kind: u8,
425 ver: u64,
426 payload: Bytes,
427 ) -> NetId64 {
428 match &self.inner.backing {
429 RingBacking::InMemory(r) => {
430 let ring = r.ring_for::<T>();
431 ring.write(self.node_id(), frame_kind, ver, payload)
432 }
433 #[cfg(unix)]
434 RingBacking::Shm(r) => {
435 let ring = r
436 .ring_for::<T>()
437 .expect("SHM ring open failed — fleet unusable");
438 ring.write(self.node_id(), frame_kind, ver, payload)
439 .expect("SHM ring write failed")
440 }
441 }
442 }
443
444 pub fn publish_batch<T: OrbitTyped>(
457 &self,
458 frame_kind: u8,
459 ver: u64,
460 payloads: Vec<Bytes>,
461 ) -> Vec<NetId64> {
462 match &self.inner.backing {
463 RingBacking::InMemory(r) => {
464 let ring = r.ring_for::<T>();
465 ring.write_batch(self.node_id(), frame_kind, ver, payloads)
466 }
467 #[cfg(unix)]
468 RingBacking::Shm(r) => {
469 let ring = r
470 .ring_for::<T>()
471 .expect("SHM ring open failed — fleet unusable");
472 ring.write_batch(self.node_id(), frame_kind, ver, payloads)
473 .expect("SHM ring batch write failed")
474 }
475 }
476 }
477
478 pub fn read(
482 &self,
483 id: NetId64,
484 ) -> Option<Frame> {
485 match &self.inner.backing {
486 RingBacking::InMemory(r) => r.lookup(id.kind())?.read(id),
487 #[cfg(unix)]
488 RingBacking::Shm(r) => r.lookup(id.kind())?.read(id),
489 }
490 }
491
492 pub fn read_head<T: OrbitTyped>(&self) -> Option<Frame> {
495 if T::RING_SPEC.topology == RingTopology::PerNode {
496 let head = self.lane_head::<T>(self.node_id());
497 return (head > 0)
498 .then(|| self.read_lane_at::<T>(self.node_id(), head - 1))
499 .flatten();
500 }
501 match &self.inner.backing {
502 RingBacking::InMemory(r) => {
503 let ring = r.ring_for::<T>();
504 ring.read_head()
505 }
506 #[cfg(unix)]
507 RingBacking::Shm(r) => {
508 let ring = r.ring_for::<T>().ok()?;
509 ring.read_head()
510 }
511 }
512 }
513
514 pub fn head<T: OrbitTyped>(&self) -> u64 {
523 if T::RING_SPEC.topology == RingTopology::PerNode {
524 return self.lane_head::<T>(self.node_id());
525 }
526 match &self.inner.backing {
527 RingBacking::InMemory(r) => r.ring_for::<T>().head(),
528 #[cfg(unix)]
529 RingBacking::Shm(r) => {
530 r.ring_for::<T>().map(|ring| ring.head()).unwrap_or(0)
531 }
532 }
533 }
534
535 pub fn read_at<T: OrbitTyped>(
543 &self,
544 counter: u64,
545 ) -> Option<Frame> {
546 if T::RING_SPEC.topology == RingTopology::PerNode {
547 return self.read_lane_at::<T>(self.node_id(), counter);
548 }
549 match &self.inner.backing {
550 RingBacking::InMemory(r) => r.ring_for::<T>().read_at(counter),
551 #[cfg(unix)]
552 RingBacking::Shm(r) => r.ring_for::<T>().ok()?.read_at(counter),
553 }
554 }
555
556 pub(crate) fn read_state_at<T: OrbitTyped>(
557 &self,
558 counter: u64,
559 ) -> crate::ring::cursor::RingRead {
560 if T::RING_SPEC.topology == RingTopology::PerNode {
561 return self.read_lane_state_at::<T>(self.node_id(), counter);
562 }
563 match &self.inner.backing {
564 RingBacking::InMemory(r) => {
565 r.ring_for::<T>().read_state_at(counter)
566 }
567 #[cfg(unix)]
568 RingBacking::Shm(r) => r
569 .ring_for::<T>()
570 .map(|ring| ring.read_state_at(counter))
571 .unwrap_or(crate::ring::cursor::RingRead::Unavailable),
572 }
573 }
574
575 pub fn lane_head<T: OrbitTyped>(
579 &self,
580 node_id: NodeId,
581 ) -> u64 {
582 match &self.inner.backing {
583 RingBacking::InMemory(r) => r.ring_for::<T>().lane_head(node_id),
584 #[cfg(unix)]
585 RingBacking::Shm(r) => r
586 .ring_for::<T>()
587 .map(|ring| ring.lane_head(node_id))
588 .unwrap_or(0),
589 }
590 }
591
592 pub fn read_lane_at<T: OrbitTyped>(
594 &self,
595 node_id: NodeId,
596 counter: u64,
597 ) -> Option<Frame> {
598 match &self.inner.backing {
599 RingBacking::InMemory(r) => {
600 r.ring_for::<T>().read_lane_at(node_id, counter)
601 }
602 #[cfg(unix)]
603 RingBacking::Shm(r) => {
604 r.ring_for::<T>().ok()?.read_lane_at(node_id, counter)
605 }
606 }
607 }
608
609 pub(crate) fn read_lane_state_at<T: OrbitTyped>(
610 &self,
611 node_id: NodeId,
612 counter: u64,
613 ) -> crate::ring::cursor::RingRead {
614 match &self.inner.backing {
615 RingBacking::InMemory(r) => {
616 r.ring_for::<T>().read_lane_state_at(node_id, counter)
617 }
618 #[cfg(unix)]
619 RingBacking::Shm(r) => r
620 .ring_for::<T>()
621 .map(|ring| ring.read_lane_state_at(node_id, counter))
622 .unwrap_or(crate::ring::cursor::RingRead::Unavailable),
623 }
624 }
625
626 pub fn ring_capacity<T: OrbitTyped>(&self) -> usize {
630 match &self.inner.backing {
631 RingBacking::InMemory(r) => r.ring_for::<T>().capacity(),
632 #[cfg(unix)]
633 RingBacking::Shm(r) => r
634 .ring_for::<T>()
635 .map(|ring| ring.capacity())
636 .unwrap_or(T::RING_SPEC.capacity),
637 }
638 }
639
640 pub fn next_ring_version<T: OrbitTyped>(&self) -> u64 {
646 match &self.inner.backing {
647 RingBacking::InMemory(r) => r.ring_for::<T>().next_version(),
648 #[cfg(unix)]
649 RingBacking::Shm(r) => r
650 .ring_for::<T>()
651 .expect("SHM ring open failed — fleet unusable")
652 .next_version(),
653 }
654 }
655
656 pub fn current_ring_version<T: OrbitTyped>(&self) -> u64 {
658 match &self.inner.backing {
659 RingBacking::InMemory(r) => r.ring_for::<T>().current_version(),
660 #[cfg(unix)]
661 RingBacking::Shm(r) => r
662 .ring_for::<T>()
663 .expect("SHM ring open failed — fleet unusable")
664 .current_version(),
665 }
666 }
667
668 pub fn with_ring_publication<T: OrbitTyped, R>(
672 &self,
673 publish: impl FnOnce() -> R,
674 ) -> std::io::Result<R> {
675 self.with_publication_lock::<T, R>(false, publish)
676 }
677
678 pub fn with_ring_publication_barrier<T: OrbitTyped, R>(
682 &self,
683 snapshot: impl FnOnce() -> R,
684 ) -> std::io::Result<R> {
685 self.with_publication_lock::<T, R>(true, snapshot)
686 }
687
688 fn with_publication_lock<T: OrbitTyped, R>(
689 &self,
690 exclusive: bool,
691 operation: impl FnOnce() -> R,
692 ) -> std::io::Result<R> {
693 self.assert_per_node::<T>();
694 match &self.inner.backing {
695 RingBacking::InMemory(rings) => {
696 let ring = rings.ring_for::<T>();
697 if exclusive {
698 let _guard = ring
699 .publication
700 .write()
701 .unwrap_or_else(|error| error.into_inner());
702 Ok(operation())
703 } else {
704 let _guard = ring
705 .publication
706 .read()
707 .unwrap_or_else(|error| error.into_inner());
708 Ok(operation())
709 }
710 }
711 #[cfg(unix)]
712 RingBacking::Shm(rings) => {
713 let ring = rings.ring_for::<T>()?;
714 let _guard = ring.publication_lock(exclusive)?;
715 Ok(operation())
716 }
717 }
718 }
719
720 pub fn reset_ring<T: OrbitTyped>(&self) -> std::io::Result<()> {
728 match &self.inner.backing {
729 RingBacking::InMemory(r) => {
730 r.ring_for::<T>().reset();
731 Ok(())
732 }
733 #[cfg(unix)]
734 RingBacking::Shm(r) => {
735 r.ring_for::<T>()?.reset();
736 Ok(())
737 }
738 }
739 }
740
741 #[cfg(any(
742 target_os = "linux",
743 target_os = "freebsd",
744 target_os = "macos"
745 ))]
746 pub fn ring_event_fd<T: OrbitTyped>(&self) -> std::io::Result<RingEventFd> {
752 match &self.inner.backing {
753 RingBacking::Shm(rings) => {
754 RingEventFd::new(rings.get_or_create_for::<T>()?)
755 }
756 RingBacking::InMemory(_) => Err(std::io::Error::new(
757 std::io::ErrorKind::Unsupported,
758 "Orbit eventfd requires a shared-memory fleet",
759 )),
760 }
761 }
762
763 #[cfg(any(
764 target_os = "linux",
765 target_os = "freebsd",
766 target_os = "macos"
767 ))]
768 pub fn publish_notified<T: OrbitTyped>(
770 &self,
771 frame_kind: u8,
772 ver: u64,
773 payload: Bytes,
774 ) -> std::io::Result<NetId64> {
775 match &self.inner.backing {
776 RingBacking::Shm(rings) => {
777 let ring = rings.ring_for::<T>()?;
778 let id =
779 ring.write(self.node_id(), frame_kind, ver, payload)?;
780 RingEventFd::notify(ring)?;
781 Ok(id)
782 }
783 RingBacking::InMemory(rings) => Ok(rings.ring_for::<T>().write(
785 self.node_id(),
786 frame_kind,
787 ver,
788 payload,
789 )),
790 }
791 }
792
793 #[cfg(any(
794 target_os = "linux",
795 target_os = "freebsd",
796 target_os = "macos"
797 ))]
798 pub fn publish_batch_notified<T: OrbitTyped>(
801 &self,
802 frame_kind: u8,
803 ver: u64,
804 payloads: Vec<Bytes>,
805 ) -> std::io::Result<Vec<NetId64>> {
806 match &self.inner.backing {
807 RingBacking::Shm(rings) => {
808 let ring = rings.ring_for::<T>()?;
809 let ids = ring.write_batch(
810 self.node_id(),
811 frame_kind,
812 ver,
813 payloads,
814 )?;
815 if !ids.is_empty() {
816 RingEventFd::notify(ring)?;
817 }
818 Ok(ids)
819 }
820 RingBacking::InMemory(rings) => Ok(rings
821 .ring_for::<T>()
822 .write_batch(self.node_id(), frame_kind, ver, payloads)),
823 }
824 }
825}
826
827impl std::fmt::Debug for Fleet {
828 fn fmt(
829 &self,
830 f: &mut std::fmt::Formatter<'_>,
831 ) -> std::fmt::Result {
832 f.debug_struct("Fleet")
833 .field("name", &self.inner.name)
834 .field("fleet_capacity", &self.inner.fleet_capacity)
835 .field("node_id", &self.inner.node_id)
836 .field("id_counters", &self.inner.id_counters.len())
837 .finish()
838 }
839}