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