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(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))]
14use crate::ring::RingEventFd;
15#[cfg(unix)]
16use crate::ring::shm::{ShmRing, ShmRingRegistry};
17use crate::ring::{Frame, Ring, RingRegistry, RingTopology};
18
19mod cursor;
20pub use cursor::{FleetLaneCursor, FleetLanePoll};
21
22/// Read-only namespace handle for inspecting an existing SHM fleet.
23///
24/// Unlike [`Fleet`], this handle has no node id, creates no rings, and can
25/// never publish, reset, or unlink. Opening a ring maps only an object that
26/// already exists; dropping the observer or a ring view only unmaps local
27/// memory.
28#[cfg(unix)]
29#[derive(Clone, Debug, PartialEq, Eq)]
30pub struct FleetObserver {
31    name: String,
32    uid: u32,
33}
34
35#[cfg(unix)]
36impl FleetObserver {
37    /// Create an observer for the effective user's existing fleet namespace.
38    ///
39    /// This constructor itself performs no SHM operation. Each [`Self::ring`]
40    /// call attaches to one exact existing kind and returns `NotFound` when it
41    /// is absent.
42    pub fn attach_existing(name: impl Into<String>) -> std::io::Result<Self> {
43        // SAFETY: `geteuid` has no error path.
44        let uid = unsafe { libc::geteuid() };
45        Self::attach_existing_for_uid(name, uid)
46    }
47
48    /// Address an explicit uid-scoped fleet namespace.
49    ///
50    /// POSIX permissions still decide whether the caller may read another
51    /// user's SHM objects.
52    pub fn attach_existing_for_uid(name: impl Into<String>, uid: u32) -> std::io::Result<Self> {
53        let name = name.into();
54        if name.is_empty() {
55            return Err(std::io::Error::new(
56                std::io::ErrorKind::InvalidInput,
57                "fleet name must not be empty",
58            ));
59        }
60        if name.contains('/') || name.contains('\0') {
61            return Err(std::io::Error::new(
62                std::io::ErrorKind::InvalidInput,
63                "fleet name must not contain '/' or a NUL byte",
64            ));
65        }
66        Ok(Self { name, uid })
67    }
68
69    pub fn name(&self) -> &str {
70        &self.name
71    }
72
73    pub fn uid(&self) -> u32 {
74        self.uid
75    }
76
77    /// Attach read-only to one exact existing ring kind.
78    pub fn ring(&self, kind: u8) -> std::io::Result<crate::ring::shm::ShmRingView> {
79        crate::ring::shm::ShmRingView::attach_existing_for_uid(&self.name, kind, self.uid)
80    }
81
82    /// Attach read-only and verify a linked [`OrbitTyped`] contract.
83    pub fn typed_ring<T: OrbitTyped>(&self) -> std::io::Result<crate::ring::shm::ShmRingView> {
84        let view = self.ring(T::KIND)?;
85        if view.metadata().spec != T::RING_SPEC {
86            return Err(std::io::Error::new(
87                std::io::ErrorKind::InvalidData,
88                format!(
89                    "OrbitTyped KIND {} declares {:?}; existing spec is {:?}",
90                    T::KIND,
91                    T::RING_SPEC,
92                    view.metadata().spec
93                ),
94            ));
95        }
96        Ok(view)
97    }
98}
99
100/// A node's physical writer slot inside the fleet.
101///
102/// Orbit validates the address against fleet capacity but does not allocate
103/// it. The embedding runtime must ensure that simultaneously active writers
104/// receive distinct ids. Read-only inspectors can examine SHM without joining
105/// as a writer.
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
107#[repr(transparent)]
108pub struct NodeId(pub u16);
109
110impl NodeId {
111    pub const ZERO: Self = Self(0);
112
113    pub const fn new(value: u16) -> Self {
114        Self(value)
115    }
116
117    pub const fn get(self) -> u16 {
118        self.0
119    }
120}
121
122impl std::fmt::Display for NodeId {
123    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124        write!(f, "node:{}", self.0)
125    }
126}
127
128/// Per-process handle into the fleet. Cheap to clone — the inner
129/// state is `Arc`-shared.
130#[derive(Clone)]
131pub struct Fleet {
132    inner: Arc<FleetInner>,
133}
134
135struct FleetInner {
136    name: &'static str,
137    fleet_capacity: u16,
138    node_id: NodeId,
139    /// Per-KIND counter for `next_id` calls that don't go through a
140    /// ring (i.e. when the caller wants a fleet-unique id without
141    /// allocating a ring slot). V0: process-local atomic. V1: still
142    /// here, parallel to the ring's own write-position.
143    id_counters: DashMap<u8, Arc<AtomicU64>>,
144    /// Per-KIND ring buffers — orbit's runtime substrate. Either
145    /// in-process for unit-test / single-process use, or POSIX SHM
146    /// for real cross-process visibility (V1, master+worker fleet).
147    backing: RingBacking,
148    /// Held for as long as this process is in a shared-memory fleet, so
149    /// a lifecycle tool can tell a live fleet from a stopped one and refuse
150    /// to remove what is in use. Read-only attachments hold none.
151    #[cfg(unix)]
152    #[allow(dead_code)]
153    membership: Option<crate::shm::FleetMembership>,
154}
155
156/// Backing storage for the fleet's ring buffers — chosen at
157/// `Fleet::join` / `Fleet::join_shm` time and frozen for the
158/// fleet's lifetime.
159enum RingBacking {
160    /// Process-local DashMap of `Ring` instances. No cross-process
161    /// visibility — peers running other processes do not see this
162    /// fleet's writes. Useful for unit tests and embedded scenarios.
163    InMemory(RingRegistry),
164    /// POSIX-SHM-backed `ShmRing` instances. Multiple processes
165    /// joining the same fleet name share the same kernel-level
166    /// memory; writes from one are visible to all immediately.
167    #[cfg(unix)]
168    Shm(ShmRingRegistry),
169}
170
171impl Fleet {
172    /// Join (or create) a fleet under `name` with `fleet_capacity` physical
173    /// node lanes. In-memory backings remain process-local.
174    pub fn join(name: &'static str, fleet_capacity: u16) -> Result<Self> {
175        Self::join_as(name, fleet_capacity, NodeId::ZERO)
176    }
177
178    /// Join (or create) a process-local fleet with an explicit node id.
179    pub fn join_as(name: &'static str, fleet_capacity: u16, node_id: NodeId) -> Result<Self> {
180        if fleet_capacity == 0 {
181            return Err(Error::EmptyFleet);
182        }
183        if node_id.get() >= fleet_capacity {
184            return Err(Error::NodeOutsideFleet {
185                node_id: node_id.get(),
186                fleet_capacity,
187            });
188        }
189        Ok(Self {
190            inner: Arc::new(FleetInner {
191                name,
192                fleet_capacity,
193                node_id,
194                id_counters: DashMap::new(),
195                backing: RingBacking::InMemory(RingRegistry::new(fleet_capacity)),
196                #[cfg(unix)]
197                membership: None,
198            }),
199        })
200    }
201
202    /// Join (or create) a fleet whose ring storage is backed by
203    /// POSIX shared memory. Multiple processes calling this with
204    /// the same `name` share the same kernel-level
205    /// segments — the fleet sees each other's writes.
206    ///
207    /// Each `OrbitTyped` kind gets its own SHM segment whose layout is
208    /// declared by `OrbitTyped::RING_SPEC`.
209    ///
210    /// Cross-process naming: segments are `/orbit-{name}-{kind}-{uid}`.
211    /// macOS limits POSIX SHM names to 31 chars (PSHMNAMLEN); a
212    /// short fleet name is required there.
213    #[cfg(unix)]
214    pub fn join_shm(name: &'static str, fleet_capacity: u16) -> Result<Self> {
215        Self::join_shm_as(name, fleet_capacity, NodeId::ZERO)
216    }
217
218    /// Join (or create) a SHM-backed fleet with an explicit node id.
219    ///
220    /// Per-node rings require one active process membership per node id.
221    /// Orbit validates the id range but does not own process lifecycle and
222    /// therefore cannot prevent duplicate live memberships.
223    #[cfg(unix)]
224    pub fn join_shm_as(name: &'static str, fleet_capacity: u16, node_id: NodeId) -> 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        let membership = crate::shm::join_fleet_membership(name).map_err(Error::Io)?;
235        Ok(Self {
236            inner: Arc::new(FleetInner {
237                name,
238                fleet_capacity,
239                node_id,
240                id_counters: DashMap::new(),
241                backing: RingBacking::Shm(ShmRingRegistry::new(name, fleet_capacity)),
242                membership: Some(membership),
243            }),
244        })
245    }
246
247    pub fn name(&self) -> &'static str {
248        self.inner.name
249    }
250
251    /// Number of physical node lanes reserved for this fleet.
252    pub fn fleet_capacity(&self) -> u16 {
253        self.inner.fleet_capacity
254    }
255
256    pub fn node_id(&self) -> NodeId {
257        self.inner.node_id
258    }
259
260    /// Mint a fresh fleet-unique [`NetId64`] for type `T` *without*
261    /// publishing anything. Use this when the caller only needs the
262    /// id (e.g. minting an id to attach to data being persisted to
263    /// DB before going through the ring).
264    ///
265    /// For most use cases prefer [`Fleet::publish`] — it mints AND
266    /// stores in a single atomic step.
267    pub fn next_id<T: OrbitTyped>(&self) -> NetId64 {
268        let counter_arc = self
269            .inner
270            .id_counters
271            .entry(T::KIND)
272            .or_insert_with(|| Arc::new(AtomicU64::new(0)))
273            .clone();
274        let counter = counter_arc.fetch_add(1, Ordering::Relaxed);
275        NetId64::make(T::KIND, self.node_id().get(), counter)
276    }
277
278    /// True when this fleet's ring storage is backed by POSIX SHM
279    /// (visible across processes). False for in-memory fleets.
280    pub fn is_shm(&self) -> bool {
281        #[cfg(unix)]
282        {
283            matches!(self.inner.backing, RingBacking::Shm(_))
284        }
285        #[cfg(not(unix))]
286        {
287            false
288        }
289    }
290
291    /// Get-or-create the in-memory ring for type `T`. Only valid
292    /// for fleets created via [`Fleet::join`]; SHM-backed fleets
293    /// should use [`Fleet::shm_ring`] instead.
294    ///
295    /// # Panics
296    ///
297    /// Panics if called on a SHM-backed fleet.
298    pub fn ring<T: OrbitTyped>(&self) -> Arc<Ring> {
299        match &self.inner.backing {
300            RingBacking::InMemory(r) => r.get_or_create::<T>(),
301            #[cfg(unix)]
302            RingBacking::Shm(_) => {
303                panic!("Fleet::ring called on SHM-backed fleet — use Fleet::shm_ring instead");
304            }
305        }
306    }
307
308    /// Get-or-create the SHM ring for type `T`. Only valid on fleets
309    /// created via [`Fleet::join_shm`].
310    ///
311    /// # Errors
312    ///
313    /// Returns an `io::Error` if the SHM segment cannot be opened
314    /// (permissions, name too long, etc.).
315    ///
316    /// # Panics
317    ///
318    /// Panics if called on an in-memory fleet.
319    #[cfg(unix)]
320    pub fn shm_ring<T: OrbitTyped>(&self) -> std::io::Result<Arc<ShmRing>> {
321        match &self.inner.backing {
322            RingBacking::Shm(r) => r.get_or_create_for::<T>(),
323            RingBacking::InMemory(_) => {
324                panic!("Fleet::shm_ring called on in-memory fleet — use Fleet::ring instead");
325            }
326        }
327    }
328
329    /// Publish a payload to the ring for type `T`. Mints a [`NetId64`],
330    /// writes the [`Frame`] to the appropriate shared or node-owned lane,
331    /// and returns the id.
332    ///
333    /// # Panics
334    ///
335    /// Panics if the ring cannot be opened or the payload exceeds
336    /// `T::RING_SPEC.payload_capacity`. Ring failures are
337    /// operator-visible, not silently ignored.
338    pub fn publish<T: OrbitTyped>(&self, frame_kind: u8, ver: u64, payload: Bytes) -> NetId64 {
339        match &self.inner.backing {
340            RingBacking::InMemory(r) => {
341                let ring = r.get_or_create::<T>();
342                ring.write(self.node_id(), frame_kind, ver, payload)
343            }
344            #[cfg(unix)]
345            RingBacking::Shm(r) => {
346                let ring = r
347                    .get_or_create_for::<T>()
348                    .expect("SHM ring open failed — fleet unusable");
349                ring.write(self.node_id(), frame_kind, ver, payload)
350                    .expect("SHM ring write failed")
351            }
352        }
353    }
354
355    /// Publish a contiguous batch into one ring lane.
356    ///
357    /// The returned ids are ordered and consecutive. For per-node rings the
358    /// lane head becomes visible only after every frame in the batch has been
359    /// committed. Semantic layers can use this to publish a multi-slot blob,
360    /// then publish a separate descriptor that references the first id and
361    /// frame count.
362    ///
363    /// # Panics
364    ///
365    /// Panics if the ring cannot be opened, a payload exceeds the declared
366    /// slot capacity, or the batch itself is larger than the ring.
367    pub fn publish_batch<T: OrbitTyped>(
368        &self,
369        frame_kind: u8,
370        ver: u64,
371        payloads: Vec<Bytes>,
372    ) -> Vec<NetId64> {
373        match &self.inner.backing {
374            RingBacking::InMemory(r) => {
375                let ring = r.get_or_create::<T>();
376                ring.write_batch(self.node_id(), frame_kind, ver, payloads)
377            }
378            #[cfg(unix)]
379            RingBacking::Shm(r) => {
380                let ring = r
381                    .get_or_create_for::<T>()
382                    .expect("SHM ring open failed — fleet unusable");
383                ring.write_batch(self.node_id(), frame_kind, ver, payloads)
384                    .expect("SHM ring batch write failed")
385            }
386        }
387    }
388
389    /// Look up a previously-published frame by its id. Returns the
390    /// frame if its slot still holds the same id (i.e. the ring has
391    /// not wrapped past it).
392    pub fn read(&self, id: NetId64) -> Option<Frame> {
393        match &self.inner.backing {
394            RingBacking::InMemory(r) => r.lookup(id.kind())?.read(id),
395            #[cfg(unix)]
396            RingBacking::Shm(r) => r.lookup(id.kind())?.read(id),
397        }
398    }
399
400    /// Read the most recent frame for type `T`. Per-node rings read this
401    /// fleet handle's local lane; shared rings read their sole lane.
402    pub fn read_head<T: OrbitTyped>(&self) -> Option<Frame> {
403        if T::RING_SPEC.topology == RingTopology::PerNode {
404            let head = self.lane_head::<T>(self.node_id());
405            return (head > 0)
406                .then(|| self.read_lane_at::<T>(self.node_id(), head - 1))
407                .flatten();
408        }
409        match &self.inner.backing {
410            RingBacking::InMemory(r) => {
411                let ring = r.get_or_create::<T>();
412                ring.read_head()
413            }
414            #[cfg(unix)]
415            RingBacking::Shm(r) => {
416                let ring = r.get_or_create_for::<T>().ok()?;
417                ring.read_head()
418            }
419        }
420    }
421
422    /// Current head for type `T`'s ring. Per-node rings report this fleet
423    /// handle's local committed head; shared rings report their sole lane's
424    /// visible head.
425    /// Lazily
426    /// creates / attaches the ring on first access — important for
427    /// cross-process readers, where a child process may need to
428    /// attach to a SHM segment a peer already populated. Returns 0
429    /// when the ring is fresh / no counters have been claimed.
430    pub fn head<T: OrbitTyped>(&self) -> u64 {
431        if T::RING_SPEC.topology == RingTopology::PerNode {
432            return self.lane_head::<T>(self.node_id());
433        }
434        match &self.inner.backing {
435            RingBacking::InMemory(r) => r.get_or_create::<T>().head(),
436            #[cfg(unix)]
437            RingBacking::Shm(r) => r
438                .get_or_create_for::<T>()
439                .map(|ring| ring.head())
440                .unwrap_or(0),
441        }
442    }
443
444    /// Read whatever frame currently occupies `counter % capacity`.
445    /// Per-node rings read this fleet handle's local lane. Lazily attaches
446    /// the ring on first access (same rationale as [`Fleet::head`]).
447    /// Returns `None` if the slot is empty/torn or attach fails.
448    ///
449    /// Used by walking readers; for typed handle-based reads,
450    /// prefer [`Fleet::read`].
451    pub fn read_at<T: OrbitTyped>(&self, counter: u64) -> Option<Frame> {
452        if T::RING_SPEC.topology == RingTopology::PerNode {
453            return self.read_lane_at::<T>(self.node_id(), counter);
454        }
455        match &self.inner.backing {
456            RingBacking::InMemory(r) => r.get_or_create::<T>().read_at(counter),
457            #[cfg(unix)]
458            RingBacking::Shm(r) => r.get_or_create_for::<T>().ok()?.read_at(counter),
459        }
460    }
461
462    pub(crate) fn read_state_at<T: OrbitTyped>(
463        &self,
464        counter: u64,
465    ) -> crate::ring::cursor::RingRead {
466        if T::RING_SPEC.topology == RingTopology::PerNode {
467            return self.read_lane_state_at::<T>(self.node_id(), counter);
468        }
469        match &self.inner.backing {
470            RingBacking::InMemory(r) => r.get_or_create::<T>().read_state_at(counter),
471            #[cfg(unix)]
472            RingBacking::Shm(r) => r
473                .get_or_create_for::<T>()
474                .map(|ring| ring.read_state_at(counter))
475                .unwrap_or(crate::ring::cursor::RingRead::Unavailable),
476        }
477    }
478
479    /// Current head for one physical node lane.
480    ///
481    /// On a shared ring every node id addresses the sole shared lane.
482    pub fn lane_head<T: OrbitTyped>(&self, node_id: NodeId) -> u64 {
483        match &self.inner.backing {
484            RingBacking::InMemory(r) => r.get_or_create::<T>().lane_head(node_id),
485            #[cfg(unix)]
486            RingBacking::Shm(r) => r
487                .get_or_create_for::<T>()
488                .map(|ring| ring.lane_head(node_id))
489                .unwrap_or(0),
490        }
491    }
492
493    /// Read the frame currently occupying one node lane's counter slot.
494    pub fn read_lane_at<T: OrbitTyped>(&self, node_id: NodeId, counter: u64) -> Option<Frame> {
495        match &self.inner.backing {
496            RingBacking::InMemory(r) => r.get_or_create::<T>().read_lane_at(node_id, counter),
497            #[cfg(unix)]
498            RingBacking::Shm(r) => r
499                .get_or_create_for::<T>()
500                .ok()?
501                .read_lane_at(node_id, counter),
502        }
503    }
504
505    pub(crate) fn read_lane_state_at<T: OrbitTyped>(
506        &self,
507        node_id: NodeId,
508        counter: u64,
509    ) -> crate::ring::cursor::RingRead {
510        match &self.inner.backing {
511            RingBacking::InMemory(r) => r.get_or_create::<T>().read_lane_state_at(node_id, counter),
512            #[cfg(unix)]
513            RingBacking::Shm(r) => r
514                .get_or_create_for::<T>()
515                .map(|ring| ring.read_lane_state_at(node_id, counter))
516                .unwrap_or(crate::ring::cursor::RingRead::Unavailable),
517        }
518    }
519
520    /// Capacity of the ring for type `T`. Lazily attaches the ring
521    /// on first access. Falls back to `T::RING_SPEC.capacity` when
522    /// SHM attach fails.
523    pub fn ring_capacity<T: OrbitTyped>(&self) -> usize {
524        match &self.inner.backing {
525            RingBacking::InMemory(r) => r.get_or_create::<T>().capacity(),
526            #[cfg(unix)]
527            RingBacking::Shm(r) => r
528                .get_or_create_for::<T>()
529                .map(|ring| ring.capacity())
530                .unwrap_or(T::RING_SPEC.capacity),
531        }
532    }
533
534    /// Allocate one semantic version shared by every writer lane of `T`.
535    ///
536    /// This is separate from each lane's physical frame counter. It is useful
537    /// for semantic layers that retain per-node write scalability but require
538    /// a deterministic fleet-wide last-write-wins order.
539    pub fn next_ring_version<T: OrbitTyped>(&self) -> u64 {
540        match &self.inner.backing {
541            RingBacking::InMemory(r) => r.get_or_create::<T>().next_version(),
542            #[cfg(unix)]
543            RingBacking::Shm(r) => r
544                .get_or_create_for::<T>()
545                .expect("SHM ring open failed — fleet unusable")
546                .next_version(),
547        }
548    }
549
550    /// Return the last semantic version allocated for `T` without advancing it.
551    pub fn current_ring_version<T: OrbitTyped>(&self) -> u64 {
552        match &self.inner.backing {
553            RingBacking::InMemory(r) => r.get_or_create::<T>().current_version(),
554            #[cfg(unix)]
555            RingBacking::Shm(r) => r
556                .get_or_create_for::<T>()
557                .expect("SHM ring open failed — fleet unusable")
558                .current_version(),
559        }
560    }
561
562    /// Clear every lane for `T` and reset all heads to zero.
563    ///
564    /// This is an owner-side boot cleanup primitive. It is safe for
565    /// runtime state such as events and periodic metrics when the
566    /// embedding application calls it before peer processes begin
567    /// publishing. It is not a coordination protocol; callers must not
568    /// reset a ring while other fleet members are actively writing it.
569    pub fn reset_ring<T: OrbitTyped>(&self) -> std::io::Result<()> {
570        match &self.inner.backing {
571            RingBacking::InMemory(r) => {
572                r.get_or_create::<T>().reset();
573                Ok(())
574            }
575            #[cfg(unix)]
576            RingBacking::Shm(r) => {
577                r.get_or_create_for::<T>()?.reset();
578                Ok(())
579            }
580        }
581    }
582
583    #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))]
584    /// Create a process-local readiness fd for one notified SHM ring.
585    ///
586    /// The fd only signals that the ring generation changed. After draining
587    /// it, callers must poll the ring with their own cursor. Multiple writes
588    /// may coalesce into one readiness notification.
589    pub fn ring_event_fd<T: OrbitTyped>(&self) -> std::io::Result<RingEventFd> {
590        match &self.inner.backing {
591            RingBacking::Shm(rings) => RingEventFd::new(rings.get_or_create_for::<T>()?),
592            RingBacking::InMemory(_) => Err(std::io::Error::new(
593                std::io::ErrorKind::Unsupported,
594                "Orbit eventfd requires a shared-memory fleet",
595            )),
596        }
597    }
598
599    #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))]
600    /// Publish one frame and notify native waiters after it commits.
601    pub fn publish_notified<T: OrbitTyped>(
602        &self,
603        frame_kind: u8,
604        ver: u64,
605        payload: Bytes,
606    ) -> std::io::Result<NetId64> {
607        match &self.inner.backing {
608            RingBacking::Shm(rings) => {
609                let ring = rings.get_or_create_for::<T>()?;
610                let id = ring.write(self.node_id(), frame_kind, ver, payload)?;
611                RingEventFd::notify(&ring)?;
612                Ok(id)
613            }
614            // Process-local fleets do not need a kernel wake bridge.
615            RingBacking::InMemory(rings) => {
616                Ok(rings
617                    .get_or_create::<T>()
618                    .write(self.node_id(), frame_kind, ver, payload))
619            }
620        }
621    }
622
623    #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))]
624    /// Publish one contiguous batch and notify native waiters once after the
625    /// complete batch commits.
626    pub fn publish_batch_notified<T: OrbitTyped>(
627        &self,
628        frame_kind: u8,
629        ver: u64,
630        payloads: Vec<Bytes>,
631    ) -> std::io::Result<Vec<NetId64>> {
632        match &self.inner.backing {
633            RingBacking::Shm(rings) => {
634                let ring = rings.get_or_create_for::<T>()?;
635                let ids = ring.write_batch(self.node_id(), frame_kind, ver, payloads)?;
636                if !ids.is_empty() {
637                    RingEventFd::notify(&ring)?;
638                }
639                Ok(ids)
640            }
641            RingBacking::InMemory(rings) => Ok(rings.get_or_create::<T>().write_batch(
642                self.node_id(),
643                frame_kind,
644                ver,
645                payloads,
646            )),
647        }
648    }
649}
650
651impl std::fmt::Debug for Fleet {
652    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
653        f.debug_struct("Fleet")
654            .field("name", &self.inner.name)
655            .field("fleet_capacity", &self.inner.fleet_capacity)
656            .field("node_id", &self.inner.node_id)
657            .field("id_counters", &self.inner.id_counters.len())
658            .finish()
659    }
660}