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, OnceLock, 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::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    pub(crate) publication: RwLock<()>,
181    lanes: Vec<RingLane>,
182}
183
184impl Ring {
185    /// Create the process-local ring declared by `T::RING_SPEC` for a
186    /// single-member fleet.
187    pub fn new<T: OrbitTyped>() -> Self {
188        Self::new_for_fleet::<T>(1)
189    }
190
191    /// Create the process-local ring declared by `T::RING_SPEC` with the
192    /// physical lane count required by `fleet_capacity`.
193    pub fn new_for_fleet<T: OrbitTyped>(fleet_capacity: u16) -> Self {
194        assert!(fleet_capacity > 0, "ring fleet capacity must be > 0");
195        let spec = T::RING_SPEC;
196        spec.assert_valid();
197        let capacity = spec.capacity;
198        let lane_count = match spec.topology {
199            RingTopology::Shared | RingTopology::SharedOrdered => 1,
200            RingTopology::PerNode => usize::from(fleet_capacity),
201        };
202        let mut lanes = Vec::with_capacity(lane_count);
203        for _ in 0..lane_count {
204            lanes.push(RingLane::new(capacity));
205        }
206        Self {
207            kind: T::KIND,
208            capacity,
209            payload_capacity: spec.payload_capacity,
210            topology: spec.topology,
211            version_counter: AtomicU64::new(0),
212            publication: RwLock::new(()),
213            lanes,
214        }
215    }
216
217    /// The KIND byte this ring carries (equals `T::KIND`).
218    pub fn kind(&self) -> u8 {
219        self.kind
220    }
221
222    /// Total slot count — fixed at construction.
223    pub fn capacity(&self) -> usize {
224        self.capacity
225    }
226
227    /// Maximum inline payload bytes for this ring lane.
228    pub fn payload_capacity(&self) -> usize {
229        self.payload_capacity
230    }
231
232    pub fn spec(&self) -> RingSpec {
233        RingSpec {
234            capacity: self.capacity,
235            payload_capacity: self.payload_capacity,
236            topology: self.topology,
237        }
238    }
239
240    /// Head of the sole shared lane, or lane zero for a per-node ring.
241    pub fn head(&self) -> u64 {
242        self.lanes[0].write_pos.load(Ordering::Acquire)
243    }
244
245    /// Number of physical writer lanes in this ring.
246    pub fn lane_count(&self) -> usize {
247        self.lanes.len()
248    }
249
250    /// Current head for `node_id`'s logical lane.
251    pub fn lane_head(
252        &self,
253        node_id: NodeId,
254    ) -> u64 {
255        self.lane(node_id).write_pos.load(Ordering::Acquire)
256    }
257
258    /// Allocate one non-zero semantic version shared by every writer lane.
259    ///
260    /// This counter is independent of physical ring positions. Semantic
261    /// layers can use it when per-node lanes need one deterministic
262    /// last-write-wins order.
263    pub fn next_version(&self) -> u64 {
264        self.version_counter
265            .fetch_add(1, Ordering::AcqRel)
266            .checked_add(1)
267            .expect("ring semantic version exhausted")
268    }
269
270    /// Last semantic version allocated for this ring.
271    pub fn current_version(&self) -> u64 {
272        self.version_counter.load(Ordering::Acquire)
273    }
274
275    /// Append a frame. Atomically reserves the next counter, mints
276    /// the [`NetId64`], and writes the frame into the corresponding
277    /// slot. Returns the minted id.
278    ///
279    /// `frame_kind` is the message class byte (V0: pass `0`).
280    /// `ver` is the version / tick at write time (V0: caller's
281    /// choice).
282    pub fn write(
283        &self,
284        node_id: NodeId,
285        frame_kind: u8,
286        ver: u64,
287        payload: Bytes,
288    ) -> NetId64 {
289        assert!(
290            payload.len() <= self.payload_capacity,
291            "payload {} > ring payload capacity {}",
292            payload.len(),
293            self.payload_capacity
294        );
295        let lane = self.lane(node_id);
296        match self.topology {
297            RingTopology::Shared => {
298                let counter = lane.write_pos.fetch_add(1, Ordering::AcqRel);
299                self.write_frame(
300                    lane, node_id, counter, frame_kind, ver, payload,
301                )
302            }
303            RingTopology::PerNode | RingTopology::SharedOrdered => {
304                let _write = lane
305                    .write_lock
306                    .lock()
307                    .unwrap_or_else(|error| error.into_inner());
308                let counter = lane.write_pos.load(Ordering::Relaxed);
309                let id = self.write_frame(
310                    lane, node_id, counter, frame_kind, ver, payload,
311                );
312                lane.write_pos
313                    .store(counter.wrapping_add(1), Ordering::Release);
314                id
315            }
316        }
317    }
318
319    /// Append a contiguous batch to one lane and return its consecutive ids.
320    ///
321    /// Per-node and shared-ordered lanes expose the new head only after the
322    /// whole batch commits. An empty batch is a no-op. A batch larger than the
323    /// ring is rejected because its first frames could not remain addressable
324    /// when the method returns.
325    pub fn write_batch(
326        &self,
327        node_id: NodeId,
328        frame_kind: u8,
329        ver: u64,
330        payloads: Vec<Bytes>,
331    ) -> Vec<NetId64> {
332        assert!(
333            payloads.len() <= self.capacity,
334            "batch {} > ring capacity {}",
335            payloads.len(),
336            self.capacity
337        );
338        for payload in &payloads {
339            assert!(
340                payload.len() <= self.payload_capacity,
341                "payload {} > ring payload capacity {}",
342                payload.len(),
343                self.payload_capacity
344            );
345        }
346        if payloads.is_empty() {
347            return Vec::new();
348        }
349
350        let lane = self.lane(node_id);
351        match self.topology {
352            RingTopology::Shared => {
353                let start = lane
354                    .write_pos
355                    .fetch_add(payloads.len() as u64, Ordering::AcqRel);
356                payloads
357                    .into_iter()
358                    .enumerate()
359                    .map(|(offset, payload)| {
360                        self.write_frame(
361                            lane,
362                            node_id,
363                            start.wrapping_add(offset as u64),
364                            frame_kind,
365                            ver,
366                            payload,
367                        )
368                    })
369                    .collect()
370            }
371            RingTopology::PerNode | RingTopology::SharedOrdered => {
372                let _write = lane
373                    .write_lock
374                    .lock()
375                    .unwrap_or_else(|error| error.into_inner());
376                let start = lane.write_pos.load(Ordering::Relaxed);
377                let ids = payloads
378                    .into_iter()
379                    .enumerate()
380                    .map(|(offset, payload)| {
381                        self.write_frame(
382                            lane,
383                            node_id,
384                            start.wrapping_add(offset as u64),
385                            frame_kind,
386                            ver,
387                            payload,
388                        )
389                    })
390                    .collect::<Vec<_>>();
391                lane.write_pos.store(
392                    start.wrapping_add(ids.len() as u64),
393                    Ordering::Release,
394                );
395                ids
396            }
397        }
398    }
399
400    /// Read the slot that the given [`NetId64`]'s counter points at.
401    ///
402    /// Returns:
403    /// - `Some(frame)` if the slot's stored id matches the queried id
404    ///   exactly (the slot has not been overwritten by a later writer).
405    /// - `None` if the slot is empty, has wrapped past, or holds a
406    ///   different id than the one asked for.
407    pub fn read(
408        &self,
409        id: NetId64,
410    ) -> Option<Frame> {
411        if id.kind() != self.kind {
412            return None;
413        }
414        let lane = self.lane_for_frame(id)?;
415        let slot_idx = (id.counter() as usize) % self.capacity;
416        let guard = lane.slots[slot_idx].read().expect("ring slot poisoned");
417        match &*guard {
418            Some(f) if f.id == id => Some(f.clone()),
419            _ => None,
420        }
421    }
422
423    /// Read the most recent frame, regardless of who wrote it.
424    /// Useful for "what's the current state?" — ignores
425    /// counter-by-counter walking.
426    pub fn read_head(&self) -> Option<Frame> {
427        let head = self.head();
428        if head == 0 {
429            return None;
430        }
431        let slot_idx = ((head - 1) as usize) % self.capacity;
432        self.lanes[0].slots[slot_idx]
433            .read()
434            .expect("ring slot poisoned")
435            .clone()
436    }
437
438    /// Read whatever frame currently occupies the slot at
439    /// `counter % capacity`, regardless of which counter is
440    /// stored in it. Used by walking readers that need slot-by-slot
441    /// access without knowing the writer's `NetId64` ahead of time.
442    ///
443    /// Returns `None` if the slot is empty.
444    pub fn read_at(
445        &self,
446        counter: u64,
447    ) -> Option<Frame> {
448        let slot_idx = (counter as usize) % self.capacity;
449        self.lanes[0].slots[slot_idx]
450            .read()
451            .expect("ring slot poisoned")
452            .clone()
453    }
454
455    pub(crate) fn read_state_at(
456        &self,
457        counter: u64,
458    ) -> cursor::RingRead {
459        match self.read_at(counter) {
460            Some(frame) if frame.id.counter() == counter => {
461                cursor::RingRead::Ready(frame)
462            }
463            Some(frame) if frame.id.counter() > counter => {
464                cursor::RingRead::Unavailable
465            }
466            Some(_) | None => cursor::RingRead::Pending,
467        }
468    }
469
470    pub(crate) fn read_lane_at(
471        &self,
472        node_id: NodeId,
473        counter: u64,
474    ) -> Option<Frame> {
475        let lane = self.lane(node_id);
476        let slot_idx = (counter as usize) % self.capacity;
477        lane.slots[slot_idx].read().expect("ring slot poisoned").clone()
478    }
479
480    pub(crate) fn read_lane_state_at(
481        &self,
482        node_id: NodeId,
483        counter: u64,
484    ) -> cursor::RingRead {
485        match self.read_lane_at(node_id, counter) {
486            Some(frame) if frame.id.counter() == counter => {
487                cursor::RingRead::Ready(frame)
488            }
489            Some(frame) if frame.id.counter() > counter => {
490                cursor::RingRead::Unavailable
491            }
492            Some(_) | None if self.topology != RingTopology::Shared => {
493                cursor::RingRead::Unavailable
494            }
495            Some(_) | None => cursor::RingRead::Pending,
496        }
497    }
498
499    /// Clear all slots and reset every lane head to zero.
500    ///
501    /// Intended for owner-controlled boot-time cleanup. Do not call
502    /// while other threads are publishing to this ring.
503    pub fn reset(&self) {
504        for lane in &self.lanes {
505            for slot in &lane.slots {
506                *slot.write().expect("ring slot poisoned") = None;
507            }
508            lane.write_pos.store(0, Ordering::Release);
509        }
510        self.version_counter.store(0, Ordering::Release);
511    }
512
513    fn lane(
514        &self,
515        node_id: NodeId,
516    ) -> &RingLane {
517        let index = match self.topology {
518            RingTopology::Shared | RingTopology::SharedOrdered => 0,
519            RingTopology::PerNode => usize::from(node_id.get()),
520        };
521        self.lanes.get(index).unwrap_or_else(|| {
522            panic!(
523                "node {} is outside ring lane count {}",
524                node_id.get(),
525                self.lanes.len()
526            )
527        })
528    }
529
530    fn lane_for_frame(
531        &self,
532        id: NetId64,
533    ) -> Option<&RingLane> {
534        let index = match self.topology {
535            RingTopology::Shared | RingTopology::SharedOrdered => 0,
536            RingTopology::PerNode => usize::from(id.node()),
537        };
538        self.lanes.get(index)
539    }
540
541    fn write_frame(
542        &self,
543        lane: &RingLane,
544        node_id: NodeId,
545        counter: u64,
546        frame_kind: u8,
547        ver: u64,
548        payload: Bytes,
549    ) -> NetId64 {
550        let id = NetId64::make(self.kind, node_id.get(), counter);
551        let slot_idx = (counter as usize) % self.capacity;
552        let frame = Frame { id, kind: frame_kind, ver, payload };
553        let mut guard =
554            lane.slots[slot_idx].write().expect("ring slot poisoned");
555        *guard = Some(frame);
556        id
557    }
558}
559
560impl cursor::RingFrameSource for Ring {
561    fn kind(&self) -> u8 {
562        Ring::kind(self)
563    }
564
565    fn head(&self) -> u64 {
566        Ring::head(self)
567    }
568
569    fn capacity(&self) -> usize {
570        Ring::capacity(self)
571    }
572
573    fn read_at(
574        &self,
575        counter: u64,
576    ) -> Option<Frame> {
577        Ring::read_at(self, counter)
578    }
579
580    fn read_state_at(
581        &self,
582        counter: u64,
583    ) -> cursor::RingRead {
584        Ring::read_state_at(self, counter)
585    }
586}
587
588#[cfg(unix)]
589impl cursor::RingFrameSource for shm::ShmRing {
590    fn kind(&self) -> u8 {
591        shm::ShmRing::kind(self)
592    }
593
594    fn head(&self) -> u64 {
595        shm::ShmRing::head(self)
596    }
597
598    fn capacity(&self) -> usize {
599        shm::ShmRing::capacity(self)
600    }
601
602    fn read_at(
603        &self,
604        counter: u64,
605    ) -> Option<Frame> {
606        shm::ShmRing::read_at(self, counter)
607    }
608
609    fn read_state_at(
610        &self,
611        counter: u64,
612    ) -> cursor::RingRead {
613        shm::ShmRing::read_state_at(self, counter)
614    }
615}
616
617impl std::fmt::Debug for Ring {
618    fn fmt(
619        &self,
620        f: &mut std::fmt::Formatter<'_>,
621    ) -> std::fmt::Result {
622        f.debug_struct("Ring")
623            .field("kind", &self.kind)
624            .field("capacity", &self.capacity)
625            .field("payload_capacity", &self.payload_capacity)
626            .field("topology", &self.topology)
627            .field("lane_count", &self.lanes.len())
628            .field("head", &self.head())
629            .finish()
630    }
631}
632
633/// One slot per `OrbitTyped::KIND` byte.
634///
635/// What a kind resolves to is fixed once it is installed, so a hot-path
636/// lookup is an index and one atomic load: no hashing, no shard lock and no
637/// reference-count traffic.
638pub(crate) struct ByKind<V> {
639    slots: Box<[OnceLock<V>; 256]>,
640}
641
642impl<V> ByKind<V> {
643    pub(crate) fn new() -> Self {
644        Self { slots: Box::new(std::array::from_fn(|_| OnceLock::new())) }
645    }
646
647    pub(crate) fn get(
648        &self,
649        kind: u8,
650    ) -> Option<&V> {
651        self.slots[usize::from(kind)].get()
652    }
653
654    pub(crate) fn get_or_init(
655        &self,
656        kind: u8,
657        init: impl FnOnce() -> V,
658    ) -> &V {
659        self.slots[usize::from(kind)].get_or_init(init)
660    }
661
662    /// Install `value` unless another caller installed one first, and return
663    /// whichever is installed.
664    pub(crate) fn get_or_insert(
665        &self,
666        kind: u8,
667        value: V,
668    ) -> &V {
669        let slot = &self.slots[usize::from(kind)];
670        let _ = slot.set(value);
671        slot.get().expect("a kind slot holds a value once set")
672    }
673
674    pub(crate) fn len(&self) -> usize {
675        self.slots.iter().filter(|slot| slot.get().is_some()).count()
676    }
677}
678
679/// Type-keyed registry of rings. A `Fleet` holds one of these and
680/// hands out the ring per `OrbitTyped` kind on demand.
681pub(crate) struct RingRegistry {
682    fleet_capacity: u16,
683    rings: ByKind<Arc<Ring>>,
684}
685
686impl RingRegistry {
687    pub fn new(fleet_capacity: u16) -> Self {
688        Self { fleet_capacity, rings: ByKind::new() }
689    }
690
691    /// Get-or-create the ring declared by `T`, borrowed for the registry's
692    /// lifetime.
693    pub fn ring_for<T: OrbitTyped>(&self) -> &Arc<Ring> {
694        let ring = self.rings.get_or_init(T::KIND, || {
695            Arc::new(Ring::new_for_fleet::<T>(self.fleet_capacity))
696        });
697        assert_eq!(
698            ring.spec(),
699            T::RING_SPEC,
700            "OrbitTyped KIND {} was reused with a different ring spec",
701            T::KIND
702        );
703        ring
704    }
705
706    /// Get-or-create the ring declared by `T` as an owned handle.
707    pub fn get_or_create<T: OrbitTyped>(&self) -> Arc<Ring> {
708        self.ring_for::<T>().clone()
709    }
710
711    /// Look up a ring by KIND byte (e.g. when only the id is known).
712    pub fn lookup(
713        &self,
714        kind: u8,
715    ) -> Option<Arc<Ring>> {
716        self.rings.get(kind).cloned()
717    }
718}