Skip to main content

orbit_core/ring/
mod.rs

1//! Ring buffers — orbit-core's runtime substrate.
2//!
3//! > *"orbit runtime yani ring"* — the place where the fleet's
4//! > shared state actually lives at the lowest level. Higher-level
5//! > shapes (cache mutations, metrics snapshots, event streams, etc.)
6//! > reduce to *one or more rings*.
7//!
8//! ## Shape
9//!
10//! One [`Ring`] per [`OrbitTyped`] kind. A ring is one or more fixed-size
11//! circular lanes of [`Frame`]s. Shared rings use one lock-free multi-writer
12//! claim sequence. Shared-ordered rings serialize writers and expose only
13//! committed counters. Per-node rings give every fleet member a disjoint lane
14//! whose head advances only after a frame is committed. When a lane head
15//! exceeds its capacity, the oldest slot in that lane is overwritten.
16//!
17//! The frame layout mirrors the `nwd1` seed (see VISION §13):
18//!
19//! ```text
20//! ┌──────────┬──────┬──────┬─────────────┐
21//! │ id (8)   │ kind │ ver  │ payload (N) │
22//! │ NetId64  │  u8  │ u64  │   bytes     │
23//! └──────────┴──────┴──────┴─────────────┘
24//! ```
25//!
26//! Two `kind` bytes coexist on the wire and they mean different
27//! things (intentional, two-axis encoding):
28//!
29//! - `frame.id.kind()` — *which Rust type* (the data shape).
30//! - `frame.kind`      — *which message class* (state / event /
31//!   command / ack / invalidate / …). V0 leaves this open at `0`;
32//!   concrete classes appear when subscriber semantics arrive.
33//!
34//! ## V0 backing
35//!
36//! `RwLock<Option<Frame>>` per slot — simple, correct, slow. The SHM
37//! backing uses per-slot sequence numbers to prevent torn reads. Per-node
38//! lanes serialize concurrent publishers inside one process, commit the
39//! slot, then release-store their lane head.
40//!
41//! ## Who mints
42//!
43//! Writes go through [`Ring::write`], which mints the [`NetId64`]. The
44//! COUNTER part is the position inside either the shared sequence or the
45//! writer node's lane. NetId64s are therefore minted
46//! **server-side, by the writer process**. Browsers / external
47//! clients receive ids; they do not generate them.
48
49use std::sync::atomic::{AtomicU64, Ordering};
50use std::sync::{Arc, Mutex, RwLock};
51
52use bytes::Bytes;
53
54use crate::id::NetId64;
55use crate::{NodeId, OrbitTyped};
56
57pub mod cursor;
58#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))]
59mod readiness;
60#[cfg(unix)]
61pub mod shm;
62
63#[cfg(any(
64    target_os = "linux",
65    target_os = "freebsd",
66    target_os = "macos"
67))]
68pub use readiness::{ParkedRingEventFd, RingEventFd};
69
70/// Writer ownership for one [`OrbitTyped`] ring.
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72#[repr(u8)]
73pub enum RingTopology {
74    /// Every fleet member publishes into one shared multi-writer sequence.
75    Shared = 0,
76    /// Every fleet member owns an independent single-process writer lane.
77    /// The embedder must not run two active processes with the same node id;
78    /// local concurrent tasks are serialized by the ring handle.
79    PerNode = 1,
80    /// Every fleet member publishes into one globally ordered sequence.
81    /// Writers are serialized by a process-recoverable OS lock associated
82    /// with the SHM name, and the head advances only after the slot commits.
83    SharedOrdered = 2,
84}
85
86/// Physical policy for one [`OrbitTyped`] ring.
87///
88/// The policy is part of the wire contract: every process using the
89/// same `OrbitTyped::KIND` must declare the same values.
90#[derive(Clone, Copy, Debug, PartialEq, Eq)]
91pub struct RingSpec {
92    /// Number of slots retained by the ring, per lane.
93    pub capacity: usize,
94    /// Maximum payload bytes stored inline in each slot.
95    pub payload_capacity: usize,
96    /// How writers own and publish physical lanes.
97    pub topology: RingTopology,
98}
99
100impl RingSpec {
101    pub const fn new(
102        capacity: usize,
103        payload_capacity: usize,
104    ) -> Self {
105        Self { capacity, payload_capacity, topology: RingTopology::Shared }
106    }
107
108    /// Declare one independent writer lane per fleet node.
109    pub const fn per_node(
110        capacity: usize,
111        payload_capacity: usize,
112    ) -> Self {
113        Self { capacity, payload_capacity, topology: RingTopology::PerNode }
114    }
115
116    /// Declare one crash-recoverable, globally ordered writer lane.
117    pub const fn shared_ordered(
118        capacity: usize,
119        payload_capacity: usize,
120    ) -> Self {
121        Self {
122            capacity,
123            payload_capacity,
124            topology: RingTopology::SharedOrdered,
125        }
126    }
127
128    pub(crate) fn assert_valid(self) {
129        assert!(self.capacity > 0, "ring capacity must be > 0");
130        assert!(
131            self.capacity.is_power_of_two(),
132            "ring capacity must be a power of two"
133        );
134        assert!(
135            self.payload_capacity <= u32::MAX as usize,
136            "ring payload capacity must fit in u32"
137        );
138    }
139}
140
141struct RingLane {
142    write_pos: AtomicU64,
143    write_lock: Mutex<()>,
144    slots: Vec<RwLock<Option<Frame>>>,
145}
146
147impl RingLane {
148    fn new(capacity: usize) -> Self {
149        let mut slots = Vec::with_capacity(capacity);
150        for _ in 0..capacity {
151            slots.push(RwLock::new(None));
152        }
153        Self { write_pos: AtomicU64::new(0), write_lock: Mutex::new(()), slots }
154    }
155}
156
157/// One record in a ring — the on-wire shape (mirrors `nwd1::Frame`).
158#[derive(Clone, Debug, PartialEq, Eq)]
159pub struct Frame {
160    pub id: NetId64,
161    pub kind: u8,
162    pub ver: u64,
163    pub payload: Bytes,
164}
165
166/// A fixed-capacity, fleet-wide append-only log keyed on KIND byte.
167///
168/// V0 is single-process; V1 is SHM-backed. The API is the same.
169pub struct Ring {
170    /// The KIND this ring carries — equals `T::KIND` for the
171    /// `OrbitTyped` value-shape it's storing.
172    kind: u8,
173    /// Number of slots; constant for the ring's lifetime.
174    capacity: usize,
175    /// Maximum inline payload bytes for this ring lane.
176    payload_capacity: usize,
177    topology: RingTopology,
178    /// Ring-wide semantic version allocator shared by every writer lane.
179    version_counter: AtomicU64,
180    lanes: Vec<RingLane>,
181}
182
183impl Ring {
184    /// Create the process-local ring declared by `T::RING_SPEC` for a
185    /// single-member fleet.
186    pub fn new<T: OrbitTyped>() -> Self {
187        Self::new_for_fleet::<T>(1)
188    }
189
190    /// Create the process-local ring declared by `T::RING_SPEC` with the
191    /// physical lane count required by `fleet_capacity`.
192    pub fn new_for_fleet<T: OrbitTyped>(fleet_capacity: u16) -> Self {
193        assert!(fleet_capacity > 0, "ring fleet capacity must be > 0");
194        let spec = T::RING_SPEC;
195        spec.assert_valid();
196        let capacity = spec.capacity;
197        let lane_count = match spec.topology {
198            RingTopology::Shared | RingTopology::SharedOrdered => 1,
199            RingTopology::PerNode => usize::from(fleet_capacity),
200        };
201        let mut lanes = Vec::with_capacity(lane_count);
202        for _ in 0..lane_count {
203            lanes.push(RingLane::new(capacity));
204        }
205        Self {
206            kind: T::KIND,
207            capacity,
208            payload_capacity: spec.payload_capacity,
209            topology: spec.topology,
210            version_counter: AtomicU64::new(0),
211            lanes,
212        }
213    }
214
215    /// The KIND byte this ring carries (equals `T::KIND`).
216    pub fn kind(&self) -> u8 {
217        self.kind
218    }
219
220    /// Total slot count — fixed at construction.
221    pub fn capacity(&self) -> usize {
222        self.capacity
223    }
224
225    /// Maximum inline payload bytes for this ring lane.
226    pub fn payload_capacity(&self) -> usize {
227        self.payload_capacity
228    }
229
230    pub fn spec(&self) -> RingSpec {
231        RingSpec {
232            capacity: self.capacity,
233            payload_capacity: self.payload_capacity,
234            topology: self.topology,
235        }
236    }
237
238    /// Head of the sole shared lane, or lane zero for a per-node ring.
239    pub fn head(&self) -> u64 {
240        self.lanes[0].write_pos.load(Ordering::Acquire)
241    }
242
243    /// Number of physical writer lanes in this ring.
244    pub fn lane_count(&self) -> usize {
245        self.lanes.len()
246    }
247
248    /// Current head for `node_id`'s logical lane.
249    pub fn lane_head(
250        &self,
251        node_id: NodeId,
252    ) -> u64 {
253        self.lane(node_id).write_pos.load(Ordering::Acquire)
254    }
255
256    /// Allocate one non-zero semantic version shared by every writer lane.
257    ///
258    /// This counter is independent of physical ring positions. Semantic
259    /// layers can use it when per-node lanes need one deterministic
260    /// last-write-wins order.
261    pub fn next_version(&self) -> u64 {
262        self.version_counter
263            .fetch_add(1, Ordering::AcqRel)
264            .checked_add(1)
265            .expect("ring semantic version exhausted")
266    }
267
268    /// Last semantic version allocated for this ring.
269    pub fn current_version(&self) -> u64 {
270        self.version_counter.load(Ordering::Acquire)
271    }
272
273    /// Append a frame. Atomically reserves the next counter, mints
274    /// the [`NetId64`], and writes the frame into the corresponding
275    /// slot. Returns the minted id.
276    ///
277    /// `frame_kind` is the message class byte (V0: pass `0`).
278    /// `ver` is the version / tick at write time (V0: caller's
279    /// choice).
280    pub fn write(
281        &self,
282        node_id: NodeId,
283        frame_kind: u8,
284        ver: u64,
285        payload: Bytes,
286    ) -> NetId64 {
287        assert!(
288            payload.len() <= self.payload_capacity,
289            "payload {} > ring payload capacity {}",
290            payload.len(),
291            self.payload_capacity
292        );
293        let lane = self.lane(node_id);
294        match self.topology {
295            RingTopology::Shared => {
296                let counter = lane.write_pos.fetch_add(1, Ordering::AcqRel);
297                self.write_frame(
298                    lane, node_id, counter, frame_kind, ver, payload,
299                )
300            }
301            RingTopology::PerNode | RingTopology::SharedOrdered => {
302                let _write = lane
303                    .write_lock
304                    .lock()
305                    .unwrap_or_else(|error| error.into_inner());
306                let counter = lane.write_pos.load(Ordering::Relaxed);
307                let id = self.write_frame(
308                    lane, node_id, counter, frame_kind, ver, payload,
309                );
310                lane.write_pos
311                    .store(counter.wrapping_add(1), Ordering::Release);
312                id
313            }
314        }
315    }
316
317    /// Append a contiguous batch to one lane and return its consecutive ids.
318    ///
319    /// Per-node and shared-ordered lanes expose the new head only after the
320    /// whole batch commits. An empty batch is a no-op. A batch larger than the
321    /// ring is rejected because its first frames could not remain addressable
322    /// when the method returns.
323    pub fn write_batch(
324        &self,
325        node_id: NodeId,
326        frame_kind: u8,
327        ver: u64,
328        payloads: Vec<Bytes>,
329    ) -> Vec<NetId64> {
330        assert!(
331            payloads.len() <= self.capacity,
332            "batch {} > ring capacity {}",
333            payloads.len(),
334            self.capacity
335        );
336        for payload in &payloads {
337            assert!(
338                payload.len() <= self.payload_capacity,
339                "payload {} > ring payload capacity {}",
340                payload.len(),
341                self.payload_capacity
342            );
343        }
344        if payloads.is_empty() {
345            return Vec::new();
346        }
347
348        let lane = self.lane(node_id);
349        match self.topology {
350            RingTopology::Shared => {
351                let start = lane
352                    .write_pos
353                    .fetch_add(payloads.len() as u64, Ordering::AcqRel);
354                payloads
355                    .into_iter()
356                    .enumerate()
357                    .map(|(offset, payload)| {
358                        self.write_frame(
359                            lane,
360                            node_id,
361                            start.wrapping_add(offset as u64),
362                            frame_kind,
363                            ver,
364                            payload,
365                        )
366                    })
367                    .collect()
368            }
369            RingTopology::PerNode | RingTopology::SharedOrdered => {
370                let _write = lane
371                    .write_lock
372                    .lock()
373                    .unwrap_or_else(|error| error.into_inner());
374                let start = lane.write_pos.load(Ordering::Relaxed);
375                let ids = payloads
376                    .into_iter()
377                    .enumerate()
378                    .map(|(offset, payload)| {
379                        self.write_frame(
380                            lane,
381                            node_id,
382                            start.wrapping_add(offset as u64),
383                            frame_kind,
384                            ver,
385                            payload,
386                        )
387                    })
388                    .collect::<Vec<_>>();
389                lane.write_pos.store(
390                    start.wrapping_add(ids.len() as u64),
391                    Ordering::Release,
392                );
393                ids
394            }
395        }
396    }
397
398    /// Read the slot that the given [`NetId64`]'s counter points at.
399    ///
400    /// Returns:
401    /// - `Some(frame)` if the slot's stored id matches the queried id
402    ///   exactly (the slot has not been overwritten by a later writer).
403    /// - `None` if the slot is empty, has wrapped past, or holds a
404    ///   different id than the one asked for.
405    pub fn read(
406        &self,
407        id: NetId64,
408    ) -> Option<Frame> {
409        if id.kind() != self.kind {
410            return None;
411        }
412        let lane = self.lane_for_frame(id)?;
413        let slot_idx = (id.counter() as usize) % self.capacity;
414        let guard = lane.slots[slot_idx].read().expect("ring slot poisoned");
415        match &*guard {
416            Some(f) if f.id == id => Some(f.clone()),
417            _ => None,
418        }
419    }
420
421    /// Read the most recent frame, regardless of who wrote it.
422    /// Useful for "what's the current state?" — ignores
423    /// counter-by-counter walking.
424    pub fn read_head(&self) -> Option<Frame> {
425        let head = self.head();
426        if head == 0 {
427            return None;
428        }
429        let slot_idx = ((head - 1) as usize) % self.capacity;
430        self.lanes[0].slots[slot_idx]
431            .read()
432            .expect("ring slot poisoned")
433            .clone()
434    }
435
436    /// Read whatever frame currently occupies the slot at
437    /// `counter % capacity`, regardless of which counter is
438    /// stored in it. Used by walking readers that need slot-by-slot
439    /// access without knowing the writer's `NetId64` ahead of time.
440    ///
441    /// Returns `None` if the slot is empty.
442    pub fn read_at(
443        &self,
444        counter: u64,
445    ) -> Option<Frame> {
446        let slot_idx = (counter as usize) % self.capacity;
447        self.lanes[0].slots[slot_idx]
448            .read()
449            .expect("ring slot poisoned")
450            .clone()
451    }
452
453    pub(crate) fn read_state_at(
454        &self,
455        counter: u64,
456    ) -> cursor::RingRead {
457        match self.read_at(counter) {
458            Some(frame) if frame.id.counter() == counter => {
459                cursor::RingRead::Ready(frame)
460            }
461            Some(frame) if frame.id.counter() > counter => {
462                cursor::RingRead::Unavailable
463            }
464            Some(_) | None => cursor::RingRead::Pending,
465        }
466    }
467
468    pub(crate) fn read_lane_at(
469        &self,
470        node_id: NodeId,
471        counter: u64,
472    ) -> Option<Frame> {
473        let lane = self.lane(node_id);
474        let slot_idx = (counter as usize) % self.capacity;
475        lane.slots[slot_idx].read().expect("ring slot poisoned").clone()
476    }
477
478    pub(crate) fn read_lane_state_at(
479        &self,
480        node_id: NodeId,
481        counter: u64,
482    ) -> cursor::RingRead {
483        match self.read_lane_at(node_id, counter) {
484            Some(frame) if frame.id.counter() == counter => {
485                cursor::RingRead::Ready(frame)
486            }
487            Some(frame) if frame.id.counter() > counter => {
488                cursor::RingRead::Unavailable
489            }
490            Some(_) | None if self.topology != RingTopology::Shared => {
491                cursor::RingRead::Unavailable
492            }
493            Some(_) | None => cursor::RingRead::Pending,
494        }
495    }
496
497    /// Clear all slots and reset every lane head to zero.
498    ///
499    /// Intended for owner-controlled boot-time cleanup. Do not call
500    /// while other threads are publishing to this ring.
501    pub fn reset(&self) {
502        for lane in &self.lanes {
503            for slot in &lane.slots {
504                *slot.write().expect("ring slot poisoned") = None;
505            }
506            lane.write_pos.store(0, Ordering::Release);
507        }
508        self.version_counter.store(0, Ordering::Release);
509    }
510
511    fn lane(
512        &self,
513        node_id: NodeId,
514    ) -> &RingLane {
515        let index = match self.topology {
516            RingTopology::Shared | RingTopology::SharedOrdered => 0,
517            RingTopology::PerNode => usize::from(node_id.get()),
518        };
519        self.lanes.get(index).unwrap_or_else(|| {
520            panic!(
521                "node {} is outside ring lane count {}",
522                node_id.get(),
523                self.lanes.len()
524            )
525        })
526    }
527
528    fn lane_for_frame(
529        &self,
530        id: NetId64,
531    ) -> Option<&RingLane> {
532        let index = match self.topology {
533            RingTopology::Shared | RingTopology::SharedOrdered => 0,
534            RingTopology::PerNode => usize::from(id.node()),
535        };
536        self.lanes.get(index)
537    }
538
539    fn write_frame(
540        &self,
541        lane: &RingLane,
542        node_id: NodeId,
543        counter: u64,
544        frame_kind: u8,
545        ver: u64,
546        payload: Bytes,
547    ) -> NetId64 {
548        let id = NetId64::make(self.kind, node_id.get(), counter);
549        let slot_idx = (counter as usize) % self.capacity;
550        let frame = Frame { id, kind: frame_kind, ver, payload };
551        let mut guard =
552            lane.slots[slot_idx].write().expect("ring slot poisoned");
553        *guard = Some(frame);
554        id
555    }
556}
557
558impl cursor::RingFrameSource for Ring {
559    fn kind(&self) -> u8 {
560        Ring::kind(self)
561    }
562
563    fn head(&self) -> u64 {
564        Ring::head(self)
565    }
566
567    fn capacity(&self) -> usize {
568        Ring::capacity(self)
569    }
570
571    fn read_at(
572        &self,
573        counter: u64,
574    ) -> Option<Frame> {
575        Ring::read_at(self, counter)
576    }
577
578    fn read_state_at(
579        &self,
580        counter: u64,
581    ) -> cursor::RingRead {
582        Ring::read_state_at(self, counter)
583    }
584}
585
586#[cfg(unix)]
587impl cursor::RingFrameSource for shm::ShmRing {
588    fn kind(&self) -> u8 {
589        shm::ShmRing::kind(self)
590    }
591
592    fn head(&self) -> u64 {
593        shm::ShmRing::head(self)
594    }
595
596    fn capacity(&self) -> usize {
597        shm::ShmRing::capacity(self)
598    }
599
600    fn read_at(
601        &self,
602        counter: u64,
603    ) -> Option<Frame> {
604        shm::ShmRing::read_at(self, counter)
605    }
606
607    fn read_state_at(
608        &self,
609        counter: u64,
610    ) -> cursor::RingRead {
611        shm::ShmRing::read_state_at(self, counter)
612    }
613}
614
615impl std::fmt::Debug for Ring {
616    fn fmt(
617        &self,
618        f: &mut std::fmt::Formatter<'_>,
619    ) -> std::fmt::Result {
620        f.debug_struct("Ring")
621            .field("kind", &self.kind)
622            .field("capacity", &self.capacity)
623            .field("payload_capacity", &self.payload_capacity)
624            .field("topology", &self.topology)
625            .field("lane_count", &self.lanes.len())
626            .field("head", &self.head())
627            .finish()
628    }
629}
630
631/// Type-keyed registry of rings. A `Fleet` holds one of these and
632/// hands out `Arc<Ring>` per `OrbitTyped` kind on demand.
633pub(crate) struct RingRegistry {
634    fleet_capacity: u16,
635    rings: dashmap::DashMap<u8, Arc<Ring>>,
636}
637
638impl RingRegistry {
639    pub fn new(fleet_capacity: u16) -> Self {
640        Self { fleet_capacity, rings: dashmap::DashMap::new() }
641    }
642
643    /// Get-or-create the ring declared by `T`.
644    pub fn get_or_create<T: OrbitTyped>(&self) -> Arc<Ring> {
645        let ring = self
646            .rings
647            .entry(T::KIND)
648            .or_insert_with(|| {
649                Arc::new(Ring::new_for_fleet::<T>(self.fleet_capacity))
650            })
651            .clone();
652        assert_eq!(
653            ring.spec(),
654            T::RING_SPEC,
655            "OrbitTyped KIND {} was reused with a different ring spec",
656            T::KIND
657        );
658        ring
659    }
660
661    /// Look up a ring by KIND byte (e.g. when only the id is known).
662    pub fn lookup(
663        &self,
664        kind: u8,
665    ) -> Option<Arc<Ring>> {
666        self.rings.get(&kind).map(|e| e.clone())
667    }
668}