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