Skip to main content

oms_modbus/monitor/
ring_buffer.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//!
3//! In-memory ring buffer for Modbus traffic capture.
4//!
5//! Default mode: bounded ring buffer (10,000 records).
6//! Optional: unlimited mode (never drops).
7//!
8//! Records are always returned in chronological order by [`RingBufferCapture::drain`] and [`RingBufferCapture::snapshot`].
9
10use std::collections::VecDeque;
11use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
12use std::sync::Mutex;
13
14use crate::intercept::{PacketData, PacketRecord};
15use crate::wire_tap::WireTap;
16
17/// In-memory packet capture with optional ring-buffer eviction.
18///
19/// Prefer [`crate::BusCapture`] for most use cases — it includes
20/// statistics counters and supports stats-only, bounded, and unbounded
21/// modes. Use `RingBufferCapture` only when you need a standalone
22/// ring buffer without statistics tracking.
23///
24/// # Example
25///
26/// ```no_run
27/// use oms_modbus::monitor::RingBufferCapture;
28/// let capture = RingBufferCapture::new(10_000); // bounded, 10k records
29/// ```
30pub struct RingBufferCapture {
31    records: Mutex<VecDeque<PacketRecord>>,
32    capacity: usize,
33    unbounded: AtomicBool,
34    dropped: AtomicU64,
35}
36
37impl RingBufferCapture {
38    /// Create a bounded ring buffer with the given capacity.
39    /// When full, oldest records are evicted.
40    ///
41    /// `capacity` is clamped to a minimum of 1. Passing 0 is equivalent to
42    /// passing 1 — a single-record buffer.
43    pub fn new(capacity: usize) -> Self {
44        let cap = capacity.max(1);
45        Self {
46            records: Mutex::new(VecDeque::with_capacity(cap)),
47            capacity: cap,
48            unbounded: AtomicBool::new(false),
49            dropped: AtomicU64::new(0),
50        }
51    }
52
53    /// Create an unbounded capture that never drops records.
54    /// Use with caution — memory grows indefinitely.
55    pub fn unbounded() -> Self {
56        Self {
57            records: Mutex::new(VecDeque::with_capacity(1024)),
58            capacity: usize::MAX,
59            unbounded: AtomicBool::new(true),
60            dropped: AtomicU64::new(0),
61        }
62    }
63
64    /// Number of records currently buffered.
65    pub fn len(&self) -> usize {
66        self.records.lock().unwrap_or_else(|e| e.into_inner()).len()
67    }
68
69    /// Check if the buffer is empty.
70    pub fn is_empty(&self) -> bool {
71        self.records
72            .lock()
73            .unwrap_or_else(|e| e.into_inner())
74            .is_empty()
75    }
76
77    /// Number of records dropped due to buffer overflow (bounded mode only).
78    pub fn dropped(&self) -> u64 {
79        self.dropped.load(Ordering::Relaxed)
80    }
81
82    /// Drain all records in chronological order. Clears the buffer.
83    pub fn drain(&self) -> Vec<PacketRecord> {
84        let mut r = self.records.lock().unwrap_or_else(|e| e.into_inner());
85        // Swap out the VecDeque under the lock, drain outside.
86        let capacity = r.capacity();
87        let old = std::mem::replace(&mut *r, VecDeque::with_capacity(capacity));
88        old.into_iter().collect()
89    }
90
91    /// Take a snapshot without clearing. Records are in chronological order.
92    ///
93    /// Clones all records under the lock. For large buffers in hot paths,
94    /// prefer [`Self::drain`] which swaps the buffer and releases the lock immediately.
95    pub fn snapshot(&self) -> Vec<PacketRecord> {
96        self.records
97            .lock()
98            .unwrap_or_else(|e| e.into_inner())
99            .iter()
100            .cloned()
101            .collect()
102    }
103
104    /// Switch to unbounded mode (existing records preserved).
105    pub fn set_unbounded(&self) {
106        self.unbounded.store(true, Ordering::Relaxed);
107    }
108
109    /// Switch to bounded mode, stopping unbounded growth.
110    ///
111    /// Capacity is fixed at construction time; this only prevents the ring
112    /// buffer from growing without bound. Use [`RingBufferCapture::new`]
113    /// to create a capture with a specific capacity.
114    pub fn set_bounded(&self) {
115        self.unbounded.store(false, Ordering::Relaxed);
116    }
117
118    fn record(&self, timestamp_us: u64, data: PacketData) {
119        let record = PacketRecord { timestamp_us, data };
120        // Read unbounded flag before lock — TOCTOU is harmless here:
121        // worst case one record uses the old mode during a mode switch.
122        let is_unbounded = self.unbounded.load(Ordering::Relaxed);
123        let mut r = self.records.lock().unwrap_or_else(|e| e.into_inner());
124        if is_unbounded || r.len() < self.capacity {
125            r.push_back(record);
126        } else {
127            // Evict oldest — VecDeque pop_front is O(1).
128            r.pop_front();
129            r.push_back(record);
130            self.dropped.fetch_add(1, Ordering::Relaxed);
131        }
132    }
133}
134
135impl Default for RingBufferCapture {
136    fn default() -> Self {
137        Self::new(10_000)
138    }
139}
140
141impl WireTap for RingBufferCapture {
142    fn on_write(&self, bytes: &[u8], ts: u64) {
143        self.record(ts, PacketData::RawTx(bytes.to_vec()));
144    }
145    fn on_read(&self, bytes: &[u8], ts: u64) {
146        self.record(ts, PacketData::RawRx(bytes.to_vec()));
147    }
148    fn on_error(&self, bytes: &[u8], error: &str, ts: u64) {
149        self.record(ts, PacketData::RawError(bytes.to_vec(), error.to_string()));
150    }
151}