1use std::collections::{HashMap, VecDeque};
16use std::sync::{Mutex, MutexGuard};
17use std::time::{Duration, Instant};
18
19use crate::CapturedRequest;
20
21struct Entry {
22 req: CapturedRequest,
23 inserted: Instant,
24 bytes: usize,
25}
26
27#[derive(Default)]
28struct RingInner {
29 map: HashMap<String, Entry>,
30 order: VecDeque<String>,
32 total_bytes: usize,
33}
34
35pub struct CaptureRing {
38 inner: Mutex<RingInner>,
39 capacity: usize,
40 max_bytes: usize,
41 ttl: Duration,
42}
43
44impl CaptureRing {
45 pub fn new(capacity: usize, max_bytes: usize, ttl: Duration) -> Self {
47 Self {
48 inner: Mutex::new(RingInner::default()),
49 capacity: capacity.max(1),
50 max_bytes,
51 ttl,
52 }
53 }
54
55 fn lock(&self) -> MutexGuard<'_, RingInner> {
57 self.inner.lock().unwrap_or_else(|e| e.into_inner())
58 }
59
60 pub fn insert(&self, req: CapturedRequest) {
63 let bytes = req.estimated_bytes();
64 let key = req.request_id.clone();
65 let mut g = self.lock();
66 self.evict_expired(&mut g);
67
68 if let Some(old) = g.map.remove(&key) {
70 g.total_bytes -= old.bytes;
71 g.order.retain(|k| k != &key);
72 }
73
74 g.total_bytes += bytes;
75 g.map.insert(key.clone(), Entry { req, inserted: Instant::now(), bytes });
76 g.order.push_back(key);
77
78 while g.map.len() > self.capacity || (g.total_bytes > self.max_bytes && g.map.len() > 1) {
79 match g.order.pop_front() {
80 Some(oldest) => {
81 if let Some(e) = g.map.remove(&oldest) {
82 g.total_bytes -= e.bytes;
83 }
84 }
85 None => break,
86 }
87 }
88 }
89
90 pub fn take(&self, request_id: &str) -> Option<CapturedRequest> {
93 let mut g = self.lock();
94 self.evict_expired(&mut g);
95 let e = g.map.remove(request_id)?;
96 g.total_bytes -= e.bytes;
97 g.order.retain(|k| k != request_id);
98 Some(e.req)
99 }
100
101 pub fn len(&self) -> usize {
103 self.lock().map.len()
104 }
105
106 pub fn is_empty(&self) -> bool {
108 self.len() == 0
109 }
110
111 fn evict_expired(&self, g: &mut RingInner) {
114 while let Some(front) = g.order.front().cloned() {
115 match g.map.get(&front) {
116 Some(e) if e.inserted.elapsed() > self.ttl => {
117 let bytes = e.bytes;
118 g.map.remove(&front);
119 g.total_bytes -= bytes;
120 g.order.pop_front();
121 }
122 _ => break,
123 }
124 }
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 fn cap(id: &str, body_len: usize) -> CapturedRequest {
133 CapturedRequest {
134 request_id: id.to_string(),
135 method: "GET".to_string(),
136 path: "/".to_string(),
137 query: None,
138 http_version: "HTTP/1.1".to_string(),
139 headers: vec![],
140 cookies: vec![],
141 body_b64: "A".repeat(body_len),
142 body_size: body_len,
143 body_truncated: false,
144 redacted: vec![],
145 }
146 }
147
148 #[test]
149 fn insert_then_take_roundtrips_and_removes() {
150 let ring = CaptureRing::new(8, 1 << 20, Duration::from_secs(5));
151 ring.insert(cap("r1", 10));
152 assert_eq!(ring.len(), 1);
153 let got = ring.take("r1").expect("present");
154 assert_eq!(got.request_id, "r1");
155 assert!(ring.take("r1").is_none(), "taken once");
156 assert!(ring.is_empty());
157 }
158
159 #[test]
160 fn evicts_oldest_over_capacity_keeping_newest() {
161 let ring = CaptureRing::new(2, 1 << 20, Duration::from_secs(5));
162 ring.insert(cap("r1", 1));
163 ring.insert(cap("r2", 1));
164 ring.insert(cap("r3", 1)); assert_eq!(ring.len(), 2);
166 assert!(ring.take("r1").is_none(), "oldest evicted");
167 assert!(ring.take("r2").is_some());
168 assert!(ring.take("r3").is_some(), "newest kept");
169 }
170
171 #[test]
172 fn evicts_over_byte_budget_but_keeps_the_newest() {
173 let ring = CaptureRing::new(100, 120, Duration::from_secs(5));
175 ring.insert(cap("r1", 100));
176 ring.insert(cap("r2", 100)); assert_eq!(ring.len(), 1);
178 assert!(ring.take("r1").is_none());
179 assert!(ring.take("r2").is_some(), "newest always survives");
180 }
181
182 #[test]
183 fn expired_entries_are_evicted() {
184 let ring = CaptureRing::new(8, 1 << 20, Duration::from_millis(1));
185 ring.insert(cap("r1", 10));
186 std::thread::sleep(Duration::from_millis(5));
187 assert!(ring.take("r1").is_none(), "expired before take");
189 assert!(ring.is_empty());
190 }
191
192 #[test]
193 fn reinsert_same_id_does_not_double_count_bytes() {
194 let ring = CaptureRing::new(8, 1 << 20, Duration::from_secs(5));
195 ring.insert(cap("r1", 10));
196 ring.insert(cap("r1", 20)); assert_eq!(ring.len(), 1);
198 let got = ring.take("r1").unwrap();
199 assert_eq!(got.body_b64.len(), 20, "latest value wins");
200 }
201}