Skip to main content

bb_runtime/framework/
record_buffer.rs

1//! `RecordBuffer` - bounded per-name ring buffer used by the
2//! `Record` syscall.
3//!
4//! bounded TWO ways: each per-name ring has a fixed
5//! capacity (oldest entry evicted at cap), AND the table caps the
6//! number of distinct ring names with a drop counter. New ring
7//! names that arrive past `max_rings` are dropped with the counter
8//! ticking; existing rings always accept records (oldest-first
9//! evicted) up to the per-ring cap.
10
11use std::collections::{HashMap, VecDeque};
12
13/// Default per-name capacity.
14const DEFAULT_CAPACITY: usize = 1024;
15/// Default distinct-name cap.
16const DEFAULT_MAX_RINGS: usize = 1024;
17
18/// Per-name ring buffer with two bounds.
19pub struct RecordBuffer {
20    rings: HashMap<String, VecDeque<Vec<u8>>>,
21    capacity: usize,
22    max_rings: usize,
23    name_drops: u64,
24}
25
26impl Default for RecordBuffer {
27    fn default() -> Self {
28        Self {
29            rings: HashMap::new(),
30            capacity: DEFAULT_CAPACITY,
31            max_rings: DEFAULT_MAX_RINGS,
32            name_drops: 0,
33        }
34    }
35}
36
37impl RecordBuffer {
38    /// Construct with the default capacities.
39    pub fn new() -> Self {
40        Self::default()
41    }
42
43    /// Construct with explicit per-ring and distinct-name caps.
44    pub fn with_caps(per_ring: usize, max_rings: usize) -> Self {
45        Self {
46            rings: HashMap::new(),
47            capacity: per_ring,
48            max_rings,
49            name_drops: 0,
50        }
51    }
52
53    /// Record `bytes` under the named ring; oldest entry evicted
54    /// when capacity reached. New ring names past `max_rings` are
55    /// dropped with the `name_drops` counter ticking.
56    pub fn record(&mut self, name: &str, bytes: Vec<u8>) {
57        if !self.rings.contains_key(name) && self.rings.len() >= self.max_rings {
58            self.name_drops += 1;
59            return;
60        }
61        let cap = self.capacity;
62        let ring = self.rings.entry(name.to_string()).or_default();
63        if ring.len() >= cap {
64            ring.pop_front();
65        }
66        ring.push_back(bytes);
67    }
68
69    /// Read the current contents of the named ring.
70    pub fn snapshot(&self, name: &str) -> Vec<Vec<u8>> {
71        self.rings
72            .get(name)
73            .map(|r| r.iter().cloned().collect())
74            .unwrap_or_default()
75    }
76
77    /// Cumulative drop count for unknown-name overflows.
78    pub fn name_drops(&self) -> u64 {
79        self.name_drops
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn record_snapshot_round_trip() {
89        let mut b = RecordBuffer::new();
90        b.record("m", b"a".to_vec());
91        b.record("m", b"b".to_vec());
92        let snap = b.snapshot("m");
93        assert_eq!(snap, vec![b"a".to_vec(), b"b".to_vec()]);
94    }
95
96    #[test]
97    fn record_overflow_drops_new_name_and_ticks_counter() {
98        let mut b = RecordBuffer::with_caps(8, 2);
99        b.record("a", b"1".to_vec());
100        b.record("b", b"2".to_vec());
101        assert_eq!(b.name_drops(), 0);
102        // Third distinct name at cap: dropped.
103        b.record("c", b"3".to_vec());
104        assert_eq!(b.name_drops(), 1);
105        assert_eq!(b.snapshot("c"), Vec::<Vec<u8>>::new());
106        // Existing rings still accept records at cap.
107        b.record("a", b"more".to_vec());
108        assert_eq!(b.snapshot("a"), vec![b"1".to_vec(), b"more".to_vec()]);
109    }
110}