Skip to main content

orbit_core/fleet/
mod.rs

1//! `Fleet` — the per-process handle for one node address in a fleet's
2//! shared physical geometry.
3
4use 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/// Read-only namespace handle for inspecting an existing SHM fleet.
29///
30/// Unlike [`Fleet`], this handle has no node id, creates no rings, and can
31/// never publish, reset, or unlink. Opening a ring maps only an object that
32/// already exists; dropping the observer or a ring view only unmaps local
33/// memory.
34#[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    /// Create an observer for the effective user's existing fleet namespace.
44    ///
45    /// This constructor itself performs no SHM operation. Each [`Self::ring`]
46    /// call attaches to one exact existing kind and returns `NotFound` when it
47    /// is absent.
48    pub fn attach_existing(name: impl Into<String>) -> std::io::Result<Self> {
49        // SAFETY: `geteuid` has no error path.
50        let uid = unsafe { libc::geteuid() };
51        Self::attach_existing_for_uid(name, uid)
52    }
53
54    /// Address an explicit uid-scoped fleet namespace.
55    ///
56    /// POSIX permissions still decide whether the caller may read another
57    /// user's SHM objects.
58    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    /// Attach read-only to one exact existing ring kind.
87    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    /// Attach read-only using an explicit per-ring maximum access policy.
97    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    /// Attach read-only and verify a linked [`OrbitTyped`] contract.
108    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    /// Attach with a policy and additionally verify the typed ring layout.
115    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/// A node's physical writer slot inside the fleet.
136///
137/// Orbit validates the address against fleet capacity but does not allocate
138/// it. The embedding runtime must ensure that simultaneously active writers
139/// receive distinct ids. Read-only inspectors can examine SHM without joining
140/// as a writer.
141#[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/// Per-process handle into the fleet. Cheap to clone — the inner
167/// state is `Arc`-shared.
168#[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    /// Per-KIND counter for `next_id` calls that don't go through a
178    /// ring (i.e. when the caller wants a fleet-unique id without
179    /// allocating a ring slot). V0: process-local atomic. V1: still
180    /// here, parallel to the ring's own write-position.
181    id_counters: DashMap<u8, Arc<AtomicU64>>,
182    /// Per-KIND ring buffers — orbit's runtime substrate. Either
183    /// in-process for unit-test / single-process use, or POSIX SHM
184    /// for real cross-process visibility (V1, master+worker fleet).
185    backing: RingBacking,
186    /// Held for as long as this process is in a shared-memory fleet, so
187    /// a lifecycle tool can tell a live fleet from a stopped one and refuse
188    /// to remove what is in use. Read-only attachments hold none.
189    #[cfg(unix)]
190    #[allow(dead_code)]
191    membership: Option<crate::shm::FleetMembership>,
192}
193
194/// Backing storage for the fleet's ring buffers — chosen at
195/// `Fleet::join` / `Fleet::join_shm` time and frozen for the
196/// fleet's lifetime.
197enum RingBacking {
198    /// Process-local DashMap of `Ring` instances. No cross-process
199    /// visibility — peers running other processes do not see this
200    /// fleet's writes. Useful for unit tests and embedded scenarios.
201    InMemory(RingRegistry),
202    /// POSIX-SHM-backed `ShmRing` instances. Multiple processes
203    /// joining the same fleet name share the same kernel-level
204    /// memory; writes from one are visible to all immediately.
205    #[cfg(unix)]
206    Shm(ShmRingRegistry),
207}
208
209impl Fleet {
210    /// Join (or create) a fleet under `name` with `fleet_capacity` physical
211    /// node lanes. In-memory backings remain process-local.
212    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    /// Join (or create) a process-local fleet with an explicit node id.
220    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    /// Join (or create) a fleet whose ring storage is backed by
250    /// POSIX shared memory. Multiple processes calling this with
251    /// the same `name` share the same kernel-level
252    /// segments — the fleet sees each other's writes.
253    ///
254    /// Each `OrbitTyped` kind gets its own SHM segment whose layout is
255    /// declared by `OrbitTyped::RING_SPEC`.
256    ///
257    /// Cross-process naming: segments are `/orbit-{name}-{kind}-{uid}`.
258    /// macOS limits POSIX SHM names to 31 chars (PSHMNAMLEN); a
259    /// short fleet name is required there.
260    #[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    /// Join (or create) a SHM-backed fleet with an explicit node id.
269    ///
270    /// Per-node rings require one active process membership per node id.
271    /// Orbit validates the id range but does not own process lifecycle and
272    /// therefore cannot prevent duplicate live memberships.
273    #[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    /// Join with immutable per-kind SHM policies. Unspecified kinds retain
283    /// `OwnerOnly`. This configures typed rings, not separate semantic tables,
284    /// membership locks or process credentials. All writers must agree on policy.
285    #[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    /// Whether these handles share the same process-local fleet state.
325    /// Cloning a handle preserves this identity; joining again does not,
326    /// even when the name matches. SHM namespace equality is separate.
327    pub fn ptr_eq(
328        &self,
329        other: &Self,
330    ) -> bool {
331        Arc::ptr_eq(&self.inner, &other.inner)
332    }
333
334    /// Number of physical node lanes reserved for this fleet.
335    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    /// Mint a fresh fleet-unique [`NetId64`] for type `T` *without*
344    /// publishing anything. Use this when the caller only needs the
345    /// id (e.g. minting an id to attach to data being persisted to
346    /// DB before going through the ring).
347    ///
348    /// For most use cases prefer [`Fleet::publish`] — it mints AND
349    /// stores in a single atomic step.
350    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    /// True when this fleet's ring storage is backed by POSIX SHM
362    /// (visible across processes). False for in-memory fleets.
363    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    /// Get-or-create the in-memory ring for type `T`. Only valid
375    /// for fleets created via [`Fleet::join`]; SHM-backed fleets
376    /// should use [`Fleet::shm_ring`] instead.
377    ///
378    /// # Panics
379    ///
380    /// Panics if called on a SHM-backed fleet.
381    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    /// Get-or-create the SHM ring for type `T`. Only valid on fleets
394    /// created via [`Fleet::join_shm`].
395    ///
396    /// # Errors
397    ///
398    /// Returns an `io::Error` if the SHM segment cannot be opened
399    /// (permissions, name too long, etc.).
400    ///
401    /// # Panics
402    ///
403    /// Panics if called on an in-memory fleet.
404    #[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    /// Publish a payload to the ring for type `T`. Mints a [`NetId64`],
417    /// writes the [`Frame`] to the appropriate shared or node-owned lane,
418    /// and returns the id.
419    ///
420    /// # Panics
421    ///
422    /// Panics if the ring cannot be opened or the payload exceeds
423    /// `T::RING_SPEC.payload_capacity`. Ring failures are
424    /// operator-visible, not silently ignored.
425    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    /// Publish a contiguous batch into one ring lane.
448    ///
449    /// The returned ids are ordered and consecutive. For per-node rings the
450    /// lane head becomes visible only after every frame in the batch has been
451    /// committed. Semantic layers can use this to publish a multi-slot blob,
452    /// then publish a separate descriptor that references the first id and
453    /// frame count.
454    ///
455    /// # Panics
456    ///
457    /// Panics if the ring cannot be opened, a payload exceeds the declared
458    /// slot capacity, or the batch itself is larger than the ring.
459    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    /// Look up a previously-published frame by its id. Returns the
482    /// frame if its slot still holds the same id (i.e. the ring has
483    /// not wrapped past it).
484    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    /// Read the most recent frame for type `T`. Per-node rings read this
496    /// fleet handle's local lane; shared rings read their sole lane.
497    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    /// Current head for type `T`'s ring. Per-node rings report this fleet
518    /// handle's local committed head; shared rings report their sole lane's
519    /// visible head.
520    /// Lazily
521    /// creates / attaches the ring on first access — important for
522    /// cross-process readers, where a child process may need to
523    /// attach to a SHM segment a peer already populated. Returns 0
524    /// when the ring is fresh / no counters have been claimed.
525    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    /// Read whatever frame currently occupies `counter % capacity`.
539    /// Per-node rings read this fleet handle's local lane. Lazily attaches
540    /// the ring on first access (same rationale as [`Fleet::head`]).
541    /// Returns `None` if the slot is empty/torn or attach fails.
542    ///
543    /// Used by walking readers; for typed handle-based reads,
544    /// prefer [`Fleet::read`].
545    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    /// Current head for one physical node lane.
581    ///
582    /// On a shared ring every node id addresses the sole shared lane.
583    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    /// Read the frame currently occupying one node lane's counter slot.
600    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    /// Capacity of the ring for type `T`. Lazily attaches the ring
634    /// on first access. Falls back to `T::RING_SPEC.capacity` when
635    /// SHM attach fails.
636    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    /// Allocate one semantic version shared by every writer lane of `T`.
648    ///
649    /// This is separate from each lane's physical frame counter. It is useful
650    /// for semantic layers that retain per-node write scalability but require
651    /// a deterministic fleet-wide last-write-wins order.
652    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    /// Return the last semantic version allocated for `T` without advancing it.
664    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    /// Clear every lane for `T` and reset all heads to zero.
678    ///
679    /// This is an owner-side boot cleanup primitive. It is safe for
680    /// runtime state such as events and periodic metrics when the
681    /// embedding application calls it before peer processes begin
682    /// publishing. It is not a coordination protocol; callers must not
683    /// reset a ring while other fleet members are actively writing it.
684    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    /// Create a process-local readiness fd for one notified SHM ring.
704    ///
705    /// The fd only signals that the ring generation changed. After draining
706    /// it, callers must poll the ring with their own cursor. Multiple writes
707    /// may coalesce into one readiness notification.
708    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    /// Publish one frame and notify native waiters after it commits.
726    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            // Process-local fleets do not need a kernel wake bridge.
741            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    /// Readiness for a ring published with
753    /// [`publish_notified_parked`](Self::publish_notified_parked). See
754    /// [`ParkedRingEventFd`]: every reader of such a ring uses this, not
755    /// [`ring_event_fd`](Self::ring_event_fd).
756    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    /// Publish one frame and wake the ring's readers only if one is parked.
776    ///
777    /// For rings read through [`ring_event_fd_parked`](Self::ring_event_fd_parked):
778    /// while every reader is busy draining, a publish is a write and an
779    /// atomic, with no syscall. Rings published with
780    /// [`publish_notified`](Self::publish_notified) are not affected.
781    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    /// Publish one contiguous batch and wake the ring's readers only if one
807    /// is parked — [`publish_notified_parked`](Self::publish_notified_parked)
808    /// for a batch. A per-node lane commits the whole batch before a reader
809    /// can see any of it.
810    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    /// Publish one contiguous batch and notify native waiters once after the
842    /// complete batch commits.
843    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}