1use std::collections::{BTreeMap, VecDeque};
9use std::time::{Duration, Instant};
10
11#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub struct StreamKey {
14 pub node_id: String,
15 pub channel_id: u32,
16}
17
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct BufferedEvent {
20 pub seq: u64,
21 pub node_id: String,
22 pub channel_id: u32,
23 pub payload: Vec<u8>,
25 pub enqueued_at: Instant,
26}
27
28#[derive(Clone, Debug)]
29pub struct BufferLimits {
30 pub max_events: usize,
32 pub max_bytes: usize,
34 pub max_age: Duration,
36}
37
38impl Default for BufferLimits {
39 fn default() -> Self {
40 Self {
41 max_events: 4096,
42 max_bytes: 16 * 1024 * 1024,
43 max_age: Duration::from_secs(300),
44 }
45 }
46}
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub enum EnqueueOutcome {
50 Accepted { window_credit: u32 },
52 Backpressure,
54}
55
56pub struct ReplayBuffer {
58 limits: BufferLimits,
59 next_seq: BTreeMap<StreamKey, u64>,
61 last_ack: BTreeMap<StreamKey, u64>,
63 events: VecDeque<BufferedEvent>,
65 total_bytes: usize,
66 credits: BTreeMap<StreamKey, u32>,
68 default_credit: u32,
69}
70
71impl ReplayBuffer {
72 pub fn new(limits: BufferLimits) -> Self {
73 Self {
74 limits,
75 next_seq: BTreeMap::new(),
76 last_ack: BTreeMap::new(),
77 events: VecDeque::new(),
78 total_bytes: 0,
79 credits: BTreeMap::new(),
80 default_credit: 64,
81 }
82 }
83
84 pub fn with_default_credit(mut self, credit: u32) -> Self {
85 self.default_credit = credit.max(1);
86 self
87 }
88
89 pub fn len(&self) -> usize {
90 self.events.len()
91 }
92
93 pub fn is_empty(&self) -> bool {
94 self.events.is_empty()
95 }
96
97 pub fn total_bytes(&self) -> usize {
98 self.total_bytes
99 }
100
101 pub fn credit(&self, key: &StreamKey) -> u32 {
102 self.credits.get(key).copied().unwrap_or(self.default_credit)
103 }
104
105 fn trim_expired(&mut self, now: Instant) {
107 while let Some(front) = self.events.front() {
108 if now.duration_since(front.enqueued_at) <= self.limits.max_age {
109 break;
110 }
111 if let Some(ev) = self.events.pop_front() {
112 self.total_bytes = self.total_bytes.saturating_sub(ev.payload.len());
113 }
114 }
115 }
116
117 pub fn enqueue(
118 &mut self,
119 node_id: impl Into<String>,
120 channel_id: u32,
121 payload: Vec<u8>,
122 now: Instant,
123 ) -> EnqueueOutcome {
124 self.trim_expired(now);
125 let node_id = node_id.into();
126 let key = StreamKey {
127 node_id: node_id.clone(),
128 channel_id,
129 };
130 let credit = self.credit(&key);
131 if credit == 0 {
132 return EnqueueOutcome::Backpressure;
133 }
134 let would_events = self.events.len() + 1;
135 let would_bytes = self.total_bytes + payload.len();
136 if would_events > self.limits.max_events || would_bytes > self.limits.max_bytes {
137 self.credits.insert(key, 0);
139 return EnqueueOutcome::Backpressure;
140 }
141 let seq = {
142 let entry = self.next_seq.entry(key.clone()).or_insert(1);
143 let seq = *entry;
144 *entry = seq + 1;
145 seq
146 };
147 self.total_bytes += payload.len();
148 self.events.push_back(BufferedEvent {
149 seq,
150 node_id,
151 channel_id,
152 payload,
153 enqueued_at: now,
154 });
155 let new_credit = credit - 1;
156 self.credits.insert(key, new_credit);
157 EnqueueOutcome::Accepted {
158 window_credit: new_credit,
159 }
160 }
161
162 pub fn ack(&mut self, node_id: &str, channel_id: u32, seq: u64) {
164 let key = StreamKey {
165 node_id: node_id.to_owned(),
166 channel_id,
167 };
168 let prev = self.last_ack.get(&key).copied().unwrap_or(0);
169 if seq <= prev {
170 return;
171 }
172 self.last_ack.insert(key.clone(), seq);
173 let mut released = 0u32;
174 let mut kept = VecDeque::new();
175 while let Some(ev) = self.events.pop_front() {
176 let same = ev.node_id == key.node_id && ev.channel_id == key.channel_id;
177 if same && ev.seq <= seq {
178 self.total_bytes = self.total_bytes.saturating_sub(ev.payload.len());
179 released = released.saturating_add(1);
180 } else {
181 kept.push_back(ev);
182 }
183 }
184 self.events = kept;
185 let credit = self.credit(&key).saturating_add(released).min(self.default_credit);
186 self.credits.insert(key, credit);
187 }
188
189 pub fn events_after(&self, node_id: &str, channel_id: u32, after: u64) -> Vec<BufferedEvent> {
193 self.events
194 .iter()
195 .filter(|ev| {
196 ev.node_id == node_id && ev.channel_id == channel_id && ev.seq > after
197 })
198 .cloned()
199 .collect()
200 }
201
202 pub fn oldest_seq(&self, node_id: &str, channel_id: u32) -> Option<u64> {
204 self.events
205 .iter()
206 .filter(|ev| ev.node_id == node_id && ev.channel_id == channel_id)
207 .map(|ev| ev.seq)
208 .min()
209 }
210
211 pub fn next_seq(&self, node_id: &str, channel_id: u32) -> u64 {
213 let key = StreamKey {
214 node_id: node_id.to_owned(),
215 channel_id,
216 };
217 self.next_seq.get(&key).copied().unwrap_or(1)
218 }
219
220 pub fn gap_before_buffer(&self, node_id: &str, channel_id: u32, after: u64) -> bool {
222 match self.oldest_seq(node_id, channel_id) {
223 None => {
224 let next = self.next_seq(node_id, channel_id);
226 after + 1 < next
227 }
228 Some(oldest) => after + 1 < oldest,
229 }
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 fn limits_tiny() -> BufferLimits {
238 BufferLimits {
239 max_events: 3,
240 max_bytes: 1024,
241 max_age: Duration::from_secs(60),
242 }
243 }
244
245 #[test]
246 fn assigns_monotonic_seq_and_acks() {
247 let mut buf = ReplayBuffer::new(limits_tiny()).with_default_credit(8);
248 let now = Instant::now();
249 assert!(matches!(
250 buf.enqueue("n1", 1, b"a".to_vec(), now),
251 EnqueueOutcome::Accepted { window_credit: 7 }
252 ));
253 assert!(matches!(
254 buf.enqueue("n1", 1, b"b".to_vec(), now),
255 EnqueueOutcome::Accepted { .. }
256 ));
257 let after0 = buf.events_after("n1", 1, 0);
258 assert_eq!(after0.len(), 2);
259 assert_eq!(after0[0].seq, 1);
260 assert_eq!(after0[1].seq, 2);
261 buf.ack("n1", 1, 1);
262 let after1 = buf.events_after("n1", 1, 0);
263 assert_eq!(after1.len(), 1);
264 assert_eq!(after1[0].seq, 2);
265 }
266
267 #[test]
268 fn overflow_signals_backpressure() {
269 let mut buf = ReplayBuffer::new(limits_tiny()).with_default_credit(8);
270 let now = Instant::now();
271 for i in 0..3 {
272 assert!(
273 matches!(
274 buf.enqueue("n1", 1, vec![i], now),
275 EnqueueOutcome::Accepted { .. }
276 ),
277 "event {i}"
278 );
279 }
280 assert_eq!(
281 buf.enqueue("n1", 1, vec![9], now),
282 EnqueueOutcome::Backpressure
283 );
284 assert_eq!(buf.credit(&StreamKey { node_id: "n1".into(), channel_id: 1 }), 0);
285 }
286
287 #[test]
288 fn resume_gap_detected_when_acked_out() {
289 let mut buf = ReplayBuffer::new(BufferLimits {
290 max_events: 10,
291 max_bytes: 4096,
292 max_age: Duration::from_secs(60),
293 })
294 .with_default_credit(8);
295 let now = Instant::now();
296 for _ in 0..5 {
297 buf.enqueue("n1", 1, b"x".to_vec(), now);
298 }
299 buf.ack("n1", 1, 3);
300 assert!(buf.gap_before_buffer("n1", 1, 1));
302 assert!(!buf.gap_before_buffer("n1", 1, 3));
303 let replay = buf.events_after("n1", 1, 3);
304 assert_eq!(replay.iter().map(|e| e.seq).collect::<Vec<_>>(), vec![4, 5]);
305 }
306
307 #[test]
308 fn age_expiry_drops_old_events() {
309 let mut buf = ReplayBuffer::new(BufferLimits {
310 max_events: 10,
311 max_bytes: 4096,
312 max_age: Duration::from_millis(5),
313 });
314 let past = Instant::now() - Duration::from_millis(50);
315 buf.enqueue("n1", 1, b"old".to_vec(), past);
316 assert_eq!(buf.len(), 1);
317 let outcome = buf.enqueue("n1", 1, b"new".to_vec(), Instant::now());
319 assert!(matches!(outcome, EnqueueOutcome::Accepted { .. }));
320 assert_eq!(buf.len(), 1);
321 assert_eq!(buf.events_after("n1", 1, 0)[0].payload, b"new");
322 }
323}