Skip to main content

orbit_core/ring/
shm.rs

1//! `ShmRing` — POSIX SHM-backed ring buffer (V1 substrate).
2//!
3//! The runtime substrate that finally makes Orbit's "fleet sees each
4//! other" promise real. Cross-process visibility is a memory-coherent
5//! `mmap` with shared atomics; no kernel round-trips per access.
6//!
7//! ## Layout (single SHM segment per `OrbitTyped` kind)
8//!
9//! ```text
10//! ┌──────────────────────────┬───────────────────────────┬──────────────┐
11//! │ ShmRingHeader (64B)      │ LaneHeader[0..L]          │ Lane slots   │
12//! │ kind/spec/topology       │ one head per writer lane  │ L × N slots  │
13//! └──────────────────────────┴───────────────────────────┴──────────────┘
14//! ```
15//!
16//! Shared topology retains the lock-free multi-writer claim-before-commit
17//! protocol. Shared-ordered topology serializes writers with a
18//! process-recoverable kernel advisory lock, commits the slot, and only then
19//! advances the head.
20//! Per-node topology assigns disjoint slots to every node and requires one
21//! active process per node id. Concurrent tasks inside that process are
22//! serialized locally, and the lane head is published only after the slot
23//! reaches its committed sequence. A process dying during a per-node write
24//! therefore leaves no visible hole for other lanes.
25//!
26//! ## Slot publication protocol
27//!
28//! 1. choose/claim the lane counter
29//! 2. `slot = slots[counter % capacity]`
30//! 3. `slot.seq = 2*counter + 1` (odd → writing)
31//! 4. fill `id`, `kind`, `ver`, `payload_len`, `payload[..len]`
32//! 5. `slot.seq = 2*counter + 2` (even → committed)
33//! 6. for per-node lanes, release-store `head = counter + 1`
34//!
35//! ## Read protocol
36//!
37//! 1. read `seq_pre = slot.seq` (Acquire)
38//! 2. if odd → writer in progress, return None / retry
39//! 3. read all fields into locals
40//! 4. read `seq_post = slot.seq` (Acquire)
41//! 5. if `seq_pre != seq_post` → torn write, retry
42//! 6. validate counter encoded in seq matches the one we want
43//!
44//! Tearing is detected, never silently merged. A reader that loses
45//! the race against the writer simply retries (or returns None for
46//! head-read).
47//!
48//! ## Per-kind payload size
49//!
50//! Every `OrbitTyped::KIND` declares its own `RingSpec`. The payload
51//! capacity determines that SHM segment's slot stride; unrelated lanes
52//! no longer pay for one global maximum. Capacity and payload capacity
53//! are persisted in the header and verified by every attaching process.
54
55#![cfg(unix)]
56
57use std::ptr;
58use std::sync::Mutex;
59use std::sync::atomic::{AtomicU8, AtomicU32, AtomicU64, Ordering, fence};
60
61use bytes::Bytes;
62
63use crate::NodeId;
64use crate::id::NetId64;
65use crate::ring::{Frame, RingSpec, RingTopology};
66use crate::shm::{self, ShmRegion};
67
68// ─────────────────────────────────────────────────────────────────────
69// On-disk (well, on-SHM) layout
70// ─────────────────────────────────────────────────────────────────────
71
72const MAGIC: u32 = 0x4f524254; // "ORBT" big-endian when displayed
73const VERSION: u32 = 3;
74const SLOT_ALIGNMENT: usize = 64;
75
76/// Cache-line aligned to keep the header on its own line.
77#[repr(C, align(64))]
78struct ShmRingHeader {
79    version_counter: AtomicU64,
80    capacity: u64,
81    magic: u32,
82    version: u32,
83    payload_capacity: u32,
84    slot_stride: u32,
85    kind: u8,
86    topology: u8,
87    lane_count: u16,
88    /// Shared futex/umtx word used by notification-enabled ring users.
89    /// It lives in the existing reserved header space, so the V3
90    /// layout remains compatible with already-created segments.
91    notification_generation: AtomicU32,
92    /// Readers parked on `notification_generation` through
93    /// [`RingEventFd`](super::RingEventFd), so a publisher wakes only when
94    /// one is waiting. Taken from the reserved space like the generation:
95    /// segments created before it read zero.
96    notification_waiters: AtomicU32,
97    /// Explicitly fills one cache line; field order avoids implicit
98    /// alignment padding that would otherwise make this header 128B.
99    _reserved: [u8; 20],
100}
101
102#[repr(C, align(64))]
103struct ShmLaneHeader {
104    write_pos: AtomicU64,
105    _reserved: [u8; 56],
106}
107
108/// Fixed prefix of one dynamically-strided SHM slot.
109#[repr(C)]
110struct ShmSlotHeader {
111    /// LMAX-style sequence: odd while a writer is mid-flight, even
112    /// once committed. Reader checks seq before/after content read
113    /// to detect torn writes.
114    seq: AtomicU64,
115    /// `NetId64::raw()` of the frame that occupies this slot.
116    id: AtomicU64,
117    /// `Frame::ver` — caller-supplied version / tick.
118    ver: AtomicU64,
119    /// Length of the meaningful prefix of `payload`.
120    payload_len: AtomicU32,
121    /// `Frame::kind` — the message-class byte (state/event/cmd/…).
122    kind: AtomicU8,
123    _reserved: [u8; 3],
124}
125
126// The content fields are atomic for the same reason `seq` is: a reader may be
127// copying this slot while a writer overwrites it. The seqlock *detects* that
128// afterwards, which is not the same as not having a race — under the memory
129// model a torn read through a plain `u64` is undefined however the hardware
130// behaves, and Miri or TSan would say so. `Relaxed` is what they need: all the
131// ordering is already carried by `seq` and the two fences around it, so these
132// compile to the same plain loads and stores they were, with the race removed
133// rather than papered over.
134
135const SLOT_HEADER_SIZE: usize = std::mem::size_of::<ShmSlotHeader>();
136const HEADER_SIZE: usize = std::mem::size_of::<ShmRingHeader>();
137const LANE_HEADER_SIZE: usize = std::mem::size_of::<ShmLaneHeader>();
138
139const _: () = assert!(HEADER_SIZE == 64);
140const _: () = assert!(LANE_HEADER_SIZE == 64);
141const _: () = assert!(SLOT_HEADER_SIZE == 32);
142
143fn invalid_input(message: impl Into<String>) -> std::io::Error {
144    std::io::Error::new(std::io::ErrorKind::InvalidInput, message.into())
145}
146
147fn lane_count_for(
148    spec: RingSpec,
149    fleet_capacity: u16,
150) -> std::io::Result<usize> {
151    if fleet_capacity == 0 {
152        return Err(invalid_input("ShmRing fleet capacity must be > 0"));
153    }
154    Ok(match spec.topology {
155        RingTopology::Shared | RingTopology::SharedOrdered => 1,
156        RingTopology::PerNode => usize::from(fleet_capacity),
157    })
158}
159
160fn checked_layout(
161    spec: RingSpec,
162    fleet_capacity: u16,
163) -> std::io::Result<(usize, usize, usize)> {
164    if spec.capacity == 0 {
165        return Err(invalid_input("ShmRing capacity must be > 0"));
166    }
167    if !spec.capacity.is_power_of_two() {
168        return Err(invalid_input("ShmRing capacity must be a power of two"));
169    }
170    if spec.payload_capacity > u32::MAX as usize {
171        return Err(invalid_input("ShmRing payload capacity must fit in u32"));
172    }
173
174    let unaligned = SLOT_HEADER_SIZE
175        .checked_add(spec.payload_capacity)
176        .ok_or_else(|| invalid_input("ShmRing slot size overflow"))?;
177    let slot_stride = unaligned
178        .checked_add(SLOT_ALIGNMENT - 1)
179        .map(|value| value & !(SLOT_ALIGNMENT - 1))
180        .ok_or_else(|| invalid_input("ShmRing slot stride overflow"))?;
181    if slot_stride > u32::MAX as usize {
182        return Err(invalid_input("ShmRing slot stride must fit in u32"));
183    }
184    let lane_count = lane_count_for(spec, fleet_capacity)?;
185    let lane_headers_size = lane_count
186        .checked_mul(LANE_HEADER_SIZE)
187        .ok_or_else(|| invalid_input("ShmRing lane header size overflow"))?;
188    let slots_offset = HEADER_SIZE
189        .checked_add(lane_headers_size)
190        .ok_or_else(|| invalid_input("ShmRing slots offset overflow"))?;
191    let slots_per_lane = spec
192        .capacity
193        .checked_mul(slot_stride)
194        .ok_or_else(|| invalid_input("ShmRing lane size overflow"))?;
195    let slots_size = lane_count
196        .checked_mul(slots_per_lane)
197        .ok_or_else(|| invalid_input("ShmRing slots size overflow"))?;
198    let segment_size = slots_offset
199        .checked_add(slots_size)
200        .ok_or_else(|| invalid_input("ShmRing segment size overflow"))?;
201    Ok((slot_stride, slots_offset, segment_size))
202}
203
204/// Compute the SHM segment size required by a ring spec.
205pub fn segment_size_for_spec(spec: RingSpec) -> std::io::Result<usize> {
206    segment_size_for_spec_and_fleet(spec, 1)
207}
208
209/// Compute the SHM segment size required for a fleet-aware ring spec.
210pub fn segment_size_for_spec_and_fleet(
211    spec: RingSpec,
212    fleet_capacity: u16,
213) -> std::io::Result<usize> {
214    checked_layout(spec, fleet_capacity)
215        .map(|(_, _, segment_size)| segment_size)
216}
217
218// ─────────────────────────────────────────────────────────────────────
219// ShmRing
220// ─────────────────────────────────────────────────────────────────────
221
222/// Cross-process ring buffer backed by a POSIX SHM segment.
223///
224/// Use [`ShmRing::open_or_create`] to attach to (or create) the
225/// segment by name. Multiple processes calling this with the same name and
226/// matching [`RingSpec`] share the underlying memory; the first process to
227/// call it does the one-time header initialization.
228pub struct ShmRing {
229    region: ShmRegion,
230    kind: u8,
231    capacity: usize,
232    payload_capacity: usize,
233    topology: RingTopology,
234    lane_count: usize,
235    slot_stride: usize,
236    slots_offset: usize,
237    lane_stride: usize,
238    write_locks: Vec<Mutex<()>>,
239}
240
241impl ShmRing {
242    /// Open or create one ring with an explicit OS access policy.
243    /// The policy is not part of the persisted ring layout.
244    pub fn open_or_create_with_policy(
245        fleet_name: &str,
246        kind: u8,
247        spec: RingSpec,
248        policy: shm::ShmAccessPolicy,
249    ) -> std::io::Result<Self> {
250        Self::open_or_create_for_fleet_with_policy(
251            fleet_name, kind, spec, 1, policy,
252        )
253    }
254
255    /// Open or create a SHM-backed ring under `fleet_name` for type
256    /// kind `kind` with `spec`. The first process to call
257    /// this initializes the header; later attachers reuse it.
258    pub fn open_or_create(
259        fleet_name: &str,
260        kind: u8,
261        spec: RingSpec,
262    ) -> std::io::Result<Self> {
263        Self::open_or_create_for_fleet(fleet_name, kind, spec, 1)
264    }
265
266    /// Open or create a SHM-backed ring using `fleet_capacity` physical
267    /// writer lanes when `spec` is [`RingTopology::PerNode`].
268    pub fn open_or_create_for_fleet(
269        fleet_name: &str,
270        kind: u8,
271        spec: RingSpec,
272        fleet_capacity: u16,
273    ) -> std::io::Result<Self> {
274        Self::open_or_create_for_fleet_with_policy(
275            fleet_name,
276            kind,
277            spec,
278            fleet_capacity,
279            shm::ShmAccessPolicy::default(),
280        )
281    }
282
283    /// Policy-aware variant of [`Self::open_or_create_for_fleet`].
284    /// Fleet writers and companion locks remain in the owner's uid namespace;
285    /// group permissions do not provide cross-uid fleet joining or coordination.
286    pub fn open_or_create_for_fleet_with_policy(
287        fleet_name: &str,
288        kind: u8,
289        spec: RingSpec,
290        fleet_capacity: u16,
291        policy: shm::ShmAccessPolicy,
292    ) -> std::io::Result<Self> {
293        let lane_count = lane_count_for(spec, fleet_capacity)?;
294        let (slot_stride, slots_offset, size) =
295            checked_layout(spec, fleet_capacity)?;
296        let lane_stride = spec
297            .capacity
298            .checked_mul(slot_stride)
299            .ok_or_else(|| invalid_input("ShmRing lane stride overflow"))?;
300        let name = shm::ring_segment_name(fleet_name, kind);
301
302        // Locked whatever the topology. Creation is two steps that a peer can
303        // arrive between — `shm_open(O_CREAT|O_EXCL)` publishes the name, and
304        // the header below is written after it — and the attach path reads that
305        // header and rejects a segment whose magic is still zero, with no
306        // retry. Only `SharedOrdered` took this lock, so every other ring was
307        // one scheduling accident away from a peer failing to boot with
308        // "wrong magic 0x00000000"; N workers starting together is exactly the
309        // case that produces it. `SharedOrdered` still keeps the lock for its
310        // writes — this is the same lock, held for a different reason.
311        let (region, _initialization_lock) =
312            ShmRegion::open_or_create_locked_with_policy(&name, size, policy)?;
313
314        // Initialize header on first creation; subsequent attachers
315        // skip and rely on whatever the creator wrote.
316        if region.created() {
317            // SAFETY: region is mapped, header lives at offset 0,
318            // size is right because we just created with this size.
319            unsafe {
320                let header_ptr = region.as_ptr() as *mut ShmRingHeader;
321                ptr::write(
322                    header_ptr,
323                    ShmRingHeader {
324                        version_counter: AtomicU64::new(0),
325                        capacity: spec.capacity as u64,
326                        magic: MAGIC,
327                        version: VERSION,
328                        payload_capacity: spec.payload_capacity as u32,
329                        slot_stride: slot_stride as u32,
330                        kind,
331                        topology: spec.topology as u8,
332                        lane_count: lane_count as u16,
333                        notification_generation: AtomicU32::new(0),
334                        notification_waiters: AtomicU32::new(0),
335                        _reserved: [0; 20],
336                    },
337                );
338
339                for lane in 0..lane_count {
340                    let lane_ptr = region
341                        .as_ptr()
342                        .add(HEADER_SIZE + lane * LANE_HEADER_SIZE)
343                        as *mut ShmLaneHeader;
344                    ptr::write(
345                        lane_ptr,
346                        ShmLaneHeader {
347                            write_pos: AtomicU64::new(0),
348                            _reserved: [0; 56],
349                        },
350                    );
351                }
352
353                // Zero out the slot region so all `seq` values start
354                // at 0 (== "never written, even, slot empty"). 0 is
355                // valid as both a u64 atomic and as bytes for our
356                // POD slot shape.
357                let slots_ptr = region.as_ptr().add(slots_offset);
358                ptr::write_bytes(slots_ptr, 0, lane_count * lane_stride);
359            }
360        } else {
361            // Attaching to an existing segment — sanity-check the header.
362            // SAFETY: region is mapped; header lives at offset 0.
363            let header = unsafe { &*(region.as_ptr() as *const ShmRingHeader) };
364            if header.magic != MAGIC {
365                return Err(std::io::Error::new(
366                    std::io::ErrorKind::InvalidData,
367                    format!(
368                        "SHM segment {} has wrong magic 0x{:08X} (expected 0x{:08X})",
369                        name, header.magic, MAGIC
370                    ),
371                ));
372            }
373            if header.version != VERSION {
374                return Err(std::io::Error::new(
375                    std::io::ErrorKind::InvalidData,
376                    format!(
377                        "SHM segment {} version {} != local {}",
378                        name, header.version, VERSION
379                    ),
380                ));
381            }
382            if header.kind != kind {
383                return Err(std::io::Error::new(
384                    std::io::ErrorKind::InvalidData,
385                    format!(
386                        "SHM segment {} kind {} != requested {}",
387                        name, header.kind, kind
388                    ),
389                ));
390            }
391            if header.topology != spec.topology as u8 {
392                return Err(std::io::Error::new(
393                    std::io::ErrorKind::InvalidData,
394                    format!(
395                        "SHM segment {} topology {} != requested {}",
396                        name, header.topology, spec.topology as u8
397                    ),
398                ));
399            }
400            if header.lane_count as usize != lane_count {
401                return Err(std::io::Error::new(
402                    std::io::ErrorKind::InvalidData,
403                    format!(
404                        "SHM segment {} lane count {} != requested {}",
405                        name, header.lane_count, lane_count
406                    ),
407                ));
408            }
409            if header.capacity as usize != spec.capacity {
410                return Err(std::io::Error::new(
411                    std::io::ErrorKind::InvalidData,
412                    format!(
413                        "SHM segment {} capacity {} != requested {}",
414                        name, header.capacity, spec.capacity
415                    ),
416                ));
417            }
418            if header.payload_capacity as usize != spec.payload_capacity {
419                return Err(std::io::Error::new(
420                    std::io::ErrorKind::InvalidData,
421                    format!(
422                        "SHM segment {} payload capacity {} != requested {}",
423                        name, header.payload_capacity, spec.payload_capacity
424                    ),
425                ));
426            }
427            if header.slot_stride as usize != slot_stride {
428                return Err(std::io::Error::new(
429                    std::io::ErrorKind::InvalidData,
430                    format!(
431                        "SHM segment {} slot stride {} != requested {}",
432                        name, header.slot_stride, slot_stride
433                    ),
434                ));
435            }
436        }
437
438        Ok(Self {
439            region,
440            kind,
441            capacity: spec.capacity,
442            payload_capacity: spec.payload_capacity,
443            topology: spec.topology,
444            lane_count,
445            slot_stride,
446            slots_offset,
447            lane_stride,
448            write_locks: (0..lane_count).map(|_| Mutex::new(())).collect(),
449        })
450    }
451
452    /// True when this handle was the one that *created* the SHM
453    /// segment (vs attaching to an already-existing one).
454    pub fn created(&self) -> bool {
455        self.region.created()
456    }
457
458    /// Remove the SHM segment name. Existing mappings stay valid;
459    /// new opens will fail until `open_or_create` recreates it.
460    /// Call from the owner process at fleet shutdown.
461    pub fn unlink(&self) -> std::io::Result<()> {
462        self.region.unlink()
463    }
464
465    /// The KIND byte this ring carries.
466    pub fn kind(&self) -> u8 {
467        self.kind
468    }
469
470    /// Slot count per lane.
471    pub fn capacity(&self) -> usize {
472        self.capacity
473    }
474
475    /// Number of physical writer lanes in this segment.
476    pub fn lane_count(&self) -> usize {
477        self.lane_count
478    }
479
480    /// Maximum inline payload bytes for this ring lane.
481    pub fn payload_capacity(&self) -> usize {
482        self.payload_capacity
483    }
484
485    pub fn spec(&self) -> RingSpec {
486        RingSpec {
487            capacity: self.capacity,
488            payload_capacity: self.payload_capacity,
489            topology: self.topology,
490        }
491    }
492
493    pub(crate) fn publication_lock(
494        &self,
495        exclusive: bool,
496    ) -> std::io::Result<shm::ShmRegionLock> {
497        if exclusive {
498            self.region.lock_exclusive()
499        } else {
500            self.region.lock_shared()
501        }
502    }
503
504    fn lane_header(
505        &self,
506        lane: usize,
507    ) -> &ShmLaneHeader {
508        debug_assert!(lane < self.lane_count);
509        unsafe {
510            &*(self.region.as_ptr().add(HEADER_SIZE + lane * LANE_HEADER_SIZE)
511                as *const ShmLaneHeader)
512        }
513    }
514
515    #[cfg(any(
516        target_os = "linux",
517        target_os = "freebsd",
518        target_os = "macos"
519    ))]
520    pub(crate) fn notification_generation(&self) -> &AtomicU32 {
521        // SAFETY: the mapped header was initialized before this ring handle
522        // was returned and remains mapped for the lifetime of `self`.
523        unsafe {
524            &(*(self.region.as_ptr() as *const ShmRingHeader))
525                .notification_generation
526        }
527    }
528
529    #[cfg(any(
530        target_os = "linux",
531        target_os = "freebsd",
532        target_os = "macos"
533    ))]
534    pub(crate) fn notification_waiters(&self) -> &AtomicU32 {
535        // SAFETY: as for `notification_generation`.
536        unsafe {
537            &(*(self.region.as_ptr() as *const ShmRingHeader))
538                .notification_waiters
539        }
540    }
541
542    fn slot_ptr(
543        &self,
544        lane: usize,
545        idx: usize,
546    ) -> *mut ShmSlotHeader {
547        debug_assert!(lane < self.lane_count);
548        debug_assert!(idx < self.capacity);
549        // SAFETY: slots region begins at `slots_offset`; lane and idx
550        // are bounded by the validated mapping layout.
551        unsafe {
552            let base = self
553                .region
554                .as_ptr()
555                .add(self.slots_offset + lane * self.lane_stride);
556            base.add(idx * self.slot_stride).cast::<ShmSlotHeader>()
557        }
558    }
559
560    /// The payload bytes, as the atomics they have to be for the same reason
561    /// the header fields are.
562    ///
563    /// The cost is a byte at a time instead of a `memcpy`, over payloads this
564    /// workspace sizes in tens to hundreds of bytes — 18 for a metric sample,
565    /// 1024 at the largest — published at snapshot rather than request rate.
566    /// Word-at-a-time would need the payload region padded to a word multiple,
567    /// which changes the segment layout and so the compatibility check every
568    /// attaching process makes; worth doing if a payload ever grows enough to
569    /// notice, and not before.
570    unsafe fn payload_ptr(slot_ptr: *mut ShmSlotHeader) -> *mut AtomicU8 {
571        unsafe {
572            slot_ptr.cast::<u8>().add(SLOT_HEADER_SIZE).cast::<AtomicU8>()
573        }
574    }
575
576    /// Head of the sole shared lane, or lane zero for a per-node ring.
577    pub fn head(&self) -> u64 {
578        self.lane_header(0).write_pos.load(Ordering::Acquire)
579    }
580
581    /// Current committed head for `node_id`'s lane.
582    pub fn lane_head(
583        &self,
584        node_id: NodeId,
585    ) -> u64 {
586        let lane = self.lane_index(node_id);
587        self.lane_header(lane).write_pos.load(Ordering::Acquire)
588    }
589
590    /// Allocate one non-zero semantic version shared by all writer lanes and
591    /// processes attached to this ring.
592    pub fn next_version(&self) -> u64 {
593        let header =
594            unsafe { &*(self.region.as_ptr() as *const ShmRingHeader) };
595        header
596            .version_counter
597            .fetch_add(1, Ordering::AcqRel)
598            .checked_add(1)
599            .expect("SHM ring semantic version exhausted")
600    }
601
602    /// Last semantic version allocated by any process attached to this ring.
603    pub fn current_version(&self) -> u64 {
604        let header =
605            unsafe { &*(self.region.as_ptr() as *const ShmRingHeader) };
606        header.version_counter.load(Ordering::Acquire)
607    }
608
609    /// Append a frame. Atomically reserves the next counter, mints
610    /// the [`NetId64`], writes the slot. Returns the minted id.
611    pub fn write(
612        &self,
613        node_id: NodeId,
614        frame_kind: u8,
615        ver: u64,
616        payload: Bytes,
617    ) -> std::io::Result<NetId64> {
618        if payload.len() > self.payload_capacity {
619            return Err(std::io::Error::new(
620                std::io::ErrorKind::InvalidInput,
621                format!(
622                    "payload {} > ring payload capacity {}",
623                    payload.len(),
624                    self.payload_capacity
625                ),
626            ));
627        }
628
629        let lane = self.lane_index(node_id);
630        match self.topology {
631            RingTopology::Shared => {
632                let counter = self
633                    .lane_header(lane)
634                    .write_pos
635                    .fetch_add(1, Ordering::AcqRel);
636                Ok(self.write_slot(
637                    lane, node_id, counter, frame_kind, ver, &payload,
638                ))
639            }
640            RingTopology::PerNode => {
641                let _write = self.write_locks[lane]
642                    .lock()
643                    .unwrap_or_else(|error| error.into_inner());
644                let lane_header = self.lane_header(lane);
645                let counter = lane_header.write_pos.load(Ordering::Relaxed);
646                let id = self.write_slot(
647                    lane, node_id, counter, frame_kind, ver, &payload,
648                );
649                lane_header
650                    .write_pos
651                    .store(counter.wrapping_add(1), Ordering::Release);
652                Ok(id)
653            }
654            RingTopology::SharedOrdered => {
655                let _write = self.write_locks[lane]
656                    .lock()
657                    .unwrap_or_else(|error| error.into_inner());
658                let _cross_process = self.region.lock_exclusive()?;
659                let lane_header = self.lane_header(lane);
660                let counter = lane_header.write_pos.load(Ordering::Relaxed);
661                let id = self.write_slot(
662                    lane, node_id, counter, frame_kind, ver, &payload,
663                );
664                lane_header
665                    .write_pos
666                    .store(counter.wrapping_add(1), Ordering::Release);
667                Ok(id)
668            }
669        }
670    }
671
672    /// Append one contiguous batch to a lane and return consecutive ids.
673    ///
674    /// Per-node and shared-ordered lane heads advance only after the complete
675    /// batch has committed. The batch may not exceed the ring capacity.
676    pub fn write_batch(
677        &self,
678        node_id: NodeId,
679        frame_kind: u8,
680        ver: u64,
681        payloads: Vec<Bytes>,
682    ) -> std::io::Result<Vec<NetId64>> {
683        if payloads.len() > self.capacity {
684            return Err(std::io::Error::new(
685                std::io::ErrorKind::InvalidInput,
686                format!(
687                    "batch {} > ring capacity {}",
688                    payloads.len(),
689                    self.capacity
690                ),
691            ));
692        }
693        if let Some(payload) = payloads
694            .iter()
695            .find(|payload| payload.len() > self.payload_capacity)
696        {
697            return Err(std::io::Error::new(
698                std::io::ErrorKind::InvalidInput,
699                format!(
700                    "payload {} > ring payload capacity {}",
701                    payload.len(),
702                    self.payload_capacity
703                ),
704            ));
705        }
706        if payloads.is_empty() {
707            return Ok(Vec::new());
708        }
709
710        let lane = self.lane_index(node_id);
711        let write_slots = |start: u64, payloads: Vec<Bytes>| {
712            payloads
713                .into_iter()
714                .enumerate()
715                .map(|(offset, payload)| {
716                    self.write_slot(
717                        lane,
718                        node_id,
719                        start.wrapping_add(offset as u64),
720                        frame_kind,
721                        ver,
722                        &payload,
723                    )
724                })
725                .collect::<Vec<_>>()
726        };
727
728        match self.topology {
729            RingTopology::Shared => {
730                let start = self
731                    .lane_header(lane)
732                    .write_pos
733                    .fetch_add(payloads.len() as u64, Ordering::AcqRel);
734                Ok(write_slots(start, payloads))
735            }
736            RingTopology::PerNode => {
737                let _write = self.write_locks[lane]
738                    .lock()
739                    .unwrap_or_else(|error| error.into_inner());
740                let lane_header = self.lane_header(lane);
741                let start = lane_header.write_pos.load(Ordering::Relaxed);
742                let ids = write_slots(start, payloads);
743                lane_header.write_pos.store(
744                    start.wrapping_add(ids.len() as u64),
745                    Ordering::Release,
746                );
747                Ok(ids)
748            }
749            RingTopology::SharedOrdered => {
750                let _write = self.write_locks[lane]
751                    .lock()
752                    .unwrap_or_else(|error| error.into_inner());
753                let _cross_process = self.region.lock_exclusive()?;
754                let lane_header = self.lane_header(lane);
755                let start = lane_header.write_pos.load(Ordering::Relaxed);
756                let ids = write_slots(start, payloads);
757                lane_header.write_pos.store(
758                    start.wrapping_add(ids.len() as u64),
759                    Ordering::Release,
760                );
761                Ok(ids)
762            }
763        }
764    }
765
766    /// Read the slot whose counter matches `id.counter()`. Returns
767    /// `None` if the slot has been overwritten, was never written,
768    /// or a torn read could not be reconciled across two retries.
769    pub fn read(
770        &self,
771        id: NetId64,
772    ) -> Option<Frame> {
773        if id.kind() != self.kind {
774            return None;
775        }
776        let lane = self.lane_index_for_frame(id)?;
777        let counter = id.counter();
778        let slot_idx = (counter as usize) & (self.capacity - 1);
779        let slot_ptr = self.slot_ptr(lane, slot_idx);
780
781        // Two retries — torn writes happen but resolve quickly.
782        for _ in 0..3 {
783            let Some(frame) = (unsafe {
784                read_committed_frame(slot_ptr, self.payload_capacity)
785            }) else {
786                continue;
787            };
788            if frame.id.counter() == counter {
789                return Some(frame);
790            } else {
791                // Slot now holds a different (later) id — wraparound.
792                return None;
793            }
794        }
795        None
796    }
797
798    /// Read the most recent frame (head - 1). Returns `None` if no
799    /// write has happened yet, or a torn read could not resolve.
800    pub fn read_head(&self) -> Option<Frame> {
801        let head = self.head();
802        if head == 0 {
803            return None;
804        }
805        let counter = head - 1;
806        let slot_idx = (counter as usize) & (self.capacity - 1);
807        let slot_ptr = self.slot_ptr(0, slot_idx);
808
809        for _ in 0..3 {
810            if let Some(frame) =
811                unsafe { read_committed_frame(slot_ptr, self.payload_capacity) }
812            {
813                return Some(frame);
814            }
815        }
816        None
817    }
818
819    /// Read whatever frame currently occupies the slot at
820    /// `counter % capacity`, regardless of which counter is stored
821    /// there. Used by walking readers that need slot-by-slot access
822    /// without knowing the writer's `NetId64` ahead of time.
823    pub fn read_at(
824        &self,
825        counter: u64,
826    ) -> Option<Frame> {
827        self.read_lane_index_at(0, counter)
828    }
829
830    /// Read the frame currently occupying `node_id`'s lane slot.
831    pub fn read_lane_at(
832        &self,
833        node_id: NodeId,
834        counter: u64,
835    ) -> Option<Frame> {
836        let lane = self.lane_index(node_id);
837        self.read_lane_index_at(lane, counter)
838    }
839
840    fn read_lane_index_at(
841        &self,
842        lane: usize,
843        counter: u64,
844    ) -> Option<Frame> {
845        let slot_idx = (counter as usize) & (self.capacity - 1);
846        let slot_ptr = self.slot_ptr(lane, slot_idx);
847        for _ in 0..3 {
848            if let Some(frame) =
849                unsafe { read_committed_frame(slot_ptr, self.payload_capacity) }
850            {
851                return Some(frame);
852            }
853        }
854        None
855    }
856
857    pub(crate) fn read_state_at(
858        &self,
859        counter: u64,
860    ) -> crate::ring::cursor::RingRead {
861        self.read_lane_index_state_at(0, counter)
862    }
863
864    pub(crate) fn read_lane_state_at(
865        &self,
866        node_id: NodeId,
867        counter: u64,
868    ) -> crate::ring::cursor::RingRead {
869        let lane = self.lane_index(node_id);
870        self.read_lane_index_state_at(lane, counter)
871    }
872
873    fn read_lane_index_state_at(
874        &self,
875        lane: usize,
876        counter: u64,
877    ) -> crate::ring::cursor::RingRead {
878        use crate::ring::cursor::RingRead;
879
880        let slot_idx = (counter as usize) & (self.capacity - 1);
881        let slot_ptr = self.slot_ptr(lane, slot_idx);
882        let expected_committed = counter
883            .checked_mul(2)
884            .and_then(|value| value.checked_add(2))
885            .expect("seq overflow");
886
887        for _ in 0..3 {
888            let sequence = unsafe { &*slot_ptr }.seq.load(Ordering::Acquire);
889            if sequence < expected_committed {
890                return if self.topology != RingTopology::Shared {
891                    RingRead::Unavailable
892                } else {
893                    RingRead::Pending
894                };
895            }
896            if sequence > expected_committed {
897                return RingRead::Unavailable;
898            }
899            if let Some(frame) =
900                unsafe { read_committed_frame(slot_ptr, self.payload_capacity) }
901            {
902                return if frame.id.counter() == counter {
903                    RingRead::Ready(frame)
904                } else {
905                    RingRead::Unavailable
906                };
907            }
908        }
909
910        let sequence = unsafe { &*slot_ptr }.seq.load(Ordering::Acquire);
911        if sequence < expected_committed {
912            if self.topology != RingTopology::Shared {
913                RingRead::Unavailable
914            } else {
915                RingRead::Pending
916            }
917        } else {
918            RingRead::Unavailable
919        }
920    }
921
922    /// Clear all slots and reset every lane head to zero.
923    ///
924    /// Intended for owner-controlled boot-time cleanup. Do not call
925    /// while other processes are publishing to this ring: it rewrites
926    /// the shared slot memory in place.
927    pub fn reset(&self) {
928        // SAFETY: the region is mapped and the slot area begins at
929        // HEADER_SIZE. The caller must ensure the ring is quiescent.
930        unsafe {
931            let slots_ptr = self.region.as_ptr().add(self.slots_offset);
932            ptr::write_bytes(slots_ptr, 0, self.lane_count * self.lane_stride);
933        }
934        for lane in 0..self.lane_count {
935            self.lane_header(lane).write_pos.store(0, Ordering::Release);
936        }
937        let header =
938            unsafe { &*(self.region.as_ptr() as *const ShmRingHeader) };
939        header.version_counter.store(0, Ordering::Release);
940    }
941
942    fn lane_index(
943        &self,
944        node_id: NodeId,
945    ) -> usize {
946        let lane = match self.topology {
947            RingTopology::Shared | RingTopology::SharedOrdered => 0,
948            RingTopology::PerNode => usize::from(node_id.get()),
949        };
950        assert!(
951            lane < self.lane_count,
952            "node {} is outside SHM ring lane count {}",
953            node_id.get(),
954            self.lane_count
955        );
956        lane
957    }
958
959    fn lane_index_for_frame(
960        &self,
961        id: NetId64,
962    ) -> Option<usize> {
963        let lane = match self.topology {
964            RingTopology::Shared | RingTopology::SharedOrdered => 0,
965            RingTopology::PerNode => usize::from(id.node()),
966        };
967        (lane < self.lane_count).then_some(lane)
968    }
969
970    fn write_slot(
971        &self,
972        lane: usize,
973        node_id: NodeId,
974        counter: u64,
975        frame_kind: u8,
976        ver: u64,
977        payload: &[u8],
978    ) -> NetId64 {
979        let id = NetId64::make(self.kind, node_id.get(), counter);
980        let slot_idx = (counter as usize) & (self.capacity - 1);
981        let slot_ptr = self.slot_ptr(lane, slot_idx);
982
983        // Disruptor-style write: seq goes odd → write content → seq goes even.
984        //
985        // The odd marker is stored `Relaxed` and followed by a `Release`
986        // *fence*, not stored `Release`. A release store orders the accesses
987        // that come *before* it and says nothing about the ones after, so the
988        // content writes below were free to become visible ahead of the marker:
989        // a reader would then see an even seq on both sides of a slot that was
990        // being torn underneath it. The fence is what forbids that, because it
991        // orders everything before it — the marker — ahead of every store after
992        // it.
993        //
994        // The closing store stays `Release`: there it is the content writes
995        // that must be visible first, which is exactly what a release store
996        // gives. It pairs with the reader's `Acquire` fence.
997        //
998        // x86's store-store ordering hides the difference; aarch64 does not.
999        unsafe {
1000            let slot = &*slot_ptr;
1001            let mid_seq = counter
1002                .checked_mul(2)
1003                .and_then(|value| value.checked_add(1))
1004                .expect("seq overflow");
1005            let final_seq = mid_seq.wrapping_add(1);
1006
1007            slot.seq.store(mid_seq, Ordering::Relaxed);
1008            fence(Ordering::Release);
1009            slot.id.store(id.raw(), Ordering::Relaxed);
1010            slot.ver.store(ver, Ordering::Relaxed);
1011            slot.payload_len.store(payload.len() as u32, Ordering::Relaxed);
1012            slot.kind.store(frame_kind, Ordering::Relaxed);
1013            let bytes = Self::payload_ptr(slot_ptr);
1014            for (index, byte) in payload.iter().enumerate() {
1015                (*bytes.add(index)).store(*byte, Ordering::Relaxed);
1016            }
1017            slot.seq.store(final_seq, Ordering::Release);
1018        }
1019
1020        id
1021    }
1022}
1023
1024// ─────────────────────────────────────────────────────────────────────
1025// Read-only inspection
1026// ─────────────────────────────────────────────────────────────────────
1027
1028/// Persisted geometry of one existing Orbit SHM ring.
1029///
1030/// The values come from the mapped ring header. An observer does not need the
1031/// producer's fleet capacity or compile-time [`RingSpec`] to inspect raw
1032/// frames.
1033#[derive(Clone, Debug, PartialEq, Eq)]
1034pub struct ShmRingMetadata {
1035    /// Exact POSIX SHM object name that was opened.
1036    pub name: String,
1037    /// Number of bytes reported by the existing SHM object.
1038    pub segment_size: usize,
1039    /// Persisted Orbit ring format version.
1040    pub format_version: u32,
1041    /// Stable Orbit type kind carried by this ring.
1042    pub kind: u8,
1043    /// Persisted per-lane capacity, payload limit, and topology.
1044    pub spec: RingSpec,
1045    /// Number of physical lanes present in the mapping.
1046    pub lane_count: usize,
1047}
1048
1049/// Read-only attachment to one existing Orbit SHM ring.
1050///
1051/// Attaching never creates, initializes, resets, locks, or unlinks a segment.
1052/// Dropping the view only unmaps this process's local mapping. Use
1053/// [`ShmRingView::lane`] to obtain a
1054/// [`RingFrameSource`](crate::ring::cursor::RingFrameSource) for one physical
1055/// lane.
1056pub struct ShmRingView {
1057    region: ShmRegion,
1058    metadata: ShmRingMetadata,
1059    slot_stride: usize,
1060    slots_offset: usize,
1061    lane_stride: usize,
1062}
1063
1064impl std::fmt::Debug for ShmRingView {
1065    fn fmt(
1066        &self,
1067        formatter: &mut std::fmt::Formatter<'_>,
1068    ) -> std::fmt::Result {
1069        formatter
1070            .debug_struct("ShmRingView")
1071            .field("metadata", &self.metadata)
1072            .finish()
1073    }
1074}
1075
1076impl ShmRingView {
1077    /// Attach read-only in the current user's namespace with an explicit policy.
1078    pub fn attach_existing_with_policy(
1079        fleet_name: &str,
1080        kind: u8,
1081        policy: shm::ShmAccessPolicy,
1082    ) -> std::io::Result<Self> {
1083        Self::attach_existing_for_uid_with_policy(
1084            fleet_name,
1085            kind,
1086            unsafe { libc::geteuid() },
1087            policy,
1088        )
1089    }
1090
1091    /// Attach to an existing ring owned by the effective user.
1092    pub fn attach_existing(
1093        fleet_name: &str,
1094        kind: u8,
1095    ) -> std::io::Result<Self> {
1096        // SAFETY: `geteuid` has no error path.
1097        let uid = unsafe { libc::geteuid() };
1098        Self::attach_existing_for_uid(fleet_name, kind, uid)
1099    }
1100
1101    /// Attach to an existing ring under an explicit uid-scoped name.
1102    ///
1103    /// POSIX permissions still decide whether the calling process may open
1104    /// another user's object.
1105    pub fn attach_existing_for_uid(
1106        fleet_name: &str,
1107        kind: u8,
1108        uid: u32,
1109    ) -> std::io::Result<Self> {
1110        Self::attach_existing_for_uid_with_policy(
1111            fleet_name,
1112            kind,
1113            uid,
1114            shm::ShmAccessPolicy::default(),
1115        )
1116    }
1117
1118    /// Attach read-only after checking the named owner's uid, selected gid and
1119    /// maximum permissions. OS access checks still apply to the calling process.
1120    pub fn attach_existing_for_uid_with_policy(
1121        fleet_name: &str,
1122        kind: u8,
1123        uid: u32,
1124        policy: shm::ShmAccessPolicy,
1125    ) -> std::io::Result<Self> {
1126        let name = shm::ring_segment_name_for_uid(fleet_name, kind, uid);
1127        let region = ShmRegion::open_existing_read_only(
1128            &name,
1129            HEADER_SIZE,
1130            uid,
1131            policy,
1132        )?;
1133
1134        // SAFETY: the mapping is at least HEADER_SIZE bytes and the header is
1135        // cache-line aligned at offset zero.
1136        let header = unsafe { &*(region.as_ptr() as *const ShmRingHeader) };
1137        if header.magic != MAGIC {
1138            return Err(std::io::Error::new(
1139                std::io::ErrorKind::InvalidData,
1140                format!(
1141                    "SHM segment {} has wrong magic 0x{:08X} (expected 0x{:08X})",
1142                    name, header.magic, MAGIC
1143                ),
1144            ));
1145        }
1146        if header.version != VERSION {
1147            return Err(std::io::Error::new(
1148                std::io::ErrorKind::InvalidData,
1149                format!(
1150                    "SHM segment {} version {} != local {}",
1151                    name, header.version, VERSION
1152                ),
1153            ));
1154        }
1155        if header.kind != kind {
1156            return Err(std::io::Error::new(
1157                std::io::ErrorKind::InvalidData,
1158                format!(
1159                    "SHM segment {} kind {} != requested {}",
1160                    name, header.kind, kind
1161                ),
1162            ));
1163        }
1164
1165        let topology = match header.topology {
1166            value if value == RingTopology::Shared as u8 => {
1167                RingTopology::Shared
1168            }
1169            value if value == RingTopology::PerNode as u8 => {
1170                RingTopology::PerNode
1171            }
1172            value if value == RingTopology::SharedOrdered as u8 => {
1173                RingTopology::SharedOrdered
1174            }
1175            value => {
1176                return Err(std::io::Error::new(
1177                    std::io::ErrorKind::InvalidData,
1178                    format!("SHM segment {name} has unknown topology {value}"),
1179                ));
1180            }
1181        };
1182        let capacity = usize::try_from(header.capacity).map_err(|_| {
1183            std::io::Error::new(
1184                std::io::ErrorKind::InvalidData,
1185                format!(
1186                    "SHM segment {name} capacity {} does not fit this platform",
1187                    header.capacity
1188                ),
1189            )
1190        })?;
1191        let lane_count = usize::from(header.lane_count);
1192        if lane_count == 0 {
1193            return Err(std::io::Error::new(
1194                std::io::ErrorKind::InvalidData,
1195                format!("SHM segment {name} has zero lanes"),
1196            ));
1197        }
1198        if topology != RingTopology::PerNode && lane_count != 1 {
1199            return Err(std::io::Error::new(
1200                std::io::ErrorKind::InvalidData,
1201                format!(
1202                    "SHM segment {name} topology {topology:?} requires one lane, found {lane_count}"
1203                ),
1204            ));
1205        }
1206        let spec = RingSpec {
1207            capacity,
1208            payload_capacity: header.payload_capacity as usize,
1209            topology,
1210        };
1211        let fleet_capacity = if topology == RingTopology::PerNode {
1212            header.lane_count
1213        } else {
1214            1
1215        };
1216        let (slot_stride, slots_offset, expected_size) =
1217            checked_layout(spec, fleet_capacity).map_err(|error| {
1218                std::io::Error::new(
1219                    std::io::ErrorKind::InvalidData,
1220                    format!("SHM segment {name} has invalid geometry: {error}"),
1221                )
1222            })?;
1223        if header.slot_stride as usize != slot_stride {
1224            return Err(std::io::Error::new(
1225                std::io::ErrorKind::InvalidData,
1226                format!(
1227                    "SHM segment {} slot stride {} != geometry {}",
1228                    name, header.slot_stride, slot_stride
1229                ),
1230            ));
1231        }
1232        if region.len() < expected_size {
1233            return Err(std::io::Error::new(
1234                std::io::ErrorKind::InvalidData,
1235                format!(
1236                    "SHM segment {} size {} is smaller than persisted geometry {}",
1237                    name,
1238                    region.len(),
1239                    expected_size
1240                ),
1241            ));
1242        }
1243        let lane_stride =
1244            capacity.checked_mul(slot_stride).ok_or_else(|| {
1245                invalid_input(format!(
1246                    "SHM segment {name} lane stride overflow"
1247                ))
1248            })?;
1249
1250        Ok(Self {
1251            metadata: ShmRingMetadata {
1252                name,
1253                segment_size: region.len(),
1254                format_version: header.version,
1255                kind,
1256                spec,
1257                lane_count,
1258            },
1259            region,
1260            slot_stride,
1261            slots_offset,
1262            lane_stride,
1263        })
1264    }
1265
1266    /// Attach and also verify the caller's typed ring contract.
1267    pub fn attach_typed<T: crate::OrbitTyped>(
1268        fleet_name: &str
1269    ) -> std::io::Result<Self> {
1270        let view = Self::attach_existing(fleet_name, T::KIND)?;
1271        if view.metadata.spec != T::RING_SPEC {
1272            return Err(std::io::Error::new(
1273                std::io::ErrorKind::InvalidData,
1274                format!(
1275                    "OrbitTyped KIND {} declares {:?}; existing spec is {:?}",
1276                    T::KIND,
1277                    T::RING_SPEC,
1278                    view.metadata.spec
1279                ),
1280            ));
1281        }
1282        Ok(view)
1283    }
1284
1285    pub fn metadata(&self) -> &ShmRingMetadata {
1286        &self.metadata
1287    }
1288
1289    /// Last semantic version allocated by the ring at observation time.
1290    pub fn current_version(&self) -> u64 {
1291        self.header().version_counter.load(Ordering::Acquire)
1292    }
1293
1294    /// Current notification generation used by native readiness bridges.
1295    pub fn notification_generation(&self) -> u32 {
1296        self.header().notification_generation.load(Ordering::Acquire)
1297    }
1298
1299    /// Select one physical lane as a generic read-only frame source.
1300    pub fn lane(
1301        &self,
1302        lane: usize,
1303    ) -> std::io::Result<ShmRingLaneView<'_>> {
1304        if lane >= self.metadata.lane_count {
1305            return Err(std::io::Error::new(
1306                std::io::ErrorKind::InvalidInput,
1307                format!(
1308                    "lane {lane} is outside SHM ring lane count {}",
1309                    self.metadata.lane_count
1310                ),
1311            ));
1312        }
1313        Ok(ShmRingLaneView { ring: self, lane })
1314    }
1315
1316    fn header(&self) -> &ShmRingHeader {
1317        // SAFETY: construction validated the mapped header.
1318        unsafe { &*(self.region.as_ptr() as *const ShmRingHeader) }
1319    }
1320
1321    fn lane_header(
1322        &self,
1323        lane: usize,
1324    ) -> &ShmLaneHeader {
1325        debug_assert!(lane < self.metadata.lane_count);
1326        // SAFETY: construction validated lane count and the complete geometry.
1327        unsafe {
1328            &*(self.region.as_ptr().add(HEADER_SIZE + lane * LANE_HEADER_SIZE)
1329                as *const ShmLaneHeader)
1330        }
1331    }
1332
1333    fn slot_ptr(
1334        &self,
1335        lane: usize,
1336        counter: u64,
1337    ) -> *mut ShmSlotHeader {
1338        debug_assert!(lane < self.metadata.lane_count);
1339        let slot = (counter as usize) & (self.metadata.spec.capacity - 1);
1340        // SAFETY: construction checked the complete persisted geometry and the
1341        // lane/slot indices are bounded above.
1342        unsafe {
1343            self.region
1344                .as_ptr()
1345                .add(
1346                    self.slots_offset
1347                        + lane * self.lane_stride
1348                        + slot * self.slot_stride,
1349                )
1350                .cast::<ShmSlotHeader>()
1351        }
1352    }
1353
1354    fn read_lane_state_at(
1355        &self,
1356        lane: usize,
1357        counter: u64,
1358    ) -> crate::ring::cursor::RingRead {
1359        use crate::ring::cursor::RingRead;
1360
1361        let slot_ptr = self.slot_ptr(lane, counter);
1362        let Some(expected_committed) =
1363            counter.checked_mul(2).and_then(|value| value.checked_add(2))
1364        else {
1365            return RingRead::Unavailable;
1366        };
1367
1368        for _ in 0..3 {
1369            // SAFETY: `slot_ptr` addresses a validated slot in the mapping.
1370            let sequence = unsafe { &*slot_ptr }.seq.load(Ordering::Acquire);
1371            if sequence < expected_committed {
1372                return if self.metadata.spec.topology == RingTopology::Shared {
1373                    RingRead::Pending
1374                } else {
1375                    RingRead::Unavailable
1376                };
1377            }
1378            if sequence > expected_committed {
1379                return RingRead::Unavailable;
1380            }
1381            // SAFETY: `slot_ptr` addresses a validated slot in the mapping.
1382            if let Some(frame) = unsafe {
1383                read_committed_frame(
1384                    slot_ptr,
1385                    self.metadata.spec.payload_capacity,
1386                )
1387            } {
1388                let lane_matches = self.metadata.spec.topology
1389                    != RingTopology::PerNode
1390                    || usize::from(frame.id.node()) == lane;
1391                return if frame.id.kind() == self.metadata.kind
1392                    && frame.id.counter() == counter
1393                    && lane_matches
1394                {
1395                    RingRead::Ready(frame)
1396                } else {
1397                    RingRead::Unavailable
1398                };
1399            }
1400        }
1401        RingRead::Unavailable
1402    }
1403}
1404
1405/// One physical lane of a [`ShmRingView`].
1406#[derive(Clone, Copy, Debug)]
1407pub struct ShmRingLaneView<'a> {
1408    ring: &'a ShmRingView,
1409    lane: usize,
1410}
1411
1412impl ShmRingLaneView<'_> {
1413    pub fn index(self) -> usize {
1414        self.lane
1415    }
1416
1417    /// Snapshot the currently visible committed/reserved head.
1418    pub fn head(self) -> u64 {
1419        self.ring.lane_header(self.lane).write_pos.load(Ordering::Acquire)
1420    }
1421
1422    /// Counter range that may still be retained at the observed head.
1423    pub fn retained_range(self) -> std::ops::Range<u64> {
1424        let head = self.head();
1425        head.saturating_sub(self.ring.metadata.spec.capacity as u64)..head
1426    }
1427
1428    /// Read the exact counter if its frame is still committed in this lane.
1429    pub fn read_at(
1430        self,
1431        counter: u64,
1432    ) -> Option<Frame> {
1433        match self.ring.read_lane_state_at(self.lane, counter) {
1434            crate::ring::cursor::RingRead::Ready(frame) => Some(frame),
1435            crate::ring::cursor::RingRead::Pending
1436            | crate::ring::cursor::RingRead::Unavailable => None,
1437        }
1438    }
1439
1440    /// Read the frame immediately below the observed head.
1441    pub fn read_head(self) -> Option<Frame> {
1442        let head = self.head();
1443        head.checked_sub(1).and_then(|counter| self.read_at(counter))
1444    }
1445}
1446
1447impl crate::ring::cursor::RingFrameSource for ShmRingLaneView<'_> {
1448    fn kind(&self) -> u8 {
1449        self.ring.metadata.kind
1450    }
1451
1452    fn head(&self) -> u64 {
1453        (*self).head()
1454    }
1455
1456    fn capacity(&self) -> usize {
1457        self.ring.metadata.spec.capacity
1458    }
1459
1460    fn read_at(
1461        &self,
1462        counter: u64,
1463    ) -> Option<Frame> {
1464        (*self).read_at(counter)
1465    }
1466
1467    fn read_state_at(
1468        &self,
1469        counter: u64,
1470    ) -> crate::ring::cursor::RingRead {
1471        self.ring.read_lane_state_at(self.lane, counter)
1472    }
1473}
1474
1475// ─────────────────────────────────────────────────────────────────────
1476// ShmRingRegistry — per-fleet, per-KIND map of ShmRings
1477// ─────────────────────────────────────────────────────────────────────
1478
1479/// Type-keyed registry of [`ShmRing`]s held by a fleet. One ring per
1480/// `OrbitTyped::KIND` byte; created on demand the first time a kind
1481/// is published or queried.
1482pub struct ShmRingRegistry {
1483    fleet_name: String,
1484    fleet_capacity: u16,
1485    policies: std::collections::BTreeMap<u8, shm::ShmAccessPolicy>,
1486    rings: crate::ring::ByKind<std::sync::Arc<ShmRing>>,
1487}
1488
1489impl ShmRingRegistry {
1490    pub fn new(
1491        fleet_name: impl Into<String>,
1492        fleet_capacity: u16,
1493    ) -> Self {
1494        Self::with_policies(fleet_name, fleet_capacity, [])
1495    }
1496
1497    /// Freeze per-kind policies before any ring opens. Unspecified kinds use
1498    /// `OwnerOnly`; repeated kind entries use the last supplied policy.
1499    pub fn with_policies(
1500        fleet_name: impl Into<String>,
1501        fleet_capacity: u16,
1502        policies: impl IntoIterator<Item = (u8, shm::ShmAccessPolicy)>,
1503    ) -> Self {
1504        Self {
1505            fleet_name: fleet_name.into(),
1506            fleet_capacity,
1507            policies: policies.into_iter().collect(),
1508            rings: crate::ring::ByKind::new(),
1509        }
1510    }
1511
1512    /// Get-or-create the SHM ring for `kind`. Failure here means the
1513    /// SHM open or attach failed (permissions, name too long, etc.)
1514    /// and is propagated as `io::Error`.
1515    pub fn get_or_create_for<T: crate::OrbitTyped>(
1516        &self
1517    ) -> std::io::Result<std::sync::Arc<ShmRing>> {
1518        self.ring_for::<T>().cloned()
1519    }
1520
1521    /// Get-or-create the SHM ring for `T`, borrowed for the registry's
1522    /// lifetime. A kind's ring is fixed once opened, so after the first call
1523    /// this is an index, an atomic load and a comparison of constants.
1524    pub(crate) fn ring_for<T: crate::OrbitTyped>(
1525        &self
1526    ) -> std::io::Result<&std::sync::Arc<ShmRing>> {
1527        let ring = match self.rings.get(T::KIND) {
1528            Some(ring) => ring,
1529            None => {
1530                let opened = ShmRing::open_or_create_for_fleet_with_policy(
1531                    &self.fleet_name,
1532                    T::KIND,
1533                    T::RING_SPEC,
1534                    self.fleet_capacity,
1535                    self.policies.get(&T::KIND).copied().unwrap_or_default(),
1536                )?;
1537                // Two first callers may both open it; one mapping stays.
1538                self.rings.get_or_insert(T::KIND, std::sync::Arc::new(opened))
1539            }
1540        };
1541        if ring.spec() != T::RING_SPEC {
1542            return Err(std::io::Error::new(
1543                std::io::ErrorKind::InvalidData,
1544                format!(
1545                    "OrbitTyped KIND {} was reused with ring spec {:?}; existing spec is {:?}",
1546                    T::KIND,
1547                    T::RING_SPEC,
1548                    ring.spec()
1549                ),
1550            ));
1551        }
1552        Ok(ring)
1553    }
1554
1555    /// Look up a ring that has already been created for `kind`.
1556    /// Returns `None` if no such ring exists yet.
1557    pub fn lookup(
1558        &self,
1559        kind: u8,
1560    ) -> Option<std::sync::Arc<ShmRing>> {
1561        self.rings.get(kind).cloned()
1562    }
1563}
1564
1565/// Read a slot's content; returns `None` if the seq indicates an
1566/// in-flight write or if pre/post seqs disagree (torn read).
1567///
1568/// # Safety
1569///
1570/// `slot_ptr` must point at a valid `ShmSlot` mapped into our
1571/// address space and aligned per the `repr(C, align(64))` layout.
1572unsafe fn read_committed_frame(
1573    slot_ptr: *mut ShmSlotHeader,
1574    payload_capacity: usize,
1575) -> Option<Frame> {
1576    let slot = unsafe { &*slot_ptr };
1577    let seq_pre = slot.seq.load(Ordering::Acquire);
1578    if seq_pre == 0 {
1579        // never written
1580        return None;
1581    }
1582    if seq_pre & 1 == 1 {
1583        // writer in progress
1584        return None;
1585    }
1586
1587    // Read content fields. `Relaxed` throughout: the seq load above and the
1588    // fence below are what order this, and a value read here is only trusted
1589    // once the two seqs agree.
1590    let id = NetId64::from_raw(slot.id.load(Ordering::Relaxed));
1591    let kind = slot.kind.load(Ordering::Relaxed);
1592    let ver = slot.ver.load(Ordering::Relaxed);
1593    let payload_len = slot.payload_len.load(Ordering::Relaxed) as usize;
1594    if payload_len > payload_capacity {
1595        // corrupt — bail
1596        return None;
1597    }
1598    // `with_capacity` + `push` rather than `vec![0; len]` + overwrite: the
1599    // zeroing would be written over by the very next loop, and a walking reader
1600    // over a 16k-slot ring pays that twice for every byte it reads. The push
1601    // cannot reallocate — the capacity is reserved above.
1602    let payload_src = unsafe { ShmRing::payload_ptr(slot_ptr) };
1603    let mut payload_buf = Vec::with_capacity(payload_len);
1604    for index in 0..payload_len {
1605        payload_buf
1606            .push(unsafe { (*payload_src.add(index)).load(Ordering::Relaxed) });
1607    }
1608
1609    // The mirror of the writer's fence. `Acquire` on a load orders the accesses
1610    // that come *after* it, so reading the closing seq with `Acquire` would
1611    // leave the content reads above free to be reordered past it — and the
1612    // comparison below would then be comparing against a slot it never actually
1613    // read. An acquire fence orders those reads ahead of the load that follows,
1614    // which is the guarantee this check is asking for; the load itself needs no
1615    // ordering of its own once the fence is there.
1616    fence(Ordering::Acquire);
1617    let seq_post = slot.seq.load(Ordering::Relaxed);
1618    if seq_pre != seq_post {
1619        // torn write — caller can retry
1620        return None;
1621    }
1622
1623    Some(Frame { id, kind, ver, payload: Bytes::from(payload_buf) })
1624}
1625
1626#[cfg(test)]
1627mod tests {
1628    use std::sync::atomic::{AtomicU64, Ordering};
1629
1630    use nix::sys::wait::{WaitStatus, waitpid};
1631    use nix::unistd::{ForkResult, fork};
1632
1633    use super::*;
1634    use crate::ring::cursor::{RingCursor, poll_ring};
1635
1636    #[test]
1637    fn cursor_retries_a_claimed_slot_after_it_commits() {
1638        static TEST_ID: AtomicU64 = AtomicU64::new(0);
1639
1640        let test_id = TEST_ID.fetch_add(1, Ordering::Relaxed);
1641        let fleet_name = format!("p{:x}{test_id:x}", std::process::id());
1642        let ring =
1643            ShmRing::open_or_create(&fleet_name, 199, RingSpec::new(4, 16))
1644                .expect("create test ring");
1645        ring.reset();
1646        let slot_ptr = ring.slot_ptr(0, 0);
1647
1648        ring.lane_header(0).write_pos.store(1, Ordering::Release);
1649        unsafe { &*slot_ptr }.seq.store(1, Ordering::Release);
1650
1651        let mut cursor = RingCursor::from_start();
1652        let pending = poll_ring(&ring, &mut cursor);
1653        assert!(pending.is_empty());
1654        assert_eq!(cursor.next_counter(), 0);
1655
1656        let payload = b"ready";
1657        unsafe {
1658            let slot = &*slot_ptr;
1659            slot.id.store(
1660                NetId64::make(199, NodeId::ZERO.get(), 0).raw(),
1661                Ordering::Relaxed,
1662            );
1663            slot.ver.store(7, Ordering::Relaxed);
1664            slot.payload_len.store(payload.len() as u32, Ordering::Relaxed);
1665            slot.kind.store(1, Ordering::Relaxed);
1666            let bytes = ShmRing::payload_ptr(slot_ptr);
1667            for (index, byte) in payload.iter().enumerate() {
1668                (*bytes.add(index)).store(*byte, Ordering::Relaxed);
1669            }
1670        }
1671        unsafe { &*slot_ptr }.seq.store(2, Ordering::Release);
1672
1673        let committed = poll_ring(&ring, &mut cursor);
1674        assert_eq!(committed.frames.len(), 1);
1675        assert_eq!(&committed.frames[0].payload[..], payload);
1676        assert_eq!(cursor.next_counter(), 1);
1677
1678        ring.unlink().expect("unlink test ring");
1679    }
1680
1681    #[test]
1682    fn per_node_head_ignores_a_writer_that_dies_before_commit() {
1683        static TEST_ID: AtomicU64 = AtomicU64::new(0);
1684
1685        let test_id = TEST_ID.fetch_add(1, Ordering::Relaxed);
1686        let fleet_name = format!("d{:x}{test_id:x}", std::process::id());
1687        let spec = RingSpec::per_node(4, 16);
1688        let abandoned =
1689            ShmRing::open_or_create_for_fleet(&fleet_name, 198, spec, 2)
1690                .expect("create per-node test ring");
1691        abandoned.reset();
1692
1693        let slot_ptr = abandoned.slot_ptr(1, 0);
1694        unsafe { &*slot_ptr }.seq.store(1, Ordering::Release);
1695        assert_eq!(abandoned.lane_head(NodeId::new(1)), 0);
1696
1697        let replacement =
1698            ShmRing::open_or_create_for_fleet(&fleet_name, 198, spec, 2)
1699                .expect("replacement attaches");
1700        let id = replacement
1701            .write(NodeId::new(1), 1, 9, Bytes::from_static(b"recovered"))
1702            .expect("replacement commits");
1703
1704        assert_eq!(id.counter(), 0);
1705        assert_eq!(replacement.lane_head(NodeId::new(1)), 1);
1706        assert_eq!(
1707            &replacement.read(id).expect("frame visible").payload[..],
1708            b"recovered"
1709        );
1710
1711        replacement.unlink().expect("unlink test ring");
1712    }
1713
1714    #[test]
1715    fn per_node_lane_serializes_concurrent_local_publishers() {
1716        static TEST_ID: AtomicU64 = AtomicU64::new(0);
1717
1718        let test_id = TEST_ID.fetch_add(1, Ordering::Relaxed);
1719        let fleet_name = format!("c{:x}{test_id:x}", std::process::id());
1720        let ring = std::sync::Arc::new(
1721            ShmRing::open_or_create_for_fleet(
1722                &fleet_name,
1723                197,
1724                RingSpec::per_node(512, 0),
1725                2,
1726            )
1727            .expect("create concurrent writer ring"),
1728        );
1729        ring.reset();
1730
1731        let mut writers = Vec::new();
1732        for _ in 0..4 {
1733            let ring = ring.clone();
1734            writers.push(std::thread::spawn(move || {
1735                (0..64)
1736                    .map(|_| {
1737                        ring.write(NodeId::new(1), 1, 0, Bytes::new())
1738                            .expect("publish")
1739                            .counter()
1740                    })
1741                    .collect::<Vec<_>>()
1742            }));
1743        }
1744
1745        let mut counters = writers
1746            .into_iter()
1747            .flat_map(|writer| writer.join().expect("writer joins"))
1748            .collect::<Vec<_>>();
1749        counters.sort_unstable();
1750        assert_eq!(counters, (0..256).collect::<Vec<_>>());
1751        assert_eq!(ring.lane_head(NodeId::new(1)), 256);
1752
1753        ring.unlink().expect("unlink test ring");
1754    }
1755
1756    #[test]
1757    fn shared_ordered_recovers_after_a_locked_writer_process_dies() {
1758        static TEST_ID: AtomicU64 = AtomicU64::new(0);
1759
1760        let test_id = TEST_ID.fetch_add(1, Ordering::Relaxed);
1761        let fleet_name = format!("o{:x}{test_id:x}", std::process::id());
1762        let spec = RingSpec::shared_ordered(4, 16);
1763        let ring = ShmRing::open_or_create_for_fleet(&fleet_name, 196, spec, 2)
1764            .expect("create shared-ordered test ring");
1765        ring.reset();
1766        // Lock once before the fork, so the parent keeps its description of
1767        // the lock file and the child inherits it idle.
1768        drop(ring.region.lock_exclusive().expect("parent locks ring"));
1769
1770        match unsafe { fork() }.expect("fork test writer") {
1771            ForkResult::Child => {
1772                // Use the handle inherited while it was idle. The fork handler
1773                // closed the parent's kept description in this child, and the
1774                // child opens a fresh one for this critical section.
1775                let _lock =
1776                    ring.region.lock_exclusive().expect("child locks ring");
1777                let slot_ptr = ring.slot_ptr(0, 0);
1778                unsafe { &*slot_ptr }.seq.store(1, Ordering::Release);
1779
1780                // Exit without destructors: the kernel, not Rust cleanup,
1781                // must release the descriptor lock.
1782                unsafe { libc::_exit(0) };
1783            }
1784            ForkResult::Parent { child } => {
1785                let status =
1786                    waitpid(child, None).expect("wait for abandoned writer");
1787                assert!(matches!(status, WaitStatus::Exited(_, 0)));
1788
1789                // A separately opened handle must acquire the lock after the
1790                // child dies. If the original region retained an idle flock fd,
1791                // fork would duplicate its open-file-description into the child
1792                // and the dead writer's lock would still be attached here.
1793                let replacement = ShmRing::open_or_create_for_fleet(
1794                    &fleet_name,
1795                    196,
1796                    spec,
1797                    2,
1798                )
1799                .expect("replacement attaches");
1800                let recovered_lock = replacement
1801                    .region
1802                    .try_lock_exclusive()
1803                    .expect("kernel released dead writer lock");
1804                drop(recovered_lock);
1805
1806                let id = replacement
1807                    .write(
1808                        NodeId::new(1),
1809                        1,
1810                        9,
1811                        Bytes::from_static(b"recovered"),
1812                    )
1813                    .expect("replacement commits counter zero");
1814                assert_eq!(id.counter(), 0);
1815                assert_eq!(replacement.head(), 1);
1816                assert_eq!(
1817                    &replacement.read(id).expect("recovered frame").payload[..],
1818                    b"recovered"
1819                );
1820
1821                replacement.unlink().expect("unlink shared-ordered test ring");
1822            }
1823        }
1824    }
1825}