use std::collections::{BTreeMap, HashMap, VecDeque};
pub type SpsStreamId = u64;
#[inline]
pub fn xorshift64(state: &mut u64) -> u64 {
let mut x = *state;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
*state = x;
x
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SpsSchedulingPolicy {
StrictPriority,
WeightedFairQueuing,
DeficitRoundRobin,
EarliestDeadlineFirst,
HierarchicalToken,
}
#[derive(Debug, Clone)]
pub struct SpsSchedulerConfig {
pub max_streams: usize,
pub quantum_bytes: u64,
pub strict_priority_threshold: u32,
pub deficit_rounds: u32,
}
impl Default for SpsSchedulerConfig {
fn default() -> Self {
Self {
max_streams: 1024,
quantum_bytes: 1500,
strict_priority_threshold: 240,
deficit_rounds: 1,
}
}
}
#[derive(Debug, Clone)]
pub struct SpsStream {
pub id: SpsStreamId,
pub priority: u32,
pub weight: u32,
pub pending_bytes: u64,
pub deficit_counter: i64,
pub send_count: u64,
pub bytes_sent: u64,
pub last_scheduled_ts: u64,
pub is_blocked: bool,
pub deadline: u64,
pub htb_tokens: i64,
pub htb_rate: u64,
pub htb_burst: u64,
}
impl SpsStream {
pub fn new(id: SpsStreamId, priority: u32, weight: u32) -> Self {
Self {
id,
priority,
weight: weight.max(1),
pending_bytes: 0,
deficit_counter: 0,
send_count: 0,
bytes_sent: 0,
last_scheduled_ts: 0,
is_blocked: false,
deadline: u64::MAX,
htb_tokens: 0,
htb_rate: 1500,
htb_burst: 65535,
}
}
#[inline]
pub fn is_eligible(&self) -> bool {
!self.is_blocked && self.pending_bytes > 0
}
}
#[derive(Debug, Clone, Default)]
pub struct SpsSchedulerStats {
pub total_scheduled: u64,
pub total_bytes: u64,
pub priority_distribution: HashMap<u32, u64>,
pub avg_wait: f64,
pub active_streams: usize,
pub blocked_streams: usize,
pub streams_removed: u64,
pub fairness_index: f64,
}
#[derive(Debug, Clone)]
struct DrrEntry {
stream_id: SpsStreamId,
priority: u32,
}
pub struct StreamPriorityScheduler {
streams: HashMap<SpsStreamId, SpsStream>,
priority_queues: BTreeMap<u32, VecDeque<SpsStreamId>>,
config: SpsSchedulerConfig,
stats: SpsSchedulerStats,
tick: u64,
drr_active: VecDeque<DrrEntry>,
rng_state: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum SpsError {
#[error("stream {0} not found")]
StreamNotFound(SpsStreamId),
#[error("maximum stream limit ({0}) reached")]
MaxStreamsReached(usize),
#[error("stream {0} already registered")]
DuplicateStream(SpsStreamId),
#[error("weight must be >= 1")]
InvalidWeight,
#[error("batch size must be > 0")]
InvalidBatchSize,
}
impl StreamPriorityScheduler {
pub fn new(config: SpsSchedulerConfig) -> Self {
Self {
streams: HashMap::new(),
priority_queues: BTreeMap::new(),
config,
stats: SpsSchedulerStats::default(),
tick: 0,
drr_active: VecDeque::new(),
rng_state: 0xdeadbeef_cafebabe,
}
}
pub fn add_stream(
&mut self,
id: SpsStreamId,
priority: u32,
weight: u32,
) -> Result<(), SpsError> {
if self.streams.len() >= self.config.max_streams {
return Err(SpsError::MaxStreamsReached(self.config.max_streams));
}
if self.streams.contains_key(&id) {
return Err(SpsError::DuplicateStream(id));
}
if weight == 0 {
return Err(SpsError::InvalidWeight);
}
let stream = SpsStream::new(id, priority, weight);
self.streams.insert(id, stream);
self.stats.active_streams = self.streams.len();
Ok(())
}
pub fn remove_stream(&mut self, id: SpsStreamId) -> Result<SpsStream, SpsError> {
let stream = self
.streams
.remove(&id)
.ok_or(SpsError::StreamNotFound(id))?;
for queue in self.priority_queues.values_mut() {
queue.retain(|&sid| sid != id);
}
self.drr_active.retain(|e| e.stream_id != id);
self.priority_queues.retain(|_, q| !q.is_empty());
self.stats.active_streams = self.streams.len();
self.stats.streams_removed += 1;
Ok(stream)
}
pub fn block_stream(&mut self, id: SpsStreamId) -> Result<(), SpsError> {
let stream = self
.streams
.get_mut(&id)
.ok_or(SpsError::StreamNotFound(id))?;
stream.is_blocked = true;
self.stats.blocked_streams = self.streams.values().filter(|s| s.is_blocked).count();
Ok(())
}
pub fn unblock_stream(&mut self, id: SpsStreamId) -> Result<(), SpsError> {
let stream = self
.streams
.get_mut(&id)
.ok_or(SpsError::StreamNotFound(id))?;
stream.is_blocked = false;
self.stats.blocked_streams = self.streams.values().filter(|s| s.is_blocked).count();
Ok(())
}
pub fn enqueue_bytes(&mut self, stream_id: SpsStreamId, bytes: u64) -> Result<(), SpsError> {
let stream = self
.streams
.get_mut(&stream_id)
.ok_or(SpsError::StreamNotFound(stream_id))?;
let was_empty = stream.pending_bytes == 0;
stream.pending_bytes = stream.pending_bytes.saturating_add(bytes);
if was_empty && !stream.is_blocked {
let priority = stream.priority;
self.priority_queues
.entry(priority)
.or_default()
.push_back(stream_id);
}
Ok(())
}
pub fn schedule_next(&mut self, policy: &SpsSchedulingPolicy) -> Option<(SpsStreamId, u64)> {
self.tick += 1;
match policy {
SpsSchedulingPolicy::StrictPriority => self.schedule_strict(),
SpsSchedulingPolicy::WeightedFairQueuing => self.schedule_wfq(),
SpsSchedulingPolicy::DeficitRoundRobin => self.schedule_drr_single(),
SpsSchedulingPolicy::EarliestDeadlineFirst => self.schedule_edf(),
SpsSchedulingPolicy::HierarchicalToken => self.schedule_htb(),
}
}
pub fn schedule_batch(
&mut self,
policy: &SpsSchedulingPolicy,
n: usize,
) -> Vec<(SpsStreamId, u64)> {
let mut results = Vec::with_capacity(n);
for _ in 0..n {
match self.schedule_next(policy) {
Some(item) => results.push(item),
None => break,
}
}
results
}
pub fn run_drr_round(&mut self) -> Vec<(SpsStreamId, u64)> {
let rounds = self.config.deficit_rounds as usize;
let quantum = self.config.quantum_bytes;
let mut results = Vec::new();
for _ in 0..rounds {
if self.drr_active.is_empty() {
for stream in self.streams.values() {
if stream.is_eligible() {
self.drr_active.push_back(DrrEntry {
stream_id: stream.id,
priority: stream.priority,
});
}
}
}
let mut processed = VecDeque::new();
let len = self.drr_active.len();
for _ in 0..len {
let entry = match self.drr_active.pop_front() {
Some(e) => e,
None => break,
};
let stream = match self.streams.get_mut(&entry.stream_id) {
Some(s) => s,
None => continue,
};
if !stream.is_eligible() {
continue;
}
stream.deficit_counter += quantum as i64;
while stream.pending_bytes > 0 && stream.deficit_counter > 0 {
let send = stream.pending_bytes.min(stream.deficit_counter as u64);
stream.deficit_counter -= send as i64;
stream.pending_bytes -= send;
stream.bytes_sent += send;
stream.send_count += 1;
stream.last_scheduled_ts = self.tick;
self.tick += 1;
self.stats.total_scheduled += 1;
self.stats.total_bytes += send;
*self
.stats
.priority_distribution
.entry(stream.priority)
.or_insert(0) += 1;
results.push((entry.stream_id, send));
}
if stream.pending_bytes > 0 {
processed.push_back(DrrEntry {
stream_id: entry.stream_id,
priority: entry.priority,
});
} else {
stream.deficit_counter = 0;
}
}
self.drr_active = processed;
}
results
}
pub fn compute_fairness(&self) -> f64 {
let streams: Vec<f64> = self.streams.values().map(|s| s.bytes_sent as f64).collect();
let n = streams.len();
if n < 2 {
return 1.0;
}
let sum: f64 = streams.iter().sum();
let sum_sq: f64 = streams.iter().map(|x| x * x).sum();
if sum_sq == 0.0 {
return 1.0;
}
(sum * sum) / (n as f64 * sum_sq)
}
pub fn scheduler_stats(&mut self) -> SpsSchedulerStats {
self.stats.active_streams = self.streams.len();
self.stats.blocked_streams = self.streams.values().filter(|s| s.is_blocked).count();
self.stats.fairness_index = self.compute_fairness();
let scheduled: Vec<u64> = self
.streams
.values()
.filter(|s| s.send_count > 0)
.map(|s| self.tick.saturating_sub(s.last_scheduled_ts))
.collect();
if scheduled.is_empty() {
self.stats.avg_wait = 0.0;
} else {
self.stats.avg_wait = scheduled.iter().sum::<u64>() as f64 / scheduled.len() as f64;
}
self.stats.clone()
}
pub fn get_stream(&self, id: SpsStreamId) -> Option<&SpsStream> {
self.streams.get(&id)
}
pub fn get_stream_mut(&mut self, id: SpsStreamId) -> Option<&mut SpsStream> {
self.streams.get_mut(&id)
}
pub fn set_deadline(&mut self, id: SpsStreamId, deadline: u64) -> Result<(), SpsError> {
let stream = self
.streams
.get_mut(&id)
.ok_or(SpsError::StreamNotFound(id))?;
stream.deadline = deadline;
Ok(())
}
pub fn set_htb_params(
&mut self,
id: SpsStreamId,
rate: u64,
burst: u64,
) -> Result<(), SpsError> {
let stream = self
.streams
.get_mut(&id)
.ok_or(SpsError::StreamNotFound(id))?;
stream.htb_rate = rate.max(1);
stream.htb_burst = burst.max(1);
Ok(())
}
pub fn htb_refill(&mut self, ticks: u64) {
for stream in self.streams.values_mut() {
let refill = (stream.htb_rate * ticks) as i64;
stream.htb_tokens = (stream.htb_tokens + refill).min(stream.htb_burst as i64);
}
}
pub fn tick(&self) -> u64 {
self.tick
}
pub fn stream_count(&self) -> usize {
self.streams.len()
}
pub fn eligible_count(&self) -> usize {
self.streams.values().filter(|s| s.is_eligible()).count()
}
pub fn drain_stream(&mut self, id: SpsStreamId) -> Result<u64, SpsError> {
let stream = self
.streams
.get_mut(&id)
.ok_or(SpsError::StreamNotFound(id))?;
let drained = stream.pending_bytes;
stream.pending_bytes = 0;
stream.deficit_counter = 0;
if let Some(queue) = self.priority_queues.get_mut(&stream.priority) {
queue.retain(|&sid| sid != id);
}
self.drr_active.retain(|e| e.stream_id != id);
Ok(drained)
}
pub fn update_priority(&mut self, id: SpsStreamId, new_priority: u32) -> Result<(), SpsError> {
let stream = self
.streams
.get_mut(&id)
.ok_or(SpsError::StreamNotFound(id))?;
let old_priority = stream.priority;
let has_data = stream.pending_bytes > 0 && !stream.is_blocked;
stream.priority = new_priority;
if has_data && old_priority != new_priority {
if let Some(q) = self.priority_queues.get_mut(&old_priority) {
q.retain(|&sid| sid != id);
}
self.priority_queues
.entry(new_priority)
.or_default()
.push_back(id);
}
Ok(())
}
fn schedule_strict(&mut self) -> Option<(SpsStreamId, u64)> {
let quantum = self.config.quantum_bytes;
let _jitter = xorshift64(&mut self.rng_state);
let chosen = self.priority_queues.iter().rev().find_map(|(_, queue)| {
queue
.iter()
.find(|&&sid| {
self.streams
.get(&sid)
.map(|s| s.is_eligible())
.unwrap_or(false)
})
.copied()
});
let id = chosen?;
self.dispatch_bytes(id, quantum)
}
fn schedule_wfq(&mut self) -> Option<(SpsStreamId, u64)> {
let quantum = self.config.quantum_bytes;
let strict_threshold = self.config.strict_priority_threshold;
let strict_id = self.priority_queues.iter().rev().find_map(|(&pri, queue)| {
if pri < strict_threshold {
return None;
}
queue
.iter()
.find(|&&sid| {
self.streams
.get(&sid)
.map(|s| s.is_eligible())
.unwrap_or(false)
})
.copied()
});
if let Some(id) = strict_id {
return self.dispatch_bytes(id, quantum);
}
let best = self
.streams
.values()
.filter(|s| s.is_eligible() && s.priority < strict_threshold)
.min_by(|a, b| {
let va = if a.weight == 0 {
f64::MAX
} else {
a.bytes_sent as f64 / a.weight as f64
};
let vb = if b.weight == 0 {
f64::MAX
} else {
b.bytes_sent as f64 / b.weight as f64
};
va.partial_cmp(&vb).unwrap_or(std::cmp::Ordering::Equal)
})
.map(|s| s.id);
let id = best?;
self.dispatch_bytes(id, quantum)
}
fn schedule_drr_single(&mut self) -> Option<(SpsStreamId, u64)> {
let quantum = self.config.quantum_bytes;
if self.drr_active.is_empty() {
for stream in self.streams.values() {
if stream.is_eligible() {
self.drr_active.push_back(DrrEntry {
stream_id: stream.id,
priority: stream.priority,
});
}
}
}
let list_len = self.drr_active.len();
for _ in 0..list_len {
let entry = self.drr_active.pop_front()?;
let stream = match self.streams.get_mut(&entry.stream_id) {
Some(s) => s,
None => continue,
};
if !stream.is_eligible() {
continue;
}
stream.deficit_counter += quantum as i64;
if stream.deficit_counter > 0 {
let send = stream.pending_bytes.min(stream.deficit_counter as u64);
stream.deficit_counter -= send as i64;
stream.pending_bytes -= send;
stream.bytes_sent += send;
stream.send_count += 1;
stream.last_scheduled_ts = self.tick;
self.stats.total_scheduled += 1;
self.stats.total_bytes += send;
*self
.stats
.priority_distribution
.entry(stream.priority)
.or_insert(0) += 1;
if stream.pending_bytes > 0 {
self.drr_active.push_back(DrrEntry {
stream_id: entry.stream_id,
priority: entry.priority,
});
} else {
stream.deficit_counter = 0;
}
return Some((entry.stream_id, send));
} else {
self.drr_active.push_back(entry);
}
}
None
}
fn schedule_edf(&mut self) -> Option<(SpsStreamId, u64)> {
let quantum = self.config.quantum_bytes;
let best = self
.streams
.values()
.filter(|s| s.is_eligible())
.min_by_key(|s| s.deadline)
.map(|s| s.id);
let id = best?;
self.dispatch_bytes(id, quantum)
}
fn schedule_htb(&mut self) -> Option<(SpsStreamId, u64)> {
let quantum = self.config.quantum_bytes;
let tokenized = self
.streams
.values()
.filter(|s| s.is_eligible() && s.htb_tokens > 0)
.max_by_key(|s| s.priority)
.map(|s| s.id);
let id = if let Some(id) = tokenized {
id
} else {
self.streams
.values()
.filter(|s| s.is_eligible())
.max_by_key(|s| s.priority)
.map(|s| s.id)?
};
if let Some(stream) = self.streams.get_mut(&id) {
let send = stream.pending_bytes.min(quantum);
stream.htb_tokens -= send as i64;
}
self.dispatch_bytes(id, quantum)
}
fn dispatch_bytes(&mut self, id: SpsStreamId, max_bytes: u64) -> Option<(SpsStreamId, u64)> {
let stream = self.streams.get_mut(&id)?;
if stream.pending_bytes == 0 {
return None;
}
let send = stream.pending_bytes.min(max_bytes);
stream.pending_bytes -= send;
stream.bytes_sent += send;
stream.send_count += 1;
stream.last_scheduled_ts = self.tick;
let priority = stream.priority;
self.stats.total_scheduled += 1;
self.stats.total_bytes += send;
*self
.stats
.priority_distribution
.entry(priority)
.or_insert(0) += 1;
if stream.pending_bytes == 0 {
if let Some(q) = self.priority_queues.get_mut(&priority) {
q.retain(|&sid| sid != id);
}
}
Some((id, send))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_scheduler() -> StreamPriorityScheduler {
StreamPriorityScheduler::new(SpsSchedulerConfig::default())
}
#[test]
fn test_new_scheduler_is_empty() {
let s = make_scheduler();
assert_eq!(s.stream_count(), 0);
assert_eq!(s.eligible_count(), 0);
}
#[test]
fn test_default_config_values() {
let cfg = SpsSchedulerConfig::default();
assert_eq!(cfg.max_streams, 1024);
assert!(cfg.quantum_bytes > 0);
assert!(cfg.deficit_rounds >= 1);
}
#[test]
fn test_add_stream_basic() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
assert_eq!(s.stream_count(), 1);
}
#[test]
fn test_add_stream_duplicate_errors() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
assert!(matches!(
s.add_stream(1, 5, 1),
Err(SpsError::DuplicateStream(1))
));
}
#[test]
fn test_add_stream_zero_weight_errors() {
let mut s = make_scheduler();
assert!(matches!(
s.add_stream(1, 10, 0),
Err(SpsError::InvalidWeight)
));
}
#[test]
fn test_add_stream_max_limit() {
let cfg = SpsSchedulerConfig {
max_streams: 2,
..Default::default()
};
let mut s = StreamPriorityScheduler::new(cfg);
s.add_stream(1, 10, 1)
.expect("test: add_stream 1 should succeed");
s.add_stream(2, 10, 1)
.expect("test: add_stream 2 should succeed");
assert!(matches!(
s.add_stream(3, 10, 1),
Err(SpsError::MaxStreamsReached(2))
));
}
#[test]
fn test_remove_existing_stream() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
let removed = s
.remove_stream(1)
.expect("test: remove_stream should succeed");
assert_eq!(removed.id, 1);
assert_eq!(s.stream_count(), 0);
}
#[test]
fn test_remove_nonexistent_stream_errors() {
let mut s = make_scheduler();
assert!(matches!(
s.remove_stream(99),
Err(SpsError::StreamNotFound(99))
));
}
#[test]
fn test_remove_stream_cleans_priority_queue() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
s.enqueue_bytes(1, 1000)
.expect("test: enqueue_bytes should succeed");
s.remove_stream(1)
.expect("test: remove_stream should succeed");
let result = s.schedule_next(&SpsSchedulingPolicy::StrictPriority);
assert!(result.is_none());
}
#[test]
fn test_block_stream() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
s.enqueue_bytes(1, 100)
.expect("test: enqueue_bytes should succeed");
s.block_stream(1)
.expect("test: block_stream should succeed");
assert!(
s.get_stream(1)
.expect("test: stream 1 should exist")
.is_blocked
);
let result = s.schedule_next(&SpsSchedulingPolicy::StrictPriority);
assert!(result.is_none());
}
#[test]
fn test_unblock_stream() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
s.enqueue_bytes(1, 100)
.expect("test: enqueue_bytes should succeed");
s.block_stream(1)
.expect("test: block_stream should succeed");
s.unblock_stream(1)
.expect("test: unblock_stream should succeed");
assert!(
!s.get_stream(1)
.expect("test: stream 1 should exist")
.is_blocked
);
s.enqueue_bytes(1, 0).unwrap_or_default(); }
#[test]
fn test_block_nonexistent_errors() {
let mut s = make_scheduler();
assert!(matches!(
s.block_stream(7),
Err(SpsError::StreamNotFound(7))
));
}
#[test]
fn test_unblock_nonexistent_errors() {
let mut s = make_scheduler();
assert!(matches!(
s.unblock_stream(7),
Err(SpsError::StreamNotFound(7))
));
}
#[test]
fn test_enqueue_bytes_increases_pending() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
s.enqueue_bytes(1, 512)
.expect("test: enqueue_bytes should succeed");
assert_eq!(
s.get_stream(1)
.expect("test: stream 1 should exist")
.pending_bytes,
512
);
}
#[test]
fn test_enqueue_bytes_accumulates() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
s.enqueue_bytes(1, 100)
.expect("test: first enqueue_bytes should succeed");
s.enqueue_bytes(1, 200)
.expect("test: second enqueue_bytes should succeed");
assert_eq!(
s.get_stream(1)
.expect("test: stream 1 should exist")
.pending_bytes,
300
);
}
#[test]
fn test_enqueue_bytes_nonexistent_errors() {
let mut s = make_scheduler();
assert!(matches!(
s.enqueue_bytes(42, 100),
Err(SpsError::StreamNotFound(42))
));
}
#[test]
fn test_strict_priority_basic() {
let mut s = make_scheduler();
s.add_stream(1, 100, 1)
.expect("test: add_stream 1 should succeed");
s.add_stream(2, 50, 1)
.expect("test: add_stream 2 should succeed");
s.enqueue_bytes(1, 1500)
.expect("test: enqueue_bytes for stream 1 should succeed");
s.enqueue_bytes(2, 1500)
.expect("test: enqueue_bytes for stream 2 should succeed");
let result = s.schedule_next(&SpsSchedulingPolicy::StrictPriority);
assert_eq!(result.map(|(id, _)| id), Some(1));
}
#[test]
fn test_strict_priority_selects_highest() {
let mut s = make_scheduler();
for pri in [10u32, 50, 200, 5] {
s.add_stream(pri as u64, pri, 1)
.expect("test: add_stream should succeed");
s.enqueue_bytes(pri as u64, 3000)
.expect("test: enqueue_bytes should succeed");
}
let (id, _) = s
.schedule_next(&SpsSchedulingPolicy::StrictPriority)
.expect("test: schedule_next should return Some");
assert_eq!(id, 200);
}
#[test]
fn test_strict_priority_no_eligible_returns_none() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
let result = s.schedule_next(&SpsSchedulingPolicy::StrictPriority);
assert!(result.is_none());
}
#[test]
fn test_strict_priority_exhausts_stream() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
s.enqueue_bytes(1, 100)
.expect("test: enqueue_bytes should succeed");
let (_, sent) = s
.schedule_next(&SpsSchedulingPolicy::StrictPriority)
.expect("test: schedule_next should return Some");
assert_eq!(sent, 100);
assert_eq!(
s.get_stream(1)
.expect("test: stream 1 should exist")
.pending_bytes,
0
);
}
#[test]
fn test_wfq_basic_scheduling() {
let mut s = make_scheduler();
s.add_stream(1, 10, 10)
.expect("test: add_stream 1 should succeed");
s.add_stream(2, 10, 1)
.expect("test: add_stream 2 should succeed");
s.enqueue_bytes(1, 5000)
.expect("test: enqueue_bytes for stream 1 should succeed");
s.enqueue_bytes(2, 5000)
.expect("test: enqueue_bytes for stream 2 should succeed");
let r = s.schedule_next(&SpsSchedulingPolicy::WeightedFairQueuing);
assert!(r.is_some());
}
#[test]
fn test_wfq_strict_threshold_override() {
let cfg = SpsSchedulerConfig {
strict_priority_threshold: 100,
..Default::default()
};
let mut s = StreamPriorityScheduler::new(cfg);
s.add_stream(1, 200, 1)
.expect("test: add_stream 1 should succeed"); s.add_stream(2, 50, 100)
.expect("test: add_stream 2 should succeed"); s.enqueue_bytes(1, 3000)
.expect("test: enqueue_bytes for stream 1 should succeed");
s.enqueue_bytes(2, 3000)
.expect("test: enqueue_bytes for stream 2 should succeed");
let (id, _) = s
.schedule_next(&SpsSchedulingPolicy::WeightedFairQueuing)
.expect("test: schedule_next should return Some");
assert_eq!(id, 1); }
#[test]
fn test_wfq_proportional_allocation() {
let mut s = make_scheduler();
s.add_stream(1, 10, 2)
.expect("test: add_stream 1 should succeed");
s.add_stream(2, 10, 1)
.expect("test: add_stream 2 should succeed");
s.enqueue_bytes(1, 100_000)
.expect("test: enqueue_bytes for stream 1 should succeed");
s.enqueue_bytes(2, 100_000)
.expect("test: enqueue_bytes for stream 2 should succeed");
let batch = s.schedule_batch(&SpsSchedulingPolicy::WeightedFairQueuing, 1000);
assert!(!batch.is_empty());
}
#[test]
fn test_drr_single_step() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
s.enqueue_bytes(1, 3000)
.expect("test: enqueue_bytes should succeed");
let r = s.schedule_next(&SpsSchedulingPolicy::DeficitRoundRobin);
assert!(r.is_some());
}
#[test]
fn test_drr_round_basic() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream 1 should succeed");
s.add_stream(2, 10, 1)
.expect("test: add_stream 2 should succeed");
s.enqueue_bytes(1, 3000)
.expect("test: enqueue_bytes for stream 1 should succeed");
s.enqueue_bytes(2, 3000)
.expect("test: enqueue_bytes for stream 2 should succeed");
let results = s.run_drr_round();
assert!(!results.is_empty());
}
#[test]
fn test_drr_multiple_rounds() {
let cfg = SpsSchedulerConfig {
quantum_bytes: 500,
deficit_rounds: 3,
..Default::default()
};
let mut s = StreamPriorityScheduler::new(cfg);
s.add_stream(1, 10, 1)
.expect("test: add_stream 1 should succeed");
s.add_stream(2, 10, 1)
.expect("test: add_stream 2 should succeed");
s.enqueue_bytes(1, 10000)
.expect("test: enqueue_bytes for stream 1 should succeed");
s.enqueue_bytes(2, 10000)
.expect("test: enqueue_bytes for stream 2 should succeed");
let results = s.run_drr_round();
assert!(!results.is_empty());
}
#[test]
fn test_drr_drain_eventually() {
let cfg = SpsSchedulerConfig {
quantum_bytes: 100,
deficit_rounds: 1,
..Default::default()
};
let mut s = StreamPriorityScheduler::new(cfg);
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
s.enqueue_bytes(1, 300)
.expect("test: enqueue_bytes should succeed");
let mut total = 0u64;
for _ in 0..20 {
for (_, b) in s.run_drr_round() {
total += b;
}
}
assert_eq!(total, 300);
}
#[test]
fn test_edf_selects_smallest_deadline() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream 1 should succeed");
s.add_stream(2, 10, 1)
.expect("test: add_stream 2 should succeed");
s.set_deadline(1, 1000)
.expect("test: set_deadline for stream 1 should succeed");
s.set_deadline(2, 500)
.expect("test: set_deadline for stream 2 should succeed");
s.enqueue_bytes(1, 1500)
.expect("test: enqueue_bytes for stream 1 should succeed");
s.enqueue_bytes(2, 1500)
.expect("test: enqueue_bytes for stream 2 should succeed");
let (id, _) = s
.schedule_next(&SpsSchedulingPolicy::EarliestDeadlineFirst)
.expect("test: schedule_next should return Some");
assert_eq!(id, 2); }
#[test]
fn test_edf_default_deadline_is_max() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
s.enqueue_bytes(1, 100)
.expect("test: enqueue_bytes should succeed");
let r = s.schedule_next(&SpsSchedulingPolicy::EarliestDeadlineFirst);
assert!(r.is_some());
}
#[test]
fn test_set_deadline_nonexistent_errors() {
let mut s = make_scheduler();
assert!(matches!(
s.set_deadline(99, 100),
Err(SpsError::StreamNotFound(99))
));
}
#[test]
fn test_htb_requires_refill() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
s.set_htb_params(1, 1000, 10000)
.expect("test: set_htb_params should succeed");
s.enqueue_bytes(1, 5000)
.expect("test: enqueue_bytes should succeed");
let r = s.schedule_next(&SpsSchedulingPolicy::HierarchicalToken);
assert!(r.is_some());
}
#[test]
fn test_htb_with_refill() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
s.set_htb_params(1, 1000, 10000)
.expect("test: set_htb_params should succeed");
s.htb_refill(5); s.enqueue_bytes(1, 5000)
.expect("test: enqueue_bytes should succeed");
let (id, _) = s
.schedule_next(&SpsSchedulingPolicy::HierarchicalToken)
.expect("test: schedule_next should return Some");
assert_eq!(id, 1);
}
#[test]
fn test_set_htb_params_nonexistent_errors() {
let mut s = make_scheduler();
assert!(matches!(
s.set_htb_params(99, 1000, 2000),
Err(SpsError::StreamNotFound(99))
));
}
#[test]
fn test_schedule_batch_returns_up_to_n() {
let mut s = make_scheduler();
for i in 1u64..=5 {
s.add_stream(i, 10, 1)
.expect("test: add_stream should succeed");
s.enqueue_bytes(i, 500)
.expect("test: enqueue_bytes should succeed");
}
let batch = s.schedule_batch(&SpsSchedulingPolicy::StrictPriority, 3);
assert!(batch.len() <= 3);
}
#[test]
fn test_schedule_batch_stops_when_empty() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
s.enqueue_bytes(1, 100)
.expect("test: enqueue_bytes should succeed");
let batch = s.schedule_batch(&SpsSchedulingPolicy::StrictPriority, 100);
assert!(!batch.is_empty());
let total: u64 = batch.iter().map(|(_, b)| b).sum();
assert_eq!(total, 100);
}
#[test]
fn test_schedule_batch_zero_n() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
s.enqueue_bytes(1, 100)
.expect("test: enqueue_bytes should succeed");
let batch = s.schedule_batch(&SpsSchedulingPolicy::StrictPriority, 0);
assert!(batch.is_empty());
}
#[test]
fn test_fairness_empty_scheduler() {
let s = make_scheduler();
assert!((s.compute_fairness() - 1.0).abs() < 1e-9);
}
#[test]
fn test_fairness_single_stream() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
assert!((s.compute_fairness() - 1.0).abs() < 1e-9);
}
#[test]
fn test_fairness_two_equal_streams() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream 1 should succeed");
s.add_stream(2, 10, 1)
.expect("test: add_stream 2 should succeed");
s.enqueue_bytes(1, 10000)
.expect("test: enqueue_bytes for stream 1 should succeed");
s.enqueue_bytes(2, 10000)
.expect("test: enqueue_bytes for stream 2 should succeed");
s.schedule_batch(&SpsSchedulingPolicy::WeightedFairQueuing, 200);
let f = s.compute_fairness();
assert!(f > 0.0 && f <= 1.0);
}
#[test]
fn test_fairness_bounds() {
let mut s = make_scheduler();
for i in 1u64..=4 {
s.add_stream(i, 10, 1)
.expect("test: add_stream should succeed");
s.enqueue_bytes(i, 5000)
.expect("test: enqueue_bytes should succeed");
}
s.schedule_batch(&SpsSchedulingPolicy::WeightedFairQueuing, 200);
let f = s.compute_fairness();
assert!((0.0..=1.0 + 1e-9).contains(&f));
}
#[test]
fn test_stats_initial_zeros() {
let mut s = make_scheduler();
let stats = s.scheduler_stats();
assert_eq!(stats.total_scheduled, 0);
assert_eq!(stats.total_bytes, 0);
assert_eq!(stats.active_streams, 0);
}
#[test]
fn test_stats_after_scheduling() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
s.enqueue_bytes(1, 1500)
.expect("test: enqueue_bytes should succeed");
s.schedule_next(&SpsSchedulingPolicy::StrictPriority);
let stats = s.scheduler_stats();
assert!(stats.total_scheduled > 0);
assert!(stats.total_bytes > 0);
}
#[test]
fn test_stats_priority_distribution() {
let mut s = make_scheduler();
s.add_stream(1, 42, 1)
.expect("test: add_stream should succeed");
s.enqueue_bytes(1, 1500)
.expect("test: enqueue_bytes should succeed");
s.schedule_next(&SpsSchedulingPolicy::StrictPriority);
let stats = s.scheduler_stats();
assert!(stats.priority_distribution.contains_key(&42));
}
#[test]
fn test_stats_active_and_blocked() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream 1 should succeed");
s.add_stream(2, 10, 1)
.expect("test: add_stream 2 should succeed");
s.block_stream(1)
.expect("test: block_stream should succeed");
let stats = s.scheduler_stats();
assert_eq!(stats.active_streams, 2);
assert_eq!(stats.blocked_streams, 1);
}
#[test]
fn test_drain_stream() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
s.enqueue_bytes(1, 5000)
.expect("test: enqueue_bytes should succeed");
let drained = s
.drain_stream(1)
.expect("test: drain_stream should succeed");
assert_eq!(drained, 5000);
assert_eq!(
s.get_stream(1)
.expect("test: stream 1 should exist")
.pending_bytes,
0
);
}
#[test]
fn test_drain_nonexistent_errors() {
let mut s = make_scheduler();
assert!(matches!(
s.drain_stream(99),
Err(SpsError::StreamNotFound(99))
));
}
#[test]
fn test_update_priority() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
s.enqueue_bytes(1, 500)
.expect("test: enqueue_bytes should succeed");
s.update_priority(1, 200)
.expect("test: update_priority should succeed");
assert_eq!(
s.get_stream(1)
.expect("test: stream 1 should exist")
.priority,
200
);
}
#[test]
fn test_update_priority_nonexistent_errors() {
let mut s = make_scheduler();
assert!(matches!(
s.update_priority(99, 100),
Err(SpsError::StreamNotFound(99))
));
}
#[test]
fn test_update_priority_affects_scheduling() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream 1 should succeed");
s.add_stream(2, 10, 1)
.expect("test: add_stream 2 should succeed");
s.enqueue_bytes(1, 5000)
.expect("test: enqueue_bytes for stream 1 should succeed");
s.enqueue_bytes(2, 5000)
.expect("test: enqueue_bytes for stream 2 should succeed");
s.update_priority(2, 200)
.expect("test: update_priority should succeed");
s.enqueue_bytes(2, 0).unwrap_or_default();
let (id, _) = s
.schedule_next(&SpsSchedulingPolicy::StrictPriority)
.expect("test: schedule_next should return Some");
assert_eq!(id, 2);
}
#[test]
fn test_get_stream_existing() {
let mut s = make_scheduler();
s.add_stream(5, 10, 2)
.expect("test: add_stream should succeed");
let stream = s.get_stream(5);
assert!(stream.is_some());
assert_eq!(stream.expect("test: stream 5 should exist").id, 5);
}
#[test]
fn test_get_stream_nonexistent() {
let s = make_scheduler();
assert!(s.get_stream(99).is_none());
}
#[test]
fn test_get_stream_mut() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
let stream = s
.get_stream_mut(1)
.expect("test: stream 1 should exist for mutable access");
stream.weight = 99;
assert_eq!(
s.get_stream(1).expect("test: stream 1 should exist").weight,
99
);
}
#[test]
fn test_xorshift64_non_zero() {
let mut state = 0xdeadbeef_u64;
let v = xorshift64(&mut state);
assert_ne!(v, 0);
}
#[test]
fn test_xorshift64_different_on_repeated_calls() {
let mut state = 12345678u64;
let a = xorshift64(&mut state);
let b = xorshift64(&mut state);
assert_ne!(a, b);
}
#[test]
fn test_eligible_count() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream 1 should succeed");
s.add_stream(2, 10, 1)
.expect("test: add_stream 2 should succeed");
s.enqueue_bytes(1, 100)
.expect("test: enqueue_bytes should succeed");
assert_eq!(s.eligible_count(), 1);
}
#[test]
fn test_tick_increments() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
s.enqueue_bytes(1, 3000)
.expect("test: enqueue_bytes should succeed");
let t0 = s.tick();
s.schedule_next(&SpsSchedulingPolicy::StrictPriority);
assert!(s.tick() > t0);
}
#[test]
fn test_bytes_sent_accumulates() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
s.enqueue_bytes(1, 3000)
.expect("test: enqueue_bytes should succeed");
s.schedule_next(&SpsSchedulingPolicy::StrictPriority);
assert!(
s.get_stream(1)
.expect("test: stream 1 should exist")
.bytes_sent
> 0
);
}
#[test]
fn test_send_count_increments() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
s.enqueue_bytes(1, 1500)
.expect("test: enqueue_bytes should succeed");
s.schedule_next(&SpsSchedulingPolicy::StrictPriority);
assert_eq!(
s.get_stream(1)
.expect("test: stream 1 should exist")
.send_count,
1
);
}
#[test]
fn test_multiple_policies_same_scheduler() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream 1 should succeed");
s.add_stream(2, 20, 2)
.expect("test: add_stream 2 should succeed");
s.enqueue_bytes(1, 5000)
.expect("test: enqueue_bytes for stream 1 should succeed");
s.enqueue_bytes(2, 5000)
.expect("test: enqueue_bytes for stream 2 should succeed");
let r1 = s.schedule_next(&SpsSchedulingPolicy::StrictPriority);
let r2 = s.schedule_next(&SpsSchedulingPolicy::WeightedFairQueuing);
let r3 = s.schedule_next(&SpsSchedulingPolicy::EarliestDeadlineFirst);
assert!(r1.is_some() || r2.is_some() || r3.is_some());
}
#[test]
fn test_large_enqueue_no_overflow() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
s.enqueue_bytes(1, u64::MAX / 2)
.expect("test: enqueue_bytes should succeed");
s.enqueue_bytes(1, 1).unwrap_or_default(); }
#[test]
fn test_streams_removed_counter() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
s.remove_stream(1)
.expect("test: remove_stream should succeed");
let stats = s.scheduler_stats();
assert_eq!(stats.streams_removed, 1);
}
#[test]
fn test_htb_refill_clamps_to_burst() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
s.set_htb_params(1, 1000, 5000)
.expect("test: set_htb_params should succeed");
s.htb_refill(1000); let tok = s
.get_stream(1)
.expect("test: stream 1 should exist")
.htb_tokens;
assert_eq!(tok, 5000);
}
#[test]
fn test_schedule_all_policies_no_eligible_returns_none() {
let mut s = make_scheduler();
let policies = [
SpsSchedulingPolicy::StrictPriority,
SpsSchedulingPolicy::WeightedFairQueuing,
SpsSchedulingPolicy::DeficitRoundRobin,
SpsSchedulingPolicy::EarliestDeadlineFirst,
SpsSchedulingPolicy::HierarchicalToken,
];
for policy in &policies {
assert!(
s.schedule_next(policy).is_none(),
"policy {:?} should return None",
policy
);
}
}
#[test]
fn test_edf_multiple_streams_ordering() {
let mut s = make_scheduler();
let deadlines = [(1u64, 300u64), (2, 100), (3, 200), (4, 50)];
for (id, dl) in &deadlines {
s.add_stream(*id, 10, 1)
.expect("test: add_stream should succeed");
s.set_deadline(*id, *dl)
.expect("test: set_deadline should succeed");
s.enqueue_bytes(*id, 1500)
.expect("test: enqueue_bytes should succeed");
}
let (first, _) = s
.schedule_next(&SpsSchedulingPolicy::EarliestDeadlineFirst)
.expect("test: schedule_next should return Some");
assert_eq!(first, 4); }
#[test]
fn test_wfq_no_eligible_returns_none() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed"); let r = s.schedule_next(&SpsSchedulingPolicy::WeightedFairQueuing);
assert!(r.is_none());
}
#[test]
fn test_drr_empty_active_list_rebuilt() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
s.enqueue_bytes(1, 500)
.expect("test: enqueue_bytes should succeed");
let r1 = s.schedule_next(&SpsSchedulingPolicy::DeficitRoundRobin);
assert!(r1.is_some());
}
#[test]
fn test_fairness_index_in_stats() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream 1 should succeed");
s.add_stream(2, 10, 1)
.expect("test: add_stream 2 should succeed");
s.enqueue_bytes(1, 5000)
.expect("test: enqueue_bytes for stream 1 should succeed");
s.enqueue_bytes(2, 5000)
.expect("test: enqueue_bytes for stream 2 should succeed");
s.schedule_batch(&SpsSchedulingPolicy::WeightedFairQueuing, 50);
let stats = s.scheduler_stats();
assert!((0.0..=1.0 + 1e-9).contains(&stats.fairness_index));
}
#[test]
fn test_update_priority_no_pending_no_queue_change() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
s.update_priority(1, 50)
.expect("test: update_priority should succeed");
assert_eq!(
s.get_stream(1)
.expect("test: stream 1 should exist")
.priority,
50
);
}
#[test]
fn test_scheduler_stats_avg_wait_zero_before_any_schedule() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
let stats = s.scheduler_stats();
assert_eq!(stats.avg_wait, 0.0);
}
#[test]
fn test_drr_run_round_with_no_streams() {
let mut s = make_scheduler();
let results = s.run_drr_round();
assert!(results.is_empty());
}
#[test]
fn test_blocked_stream_not_scheduled() {
let mut s = make_scheduler();
s.add_stream(1, 100, 1)
.expect("test: add_stream 1 should succeed");
s.add_stream(2, 10, 1)
.expect("test: add_stream 2 should succeed");
s.enqueue_bytes(1, 5000)
.expect("test: enqueue_bytes for stream 1 should succeed");
s.enqueue_bytes(2, 5000)
.expect("test: enqueue_bytes for stream 2 should succeed");
s.block_stream(1)
.expect("test: block_stream should succeed");
let (id, _) = s
.schedule_next(&SpsSchedulingPolicy::StrictPriority)
.expect("test: schedule_next should return Some");
assert_eq!(id, 2); }
#[test]
fn test_stream_is_eligible_after_enqueue() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
assert!(!s
.get_stream(1)
.expect("test: stream 1 should exist")
.is_eligible());
s.enqueue_bytes(1, 100)
.expect("test: enqueue_bytes should succeed");
assert!(s
.get_stream(1)
.expect("test: stream 1 should exist")
.is_eligible());
}
#[test]
fn test_stream_not_eligible_when_blocked_even_with_data() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
s.enqueue_bytes(1, 100)
.expect("test: enqueue_bytes should succeed");
s.block_stream(1)
.expect("test: block_stream should succeed");
assert!(!s
.get_stream(1)
.expect("test: stream 1 should exist")
.is_eligible());
}
#[test]
fn test_sps_stream_new_defaults() {
let stream = SpsStream::new(42, 100, 5);
assert_eq!(stream.id, 42);
assert_eq!(stream.priority, 100);
assert_eq!(stream.weight, 5);
assert_eq!(stream.pending_bytes, 0);
assert!(!stream.is_blocked);
}
#[test]
fn test_sps_error_display() {
let e = SpsError::StreamNotFound(5);
assert!(e.to_string().contains("5"));
}
#[test]
fn test_add_multiple_streams_different_priorities() {
let mut s = make_scheduler();
for i in 0u64..10 {
s.add_stream(i, (i * 10) as u32, 1)
.expect("test: add_stream should succeed");
}
assert_eq!(s.stream_count(), 10);
}
#[test]
fn test_remove_and_readd_same_id() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream should succeed");
s.remove_stream(1)
.expect("test: first remove_stream should succeed");
s.add_stream(1, 20, 2)
.expect("test: re-add_stream should succeed");
assert_eq!(
s.get_stream(1)
.expect("test: stream 1 should exist after re-add")
.priority,
20
);
}
#[test]
fn test_htb_fallback_with_no_tokens() {
let mut s = make_scheduler();
s.add_stream(1, 10, 1)
.expect("test: add_stream 1 should succeed");
s.add_stream(2, 20, 1)
.expect("test: add_stream 2 should succeed");
s.enqueue_bytes(1, 5000)
.expect("test: enqueue_bytes for stream 1 should succeed");
s.enqueue_bytes(2, 5000)
.expect("test: enqueue_bytes for stream 2 should succeed");
let (id, _) = s
.schedule_next(&SpsSchedulingPolicy::HierarchicalToken)
.expect("test: schedule_next should return Some");
assert_eq!(id, 2); }
}