use std::collections::{BTreeMap, VecDeque};
use std::time::{Duration, Instant};
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct StreamKey {
pub node_id: String,
pub channel_id: u32,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BufferedEvent {
pub seq: u64,
pub node_id: String,
pub channel_id: u32,
pub payload: Vec<u8>,
pub enqueued_at: Instant,
}
#[derive(Clone, Debug)]
pub struct BufferLimits {
pub max_events: usize,
pub max_bytes: usize,
pub max_age: Duration,
}
impl Default for BufferLimits {
fn default() -> Self {
Self {
max_events: 4096,
max_bytes: 16 * 1024 * 1024,
max_age: Duration::from_secs(300),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EnqueueOutcome {
Accepted { window_credit: u32 },
Backpressure,
}
pub struct ReplayBuffer {
limits: BufferLimits,
next_seq: BTreeMap<StreamKey, u64>,
last_ack: BTreeMap<StreamKey, u64>,
events: VecDeque<BufferedEvent>,
total_bytes: usize,
credits: BTreeMap<StreamKey, u32>,
default_credit: u32,
}
impl ReplayBuffer {
pub fn new(limits: BufferLimits) -> Self {
Self {
limits,
next_seq: BTreeMap::new(),
last_ack: BTreeMap::new(),
events: VecDeque::new(),
total_bytes: 0,
credits: BTreeMap::new(),
default_credit: 64,
}
}
pub fn with_default_credit(mut self, credit: u32) -> Self {
self.default_credit = credit.max(1);
self
}
pub fn len(&self) -> usize {
self.events.len()
}
pub fn is_empty(&self) -> bool {
self.events.is_empty()
}
pub fn total_bytes(&self) -> usize {
self.total_bytes
}
pub fn credit(&self, key: &StreamKey) -> u32 {
self.credits.get(key).copied().unwrap_or(self.default_credit)
}
pub fn ensure_handshake_credit(&mut self, node_id: &str, channel_id: u32) {
let key = StreamKey {
node_id: node_id.to_owned(),
channel_id,
};
self.credits.insert(key.clone(), self.default_credit);
const NEED: usize = 2048;
while self.events.len() >= self.limits.max_events
|| self.total_bytes + NEED > self.limits.max_bytes
{
let Some(idx) = self
.events
.iter()
.position(|ev| ev.node_id == key.node_id && ev.channel_id == key.channel_id)
else {
break;
};
if let Some(ev) = self.events.remove(idx) {
self.total_bytes = self.total_bytes.saturating_sub(ev.payload.len());
} else {
break;
}
}
}
pub fn reset_channel_credit(&mut self, node_id: &str, channel_id: u32) {
let key = StreamKey {
node_id: node_id.to_owned(),
channel_id,
};
self.credits.insert(key, self.default_credit);
}
fn trim_expired(&mut self, now: Instant) {
while let Some(front) = self.events.front() {
if now.duration_since(front.enqueued_at) <= self.limits.max_age {
break;
}
if let Some(ev) = self.events.pop_front() {
self.total_bytes = self.total_bytes.saturating_sub(ev.payload.len());
}
}
}
pub fn enqueue(
&mut self,
node_id: impl Into<String>,
channel_id: u32,
payload: Vec<u8>,
now: Instant,
) -> EnqueueOutcome {
self.trim_expired(now);
let node_id = node_id.into();
let key = StreamKey {
node_id: node_id.clone(),
channel_id,
};
let credit = self.credit(&key);
if credit == 0 {
return EnqueueOutcome::Backpressure;
}
let would_events = self.events.len() + 1;
let would_bytes = self.total_bytes + payload.len();
if would_events > self.limits.max_events || would_bytes > self.limits.max_bytes {
self.credits.insert(key, 0);
return EnqueueOutcome::Backpressure;
}
let seq = {
let entry = self.next_seq.entry(key.clone()).or_insert(1);
let seq = *entry;
*entry = seq + 1;
seq
};
self.total_bytes += payload.len();
self.events.push_back(BufferedEvent {
seq,
node_id,
channel_id,
payload,
enqueued_at: now,
});
let new_credit = credit - 1;
self.credits.insert(key, new_credit);
EnqueueOutcome::Accepted {
window_credit: new_credit,
}
}
pub fn ack(&mut self, node_id: &str, channel_id: u32, seq: u64) {
let key = StreamKey {
node_id: node_id.to_owned(),
channel_id,
};
let prev = self.last_ack.get(&key).copied().unwrap_or(0);
if seq <= prev {
return;
}
self.last_ack.insert(key.clone(), seq);
let mut released = 0u32;
let mut kept = VecDeque::new();
while let Some(ev) = self.events.pop_front() {
let same = ev.node_id == key.node_id && ev.channel_id == key.channel_id;
if same && ev.seq <= seq {
self.total_bytes = self.total_bytes.saturating_sub(ev.payload.len());
released = released.saturating_add(1);
} else {
kept.push_back(ev);
}
}
self.events = kept;
let credit = self.credit(&key).saturating_add(released).min(self.default_credit);
self.credits.insert(key, credit);
}
pub fn events_after(&self, node_id: &str, channel_id: u32, after: u64) -> Vec<BufferedEvent> {
self.events
.iter()
.filter(|ev| {
ev.node_id == node_id && ev.channel_id == channel_id && ev.seq > after
})
.cloned()
.collect()
}
pub fn oldest_seq(&self, node_id: &str, channel_id: u32) -> Option<u64> {
self.events
.iter()
.filter(|ev| ev.node_id == node_id && ev.channel_id == channel_id)
.map(|ev| ev.seq)
.min()
}
pub fn next_seq(&self, node_id: &str, channel_id: u32) -> u64 {
let key = StreamKey {
node_id: node_id.to_owned(),
channel_id,
};
self.next_seq.get(&key).copied().unwrap_or(1)
}
pub fn gap_before_buffer(&self, node_id: &str, channel_id: u32, after: u64) -> bool {
match self.oldest_seq(node_id, channel_id) {
None => {
let next = self.next_seq(node_id, channel_id);
after + 1 < next
}
Some(oldest) => after + 1 < oldest,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn limits_tiny() -> BufferLimits {
BufferLimits {
max_events: 3,
max_bytes: 1024,
max_age: Duration::from_secs(60),
}
}
#[test]
fn assigns_monotonic_seq_and_acks() {
let mut buf = ReplayBuffer::new(limits_tiny()).with_default_credit(8);
let now = Instant::now();
assert!(matches!(
buf.enqueue("n1", 1, b"a".to_vec(), now),
EnqueueOutcome::Accepted { window_credit: 7 }
));
assert!(matches!(
buf.enqueue("n1", 1, b"b".to_vec(), now),
EnqueueOutcome::Accepted { .. }
));
let after0 = buf.events_after("n1", 1, 0);
assert_eq!(after0.len(), 2);
assert_eq!(after0[0].seq, 1);
assert_eq!(after0[1].seq, 2);
buf.ack("n1", 1, 1);
let after1 = buf.events_after("n1", 1, 0);
assert_eq!(after1.len(), 1);
assert_eq!(after1[0].seq, 2);
}
#[test]
fn overflow_signals_backpressure() {
let mut buf = ReplayBuffer::new(limits_tiny()).with_default_credit(8);
let now = Instant::now();
for i in 0..3 {
assert!(
matches!(
buf.enqueue("n1", 1, vec![i], now),
EnqueueOutcome::Accepted { .. }
),
"event {i}"
);
}
assert_eq!(
buf.enqueue("n1", 1, vec![9], now),
EnqueueOutcome::Backpressure
);
assert_eq!(buf.credit(&StreamKey { node_id: "n1".into(), channel_id: 1 }), 0);
}
#[test]
fn resume_gap_detected_when_acked_out() {
let mut buf = ReplayBuffer::new(BufferLimits {
max_events: 10,
max_bytes: 4096,
max_age: Duration::from_secs(60),
})
.with_default_credit(8);
let now = Instant::now();
for _ in 0..5 {
buf.enqueue("n1", 1, b"x".to_vec(), now);
}
buf.ack("n1", 1, 3);
assert!(buf.gap_before_buffer("n1", 1, 1));
assert!(!buf.gap_before_buffer("n1", 1, 3));
let replay = buf.events_after("n1", 1, 3);
assert_eq!(replay.iter().map(|e| e.seq).collect::<Vec<_>>(), vec![4, 5]);
}
#[test]
fn age_expiry_drops_old_events() {
let mut buf = ReplayBuffer::new(BufferLimits {
max_events: 10,
max_bytes: 4096,
max_age: Duration::from_millis(5),
});
let past = Instant::now() - Duration::from_millis(50);
buf.enqueue("n1", 1, b"old".to_vec(), past);
assert_eq!(buf.len(), 1);
let outcome = buf.enqueue("n1", 1, b"new".to_vec(), Instant::now());
assert!(matches!(outcome, EnqueueOutcome::Accepted { .. }));
assert_eq!(buf.len(), 1);
assert_eq!(buf.events_after("n1", 1, 0)[0].payload, b"new");
}
#[test]
fn reset_channel_credit_restores_after_exhaustion() {
let mut buf = ReplayBuffer::new(BufferLimits {
max_events: 64,
max_bytes: 64 * 1024,
max_age: Duration::from_secs(60),
})
.with_default_credit(2);
let now = Instant::now();
assert!(matches!(buf.enqueue("n1", 7, b"a".to_vec(), now), EnqueueOutcome::Accepted { .. }));
assert!(matches!(buf.enqueue("n1", 7, b"b".to_vec(), now), EnqueueOutcome::Accepted { .. }));
assert!(matches!(buf.enqueue("n1", 7, b"c".to_vec(), now), EnqueueOutcome::Backpressure));
assert_eq!(buf.credit(&StreamKey { node_id: "n1".into(), channel_id: 7 }), 0);
buf.reset_channel_credit("n1", 7);
assert_eq!(buf.credit(&StreamKey { node_id: "n1".into(), channel_id: 7 }), 2);
assert!(matches!(buf.enqueue("n1", 7, b"d".to_vec(), now), EnqueueOutcome::Accepted { .. }));
}
#[test]
fn reset_channel_credit_is_per_channel_under_parallel_opens() {
let mut buf = ReplayBuffer::new(BufferLimits {
max_events: 256,
max_bytes: 256 * 1024,
max_age: Duration::from_secs(60),
})
.with_default_credit(4);
let now = Instant::now();
for ch in 1u32..=50 {
for _ in 0..4 {
assert!(matches!(
buf.enqueue("box", ch, b"x".to_vec(), now),
EnqueueOutcome::Accepted { .. }
));
}
assert_eq!(buf.credit(&StreamKey { node_id: "box".into(), channel_id: ch }), 0);
}
for ch in 1u32..=50 {
buf.reset_channel_credit("box", ch);
assert_eq!(buf.credit(&StreamKey { node_id: "box".into(), channel_id: ch }), 4);
}
for _ in 0..4 {
let _ = buf.enqueue("box", 1, b"y".to_vec(), now);
}
assert_eq!(buf.credit(&StreamKey { node_id: "box".into(), channel_id: 1 }), 0);
assert_eq!(buf.credit(&StreamKey { node_id: "box".into(), channel_id: 2 }), 4);
}
}