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::NodeId;
55use crate::OrbitTyped;
56use crate::id::NetId64;
57
58pub mod cursor;
59#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))]
60mod readiness;
61#[cfg(unix)]
62pub mod shm;
63
64#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))]
65pub use readiness::RingEventFd;
66
67/// Writer ownership for one [`OrbitTyped`] ring.
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
69#[repr(u8)]
70pub enum RingTopology {
71    /// Every fleet member publishes into one shared multi-writer sequence.
72    Shared = 0,
73    /// Every fleet member owns an independent single-process writer lane.
74    /// The embedder must not run two active processes with the same node id;
75    /// local concurrent tasks are serialized by the ring handle.
76    PerNode = 1,
77    /// Every fleet member publishes into one globally ordered sequence.
78    /// Writers are serialized by a process-recoverable OS lock associated
79    /// with the SHM name, and the head advances only after the slot commits.
80    SharedOrdered = 2,
81}
82
83/// Physical policy for one [`OrbitTyped`] ring.
84///
85/// The policy is part of the wire contract: every process using the
86/// same `OrbitTyped::KIND` must declare the same values.
87#[derive(Clone, Copy, Debug, PartialEq, Eq)]
88pub struct RingSpec {
89    /// Number of slots retained by the ring, per lane.
90    pub capacity: usize,
91    /// Maximum payload bytes stored inline in each slot.
92    pub payload_capacity: usize,
93    /// How writers own and publish physical lanes.
94    pub topology: RingTopology,
95}
96
97impl RingSpec {
98    pub const fn new(capacity: usize, payload_capacity: usize) -> Self {
99        Self {
100            capacity,
101            payload_capacity,
102            topology: RingTopology::Shared,
103        }
104    }
105
106    /// Declare one independent writer lane per fleet node.
107    pub const fn per_node(capacity: usize, payload_capacity: usize) -> Self {
108        Self {
109            capacity,
110            payload_capacity,
111            topology: RingTopology::PerNode,
112        }
113    }
114
115    /// Declare one crash-recoverable, globally ordered writer lane.
116    pub const fn shared_ordered(capacity: usize, payload_capacity: usize) -> Self {
117        Self {
118            capacity,
119            payload_capacity,
120            topology: RingTopology::SharedOrdered,
121        }
122    }
123
124    pub(crate) fn assert_valid(self) {
125        assert!(self.capacity > 0, "ring capacity must be > 0");
126        assert!(
127            self.capacity.is_power_of_two(),
128            "ring capacity must be a power of two"
129        );
130        assert!(
131            self.payload_capacity <= u32::MAX as usize,
132            "ring payload capacity must fit in u32"
133        );
134    }
135}
136
137struct RingLane {
138    write_pos: AtomicU64,
139    write_lock: Mutex<()>,
140    slots: Vec<RwLock<Option<Frame>>>,
141}
142
143impl RingLane {
144    fn new(capacity: usize) -> Self {
145        let mut slots = Vec::with_capacity(capacity);
146        for _ in 0..capacity {
147            slots.push(RwLock::new(None));
148        }
149        Self {
150            write_pos: AtomicU64::new(0),
151            write_lock: Mutex::new(()),
152            slots,
153        }
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(&self, node_id: NodeId) -> u64 {
250        self.lane(node_id).write_pos.load(Ordering::Acquire)
251    }
252
253    /// Allocate one non-zero semantic version shared by every writer lane.
254    ///
255    /// This counter is independent of physical ring positions. Semantic
256    /// layers can use it when per-node lanes need one deterministic
257    /// last-write-wins order.
258    pub fn next_version(&self) -> u64 {
259        self.version_counter
260            .fetch_add(1, Ordering::AcqRel)
261            .checked_add(1)
262            .expect("ring semantic version exhausted")
263    }
264
265    /// Last semantic version allocated for this ring.
266    pub fn current_version(&self) -> u64 {
267        self.version_counter.load(Ordering::Acquire)
268    }
269
270    /// Append a frame. Atomically reserves the next counter, mints
271    /// the [`NetId64`], and writes the frame into the corresponding
272    /// slot. Returns the minted id.
273    ///
274    /// `frame_kind` is the message class byte (V0: pass `0`).
275    /// `ver` is the version / tick at write time (V0: caller's
276    /// choice).
277    pub fn write(&self, node_id: NodeId, frame_kind: u8, ver: u64, payload: Bytes) -> NetId64 {
278        assert!(
279            payload.len() <= self.payload_capacity,
280            "payload {} > ring payload capacity {}",
281            payload.len(),
282            self.payload_capacity
283        );
284        let lane = self.lane(node_id);
285        match self.topology {
286            RingTopology::Shared => {
287                let counter = lane.write_pos.fetch_add(1, Ordering::AcqRel);
288                self.write_frame(lane, node_id, counter, frame_kind, ver, payload)
289            }
290            RingTopology::PerNode | RingTopology::SharedOrdered => {
291                let _write = lane
292                    .write_lock
293                    .lock()
294                    .unwrap_or_else(|error| error.into_inner());
295                let counter = lane.write_pos.load(Ordering::Relaxed);
296                let id = self.write_frame(lane, node_id, counter, frame_kind, ver, payload);
297                lane.write_pos
298                    .store(counter.wrapping_add(1), Ordering::Release);
299                id
300            }
301        }
302    }
303
304    /// Append a contiguous batch to one lane and return its consecutive ids.
305    ///
306    /// Per-node and shared-ordered lanes expose the new head only after the
307    /// whole batch commits. An empty batch is a no-op. A batch larger than the
308    /// ring is rejected because its first frames could not remain addressable
309    /// when the method returns.
310    pub fn write_batch(
311        &self,
312        node_id: NodeId,
313        frame_kind: u8,
314        ver: u64,
315        payloads: Vec<Bytes>,
316    ) -> Vec<NetId64> {
317        assert!(
318            payloads.len() <= self.capacity,
319            "batch {} > ring capacity {}",
320            payloads.len(),
321            self.capacity
322        );
323        for payload in &payloads {
324            assert!(
325                payload.len() <= self.payload_capacity,
326                "payload {} > ring payload capacity {}",
327                payload.len(),
328                self.payload_capacity
329            );
330        }
331        if payloads.is_empty() {
332            return Vec::new();
333        }
334
335        let lane = self.lane(node_id);
336        match self.topology {
337            RingTopology::Shared => {
338                let start = lane
339                    .write_pos
340                    .fetch_add(payloads.len() as u64, Ordering::AcqRel);
341                payloads
342                    .into_iter()
343                    .enumerate()
344                    .map(|(offset, payload)| {
345                        self.write_frame(
346                            lane,
347                            node_id,
348                            start.wrapping_add(offset as u64),
349                            frame_kind,
350                            ver,
351                            payload,
352                        )
353                    })
354                    .collect()
355            }
356            RingTopology::PerNode | RingTopology::SharedOrdered => {
357                let _write = lane
358                    .write_lock
359                    .lock()
360                    .unwrap_or_else(|error| error.into_inner());
361                let start = lane.write_pos.load(Ordering::Relaxed);
362                let ids = payloads
363                    .into_iter()
364                    .enumerate()
365                    .map(|(offset, payload)| {
366                        self.write_frame(
367                            lane,
368                            node_id,
369                            start.wrapping_add(offset as u64),
370                            frame_kind,
371                            ver,
372                            payload,
373                        )
374                    })
375                    .collect::<Vec<_>>();
376                lane.write_pos
377                    .store(start.wrapping_add(ids.len() as u64), Ordering::Release);
378                ids
379            }
380        }
381    }
382
383    /// Read the slot that the given [`NetId64`]'s counter points at.
384    ///
385    /// Returns:
386    /// - `Some(frame)` if the slot's stored id matches the queried id
387    ///   exactly (the slot has not been overwritten by a later writer).
388    /// - `None` if the slot is empty, has wrapped past, or holds a
389    ///   different id than the one asked for.
390    pub fn read(&self, id: NetId64) -> Option<Frame> {
391        if id.kind() != self.kind {
392            return None;
393        }
394        let lane = self.lane_for_frame(id)?;
395        let slot_idx = (id.counter() as usize) % self.capacity;
396        let guard = lane.slots[slot_idx].read().expect("ring slot poisoned");
397        match &*guard {
398            Some(f) if f.id == id => Some(f.clone()),
399            _ => None,
400        }
401    }
402
403    /// Read the most recent frame, regardless of who wrote it.
404    /// Useful for "what's the current state?" — ignores
405    /// counter-by-counter walking.
406    pub fn read_head(&self) -> Option<Frame> {
407        let head = self.head();
408        if head == 0 {
409            return None;
410        }
411        let slot_idx = ((head - 1) as usize) % self.capacity;
412        self.lanes[0].slots[slot_idx]
413            .read()
414            .expect("ring slot poisoned")
415            .clone()
416    }
417
418    /// Read whatever frame currently occupies the slot at
419    /// `counter % capacity`, regardless of which counter is
420    /// stored in it. Used by walking readers that need slot-by-slot
421    /// access without knowing the writer's `NetId64` ahead of time.
422    ///
423    /// Returns `None` if the slot is empty.
424    pub fn read_at(&self, counter: u64) -> Option<Frame> {
425        let slot_idx = (counter as usize) % self.capacity;
426        self.lanes[0].slots[slot_idx]
427            .read()
428            .expect("ring slot poisoned")
429            .clone()
430    }
431
432    pub(crate) fn read_state_at(&self, counter: u64) -> cursor::RingRead {
433        match self.read_at(counter) {
434            Some(frame) if frame.id.counter() == counter => cursor::RingRead::Ready(frame),
435            Some(frame) if frame.id.counter() > counter => cursor::RingRead::Unavailable,
436            Some(_) | None => cursor::RingRead::Pending,
437        }
438    }
439
440    pub(crate) fn read_lane_at(&self, node_id: NodeId, counter: u64) -> Option<Frame> {
441        let lane = self.lane(node_id);
442        let slot_idx = (counter as usize) % self.capacity;
443        lane.slots[slot_idx]
444            .read()
445            .expect("ring slot poisoned")
446            .clone()
447    }
448
449    pub(crate) fn read_lane_state_at(&self, node_id: NodeId, counter: u64) -> cursor::RingRead {
450        match self.read_lane_at(node_id, counter) {
451            Some(frame) if frame.id.counter() == counter => cursor::RingRead::Ready(frame),
452            Some(frame) if frame.id.counter() > counter => cursor::RingRead::Unavailable,
453            Some(_) | None if self.topology != RingTopology::Shared => {
454                cursor::RingRead::Unavailable
455            }
456            Some(_) | None => cursor::RingRead::Pending,
457        }
458    }
459
460    /// Clear all slots and reset every lane head to zero.
461    ///
462    /// Intended for owner-controlled boot-time cleanup. Do not call
463    /// while other threads are publishing to this ring.
464    pub fn reset(&self) {
465        for lane in &self.lanes {
466            for slot in &lane.slots {
467                *slot.write().expect("ring slot poisoned") = None;
468            }
469            lane.write_pos.store(0, Ordering::Release);
470        }
471        self.version_counter.store(0, Ordering::Release);
472    }
473
474    fn lane(&self, node_id: NodeId) -> &RingLane {
475        let index = match self.topology {
476            RingTopology::Shared | RingTopology::SharedOrdered => 0,
477            RingTopology::PerNode => usize::from(node_id.get()),
478        };
479        self.lanes.get(index).unwrap_or_else(|| {
480            panic!(
481                "node {} is outside ring lane count {}",
482                node_id.get(),
483                self.lanes.len()
484            )
485        })
486    }
487
488    fn lane_for_frame(&self, id: NetId64) -> Option<&RingLane> {
489        let index = match self.topology {
490            RingTopology::Shared | RingTopology::SharedOrdered => 0,
491            RingTopology::PerNode => usize::from(id.node()),
492        };
493        self.lanes.get(index)
494    }
495
496    fn write_frame(
497        &self,
498        lane: &RingLane,
499        node_id: NodeId,
500        counter: u64,
501        frame_kind: u8,
502        ver: u64,
503        payload: Bytes,
504    ) -> NetId64 {
505        let id = NetId64::make(self.kind, node_id.get(), counter);
506        let slot_idx = (counter as usize) % self.capacity;
507        let frame = Frame {
508            id,
509            kind: frame_kind,
510            ver,
511            payload,
512        };
513        let mut guard = lane.slots[slot_idx].write().expect("ring slot poisoned");
514        *guard = Some(frame);
515        id
516    }
517}
518
519impl cursor::RingFrameSource for Ring {
520    fn kind(&self) -> u8 {
521        Ring::kind(self)
522    }
523
524    fn head(&self) -> u64 {
525        Ring::head(self)
526    }
527
528    fn capacity(&self) -> usize {
529        Ring::capacity(self)
530    }
531
532    fn read_at(&self, counter: u64) -> Option<Frame> {
533        Ring::read_at(self, counter)
534    }
535
536    fn read_state_at(&self, counter: u64) -> cursor::RingRead {
537        Ring::read_state_at(self, counter)
538    }
539}
540
541#[cfg(unix)]
542impl cursor::RingFrameSource for shm::ShmRing {
543    fn kind(&self) -> u8 {
544        shm::ShmRing::kind(self)
545    }
546
547    fn head(&self) -> u64 {
548        shm::ShmRing::head(self)
549    }
550
551    fn capacity(&self) -> usize {
552        shm::ShmRing::capacity(self)
553    }
554
555    fn read_at(&self, counter: u64) -> Option<Frame> {
556        shm::ShmRing::read_at(self, counter)
557    }
558
559    fn read_state_at(&self, counter: u64) -> cursor::RingRead {
560        shm::ShmRing::read_state_at(self, counter)
561    }
562}
563
564impl std::fmt::Debug for Ring {
565    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
566        f.debug_struct("Ring")
567            .field("kind", &self.kind)
568            .field("capacity", &self.capacity)
569            .field("payload_capacity", &self.payload_capacity)
570            .field("topology", &self.topology)
571            .field("lane_count", &self.lanes.len())
572            .field("head", &self.head())
573            .finish()
574    }
575}
576
577/// Type-keyed registry of rings. A `Fleet` holds one of these and
578/// hands out `Arc<Ring>` per `OrbitTyped` kind on demand.
579pub(crate) struct RingRegistry {
580    fleet_capacity: u16,
581    rings: dashmap::DashMap<u8, Arc<Ring>>,
582}
583
584impl RingRegistry {
585    pub fn new(fleet_capacity: u16) -> Self {
586        Self {
587            fleet_capacity,
588            rings: dashmap::DashMap::new(),
589        }
590    }
591
592    /// Get-or-create the ring declared by `T`.
593    pub fn get_or_create<T: OrbitTyped>(&self) -> Arc<Ring> {
594        let ring = self
595            .rings
596            .entry(T::KIND)
597            .or_insert_with(|| Arc::new(Ring::new_for_fleet::<T>(self.fleet_capacity)))
598            .clone();
599        assert_eq!(
600            ring.spec(),
601            T::RING_SPEC,
602            "OrbitTyped KIND {} was reused with a different ring spec",
603            T::KIND
604        );
605        ring
606    }
607
608    /// Look up a ring by KIND byte (e.g. when only the id is known).
609    pub fn lookup(&self, kind: u8) -> Option<Arc<Ring>> {
610        self.rings.get(&kind).map(|e| e.clone())
611    }
612}