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 pub fn ensure_handshake_credit(&mut self, node_id: &str, channel_id: u32) {
110 let key = StreamKey {
111 node_id: node_id.to_owned(),
112 channel_id,
113 };
114 self.credits.insert(key.clone(), self.default_credit);
115 const NEED: usize = 2048;
117 while self.events.len() >= self.limits.max_events
118 || self.total_bytes + NEED > self.limits.max_bytes
119 {
120 let idx = self
121 .events
122 .iter()
123 .position(|ev| ev.node_id == key.node_id && ev.channel_id == key.channel_id)
124 .or_else(|| if self.events.is_empty() { None } else { Some(0) });
125 let Some(idx) = idx else {
126 break;
127 };
128 if let Some(ev) = self.events.remove(idx) {
129 self.total_bytes = self.total_bytes.saturating_sub(ev.payload.len());
130 } else {
131 break;
132 }
133 }
134 }
135
136 pub fn reset_channel_credit(&mut self, node_id: &str, channel_id: u32) {
138 let key = StreamKey {
139 node_id: node_id.to_owned(),
140 channel_id,
141 };
142 self.credits.insert(key, self.default_credit);
143 }
144
145 fn trim_expired(&mut self, now: Instant) {
147 while let Some(front) = self.events.front() {
148 if now.duration_since(front.enqueued_at) <= self.limits.max_age {
149 break;
150 }
151 if let Some(ev) = self.events.pop_front() {
152 self.total_bytes = self.total_bytes.saturating_sub(ev.payload.len());
153 }
154 }
155 }
156
157 pub fn enqueue(
158 &mut self,
159 node_id: impl Into<String>,
160 channel_id: u32,
161 payload: Vec<u8>,
162 now: Instant,
163 ) -> EnqueueOutcome {
164 self.trim_expired(now);
165 let node_id = node_id.into();
166 let key = StreamKey {
167 node_id: node_id.clone(),
168 channel_id,
169 };
170 let credit = self.credit(&key);
171 if credit == 0 {
172 return EnqueueOutcome::Backpressure;
173 }
174 let would_events = self.events.len() + 1;
175 let would_bytes = self.total_bytes + payload.len();
176 if would_events > self.limits.max_events || would_bytes > self.limits.max_bytes {
177 self.credits.insert(key, 0);
179 return EnqueueOutcome::Backpressure;
180 }
181 let seq = {
182 let entry = self.next_seq.entry(key.clone()).or_insert(1);
183 let seq = *entry;
184 *entry = seq + 1;
185 seq
186 };
187 self.total_bytes += payload.len();
188 self.events.push_back(BufferedEvent {
189 seq,
190 node_id,
191 channel_id,
192 payload,
193 enqueued_at: now,
194 });
195 let new_credit = credit - 1;
196 self.credits.insert(key, new_credit);
197 EnqueueOutcome::Accepted {
198 window_credit: new_credit,
199 }
200 }
201
202 pub fn ack(&mut self, node_id: &str, channel_id: u32, seq: u64) {
204 let key = StreamKey {
205 node_id: node_id.to_owned(),
206 channel_id,
207 };
208 let prev = self.last_ack.get(&key).copied().unwrap_or(0);
209 if seq <= prev {
210 return;
211 }
212 self.last_ack.insert(key.clone(), seq);
213 let mut released = 0u32;
214 let mut kept = VecDeque::new();
215 while let Some(ev) = self.events.pop_front() {
216 let same = ev.node_id == key.node_id && ev.channel_id == key.channel_id;
217 if same && ev.seq <= seq {
218 self.total_bytes = self.total_bytes.saturating_sub(ev.payload.len());
219 released = released.saturating_add(1);
220 } else {
221 kept.push_back(ev);
222 }
223 }
224 self.events = kept;
225 let credit = self.credit(&key).saturating_add(released).min(self.default_credit);
226 self.credits.insert(key, credit);
227 }
228
229 pub fn events_after(&self, node_id: &str, channel_id: u32, after: u64) -> Vec<BufferedEvent> {
233 self.events
234 .iter()
235 .filter(|ev| {
236 ev.node_id == node_id && ev.channel_id == channel_id && ev.seq > after
237 })
238 .cloned()
239 .collect()
240 }
241
242 pub fn oldest_seq(&self, node_id: &str, channel_id: u32) -> Option<u64> {
244 self.events
245 .iter()
246 .filter(|ev| ev.node_id == node_id && ev.channel_id == channel_id)
247 .map(|ev| ev.seq)
248 .min()
249 }
250
251 pub fn next_seq(&self, node_id: &str, channel_id: u32) -> u64 {
253 let key = StreamKey {
254 node_id: node_id.to_owned(),
255 channel_id,
256 };
257 self.next_seq.get(&key).copied().unwrap_or(1)
258 }
259
260 pub fn gap_before_buffer(&self, node_id: &str, channel_id: u32, after: u64) -> bool {
262 match self.oldest_seq(node_id, channel_id) {
263 None => {
264 let next = self.next_seq(node_id, channel_id);
266 after + 1 < next
267 }
268 Some(oldest) => after + 1 < oldest,
269 }
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 fn limits_tiny() -> BufferLimits {
278 BufferLimits {
279 max_events: 3,
280 max_bytes: 1024,
281 max_age: Duration::from_secs(60),
282 }
283 }
284
285 #[test]
286 fn assigns_monotonic_seq_and_acks() {
287 let mut buf = ReplayBuffer::new(limits_tiny()).with_default_credit(8);
288 let now = Instant::now();
289 assert!(matches!(
290 buf.enqueue("n1", 1, b"a".to_vec(), now),
291 EnqueueOutcome::Accepted { window_credit: 7 }
292 ));
293 assert!(matches!(
294 buf.enqueue("n1", 1, b"b".to_vec(), now),
295 EnqueueOutcome::Accepted { .. }
296 ));
297 let after0 = buf.events_after("n1", 1, 0);
298 assert_eq!(after0.len(), 2);
299 assert_eq!(after0[0].seq, 1);
300 assert_eq!(after0[1].seq, 2);
301 buf.ack("n1", 1, 1);
302 let after1 = buf.events_after("n1", 1, 0);
303 assert_eq!(after1.len(), 1);
304 assert_eq!(after1[0].seq, 2);
305 }
306
307 #[test]
308 fn overflow_signals_backpressure() {
309 let mut buf = ReplayBuffer::new(limits_tiny()).with_default_credit(8);
310 let now = Instant::now();
311 for i in 0..3 {
312 assert!(
313 matches!(
314 buf.enqueue("n1", 1, vec![i], now),
315 EnqueueOutcome::Accepted { .. }
316 ),
317 "event {i}"
318 );
319 }
320 assert_eq!(
321 buf.enqueue("n1", 1, vec![9], now),
322 EnqueueOutcome::Backpressure
323 );
324 assert_eq!(buf.credit(&StreamKey { node_id: "n1".into(), channel_id: 1 }), 0);
325 }
326
327 #[test]
328 fn resume_gap_detected_when_acked_out() {
329 let mut buf = ReplayBuffer::new(BufferLimits {
330 max_events: 10,
331 max_bytes: 4096,
332 max_age: Duration::from_secs(60),
333 })
334 .with_default_credit(8);
335 let now = Instant::now();
336 for _ in 0..5 {
337 buf.enqueue("n1", 1, b"x".to_vec(), now);
338 }
339 buf.ack("n1", 1, 3);
340 assert!(buf.gap_before_buffer("n1", 1, 1));
342 assert!(!buf.gap_before_buffer("n1", 1, 3));
343 let replay = buf.events_after("n1", 1, 3);
344 assert_eq!(replay.iter().map(|e| e.seq).collect::<Vec<_>>(), vec![4, 5]);
345 }
346
347 #[test]
348 fn age_expiry_drops_old_events() {
349 let mut buf = ReplayBuffer::new(BufferLimits {
350 max_events: 10,
351 max_bytes: 4096,
352 max_age: Duration::from_millis(5),
353 });
354 let past = Instant::now() - Duration::from_millis(50);
355 buf.enqueue("n1", 1, b"old".to_vec(), past);
356 assert_eq!(buf.len(), 1);
357 let outcome = buf.enqueue("n1", 1, b"new".to_vec(), Instant::now());
359 assert!(matches!(outcome, EnqueueOutcome::Accepted { .. }));
360 assert_eq!(buf.len(), 1);
361 assert_eq!(buf.events_after("n1", 1, 0)[0].payload, b"new");
362 }
363
364 #[test]
365 fn reset_channel_credit_restores_after_exhaustion() {
366 let mut buf = ReplayBuffer::new(BufferLimits {
367 max_events: 64,
368 max_bytes: 64 * 1024,
369 max_age: Duration::from_secs(60),
370 })
371 .with_default_credit(2);
372 let now = Instant::now();
373 assert!(matches!(buf.enqueue("n1", 7, b"a".to_vec(), now), EnqueueOutcome::Accepted { .. }));
374 assert!(matches!(buf.enqueue("n1", 7, b"b".to_vec(), now), EnqueueOutcome::Accepted { .. }));
375 assert!(matches!(buf.enqueue("n1", 7, b"c".to_vec(), now), EnqueueOutcome::Backpressure));
376 assert_eq!(buf.credit(&StreamKey { node_id: "n1".into(), channel_id: 7 }), 0);
377 buf.reset_channel_credit("n1", 7);
379 assert_eq!(buf.credit(&StreamKey { node_id: "n1".into(), channel_id: 7 }), 2);
380 assert!(matches!(buf.enqueue("n1", 7, b"d".to_vec(), now), EnqueueOutcome::Accepted { .. }));
381 }
382
383 #[test]
386 fn reset_channel_credit_is_per_channel_under_parallel_opens() {
387 let mut buf = ReplayBuffer::new(BufferLimits {
388 max_events: 256,
389 max_bytes: 256 * 1024,
390 max_age: Duration::from_secs(60),
391 })
392 .with_default_credit(4);
393 let now = Instant::now();
394 for ch in 1u32..=50 {
395 for _ in 0..4 {
396 assert!(matches!(
397 buf.enqueue("box", ch, b"x".to_vec(), now),
398 EnqueueOutcome::Accepted { .. }
399 ));
400 }
401 assert_eq!(buf.credit(&StreamKey { node_id: "box".into(), channel_id: ch }), 0);
402 }
403 for ch in 1u32..=50 {
404 buf.reset_channel_credit("box", ch);
405 assert_eq!(buf.credit(&StreamKey { node_id: "box".into(), channel_id: ch }), 4);
406 }
407 for _ in 0..4 {
409 let _ = buf.enqueue("box", 1, b"y".to_vec(), now);
410 }
411 assert_eq!(buf.credit(&StreamKey { node_id: "box".into(), channel_id: 1 }), 0);
412 assert_eq!(buf.credit(&StreamKey { node_id: "box".into(), channel_id: 2 }), 4);
413 }
414}