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;
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/// Read-only namespace handle for inspecting an existing SHM fleet.
28///
29/// Unlike [`Fleet`], this handle has no node id, creates no rings, and can
30/// never publish, reset, or unlink. Opening a ring maps only an object that
31/// already exists; dropping the observer or a ring view only unmaps local
32/// memory.
33#[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    /// Create an observer for the effective user's existing fleet namespace.
43    ///
44    /// This constructor itself performs no SHM operation. Each [`Self::ring`]
45    /// call attaches to one exact existing kind and returns `NotFound` when it
46    /// is absent.
47    pub fn attach_existing(name: impl Into<String>) -> std::io::Result<Self> {
48        // SAFETY: `geteuid` has no error path.
49        let uid = unsafe { libc::geteuid() };
50        Self::attach_existing_for_uid(name, uid)
51    }
52
53    /// Address an explicit uid-scoped fleet namespace.
54    ///
55    /// POSIX permissions still decide whether the caller may read another
56    /// user's SHM objects.
57    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    /// Attach read-only to one exact existing ring kind.
86    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    /// Attach read-only using an explicit per-ring maximum access policy.
96    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    /// Attach read-only and verify a linked [`OrbitTyped`] contract.
107    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    /// Attach with a policy and additionally verify the typed ring layout.
114    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/// A node's physical writer slot inside the fleet.
135///
136/// Orbit validates the address against fleet capacity but does not allocate
137/// it. The embedding runtime must ensure that simultaneously active writers
138/// receive distinct ids. Read-only inspectors can examine SHM without joining
139/// as a writer.
140#[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/// Per-process handle into the fleet. Cheap to clone — the inner
166/// state is `Arc`-shared.
167#[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    /// Per-KIND counter for `next_id` calls that don't go through a
177    /// ring (i.e. when the caller wants a fleet-unique id without
178    /// allocating a ring slot). V0: process-local atomic. V1: still
179    /// here, parallel to the ring's own write-position.
180    id_counters: crate::ring::ByKind<AtomicU64>,
181    /// Per-KIND ring buffers — orbit's runtime substrate. Either
182    /// in-process for unit-test / single-process use, or POSIX SHM
183    /// for real cross-process visibility (V1, master+worker fleet).
184    backing: RingBacking,
185    /// Held for as long as this process is in a shared-memory fleet, so
186    /// a lifecycle tool can tell a live fleet from a stopped one and refuse
187    /// to remove what is in use. Read-only attachments hold none.
188    #[cfg(unix)]
189    #[allow(dead_code)]
190    membership: Option<crate::shm::FleetMembership>,
191}
192
193/// Backing storage for the fleet's ring buffers — chosen at
194/// `Fleet::join` / `Fleet::join_shm` time and frozen for the
195/// fleet's lifetime.
196enum RingBacking {
197    /// Process-local table of `Ring` instances. No cross-process
198    /// visibility — peers running other processes do not see this
199    /// fleet's writes. Useful for unit tests and embedded scenarios.
200    InMemory(RingRegistry),
201    /// POSIX-SHM-backed `ShmRing` instances. Multiple processes
202    /// joining the same fleet name share the same kernel-level
203    /// memory; writes from one are visible to all immediately.
204    #[cfg(unix)]
205    Shm(ShmRingRegistry),
206}
207
208impl Fleet {
209    /// Join (or create) a fleet under `name` with `fleet_capacity` physical
210    /// node lanes. In-memory backings remain process-local.
211    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    /// Join (or create) a process-local fleet with an explicit node id.
219    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    /// Join (or create) a fleet whose ring storage is backed by
249    /// POSIX shared memory. Multiple processes calling this with
250    /// the same `name` share the same kernel-level
251    /// segments — the fleet sees each other's writes.
252    ///
253    /// Each `OrbitTyped` kind gets its own SHM segment whose layout is
254    /// declared by `OrbitTyped::RING_SPEC`.
255    ///
256    /// Cross-process naming: segments are `/orbit-{name}-{kind}-{uid}`.
257    /// macOS limits POSIX SHM names to 31 chars (PSHMNAMLEN); a
258    /// short fleet name is required there.
259    #[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    /// Join (or create) a SHM-backed fleet with an explicit node id.
268    ///
269    /// Per-node rings require one active process membership per node id.
270    /// Orbit validates the id range but does not own process lifecycle and
271    /// therefore cannot prevent duplicate live memberships.
272    #[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    /// Join with immutable per-kind SHM policies. Unspecified kinds retain
282    /// `OwnerOnly`. This configures typed rings, not separate semantic tables,
283    /// membership locks or process credentials. All writers must agree on policy.
284    #[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    /// Whether these handles share the same process-local fleet state.
324    /// Cloning a handle preserves this identity; joining again does not,
325    /// even when the name matches. SHM namespace equality is separate.
326    pub fn ptr_eq(
327        &self,
328        other: &Self,
329    ) -> bool {
330        Arc::ptr_eq(&self.inner, &other.inner)
331    }
332
333    /// Number of physical node lanes reserved for this fleet.
334    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    /// Mint a fresh fleet-unique [`NetId64`] for type `T` *without*
343    /// publishing anything. Use this when the caller only needs the
344    /// id (e.g. minting an id to attach to data being persisted to
345    /// DB before going through the ring).
346    ///
347    /// For most use cases prefer [`Fleet::publish`] — it mints AND
348    /// stores in a single atomic step.
349    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    /// True when this fleet's ring storage is backed by POSIX SHM
359    /// (visible across processes). False for in-memory fleets.
360    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    /// Get-or-create the in-memory ring for type `T`. Only valid
372    /// for fleets created via [`Fleet::join`]; SHM-backed fleets
373    /// should use [`Fleet::shm_ring`] instead.
374    ///
375    /// # Panics
376    ///
377    /// Panics if called on a SHM-backed fleet.
378    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    /// Get-or-create the SHM ring for type `T`. Only valid on fleets
391    /// created via [`Fleet::join_shm`].
392    ///
393    /// # Errors
394    ///
395    /// Returns an `io::Error` if the SHM segment cannot be opened
396    /// (permissions, name too long, etc.).
397    ///
398    /// # Panics
399    ///
400    /// Panics if called on an in-memory fleet.
401    #[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    /// Publish a payload to the ring for type `T`. Mints a [`NetId64`],
414    /// writes the [`Frame`] to the appropriate shared or node-owned lane,
415    /// and returns the id.
416    ///
417    /// # Panics
418    ///
419    /// Panics if the ring cannot be opened or the payload exceeds
420    /// `T::RING_SPEC.payload_capacity`. Ring failures are
421    /// operator-visible, not silently ignored.
422    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    /// Publish a contiguous batch into one ring lane.
445    ///
446    /// The returned ids are ordered and consecutive. For per-node rings the
447    /// lane head becomes visible only after every frame in the batch has been
448    /// committed. Semantic layers can use this to publish a multi-slot blob,
449    /// then publish a separate descriptor that references the first id and
450    /// frame count.
451    ///
452    /// # Panics
453    ///
454    /// Panics if the ring cannot be opened, a payload exceeds the declared
455    /// slot capacity, or the batch itself is larger than the ring.
456    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    /// Look up a previously-published frame by its id. Returns the
479    /// frame if its slot still holds the same id (i.e. the ring has
480    /// not wrapped past it).
481    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    /// Read the most recent frame for type `T`. Per-node rings read this
493    /// fleet handle's local lane; shared rings read their sole lane.
494    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    /// Current head for type `T`'s ring. Per-node rings report this fleet
515    /// handle's local committed head; shared rings report their sole lane's
516    /// visible head.
517    /// Lazily
518    /// creates / attaches the ring on first access — important for
519    /// cross-process readers, where a child process may need to
520    /// attach to a SHM segment a peer already populated. Returns 0
521    /// when the ring is fresh / no counters have been claimed.
522    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    /// Read whatever frame currently occupies `counter % capacity`.
536    /// Per-node rings read this fleet handle's local lane. Lazily attaches
537    /// the ring on first access (same rationale as [`Fleet::head`]).
538    /// Returns `None` if the slot is empty/torn or attach fails.
539    ///
540    /// Used by walking readers; for typed handle-based reads,
541    /// prefer [`Fleet::read`].
542    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    /// Current head for one physical node lane.
576    ///
577    /// On a shared ring every node id addresses the sole shared lane.
578    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    /// Read the frame currently occupying one node lane's counter slot.
593    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    /// Capacity of the ring for type `T`. Lazily attaches the ring
627    /// on first access. Falls back to `T::RING_SPEC.capacity` when
628    /// SHM attach fails.
629    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    /// Allocate one semantic version shared by every writer lane of `T`.
641    ///
642    /// This is separate from each lane's physical frame counter. It is useful
643    /// for semantic layers that retain per-node write scalability but require
644    /// a deterministic fleet-wide last-write-wins order.
645    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    /// Return the last semantic version allocated for `T` without advancing it.
657    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    /// Run a per-node ring publication that participates in a publication
669    /// barrier. Allocate its semantic version and commit the frame within
670    /// the closure. Other participating publishers can run concurrently.
671    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    /// Exclude cooperating publishers while reading a consistent set of
679    /// committed lane heads and the semantic version. Every writer for the
680    /// kind must use [`Self::with_ring_publication`].
681    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    /// Clear every lane for `T` and reset all heads to zero.
721    ///
722    /// This is an owner-side boot cleanup primitive. It is safe for
723    /// runtime state such as events and periodic metrics when the
724    /// embedding application calls it before peer processes begin
725    /// publishing. It is not a coordination protocol; callers must not
726    /// reset a ring while other fleet members are actively writing it.
727    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    /// Create a process-local readiness fd for one notified SHM ring.
747    ///
748    /// The fd only signals that the ring generation changed. After draining
749    /// it, callers must poll the ring with their own cursor. Multiple writes
750    /// may coalesce into one readiness notification.
751    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    /// Publish one frame and notify native waiters after it commits.
769    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            // Process-local fleets do not need a kernel wake bridge.
784            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    /// Publish one contiguous batch and notify native waiters once after the
799    /// complete batch commits.
800    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}