Skip to main content

chio_bounded/
ring.rs

1use std::collections::VecDeque;
2
3use crate::SizeGauge;
4
5/// Fixed-capacity append-only ring for process-local mirrors. `capacity == 0`
6/// means "disabled" (stores nothing, hands each item straight back), the
7/// correct default when a durable store is authoritative.
8///
9/// `Clone` produces a snapshot with its OWN independent `SizeGauge`, seeded to
10/// the current length. `push` is public, so a clone can be appended to; giving
11/// the clone a fresh gauge guarantees that writing through a snapshot (for
12/// example the public `append` on a `receipt_log()` snapshot) can never corrupt
13/// the owning structure's telemetry gauge.
14#[derive(Debug)]
15pub struct Ring<T> {
16    buf: VecDeque<T>,
17    capacity: usize,
18    gauge: SizeGauge,
19}
20
21impl<T: Clone> Clone for Ring<T> {
22    fn clone(&self) -> Self {
23        // Independent gauge: the snapshot tracks only its own length so pushes
24        // to the clone never write through to the owner's gauge handle.
25        let gauge = SizeGauge::new();
26        gauge.set(self.buf.len());
27        Self {
28            buf: self.buf.clone(),
29            capacity: self.capacity,
30            gauge,
31        }
32    }
33}
34
35impl<T> Ring<T> {
36    pub fn with_capacity(capacity: usize, gauge: SizeGauge) -> Self {
37        Self {
38            buf: VecDeque::new(),
39            capacity,
40            gauge,
41        }
42    }
43
44    /// Push, evicting the oldest entry when at capacity. Returns the evicted
45    /// item (if any) so callers may act before it drops. Never grows past
46    /// `capacity`.
47    pub fn push(&mut self, item: T) -> Option<T> {
48        if self.capacity == 0 {
49            return Some(item);
50        }
51        let evicted = if self.buf.len() >= self.capacity {
52            self.buf.pop_front()
53        } else {
54            None
55        };
56        self.buf.push_back(item);
57        self.gauge.set(self.buf.len());
58        evicted
59    }
60
61    pub fn iter(&self) -> impl Iterator<Item = &T> {
62        self.buf.iter()
63    }
64
65    pub fn len(&self) -> usize {
66        self.buf.len()
67    }
68
69    pub fn is_empty(&self) -> bool {
70        self.buf.is_empty()
71    }
72
73    pub fn capacity(&self) -> usize {
74        self.capacity
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use crate::{Ring, SizeGauge};
81
82    #[test]
83    fn push_never_exceeds_capacity_and_returns_evicted() {
84        let gauge = SizeGauge::new();
85        let mut ring: Ring<u32> = Ring::with_capacity(3, gauge.clone());
86        assert_eq!(ring.push(1), None);
87        assert_eq!(ring.push(2), None);
88        assert_eq!(ring.push(3), None);
89        assert_eq!(ring.len(), 3);
90        assert_eq!(gauge.get(), 3);
91        // At capacity: the oldest (1) is evicted and returned.
92        assert_eq!(ring.push(4), Some(1));
93        assert_eq!(ring.len(), 3);
94        assert_eq!(gauge.get(), 3);
95        let items: Vec<u32> = ring.iter().copied().collect();
96        assert_eq!(items, vec![2, 3, 4]);
97    }
98
99    #[test]
100    fn clone_has_independent_gauge_so_snapshot_pushes_do_not_corrupt_owner() {
101        let owner_gauge = SizeGauge::new();
102        let mut ring: Ring<u32> = Ring::with_capacity(4, owner_gauge.clone());
103        ring.push(1);
104        ring.push(2);
105        assert_eq!(owner_gauge.get(), 2);
106
107        // A snapshot clone must not share the owner's gauge handle.
108        let mut snapshot = ring.clone();
109        assert_eq!(snapshot.len(), 2);
110        snapshot.push(3);
111        snapshot.push(4);
112        // The clone tracks its own length; the owner's gauge is untouched.
113        assert_eq!(snapshot.len(), 4);
114        assert_eq!(
115            owner_gauge.get(),
116            2,
117            "owner gauge corrupted by a push to a snapshot clone"
118        );
119    }
120
121    #[test]
122    fn zero_capacity_stores_nothing_and_hands_item_back() {
123        let gauge = SizeGauge::new();
124        let mut ring: Ring<u32> = Ring::with_capacity(0, gauge.clone());
125        assert_eq!(ring.push(7), Some(7));
126        assert_eq!(ring.len(), 0);
127        assert!(ring.is_empty());
128        assert_eq!(gauge.get(), 0);
129    }
130}