Skip to main content

sim_lib_stream_host/
ring.rs

1//! Preallocated bounded ring buffer for process-site stream boundaries.
2
3use sim_kernel::{Error, Result};
4use sim_lib_stream_core::StreamStats;
5
6/// Push result for the bounded process-site ring.
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub enum ProcessRingPush<T> {
9    /// The item was stored in the ring.
10    Accepted,
11    /// The ring was full; the rejected item is returned.
12    DroppedNewest(T),
13    /// The ring was closed; the rejected item is returned.
14    Closed(T),
15}
16
17/// Capacity snapshot for steady-state process-site checks.
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub struct ProcessRingSnapshot {
20    capacity: usize,
21    len: usize,
22    allocated_slots: usize,
23}
24
25/// Preallocated bounded ring used at process-site stream boundaries.
26///
27/// Cloning is unsupported: a ring handle owns one process-boundary buffer, so
28/// copies must be explicit higher-level handles with documented shared state.
29///
30/// ```compile_fail
31/// use sim_lib_stream_host::ProcessSharedRing;
32///
33/// let ring = ProcessSharedRing::<u8>::with_capacity(1).unwrap();
34/// let _copy = ring.clone();
35/// ```
36#[derive(Debug)]
37pub struct ProcessSharedRing<T> {
38    slots: Vec<Option<T>>,
39    head: usize,
40    len: usize,
41    closed: bool,
42    stats: StreamStats,
43}
44
45impl<T> ProcessSharedRing<T> {
46    /// Creates a ring preallocated to hold `capacity` items.
47    ///
48    /// Returns an evaluation error when `capacity` is zero.
49    ///
50    /// # Examples
51    ///
52    /// ```
53    /// use sim_lib_stream_host::{ProcessRingPush, ProcessSharedRing};
54    ///
55    /// let mut ring = ProcessSharedRing::with_capacity(2).unwrap();
56    /// assert_eq!(ring.try_push(1), ProcessRingPush::Accepted);
57    /// assert_eq!(ring.try_push(2), ProcessRingPush::Accepted);
58    /// assert_eq!(ring.try_push(3), ProcessRingPush::DroppedNewest(3));
59    /// assert_eq!(ring.try_pop(), Some(1));
60    /// ```
61    pub fn with_capacity(capacity: usize) -> Result<Self> {
62        if capacity == 0 {
63            return Err(Error::Eval(
64                "process ring capacity must be greater than zero".to_owned(),
65            ));
66        }
67        let slots = std::iter::repeat_with(|| None).take(capacity).collect();
68        Ok(Self {
69            slots,
70            head: 0,
71            len: 0,
72            closed: false,
73            stats: StreamStats::default(),
74        })
75    }
76
77    /// Pushes an item, returning whether it was accepted, dropped (full), or
78    /// rejected (closed).
79    pub fn try_push(&mut self, item: T) -> ProcessRingPush<T> {
80        self.stats.pushed = self.stats.pushed.saturating_add(1);
81        if self.closed {
82            self.stats.closed = true;
83            return ProcessRingPush::Closed(item);
84        }
85        if self.len == self.capacity() {
86            self.stats.dropped_newest = self.stats.dropped_newest.saturating_add(1);
87            return ProcessRingPush::DroppedNewest(item);
88        }
89        let tail = (self.head + self.len) % self.capacity();
90        self.slots[tail] = Some(item);
91        self.len += 1;
92        self.stats.accepted = self.stats.accepted.saturating_add(1);
93        ProcessRingPush::Accepted
94    }
95
96    /// Pops the oldest item, or returns `None` when the ring is empty.
97    pub fn try_pop(&mut self) -> Option<T> {
98        if self.len == 0 {
99            return None;
100        }
101        let item = self.slots[self.head].take();
102        self.head = (self.head + 1) % self.capacity();
103        self.len -= 1;
104        if item.is_some() {
105            self.stats.yielded = self.stats.yielded.saturating_add(1);
106        }
107        item
108    }
109
110    /// Closes the ring; further pushes are rejected.
111    pub fn close(&mut self) {
112        self.closed = true;
113        self.stats.closed = true;
114    }
115
116    /// Drains all buffered items and closes the ring, recording cancellation.
117    pub fn cancel(&mut self) {
118        while self.try_pop().is_some() {}
119        self.closed = true;
120        self.stats.closed = true;
121        self.stats.cancelled = true;
122    }
123
124    /// Returns the number of buffered items.
125    pub fn len(&self) -> usize {
126        self.len
127    }
128
129    /// Returns whether the ring is empty.
130    pub fn is_empty(&self) -> bool {
131        self.len == 0
132    }
133
134    /// Returns the ring capacity in items.
135    pub fn capacity(&self) -> usize {
136        self.slots.len()
137    }
138
139    /// Returns the number of slots backing storage has allocated.
140    pub fn allocated_slots(&self) -> usize {
141        self.slots.capacity()
142    }
143
144    /// Returns whether the ring has been closed.
145    pub fn is_closed(&self) -> bool {
146        self.closed
147    }
148
149    /// Captures a capacity snapshot for steady-state checks.
150    pub fn snapshot(&self) -> ProcessRingSnapshot {
151        ProcessRingSnapshot {
152            capacity: self.capacity(),
153            len: self.len,
154            allocated_slots: self.allocated_slots(),
155        }
156    }
157
158    /// Returns a clone of the ring's running statistics.
159    pub fn stats(&self) -> StreamStats {
160        self.stats.clone()
161    }
162}
163
164impl ProcessRingSnapshot {
165    /// Returns the captured ring capacity.
166    pub fn capacity(self) -> usize {
167        self.capacity
168    }
169
170    /// Returns the captured buffered length.
171    pub fn len(self) -> usize {
172        self.len
173    }
174
175    /// Returns whether the ring was empty when captured.
176    pub fn is_empty(self) -> bool {
177        self.len == 0
178    }
179
180    /// Returns the captured number of allocated slots.
181    pub fn allocated_slots(self) -> usize {
182        self.allocated_slots
183    }
184}