use std::{collections::HashMap, sync::Arc, time::Instant};
#[derive(Debug)]
pub struct CommandFragmentBuffer {
channel_id: u8,
fragment_count: u8,
ordered: bool,
fragments: HashMap<u8, Arc<[u8]>>,
created_at: Instant,
}
impl CommandFragmentBuffer {
pub fn new(channel_id: u8, fragment_count: u8, ordered: bool, created_at: Instant) -> Self {
Self { channel_id, fragment_count, ordered, fragments: HashMap::new(), created_at }
}
pub fn channel_id(&self) -> u8 {
self.channel_id
}
pub fn is_ordered(&self) -> bool {
self.ordered
}
pub fn add_fragment(&mut self, fragment_id: u8, data: Arc<[u8]>) {
self.fragments.insert(fragment_id, data);
}
pub fn is_complete(&self) -> bool {
self.fragments.len() == self.fragment_count as usize
}
pub fn reassemble(mut self) -> Option<Vec<u8>> {
if !self.is_complete() {
return None;
}
let mut result = Vec::new();
for fragment_id in 0..self.fragment_count {
if let Some(data) = self.fragments.remove(&fragment_id) {
result.extend_from_slice(&data);
} else {
return None; }
}
Some(result)
}
pub fn created_at(&self) -> Instant {
self.created_at
}
}
pub const FRAGMENT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
pub fn cleanup_stale_fragments(
command_fragments: &mut HashMap<u16, CommandFragmentBuffer>,
time: Instant,
) {
let stale_sequences: Vec<u16> = command_fragments
.iter()
.filter_map(|(seq, buffer)| {
if time.duration_since(buffer.created_at()) > FRAGMENT_TIMEOUT {
Some(*seq)
} else {
None
}
})
.collect();
if !stale_sequences.is_empty() {
tracing::warn!(
"Cleaning up {} stale fragment buffer(s) that timed out after {:?}",
stale_sequences.len(),
FRAGMENT_TIMEOUT
);
for seq in stale_sequences {
command_fragments.remove(&seq);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fragment_buffer_creation() {
let buffer = CommandFragmentBuffer::new(0, 3, true, Instant::now());
assert_eq!(buffer.channel_id(), 0);
assert_eq!(buffer.is_ordered(), true);
assert!(!buffer.is_complete());
}
#[test]
fn test_fragment_buffer_add_and_complete() {
let mut buffer = CommandFragmentBuffer::new(1, 3, false, Instant::now());
buffer.add_fragment(0, Arc::from(vec![1, 2, 3]));
assert!(!buffer.is_complete());
buffer.add_fragment(1, Arc::from(vec![4, 5, 6]));
assert!(!buffer.is_complete());
buffer.add_fragment(2, Arc::from(vec![7, 8, 9]));
assert!(buffer.is_complete());
}
#[test]
fn test_fragment_buffer_reassemble() {
let mut buffer = CommandFragmentBuffer::new(0, 3, true, Instant::now());
buffer.add_fragment(0, Arc::from(vec![1, 2, 3]));
buffer.add_fragment(1, Arc::from(vec![4, 5, 6]));
buffer.add_fragment(2, Arc::from(vec![7, 8, 9]));
let result = buffer.reassemble().unwrap();
assert_eq!(result, vec![1, 2, 3, 4, 5, 6, 7, 8, 9]);
}
#[test]
fn test_fragment_buffer_reassemble_incomplete() {
let mut buffer = CommandFragmentBuffer::new(0, 3, true, Instant::now());
buffer.add_fragment(0, Arc::from(vec![1, 2, 3]));
buffer.add_fragment(1, Arc::from(vec![4, 5, 6]));
assert!(buffer.reassemble().is_none());
}
#[test]
fn test_cleanup_stale_fragments() {
let mut fragments: HashMap<u16, CommandFragmentBuffer> = HashMap::new();
let start_time = Instant::now();
let buffer = CommandFragmentBuffer::new(0, 3, true, start_time);
fragments.insert(100, buffer);
cleanup_stale_fragments(&mut fragments, start_time);
assert_eq!(fragments.len(), 1, "Fragment buffer should still exist");
let later = start_time + std::time::Duration::from_secs(6);
cleanup_stale_fragments(&mut fragments, later);
assert_eq!(fragments.len(), 0, "Stale fragment buffer should be cleaned up");
}
#[test]
fn test_cleanup_multiple_stale_fragments() {
let mut fragments: HashMap<u16, CommandFragmentBuffer> = HashMap::new();
let start_time = Instant::now();
for seq in 0..5 {
let buffer = CommandFragmentBuffer::new(0, 2, true, start_time);
fragments.insert(seq, buffer);
}
assert_eq!(fragments.len(), 5);
let later = start_time + std::time::Duration::from_secs(6);
cleanup_stale_fragments(&mut fragments, later);
assert_eq!(fragments.len(), 0, "All stale fragment buffers should be cleaned up");
}
#[test]
fn test_cleanup_mixed_fresh_and_stale_fragments() {
let mut fragments: HashMap<u16, CommandFragmentBuffer> = HashMap::new();
let start_time = Instant::now();
let old_buffer = CommandFragmentBuffer::new(0, 3, true, start_time);
fragments.insert(100, old_buffer);
let later = start_time + std::time::Duration::from_secs(6);
let new_buffer = CommandFragmentBuffer::new(1, 3, false, later);
fragments.insert(200, new_buffer);
cleanup_stale_fragments(&mut fragments, later);
assert_eq!(fragments.len(), 1, "Only fresh fragment buffer should remain");
assert!(fragments.contains_key(&200), "Fresh buffer should still exist");
assert!(!fragments.contains_key(&100), "Stale buffer should be removed");
}
}