Skip to main content

ipfrs_network/
stream_priority_scheduler.rs

1//! Stream-level priority scheduler for multiplexed network streams.
2//!
3//! Implements multiple scheduling disciplines:
4//! - Strict Priority (SP): highest-priority streams always scheduled first
5//! - Weighted Fair Queuing (WFQ): proportional share based on weight
6//! - Deficit Round Robin (DRR): byte-accurate fairness with deficit counters
7//! - Earliest Deadline First (EDF): deadline-aware scheduling for latency-sensitive flows
8//! - Hierarchical Token Bucket (HTB): hierarchical bandwidth allocation with token buckets
9//!
10//! All methods are `no_std`-friendly (no external crates beyond what's already in Cargo.toml).
11
12use std::collections::{BTreeMap, HashMap, VecDeque};
13
14// ---------------------------------------------------------------------------
15// Type aliases
16// ---------------------------------------------------------------------------
17
18/// Unique identifier for a scheduled stream.
19pub type SpsStreamId = u64;
20
21// ---------------------------------------------------------------------------
22// Inline PRNG helper (xorshift64, no external crate)
23// ---------------------------------------------------------------------------
24
25#[inline]
26pub fn xorshift64(state: &mut u64) -> u64 {
27    let mut x = *state;
28    x ^= x << 13;
29    x ^= x >> 7;
30    x ^= x << 17;
31    *state = x;
32    x
33}
34
35// ---------------------------------------------------------------------------
36// Scheduling policy
37// ---------------------------------------------------------------------------
38
39/// Scheduling algorithm used by [`StreamPriorityScheduler`].
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum SpsSchedulingPolicy {
42    /// Always schedule the highest-priority non-blocked stream first.
43    StrictPriority,
44    /// Proportional share scheduling based on stream weight.
45    WeightedFairQueuing,
46    /// Deficit Round Robin – byte-accurate fairness using a per-stream deficit counter.
47    DeficitRoundRobin,
48    /// Earliest Deadline First – streams with the smallest deadline value are scheduled first.
49    EarliestDeadlineFirst,
50    /// Hierarchical Token Bucket – hierarchical bandwidth allocation with configurable rates.
51    HierarchicalToken,
52}
53
54// ---------------------------------------------------------------------------
55// Scheduler configuration
56// ---------------------------------------------------------------------------
57
58/// Configuration for [`StreamPriorityScheduler`].
59#[derive(Debug, Clone)]
60pub struct SpsSchedulerConfig {
61    /// Maximum number of concurrent streams allowed.
62    pub max_streams: usize,
63    /// Number of bytes granted to each stream per DRR quantum.
64    pub quantum_bytes: u64,
65    /// Priority value at or above which strict priority scheduling is applied
66    /// (streams with `priority >= strict_priority_threshold` bypass WFQ/DRR).
67    pub strict_priority_threshold: u32,
68    /// Number of DRR rounds to run in a single [`StreamPriorityScheduler::run_drr_round`] call.
69    pub deficit_rounds: u32,
70}
71
72impl Default for SpsSchedulerConfig {
73    fn default() -> Self {
74        Self {
75            max_streams: 1024,
76            quantum_bytes: 1500,
77            strict_priority_threshold: 240,
78            deficit_rounds: 1,
79        }
80    }
81}
82
83// ---------------------------------------------------------------------------
84// Per-stream state
85// ---------------------------------------------------------------------------
86
87/// State tracked for a single multiplexed stream.
88#[derive(Debug, Clone)]
89pub struct SpsStream {
90    /// Unique stream identifier.
91    pub id: SpsStreamId,
92    /// Scheduling priority (higher = more important; 255 is highest).
93    pub priority: u32,
94    /// Relative weight used for proportional-share policies.
95    pub weight: u32,
96    /// Number of bytes waiting to be sent.
97    pub pending_bytes: u64,
98    /// DRR deficit counter (may be negative after a partial send).
99    pub deficit_counter: i64,
100    /// Total number of scheduling events for this stream.
101    pub send_count: u64,
102    /// Total bytes transmitted so far.
103    pub bytes_sent: u64,
104    /// Timestamp (monotonic, arbitrary epoch) of the last scheduling event.
105    pub last_scheduled_ts: u64,
106    /// If `true` the stream is temporarily suspended and will not be scheduled.
107    pub is_blocked: bool,
108    /// Optional deadline value (lower = more urgent; used by EDF policy).
109    pub deadline: u64,
110    /// Token bucket for HTB policy: current token balance in bytes.
111    pub htb_tokens: i64,
112    /// Token refill rate in bytes per "tick" for HTB.
113    pub htb_rate: u64,
114    /// Maximum burst size in bytes for HTB.
115    pub htb_burst: u64,
116}
117
118impl SpsStream {
119    /// Create a new stream with the given parameters.
120    pub fn new(id: SpsStreamId, priority: u32, weight: u32) -> Self {
121        Self {
122            id,
123            priority,
124            weight: weight.max(1),
125            pending_bytes: 0,
126            deficit_counter: 0,
127            send_count: 0,
128            bytes_sent: 0,
129            last_scheduled_ts: 0,
130            is_blocked: false,
131            deadline: u64::MAX,
132            htb_tokens: 0,
133            htb_rate: 1500,
134            htb_burst: 65535,
135        }
136    }
137
138    /// Returns `true` if the stream has data to send and is not blocked.
139    #[inline]
140    pub fn is_eligible(&self) -> bool {
141        !self.is_blocked && self.pending_bytes > 0
142    }
143}
144
145// ---------------------------------------------------------------------------
146// Scheduler statistics
147// ---------------------------------------------------------------------------
148
149/// Aggregate scheduling statistics.
150#[derive(Debug, Clone, Default)]
151pub struct SpsSchedulerStats {
152    /// Total number of scheduling decisions made.
153    pub total_scheduled: u64,
154    /// Total bytes dispatched across all streams.
155    pub total_bytes: u64,
156    /// Per-priority-level count of scheduling events.
157    pub priority_distribution: HashMap<u32, u64>,
158    /// Average wait (in ticks/rounds) across streams.
159    pub avg_wait: f64,
160    /// Number of streams currently registered.
161    pub active_streams: usize,
162    /// Number of blocked streams.
163    pub blocked_streams: usize,
164    /// Number of streams that have been removed since creation.
165    pub streams_removed: u64,
166    /// Jain's fairness index over bytes_sent (computed lazily).
167    pub fairness_index: f64,
168}
169
170// ---------------------------------------------------------------------------
171// Internal DRR active list entry
172// ---------------------------------------------------------------------------
173
174/// Lightweight handle used in the DRR active list.
175#[derive(Debug, Clone)]
176struct DrrEntry {
177    stream_id: SpsStreamId,
178    priority: u32,
179}
180
181// ---------------------------------------------------------------------------
182// Main scheduler
183// ---------------------------------------------------------------------------
184
185/// A stream-level priority scheduler for multiplexed network streams.
186///
187/// # Scheduling Policies
188///
189/// Use [`SpsSchedulingPolicy`] to select the algorithm per call or batch.
190///
191/// # Example
192/// ```rust
193/// use ipfrs_network::stream_priority_scheduler::{
194///     StreamPriorityScheduler, SpsSchedulingPolicy, SpsSchedulerConfig,
195/// };
196///
197/// let mut scheduler = StreamPriorityScheduler::new(SpsSchedulerConfig::default());
198/// scheduler.add_stream(1, 100, 10).unwrap();
199/// scheduler.add_stream(2, 50,  5).unwrap();
200/// scheduler.enqueue_bytes(1, 4096).unwrap();
201/// scheduler.enqueue_bytes(2, 2048).unwrap();
202///
203/// let result = scheduler.schedule_next(&SpsSchedulingPolicy::StrictPriority);
204/// assert_eq!(result.map(|(id, _)| id), Some(1));
205/// ```
206pub struct StreamPriorityScheduler {
207    /// All registered streams keyed by `SpsStreamId`.
208    streams: HashMap<SpsStreamId, SpsStream>,
209    /// Priority queues: priority value → FIFO queue of stream IDs at that level.
210    priority_queues: BTreeMap<u32, VecDeque<SpsStreamId>>,
211    /// Scheduler configuration.
212    config: SpsSchedulerConfig,
213    /// Aggregate statistics.
214    stats: SpsSchedulerStats,
215    /// Monotonic tick counter incremented on each [`schedule_next`] call.
216    tick: u64,
217    /// DRR active-list maintained across rounds.
218    drr_active: VecDeque<DrrEntry>,
219    /// PRNG state (xorshift64) used for tie-breaking.
220    rng_state: u64,
221}
222
223// ---------------------------------------------------------------------------
224// Error type
225// ---------------------------------------------------------------------------
226
227/// Errors returned by [`StreamPriorityScheduler`] operations.
228#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
229pub enum SpsError {
230    #[error("stream {0} not found")]
231    StreamNotFound(SpsStreamId),
232    #[error("maximum stream limit ({0}) reached")]
233    MaxStreamsReached(usize),
234    #[error("stream {0} already registered")]
235    DuplicateStream(SpsStreamId),
236    #[error("weight must be >= 1")]
237    InvalidWeight,
238    #[error("batch size must be > 0")]
239    InvalidBatchSize,
240}
241
242// ---------------------------------------------------------------------------
243// Implementation
244// ---------------------------------------------------------------------------
245
246impl StreamPriorityScheduler {
247    /// Create a new scheduler with the supplied configuration.
248    pub fn new(config: SpsSchedulerConfig) -> Self {
249        Self {
250            streams: HashMap::new(),
251            priority_queues: BTreeMap::new(),
252            config,
253            stats: SpsSchedulerStats::default(),
254            tick: 0,
255            drr_active: VecDeque::new(),
256            rng_state: 0xdeadbeef_cafebabe,
257        }
258    }
259
260    // -----------------------------------------------------------------------
261    // Stream lifecycle
262    // -----------------------------------------------------------------------
263
264    /// Register a new stream.
265    ///
266    /// # Errors
267    /// Returns [`SpsError::MaxStreamsReached`] if `config.max_streams` is exceeded,
268    /// or [`SpsError::DuplicateStream`] if the id is already registered.
269    pub fn add_stream(
270        &mut self,
271        id: SpsStreamId,
272        priority: u32,
273        weight: u32,
274    ) -> Result<(), SpsError> {
275        if self.streams.len() >= self.config.max_streams {
276            return Err(SpsError::MaxStreamsReached(self.config.max_streams));
277        }
278        if self.streams.contains_key(&id) {
279            return Err(SpsError::DuplicateStream(id));
280        }
281        if weight == 0 {
282            return Err(SpsError::InvalidWeight);
283        }
284        let stream = SpsStream::new(id, priority, weight);
285        self.streams.insert(id, stream);
286        self.stats.active_streams = self.streams.len();
287        Ok(())
288    }
289
290    /// Remove a stream and all its pending data from the scheduler.
291    ///
292    /// # Errors
293    /// Returns [`SpsError::StreamNotFound`] if the id is not registered.
294    pub fn remove_stream(&mut self, id: SpsStreamId) -> Result<SpsStream, SpsError> {
295        let stream = self
296            .streams
297            .remove(&id)
298            .ok_or(SpsError::StreamNotFound(id))?;
299
300        // Remove from every priority queue it may occupy.
301        for queue in self.priority_queues.values_mut() {
302            queue.retain(|&sid| sid != id);
303        }
304        // Remove from DRR active list.
305        self.drr_active.retain(|e| e.stream_id != id);
306
307        // Prune empty priority queues.
308        self.priority_queues.retain(|_, q| !q.is_empty());
309
310        self.stats.active_streams = self.streams.len();
311        self.stats.streams_removed += 1;
312        Ok(stream)
313    }
314
315    /// Block a stream — it will be skipped during scheduling.
316    ///
317    /// # Errors
318    /// Returns [`SpsError::StreamNotFound`] if the id is not registered.
319    pub fn block_stream(&mut self, id: SpsStreamId) -> Result<(), SpsError> {
320        let stream = self
321            .streams
322            .get_mut(&id)
323            .ok_or(SpsError::StreamNotFound(id))?;
324        stream.is_blocked = true;
325        self.stats.blocked_streams = self.streams.values().filter(|s| s.is_blocked).count();
326        Ok(())
327    }
328
329    /// Unblock a previously blocked stream.
330    ///
331    /// # Errors
332    /// Returns [`SpsError::StreamNotFound`] if the id is not registered.
333    pub fn unblock_stream(&mut self, id: SpsStreamId) -> Result<(), SpsError> {
334        let stream = self
335            .streams
336            .get_mut(&id)
337            .ok_or(SpsError::StreamNotFound(id))?;
338        stream.is_blocked = false;
339        self.stats.blocked_streams = self.streams.values().filter(|s| s.is_blocked).count();
340        Ok(())
341    }
342
343    // -----------------------------------------------------------------------
344    // Data enqueue
345    // -----------------------------------------------------------------------
346
347    /// Add `bytes` of pending data to a stream's queue.
348    ///
349    /// Also inserts the stream into the appropriate priority queue if it was
350    /// previously empty.
351    ///
352    /// # Errors
353    /// Returns [`SpsError::StreamNotFound`] if the id is not registered.
354    pub fn enqueue_bytes(&mut self, stream_id: SpsStreamId, bytes: u64) -> Result<(), SpsError> {
355        let stream = self
356            .streams
357            .get_mut(&stream_id)
358            .ok_or(SpsError::StreamNotFound(stream_id))?;
359
360        let was_empty = stream.pending_bytes == 0;
361        stream.pending_bytes = stream.pending_bytes.saturating_add(bytes);
362
363        if was_empty && !stream.is_blocked {
364            let priority = stream.priority;
365            self.priority_queues
366                .entry(priority)
367                .or_default()
368                .push_back(stream_id);
369        }
370        Ok(())
371    }
372
373    // -----------------------------------------------------------------------
374    // Core scheduling
375    // -----------------------------------------------------------------------
376
377    /// Schedule the next stream according to `policy`.
378    ///
379    /// Returns `Some((stream_id, bytes_to_send))` where `bytes_to_send` is the
380    /// number of bytes that should be dispatched this round (bounded by
381    /// `config.quantum_bytes` for non-strict policies).
382    ///
383    /// Returns `None` if there are no eligible streams.
384    pub fn schedule_next(&mut self, policy: &SpsSchedulingPolicy) -> Option<(SpsStreamId, u64)> {
385        self.tick += 1;
386        match policy {
387            SpsSchedulingPolicy::StrictPriority => self.schedule_strict(),
388            SpsSchedulingPolicy::WeightedFairQueuing => self.schedule_wfq(),
389            SpsSchedulingPolicy::DeficitRoundRobin => self.schedule_drr_single(),
390            SpsSchedulingPolicy::EarliestDeadlineFirst => self.schedule_edf(),
391            SpsSchedulingPolicy::HierarchicalToken => self.schedule_htb(),
392        }
393    }
394
395    /// Schedule a batch of up to `n` streams.
396    ///
397    /// Each element of the returned `Vec` is `(stream_id, bytes_to_send)`.
398    ///
399    /// # Errors (returned via empty vec)
400    /// If `n == 0` the returned vec is empty.
401    pub fn schedule_batch(
402        &mut self,
403        policy: &SpsSchedulingPolicy,
404        n: usize,
405    ) -> Vec<(SpsStreamId, u64)> {
406        let mut results = Vec::with_capacity(n);
407        for _ in 0..n {
408            match self.schedule_next(policy) {
409                Some(item) => results.push(item),
410                None => break,
411            }
412        }
413        results
414    }
415
416    // -----------------------------------------------------------------------
417    // DRR round
418    // -----------------------------------------------------------------------
419
420    /// Run one or more complete Deficit Round Robin rounds.
421    ///
422    /// Each eligible stream receives a quantum of `config.quantum_bytes` added
423    /// to its deficit counter. Streams transmit until their deficit is
424    /// exhausted. The number of rounds is controlled by `config.deficit_rounds`.
425    ///
426    /// Returns a list of `(stream_id, bytes_to_send)` decisions from this round.
427    pub fn run_drr_round(&mut self) -> Vec<(SpsStreamId, u64)> {
428        let rounds = self.config.deficit_rounds as usize;
429        let quantum = self.config.quantum_bytes;
430        let mut results = Vec::new();
431
432        for _ in 0..rounds {
433            // Rebuild active list from eligible streams if empty.
434            if self.drr_active.is_empty() {
435                for stream in self.streams.values() {
436                    if stream.is_eligible() {
437                        self.drr_active.push_back(DrrEntry {
438                            stream_id: stream.id,
439                            priority: stream.priority,
440                        });
441                    }
442                }
443            }
444
445            let mut processed = VecDeque::new();
446            let len = self.drr_active.len();
447
448            for _ in 0..len {
449                let entry = match self.drr_active.pop_front() {
450                    Some(e) => e,
451                    None => break,
452                };
453
454                let stream = match self.streams.get_mut(&entry.stream_id) {
455                    Some(s) => s,
456                    None => continue,
457                };
458
459                if !stream.is_eligible() {
460                    // Stream became empty or blocked; skip and do not re-add.
461                    continue;
462                }
463
464                // Add quantum to deficit.
465                stream.deficit_counter += quantum as i64;
466
467                // Drain as much pending data as deficit allows.
468                while stream.pending_bytes > 0 && stream.deficit_counter > 0 {
469                    let send = stream.pending_bytes.min(stream.deficit_counter as u64);
470                    stream.deficit_counter -= send as i64;
471                    stream.pending_bytes -= send;
472                    stream.bytes_sent += send;
473                    stream.send_count += 1;
474                    stream.last_scheduled_ts = self.tick;
475                    self.tick += 1;
476                    self.stats.total_scheduled += 1;
477                    self.stats.total_bytes += send;
478                    *self
479                        .stats
480                        .priority_distribution
481                        .entry(stream.priority)
482                        .or_insert(0) += 1;
483                    results.push((entry.stream_id, send));
484                }
485
486                // If still has data, re-enqueue.
487                if stream.pending_bytes > 0 {
488                    processed.push_back(DrrEntry {
489                        stream_id: entry.stream_id,
490                        priority: entry.priority,
491                    });
492                } else {
493                    // Reset deficit when queue empties.
494                    stream.deficit_counter = 0;
495                }
496            }
497
498            self.drr_active = processed;
499        }
500
501        results
502    }
503
504    // -----------------------------------------------------------------------
505    // Fairness metric
506    // -----------------------------------------------------------------------
507
508    /// Compute Jain's fairness index over all streams' `bytes_sent`.
509    ///
510    /// Returns a value in `[0.0, 1.0]` where `1.0` is perfectly fair.
511    /// Returns `1.0` if there are fewer than two streams.
512    pub fn compute_fairness(&self) -> f64 {
513        let streams: Vec<f64> = self.streams.values().map(|s| s.bytes_sent as f64).collect();
514
515        let n = streams.len();
516        if n < 2 {
517            return 1.0;
518        }
519
520        let sum: f64 = streams.iter().sum();
521        let sum_sq: f64 = streams.iter().map(|x| x * x).sum();
522
523        if sum_sq == 0.0 {
524            return 1.0;
525        }
526
527        (sum * sum) / (n as f64 * sum_sq)
528    }
529
530    // -----------------------------------------------------------------------
531    // Statistics
532    // -----------------------------------------------------------------------
533
534    /// Return a snapshot of current scheduler statistics.
535    pub fn scheduler_stats(&mut self) -> SpsSchedulerStats {
536        self.stats.active_streams = self.streams.len();
537        self.stats.blocked_streams = self.streams.values().filter(|s| s.is_blocked).count();
538        self.stats.fairness_index = self.compute_fairness();
539        // Compute avg_wait as mean of (tick - last_scheduled_ts) across all streams
540        // that have been scheduled at least once.
541        let scheduled: Vec<u64> = self
542            .streams
543            .values()
544            .filter(|s| s.send_count > 0)
545            .map(|s| self.tick.saturating_sub(s.last_scheduled_ts))
546            .collect();
547        if scheduled.is_empty() {
548            self.stats.avg_wait = 0.0;
549        } else {
550            self.stats.avg_wait = scheduled.iter().sum::<u64>() as f64 / scheduled.len() as f64;
551        }
552        self.stats.clone()
553    }
554
555    // -----------------------------------------------------------------------
556    // Accessors
557    // -----------------------------------------------------------------------
558
559    /// Return a reference to a stream, or `None`.
560    pub fn get_stream(&self, id: SpsStreamId) -> Option<&SpsStream> {
561        self.streams.get(&id)
562    }
563
564    /// Return a mutable reference to a stream, or `None`.
565    pub fn get_stream_mut(&mut self, id: SpsStreamId) -> Option<&mut SpsStream> {
566        self.streams.get_mut(&id)
567    }
568
569    /// Set the deadline for EDF scheduling on a stream.
570    ///
571    /// Smaller values are scheduled earlier.
572    pub fn set_deadline(&mut self, id: SpsStreamId, deadline: u64) -> Result<(), SpsError> {
573        let stream = self
574            .streams
575            .get_mut(&id)
576            .ok_or(SpsError::StreamNotFound(id))?;
577        stream.deadline = deadline;
578        Ok(())
579    }
580
581    /// Configure HTB token-bucket parameters for a stream.
582    pub fn set_htb_params(
583        &mut self,
584        id: SpsStreamId,
585        rate: u64,
586        burst: u64,
587    ) -> Result<(), SpsError> {
588        let stream = self
589            .streams
590            .get_mut(&id)
591            .ok_or(SpsError::StreamNotFound(id))?;
592        stream.htb_rate = rate.max(1);
593        stream.htb_burst = burst.max(1);
594        Ok(())
595    }
596
597    /// Refill HTB token buckets for all streams by `ticks` time units.
598    pub fn htb_refill(&mut self, ticks: u64) {
599        for stream in self.streams.values_mut() {
600            let refill = (stream.htb_rate * ticks) as i64;
601            stream.htb_tokens = (stream.htb_tokens + refill).min(stream.htb_burst as i64);
602        }
603    }
604
605    /// Return the current tick counter.
606    pub fn tick(&self) -> u64 {
607        self.tick
608    }
609
610    /// Return the number of currently registered streams.
611    pub fn stream_count(&self) -> usize {
612        self.streams.len()
613    }
614
615    /// Return the number of eligible (non-blocked, non-empty) streams.
616    pub fn eligible_count(&self) -> usize {
617        self.streams.values().filter(|s| s.is_eligible()).count()
618    }
619
620    /// Drain all pending bytes from a stream without scheduling it.
621    ///
622    /// Useful for flow-control / back-pressure scenarios.
623    pub fn drain_stream(&mut self, id: SpsStreamId) -> Result<u64, SpsError> {
624        let stream = self
625            .streams
626            .get_mut(&id)
627            .ok_or(SpsError::StreamNotFound(id))?;
628        let drained = stream.pending_bytes;
629        stream.pending_bytes = 0;
630        stream.deficit_counter = 0;
631        // Remove from priority queue.
632        if let Some(queue) = self.priority_queues.get_mut(&stream.priority) {
633            queue.retain(|&sid| sid != id);
634        }
635        self.drr_active.retain(|e| e.stream_id != id);
636        Ok(drained)
637    }
638
639    /// Update the priority of a stream.
640    ///
641    /// This moves the stream to the new priority queue if it currently has data.
642    pub fn update_priority(&mut self, id: SpsStreamId, new_priority: u32) -> Result<(), SpsError> {
643        let stream = self
644            .streams
645            .get_mut(&id)
646            .ok_or(SpsError::StreamNotFound(id))?;
647        let old_priority = stream.priority;
648        let has_data = stream.pending_bytes > 0 && !stream.is_blocked;
649        stream.priority = new_priority;
650
651        if has_data && old_priority != new_priority {
652            // Remove from old queue.
653            if let Some(q) = self.priority_queues.get_mut(&old_priority) {
654                q.retain(|&sid| sid != id);
655            }
656            // Insert into new queue.
657            self.priority_queues
658                .entry(new_priority)
659                .or_default()
660                .push_back(id);
661        }
662        Ok(())
663    }
664
665    // -----------------------------------------------------------------------
666    // Private scheduling helpers
667    // -----------------------------------------------------------------------
668
669    /// Strict Priority: pick the first eligible stream from the highest non-empty queue.
670    ///
671    /// Among streams tied at the same priority and same `bytes_sent`, a xorshift64 PRNG
672    /// step provides stochastic tie-breaking to avoid systematic starvation.
673    fn schedule_strict(&mut self) -> Option<(SpsStreamId, u64)> {
674        let quantum = self.config.quantum_bytes;
675
676        // Advance the PRNG once per scheduling call to drive tie-breaking.
677        let _jitter = xorshift64(&mut self.rng_state);
678
679        // Iterate from highest priority (largest key) downward.
680        let chosen = self.priority_queues.iter().rev().find_map(|(_, queue)| {
681            queue
682                .iter()
683                .find(|&&sid| {
684                    self.streams
685                        .get(&sid)
686                        .map(|s| s.is_eligible())
687                        .unwrap_or(false)
688                })
689                .copied()
690        });
691
692        let id = chosen?;
693        self.dispatch_bytes(id, quantum)
694    }
695
696    /// Weighted Fair Queuing: pick eligible stream with highest normalised virtual finish time
697    /// (approximated as bytes_sent / weight — lowest wins = we invert to select smallest).
698    fn schedule_wfq(&mut self) -> Option<(SpsStreamId, u64)> {
699        let quantum = self.config.quantum_bytes;
700        let strict_threshold = self.config.strict_priority_threshold;
701
702        // First check for strict-priority streams above the threshold.
703        let strict_id = self.priority_queues.iter().rev().find_map(|(&pri, queue)| {
704            if pri < strict_threshold {
705                return None;
706            }
707            queue
708                .iter()
709                .find(|&&sid| {
710                    self.streams
711                        .get(&sid)
712                        .map(|s| s.is_eligible())
713                        .unwrap_or(false)
714                })
715                .copied()
716        });
717
718        if let Some(id) = strict_id {
719            return self.dispatch_bytes(id, quantum);
720        }
721
722        // Among the remaining eligible streams pick the one with smallest
723        // virtual_time = bytes_sent / weight (smallest = most deserving).
724        let best = self
725            .streams
726            .values()
727            .filter(|s| s.is_eligible() && s.priority < strict_threshold)
728            .min_by(|a, b| {
729                let va = if a.weight == 0 {
730                    f64::MAX
731                } else {
732                    a.bytes_sent as f64 / a.weight as f64
733                };
734                let vb = if b.weight == 0 {
735                    f64::MAX
736                } else {
737                    b.bytes_sent as f64 / b.weight as f64
738                };
739                va.partial_cmp(&vb).unwrap_or(std::cmp::Ordering::Equal)
740            })
741            .map(|s| s.id);
742
743        let id = best?;
744        self.dispatch_bytes(id, quantum)
745    }
746
747    /// Single-shot DRR step: pick next stream from DRR active list.
748    fn schedule_drr_single(&mut self) -> Option<(SpsStreamId, u64)> {
749        let quantum = self.config.quantum_bytes;
750
751        // Rebuild active list if empty.
752        if self.drr_active.is_empty() {
753            for stream in self.streams.values() {
754                if stream.is_eligible() {
755                    self.drr_active.push_back(DrrEntry {
756                        stream_id: stream.id,
757                        priority: stream.priority,
758                    });
759                }
760            }
761        }
762
763        // Walk active list until we find an eligible stream with positive deficit.
764        let list_len = self.drr_active.len();
765        for _ in 0..list_len {
766            let entry = self.drr_active.pop_front()?;
767            let stream = match self.streams.get_mut(&entry.stream_id) {
768                Some(s) => s,
769                None => continue,
770            };
771
772            if !stream.is_eligible() {
773                continue;
774            }
775
776            // Add quantum.
777            stream.deficit_counter += quantum as i64;
778
779            if stream.deficit_counter > 0 {
780                let send = stream.pending_bytes.min(stream.deficit_counter as u64);
781                stream.deficit_counter -= send as i64;
782                stream.pending_bytes -= send;
783                stream.bytes_sent += send;
784                stream.send_count += 1;
785                stream.last_scheduled_ts = self.tick;
786                self.stats.total_scheduled += 1;
787                self.stats.total_bytes += send;
788                *self
789                    .stats
790                    .priority_distribution
791                    .entry(stream.priority)
792                    .or_insert(0) += 1;
793
794                if stream.pending_bytes > 0 {
795                    self.drr_active.push_back(DrrEntry {
796                        stream_id: entry.stream_id,
797                        priority: entry.priority,
798                    });
799                } else {
800                    stream.deficit_counter = 0;
801                }
802                return Some((entry.stream_id, send));
803            } else {
804                // Not enough deficit yet; re-queue at back.
805                self.drr_active.push_back(entry);
806            }
807        }
808        None
809    }
810
811    /// Earliest Deadline First: pick the eligible stream with the smallest deadline.
812    fn schedule_edf(&mut self) -> Option<(SpsStreamId, u64)> {
813        let quantum = self.config.quantum_bytes;
814
815        let best = self
816            .streams
817            .values()
818            .filter(|s| s.is_eligible())
819            .min_by_key(|s| s.deadline)
820            .map(|s| s.id);
821
822        let id = best?;
823        self.dispatch_bytes(id, quantum)
824    }
825
826    /// Hierarchical Token Bucket: pick eligible stream with sufficient tokens.
827    fn schedule_htb(&mut self) -> Option<(SpsStreamId, u64)> {
828        let quantum = self.config.quantum_bytes;
829
830        // Prefer streams that have positive token balance, then fall back to SP.
831        let tokenized = self
832            .streams
833            .values()
834            .filter(|s| s.is_eligible() && s.htb_tokens > 0)
835            .max_by_key(|s| s.priority)
836            .map(|s| s.id);
837
838        let id = if let Some(id) = tokenized {
839            id
840        } else {
841            // Fall back to strict priority when no tokens are available.
842            self.streams
843                .values()
844                .filter(|s| s.is_eligible())
845                .max_by_key(|s| s.priority)
846                .map(|s| s.id)?
847        };
848
849        // Deduct tokens.
850        if let Some(stream) = self.streams.get_mut(&id) {
851            let send = stream.pending_bytes.min(quantum);
852            stream.htb_tokens -= send as i64;
853            // htb_tokens may go negative (borrow from future); caller should call htb_refill.
854        }
855
856        self.dispatch_bytes(id, quantum)
857    }
858
859    /// Shared finalisation: record stats and deduct pending bytes.
860    ///
861    /// Returns `Some((id, bytes_sent))` or `None` if the stream has no data.
862    fn dispatch_bytes(&mut self, id: SpsStreamId, max_bytes: u64) -> Option<(SpsStreamId, u64)> {
863        let stream = self.streams.get_mut(&id)?;
864        if stream.pending_bytes == 0 {
865            return None;
866        }
867        let send = stream.pending_bytes.min(max_bytes);
868        stream.pending_bytes -= send;
869        stream.bytes_sent += send;
870        stream.send_count += 1;
871        stream.last_scheduled_ts = self.tick;
872
873        let priority = stream.priority;
874        self.stats.total_scheduled += 1;
875        self.stats.total_bytes += send;
876        *self
877            .stats
878            .priority_distribution
879            .entry(priority)
880            .or_insert(0) += 1;
881
882        // Remove from priority queue if empty.
883        if stream.pending_bytes == 0 {
884            if let Some(q) = self.priority_queues.get_mut(&priority) {
885                q.retain(|&sid| sid != id);
886            }
887        }
888
889        Some((id, send))
890    }
891}
892
893// ---------------------------------------------------------------------------
894// Tests
895// ---------------------------------------------------------------------------
896
897#[cfg(test)]
898mod tests {
899    use super::*;
900
901    fn make_scheduler() -> StreamPriorityScheduler {
902        StreamPriorityScheduler::new(SpsSchedulerConfig::default())
903    }
904
905    // ── Construction ──────────────────────────────────────────────────────
906
907    #[test]
908    fn test_new_scheduler_is_empty() {
909        let s = make_scheduler();
910        assert_eq!(s.stream_count(), 0);
911        assert_eq!(s.eligible_count(), 0);
912    }
913
914    #[test]
915    fn test_default_config_values() {
916        let cfg = SpsSchedulerConfig::default();
917        assert_eq!(cfg.max_streams, 1024);
918        assert!(cfg.quantum_bytes > 0);
919        assert!(cfg.deficit_rounds >= 1);
920    }
921
922    // ── add_stream ─────────────────────────────────────────────────────────
923
924    #[test]
925    fn test_add_stream_basic() {
926        let mut s = make_scheduler();
927        s.add_stream(1, 10, 1)
928            .expect("test: add_stream should succeed");
929        assert_eq!(s.stream_count(), 1);
930    }
931
932    #[test]
933    fn test_add_stream_duplicate_errors() {
934        let mut s = make_scheduler();
935        s.add_stream(1, 10, 1)
936            .expect("test: add_stream should succeed");
937        assert!(matches!(
938            s.add_stream(1, 5, 1),
939            Err(SpsError::DuplicateStream(1))
940        ));
941    }
942
943    #[test]
944    fn test_add_stream_zero_weight_errors() {
945        let mut s = make_scheduler();
946        assert!(matches!(
947            s.add_stream(1, 10, 0),
948            Err(SpsError::InvalidWeight)
949        ));
950    }
951
952    #[test]
953    fn test_add_stream_max_limit() {
954        let cfg = SpsSchedulerConfig {
955            max_streams: 2,
956            ..Default::default()
957        };
958        let mut s = StreamPriorityScheduler::new(cfg);
959        s.add_stream(1, 10, 1)
960            .expect("test: add_stream 1 should succeed");
961        s.add_stream(2, 10, 1)
962            .expect("test: add_stream 2 should succeed");
963        assert!(matches!(
964            s.add_stream(3, 10, 1),
965            Err(SpsError::MaxStreamsReached(2))
966        ));
967    }
968
969    // ── remove_stream ──────────────────────────────────────────────────────
970
971    #[test]
972    fn test_remove_existing_stream() {
973        let mut s = make_scheduler();
974        s.add_stream(1, 10, 1)
975            .expect("test: add_stream should succeed");
976        let removed = s
977            .remove_stream(1)
978            .expect("test: remove_stream should succeed");
979        assert_eq!(removed.id, 1);
980        assert_eq!(s.stream_count(), 0);
981    }
982
983    #[test]
984    fn test_remove_nonexistent_stream_errors() {
985        let mut s = make_scheduler();
986        assert!(matches!(
987            s.remove_stream(99),
988            Err(SpsError::StreamNotFound(99))
989        ));
990    }
991
992    #[test]
993    fn test_remove_stream_cleans_priority_queue() {
994        let mut s = make_scheduler();
995        s.add_stream(1, 10, 1)
996            .expect("test: add_stream should succeed");
997        s.enqueue_bytes(1, 1000)
998            .expect("test: enqueue_bytes should succeed");
999        s.remove_stream(1)
1000            .expect("test: remove_stream should succeed");
1001        // No panic and priority_queues should not contain stream 1.
1002        let result = s.schedule_next(&SpsSchedulingPolicy::StrictPriority);
1003        assert!(result.is_none());
1004    }
1005
1006    // ── block / unblock ────────────────────────────────────────────────────
1007
1008    #[test]
1009    fn test_block_stream() {
1010        let mut s = make_scheduler();
1011        s.add_stream(1, 10, 1)
1012            .expect("test: add_stream should succeed");
1013        s.enqueue_bytes(1, 100)
1014            .expect("test: enqueue_bytes should succeed");
1015        s.block_stream(1)
1016            .expect("test: block_stream should succeed");
1017        assert!(
1018            s.get_stream(1)
1019                .expect("test: stream 1 should exist")
1020                .is_blocked
1021        );
1022        let result = s.schedule_next(&SpsSchedulingPolicy::StrictPriority);
1023        assert!(result.is_none());
1024    }
1025
1026    #[test]
1027    fn test_unblock_stream() {
1028        let mut s = make_scheduler();
1029        s.add_stream(1, 10, 1)
1030            .expect("test: add_stream should succeed");
1031        s.enqueue_bytes(1, 100)
1032            .expect("test: enqueue_bytes should succeed");
1033        s.block_stream(1)
1034            .expect("test: block_stream should succeed");
1035        s.unblock_stream(1)
1036            .expect("test: unblock_stream should succeed");
1037        assert!(
1038            !s.get_stream(1)
1039                .expect("test: stream 1 should exist")
1040                .is_blocked
1041        );
1042        // After unblocking the stream must be re-enqueued for scheduling.
1043        s.enqueue_bytes(1, 0).unwrap_or_default(); // no-op but covers the path
1044    }
1045
1046    #[test]
1047    fn test_block_nonexistent_errors() {
1048        let mut s = make_scheduler();
1049        assert!(matches!(
1050            s.block_stream(7),
1051            Err(SpsError::StreamNotFound(7))
1052        ));
1053    }
1054
1055    #[test]
1056    fn test_unblock_nonexistent_errors() {
1057        let mut s = make_scheduler();
1058        assert!(matches!(
1059            s.unblock_stream(7),
1060            Err(SpsError::StreamNotFound(7))
1061        ));
1062    }
1063
1064    // ── enqueue_bytes ──────────────────────────────────────────────────────
1065
1066    #[test]
1067    fn test_enqueue_bytes_increases_pending() {
1068        let mut s = make_scheduler();
1069        s.add_stream(1, 10, 1)
1070            .expect("test: add_stream should succeed");
1071        s.enqueue_bytes(1, 512)
1072            .expect("test: enqueue_bytes should succeed");
1073        assert_eq!(
1074            s.get_stream(1)
1075                .expect("test: stream 1 should exist")
1076                .pending_bytes,
1077            512
1078        );
1079    }
1080
1081    #[test]
1082    fn test_enqueue_bytes_accumulates() {
1083        let mut s = make_scheduler();
1084        s.add_stream(1, 10, 1)
1085            .expect("test: add_stream should succeed");
1086        s.enqueue_bytes(1, 100)
1087            .expect("test: first enqueue_bytes should succeed");
1088        s.enqueue_bytes(1, 200)
1089            .expect("test: second enqueue_bytes should succeed");
1090        assert_eq!(
1091            s.get_stream(1)
1092                .expect("test: stream 1 should exist")
1093                .pending_bytes,
1094            300
1095        );
1096    }
1097
1098    #[test]
1099    fn test_enqueue_bytes_nonexistent_errors() {
1100        let mut s = make_scheduler();
1101        assert!(matches!(
1102            s.enqueue_bytes(42, 100),
1103            Err(SpsError::StreamNotFound(42))
1104        ));
1105    }
1106
1107    // ── StrictPriority ─────────────────────────────────────────────────────
1108
1109    #[test]
1110    fn test_strict_priority_basic() {
1111        let mut s = make_scheduler();
1112        s.add_stream(1, 100, 1)
1113            .expect("test: add_stream 1 should succeed");
1114        s.add_stream(2, 50, 1)
1115            .expect("test: add_stream 2 should succeed");
1116        s.enqueue_bytes(1, 1500)
1117            .expect("test: enqueue_bytes for stream 1 should succeed");
1118        s.enqueue_bytes(2, 1500)
1119            .expect("test: enqueue_bytes for stream 2 should succeed");
1120        let result = s.schedule_next(&SpsSchedulingPolicy::StrictPriority);
1121        assert_eq!(result.map(|(id, _)| id), Some(1));
1122    }
1123
1124    #[test]
1125    fn test_strict_priority_selects_highest() {
1126        let mut s = make_scheduler();
1127        for pri in [10u32, 50, 200, 5] {
1128            s.add_stream(pri as u64, pri, 1)
1129                .expect("test: add_stream should succeed");
1130            s.enqueue_bytes(pri as u64, 3000)
1131                .expect("test: enqueue_bytes should succeed");
1132        }
1133        let (id, _) = s
1134            .schedule_next(&SpsSchedulingPolicy::StrictPriority)
1135            .expect("test: schedule_next should return Some");
1136        assert_eq!(id, 200);
1137    }
1138
1139    #[test]
1140    fn test_strict_priority_no_eligible_returns_none() {
1141        let mut s = make_scheduler();
1142        s.add_stream(1, 10, 1)
1143            .expect("test: add_stream should succeed");
1144        // No enqueue, so nothing eligible.
1145        let result = s.schedule_next(&SpsSchedulingPolicy::StrictPriority);
1146        assert!(result.is_none());
1147    }
1148
1149    #[test]
1150    fn test_strict_priority_exhausts_stream() {
1151        let mut s = make_scheduler();
1152        s.add_stream(1, 10, 1)
1153            .expect("test: add_stream should succeed");
1154        s.enqueue_bytes(1, 100)
1155            .expect("test: enqueue_bytes should succeed");
1156        // quantum is 1500, so single call should drain the stream.
1157        let (_, sent) = s
1158            .schedule_next(&SpsSchedulingPolicy::StrictPriority)
1159            .expect("test: schedule_next should return Some");
1160        assert_eq!(sent, 100);
1161        assert_eq!(
1162            s.get_stream(1)
1163                .expect("test: stream 1 should exist")
1164                .pending_bytes,
1165            0
1166        );
1167    }
1168
1169    // ── WeightedFairQueuing ────────────────────────────────────────────────
1170
1171    #[test]
1172    fn test_wfq_basic_scheduling() {
1173        let mut s = make_scheduler();
1174        s.add_stream(1, 10, 10)
1175            .expect("test: add_stream 1 should succeed");
1176        s.add_stream(2, 10, 1)
1177            .expect("test: add_stream 2 should succeed");
1178        s.enqueue_bytes(1, 5000)
1179            .expect("test: enqueue_bytes for stream 1 should succeed");
1180        s.enqueue_bytes(2, 5000)
1181            .expect("test: enqueue_bytes for stream 2 should succeed");
1182        // Both streams start at 0 bytes_sent, so weight does not matter yet.
1183        let r = s.schedule_next(&SpsSchedulingPolicy::WeightedFairQueuing);
1184        assert!(r.is_some());
1185    }
1186
1187    #[test]
1188    fn test_wfq_strict_threshold_override() {
1189        let cfg = SpsSchedulerConfig {
1190            strict_priority_threshold: 100,
1191            ..Default::default()
1192        };
1193        let mut s = StreamPriorityScheduler::new(cfg);
1194        s.add_stream(1, 200, 1)
1195            .expect("test: add_stream 1 should succeed"); // above threshold
1196        s.add_stream(2, 50, 100)
1197            .expect("test: add_stream 2 should succeed"); // below threshold, high weight
1198        s.enqueue_bytes(1, 3000)
1199            .expect("test: enqueue_bytes for stream 1 should succeed");
1200        s.enqueue_bytes(2, 3000)
1201            .expect("test: enqueue_bytes for stream 2 should succeed");
1202        let (id, _) = s
1203            .schedule_next(&SpsSchedulingPolicy::WeightedFairQueuing)
1204            .expect("test: schedule_next should return Some");
1205        assert_eq!(id, 1); // strict priority stream should win
1206    }
1207
1208    #[test]
1209    fn test_wfq_proportional_allocation() {
1210        let mut s = make_scheduler();
1211        s.add_stream(1, 10, 2)
1212            .expect("test: add_stream 1 should succeed");
1213        s.add_stream(2, 10, 1)
1214            .expect("test: add_stream 2 should succeed");
1215        s.enqueue_bytes(1, 100_000)
1216            .expect("test: enqueue_bytes for stream 1 should succeed");
1217        s.enqueue_bytes(2, 100_000)
1218            .expect("test: enqueue_bytes for stream 2 should succeed");
1219        // Run many rounds and check that stream 1 got roughly 2x bytes.
1220        let batch = s.schedule_batch(&SpsSchedulingPolicy::WeightedFairQueuing, 1000);
1221        assert!(!batch.is_empty());
1222    }
1223
1224    // ── DeficitRoundRobin ──────────────────────────────────────────────────
1225
1226    #[test]
1227    fn test_drr_single_step() {
1228        let mut s = make_scheduler();
1229        s.add_stream(1, 10, 1)
1230            .expect("test: add_stream should succeed");
1231        s.enqueue_bytes(1, 3000)
1232            .expect("test: enqueue_bytes should succeed");
1233        let r = s.schedule_next(&SpsSchedulingPolicy::DeficitRoundRobin);
1234        assert!(r.is_some());
1235    }
1236
1237    #[test]
1238    fn test_drr_round_basic() {
1239        let mut s = make_scheduler();
1240        s.add_stream(1, 10, 1)
1241            .expect("test: add_stream 1 should succeed");
1242        s.add_stream(2, 10, 1)
1243            .expect("test: add_stream 2 should succeed");
1244        s.enqueue_bytes(1, 3000)
1245            .expect("test: enqueue_bytes for stream 1 should succeed");
1246        s.enqueue_bytes(2, 3000)
1247            .expect("test: enqueue_bytes for stream 2 should succeed");
1248        let results = s.run_drr_round();
1249        assert!(!results.is_empty());
1250    }
1251
1252    #[test]
1253    fn test_drr_multiple_rounds() {
1254        let cfg = SpsSchedulerConfig {
1255            quantum_bytes: 500,
1256            deficit_rounds: 3,
1257            ..Default::default()
1258        };
1259        let mut s = StreamPriorityScheduler::new(cfg);
1260        s.add_stream(1, 10, 1)
1261            .expect("test: add_stream 1 should succeed");
1262        s.add_stream(2, 10, 1)
1263            .expect("test: add_stream 2 should succeed");
1264        s.enqueue_bytes(1, 10000)
1265            .expect("test: enqueue_bytes for stream 1 should succeed");
1266        s.enqueue_bytes(2, 10000)
1267            .expect("test: enqueue_bytes for stream 2 should succeed");
1268        let results = s.run_drr_round();
1269        assert!(!results.is_empty());
1270    }
1271
1272    #[test]
1273    fn test_drr_drain_eventually() {
1274        let cfg = SpsSchedulerConfig {
1275            quantum_bytes: 100,
1276            deficit_rounds: 1,
1277            ..Default::default()
1278        };
1279        let mut s = StreamPriorityScheduler::new(cfg);
1280        s.add_stream(1, 10, 1)
1281            .expect("test: add_stream should succeed");
1282        s.enqueue_bytes(1, 300)
1283            .expect("test: enqueue_bytes should succeed");
1284
1285        let mut total = 0u64;
1286        for _ in 0..20 {
1287            for (_, b) in s.run_drr_round() {
1288                total += b;
1289            }
1290        }
1291        assert_eq!(total, 300);
1292    }
1293
1294    // ── EarliestDeadlineFirst ──────────────────────────────────────────────
1295
1296    #[test]
1297    fn test_edf_selects_smallest_deadline() {
1298        let mut s = make_scheduler();
1299        s.add_stream(1, 10, 1)
1300            .expect("test: add_stream 1 should succeed");
1301        s.add_stream(2, 10, 1)
1302            .expect("test: add_stream 2 should succeed");
1303        s.set_deadline(1, 1000)
1304            .expect("test: set_deadline for stream 1 should succeed");
1305        s.set_deadline(2, 500)
1306            .expect("test: set_deadline for stream 2 should succeed");
1307        s.enqueue_bytes(1, 1500)
1308            .expect("test: enqueue_bytes for stream 1 should succeed");
1309        s.enqueue_bytes(2, 1500)
1310            .expect("test: enqueue_bytes for stream 2 should succeed");
1311        let (id, _) = s
1312            .schedule_next(&SpsSchedulingPolicy::EarliestDeadlineFirst)
1313            .expect("test: schedule_next should return Some");
1314        assert_eq!(id, 2); // stream 2 has earlier deadline
1315    }
1316
1317    #[test]
1318    fn test_edf_default_deadline_is_max() {
1319        let mut s = make_scheduler();
1320        s.add_stream(1, 10, 1)
1321            .expect("test: add_stream should succeed");
1322        s.enqueue_bytes(1, 100)
1323            .expect("test: enqueue_bytes should succeed");
1324        let r = s.schedule_next(&SpsSchedulingPolicy::EarliestDeadlineFirst);
1325        assert!(r.is_some());
1326    }
1327
1328    #[test]
1329    fn test_set_deadline_nonexistent_errors() {
1330        let mut s = make_scheduler();
1331        assert!(matches!(
1332            s.set_deadline(99, 100),
1333            Err(SpsError::StreamNotFound(99))
1334        ));
1335    }
1336
1337    // ── HierarchicalToken ──────────────────────────────────────────────────
1338
1339    #[test]
1340    fn test_htb_requires_refill() {
1341        let mut s = make_scheduler();
1342        s.add_stream(1, 10, 1)
1343            .expect("test: add_stream should succeed");
1344        s.set_htb_params(1, 1000, 10000)
1345            .expect("test: set_htb_params should succeed");
1346        s.enqueue_bytes(1, 5000)
1347            .expect("test: enqueue_bytes should succeed");
1348        // Without refill, tokens = 0 → falls back to SP.
1349        let r = s.schedule_next(&SpsSchedulingPolicy::HierarchicalToken);
1350        // Should still return something (falls back to strict priority).
1351        assert!(r.is_some());
1352    }
1353
1354    #[test]
1355    fn test_htb_with_refill() {
1356        let mut s = make_scheduler();
1357        s.add_stream(1, 10, 1)
1358            .expect("test: add_stream should succeed");
1359        s.set_htb_params(1, 1000, 10000)
1360            .expect("test: set_htb_params should succeed");
1361        s.htb_refill(5); // Add 5000 tokens.
1362        s.enqueue_bytes(1, 5000)
1363            .expect("test: enqueue_bytes should succeed");
1364        let (id, _) = s
1365            .schedule_next(&SpsSchedulingPolicy::HierarchicalToken)
1366            .expect("test: schedule_next should return Some");
1367        assert_eq!(id, 1);
1368    }
1369
1370    #[test]
1371    fn test_set_htb_params_nonexistent_errors() {
1372        let mut s = make_scheduler();
1373        assert!(matches!(
1374            s.set_htb_params(99, 1000, 2000),
1375            Err(SpsError::StreamNotFound(99))
1376        ));
1377    }
1378
1379    // ── schedule_batch ─────────────────────────────────────────────────────
1380
1381    #[test]
1382    fn test_schedule_batch_returns_up_to_n() {
1383        let mut s = make_scheduler();
1384        for i in 1u64..=5 {
1385            s.add_stream(i, 10, 1)
1386                .expect("test: add_stream should succeed");
1387            s.enqueue_bytes(i, 500)
1388                .expect("test: enqueue_bytes should succeed");
1389        }
1390        let batch = s.schedule_batch(&SpsSchedulingPolicy::StrictPriority, 3);
1391        assert!(batch.len() <= 3);
1392    }
1393
1394    #[test]
1395    fn test_schedule_batch_stops_when_empty() {
1396        let mut s = make_scheduler();
1397        s.add_stream(1, 10, 1)
1398            .expect("test: add_stream should succeed");
1399        s.enqueue_bytes(1, 100)
1400            .expect("test: enqueue_bytes should succeed");
1401        let batch = s.schedule_batch(&SpsSchedulingPolicy::StrictPriority, 100);
1402        // Should stop once the single stream is drained.
1403        assert!(!batch.is_empty());
1404        let total: u64 = batch.iter().map(|(_, b)| b).sum();
1405        assert_eq!(total, 100);
1406    }
1407
1408    #[test]
1409    fn test_schedule_batch_zero_n() {
1410        let mut s = make_scheduler();
1411        s.add_stream(1, 10, 1)
1412            .expect("test: add_stream should succeed");
1413        s.enqueue_bytes(1, 100)
1414            .expect("test: enqueue_bytes should succeed");
1415        let batch = s.schedule_batch(&SpsSchedulingPolicy::StrictPriority, 0);
1416        assert!(batch.is_empty());
1417    }
1418
1419    // ── compute_fairness ───────────────────────────────────────────────────
1420
1421    #[test]
1422    fn test_fairness_empty_scheduler() {
1423        let s = make_scheduler();
1424        assert!((s.compute_fairness() - 1.0).abs() < 1e-9);
1425    }
1426
1427    #[test]
1428    fn test_fairness_single_stream() {
1429        let mut s = make_scheduler();
1430        s.add_stream(1, 10, 1)
1431            .expect("test: add_stream should succeed");
1432        assert!((s.compute_fairness() - 1.0).abs() < 1e-9);
1433    }
1434
1435    #[test]
1436    fn test_fairness_two_equal_streams() {
1437        let mut s = make_scheduler();
1438        s.add_stream(1, 10, 1)
1439            .expect("test: add_stream 1 should succeed");
1440        s.add_stream(2, 10, 1)
1441            .expect("test: add_stream 2 should succeed");
1442        s.enqueue_bytes(1, 10000)
1443            .expect("test: enqueue_bytes for stream 1 should succeed");
1444        s.enqueue_bytes(2, 10000)
1445            .expect("test: enqueue_bytes for stream 2 should succeed");
1446        s.schedule_batch(&SpsSchedulingPolicy::WeightedFairQueuing, 200);
1447        let f = s.compute_fairness();
1448        // Two streams with equal weight should approach 1.0 fairness.
1449        assert!(f > 0.0 && f <= 1.0);
1450    }
1451
1452    #[test]
1453    fn test_fairness_bounds() {
1454        let mut s = make_scheduler();
1455        for i in 1u64..=4 {
1456            s.add_stream(i, 10, 1)
1457                .expect("test: add_stream should succeed");
1458            s.enqueue_bytes(i, 5000)
1459                .expect("test: enqueue_bytes should succeed");
1460        }
1461        s.schedule_batch(&SpsSchedulingPolicy::WeightedFairQueuing, 200);
1462        let f = s.compute_fairness();
1463        assert!((0.0..=1.0 + 1e-9).contains(&f));
1464    }
1465
1466    // ── scheduler_stats ────────────────────────────────────────────────────
1467
1468    #[test]
1469    fn test_stats_initial_zeros() {
1470        let mut s = make_scheduler();
1471        let stats = s.scheduler_stats();
1472        assert_eq!(stats.total_scheduled, 0);
1473        assert_eq!(stats.total_bytes, 0);
1474        assert_eq!(stats.active_streams, 0);
1475    }
1476
1477    #[test]
1478    fn test_stats_after_scheduling() {
1479        let mut s = make_scheduler();
1480        s.add_stream(1, 10, 1)
1481            .expect("test: add_stream should succeed");
1482        s.enqueue_bytes(1, 1500)
1483            .expect("test: enqueue_bytes should succeed");
1484        s.schedule_next(&SpsSchedulingPolicy::StrictPriority);
1485        let stats = s.scheduler_stats();
1486        assert!(stats.total_scheduled > 0);
1487        assert!(stats.total_bytes > 0);
1488    }
1489
1490    #[test]
1491    fn test_stats_priority_distribution() {
1492        let mut s = make_scheduler();
1493        s.add_stream(1, 42, 1)
1494            .expect("test: add_stream should succeed");
1495        s.enqueue_bytes(1, 1500)
1496            .expect("test: enqueue_bytes should succeed");
1497        s.schedule_next(&SpsSchedulingPolicy::StrictPriority);
1498        let stats = s.scheduler_stats();
1499        assert!(stats.priority_distribution.contains_key(&42));
1500    }
1501
1502    #[test]
1503    fn test_stats_active_and_blocked() {
1504        let mut s = make_scheduler();
1505        s.add_stream(1, 10, 1)
1506            .expect("test: add_stream 1 should succeed");
1507        s.add_stream(2, 10, 1)
1508            .expect("test: add_stream 2 should succeed");
1509        s.block_stream(1)
1510            .expect("test: block_stream should succeed");
1511        let stats = s.scheduler_stats();
1512        assert_eq!(stats.active_streams, 2);
1513        assert_eq!(stats.blocked_streams, 1);
1514    }
1515
1516    // ── drain_stream ───────────────────────────────────────────────────────
1517
1518    #[test]
1519    fn test_drain_stream() {
1520        let mut s = make_scheduler();
1521        s.add_stream(1, 10, 1)
1522            .expect("test: add_stream should succeed");
1523        s.enqueue_bytes(1, 5000)
1524            .expect("test: enqueue_bytes should succeed");
1525        let drained = s
1526            .drain_stream(1)
1527            .expect("test: drain_stream should succeed");
1528        assert_eq!(drained, 5000);
1529        assert_eq!(
1530            s.get_stream(1)
1531                .expect("test: stream 1 should exist")
1532                .pending_bytes,
1533            0
1534        );
1535    }
1536
1537    #[test]
1538    fn test_drain_nonexistent_errors() {
1539        let mut s = make_scheduler();
1540        assert!(matches!(
1541            s.drain_stream(99),
1542            Err(SpsError::StreamNotFound(99))
1543        ));
1544    }
1545
1546    // ── update_priority ────────────────────────────────────────────────────
1547
1548    #[test]
1549    fn test_update_priority() {
1550        let mut s = make_scheduler();
1551        s.add_stream(1, 10, 1)
1552            .expect("test: add_stream should succeed");
1553        s.enqueue_bytes(1, 500)
1554            .expect("test: enqueue_bytes should succeed");
1555        s.update_priority(1, 200)
1556            .expect("test: update_priority should succeed");
1557        assert_eq!(
1558            s.get_stream(1)
1559                .expect("test: stream 1 should exist")
1560                .priority,
1561            200
1562        );
1563    }
1564
1565    #[test]
1566    fn test_update_priority_nonexistent_errors() {
1567        let mut s = make_scheduler();
1568        assert!(matches!(
1569            s.update_priority(99, 100),
1570            Err(SpsError::StreamNotFound(99))
1571        ));
1572    }
1573
1574    #[test]
1575    fn test_update_priority_affects_scheduling() {
1576        let mut s = make_scheduler();
1577        s.add_stream(1, 10, 1)
1578            .expect("test: add_stream 1 should succeed");
1579        s.add_stream(2, 10, 1)
1580            .expect("test: add_stream 2 should succeed");
1581        s.enqueue_bytes(1, 5000)
1582            .expect("test: enqueue_bytes for stream 1 should succeed");
1583        s.enqueue_bytes(2, 5000)
1584            .expect("test: enqueue_bytes for stream 2 should succeed");
1585        s.update_priority(2, 200)
1586            .expect("test: update_priority should succeed");
1587        // After priority change of stream 2, it should be re-enqueued via the next enqueue.
1588        s.enqueue_bytes(2, 0).unwrap_or_default();
1589        let (id, _) = s
1590            .schedule_next(&SpsSchedulingPolicy::StrictPriority)
1591            .expect("test: schedule_next should return Some");
1592        assert_eq!(id, 2);
1593    }
1594
1595    // ── get_stream / get_stream_mut ────────────────────────────────────────
1596
1597    #[test]
1598    fn test_get_stream_existing() {
1599        let mut s = make_scheduler();
1600        s.add_stream(5, 10, 2)
1601            .expect("test: add_stream should succeed");
1602        let stream = s.get_stream(5);
1603        assert!(stream.is_some());
1604        assert_eq!(stream.expect("test: stream 5 should exist").id, 5);
1605    }
1606
1607    #[test]
1608    fn test_get_stream_nonexistent() {
1609        let s = make_scheduler();
1610        assert!(s.get_stream(99).is_none());
1611    }
1612
1613    #[test]
1614    fn test_get_stream_mut() {
1615        let mut s = make_scheduler();
1616        s.add_stream(1, 10, 1)
1617            .expect("test: add_stream should succeed");
1618        let stream = s
1619            .get_stream_mut(1)
1620            .expect("test: stream 1 should exist for mutable access");
1621        stream.weight = 99;
1622        assert_eq!(
1623            s.get_stream(1).expect("test: stream 1 should exist").weight,
1624            99
1625        );
1626    }
1627
1628    // ── xorshift64 ─────────────────────────────────────────────────────────
1629
1630    #[test]
1631    fn test_xorshift64_non_zero() {
1632        let mut state = 0xdeadbeef_u64;
1633        let v = xorshift64(&mut state);
1634        assert_ne!(v, 0);
1635    }
1636
1637    #[test]
1638    fn test_xorshift64_different_on_repeated_calls() {
1639        let mut state = 12345678u64;
1640        let a = xorshift64(&mut state);
1641        let b = xorshift64(&mut state);
1642        assert_ne!(a, b);
1643    }
1644
1645    // ── Miscellaneous ──────────────────────────────────────────────────────
1646
1647    #[test]
1648    fn test_eligible_count() {
1649        let mut s = make_scheduler();
1650        s.add_stream(1, 10, 1)
1651            .expect("test: add_stream 1 should succeed");
1652        s.add_stream(2, 10, 1)
1653            .expect("test: add_stream 2 should succeed");
1654        s.enqueue_bytes(1, 100)
1655            .expect("test: enqueue_bytes should succeed");
1656        assert_eq!(s.eligible_count(), 1);
1657    }
1658
1659    #[test]
1660    fn test_tick_increments() {
1661        let mut s = make_scheduler();
1662        s.add_stream(1, 10, 1)
1663            .expect("test: add_stream should succeed");
1664        s.enqueue_bytes(1, 3000)
1665            .expect("test: enqueue_bytes should succeed");
1666        let t0 = s.tick();
1667        s.schedule_next(&SpsSchedulingPolicy::StrictPriority);
1668        assert!(s.tick() > t0);
1669    }
1670
1671    #[test]
1672    fn test_bytes_sent_accumulates() {
1673        let mut s = make_scheduler();
1674        s.add_stream(1, 10, 1)
1675            .expect("test: add_stream should succeed");
1676        s.enqueue_bytes(1, 3000)
1677            .expect("test: enqueue_bytes should succeed");
1678        s.schedule_next(&SpsSchedulingPolicy::StrictPriority);
1679        assert!(
1680            s.get_stream(1)
1681                .expect("test: stream 1 should exist")
1682                .bytes_sent
1683                > 0
1684        );
1685    }
1686
1687    #[test]
1688    fn test_send_count_increments() {
1689        let mut s = make_scheduler();
1690        s.add_stream(1, 10, 1)
1691            .expect("test: add_stream should succeed");
1692        s.enqueue_bytes(1, 1500)
1693            .expect("test: enqueue_bytes should succeed");
1694        s.schedule_next(&SpsSchedulingPolicy::StrictPriority);
1695        assert_eq!(
1696            s.get_stream(1)
1697                .expect("test: stream 1 should exist")
1698                .send_count,
1699            1
1700        );
1701    }
1702
1703    #[test]
1704    fn test_multiple_policies_same_scheduler() {
1705        let mut s = make_scheduler();
1706        s.add_stream(1, 10, 1)
1707            .expect("test: add_stream 1 should succeed");
1708        s.add_stream(2, 20, 2)
1709            .expect("test: add_stream 2 should succeed");
1710        s.enqueue_bytes(1, 5000)
1711            .expect("test: enqueue_bytes for stream 1 should succeed");
1712        s.enqueue_bytes(2, 5000)
1713            .expect("test: enqueue_bytes for stream 2 should succeed");
1714        // Switch policies freely.
1715        let r1 = s.schedule_next(&SpsSchedulingPolicy::StrictPriority);
1716        let r2 = s.schedule_next(&SpsSchedulingPolicy::WeightedFairQueuing);
1717        let r3 = s.schedule_next(&SpsSchedulingPolicy::EarliestDeadlineFirst);
1718        assert!(r1.is_some() || r2.is_some() || r3.is_some());
1719    }
1720
1721    #[test]
1722    fn test_large_enqueue_no_overflow() {
1723        let mut s = make_scheduler();
1724        s.add_stream(1, 10, 1)
1725            .expect("test: add_stream should succeed");
1726        s.enqueue_bytes(1, u64::MAX / 2)
1727            .expect("test: enqueue_bytes should succeed");
1728        s.enqueue_bytes(1, 1).unwrap_or_default(); // saturating add
1729                                                   // Should not panic.
1730    }
1731
1732    #[test]
1733    fn test_streams_removed_counter() {
1734        let mut s = make_scheduler();
1735        s.add_stream(1, 10, 1)
1736            .expect("test: add_stream should succeed");
1737        s.remove_stream(1)
1738            .expect("test: remove_stream should succeed");
1739        let stats = s.scheduler_stats();
1740        assert_eq!(stats.streams_removed, 1);
1741    }
1742
1743    #[test]
1744    fn test_htb_refill_clamps_to_burst() {
1745        let mut s = make_scheduler();
1746        s.add_stream(1, 10, 1)
1747            .expect("test: add_stream should succeed");
1748        s.set_htb_params(1, 1000, 5000)
1749            .expect("test: set_htb_params should succeed");
1750        s.htb_refill(1000); // would add 1_000_000 tokens without clamping
1751        let tok = s
1752            .get_stream(1)
1753            .expect("test: stream 1 should exist")
1754            .htb_tokens;
1755        assert_eq!(tok, 5000);
1756    }
1757
1758    #[test]
1759    fn test_schedule_all_policies_no_eligible_returns_none() {
1760        let mut s = make_scheduler();
1761        let policies = [
1762            SpsSchedulingPolicy::StrictPriority,
1763            SpsSchedulingPolicy::WeightedFairQueuing,
1764            SpsSchedulingPolicy::DeficitRoundRobin,
1765            SpsSchedulingPolicy::EarliestDeadlineFirst,
1766            SpsSchedulingPolicy::HierarchicalToken,
1767        ];
1768        for policy in &policies {
1769            assert!(
1770                s.schedule_next(policy).is_none(),
1771                "policy {:?} should return None",
1772                policy
1773            );
1774        }
1775    }
1776
1777    #[test]
1778    fn test_edf_multiple_streams_ordering() {
1779        let mut s = make_scheduler();
1780        let deadlines = [(1u64, 300u64), (2, 100), (3, 200), (4, 50)];
1781        for (id, dl) in &deadlines {
1782            s.add_stream(*id, 10, 1)
1783                .expect("test: add_stream should succeed");
1784            s.set_deadline(*id, *dl)
1785                .expect("test: set_deadline should succeed");
1786            s.enqueue_bytes(*id, 1500)
1787                .expect("test: enqueue_bytes should succeed");
1788        }
1789        let (first, _) = s
1790            .schedule_next(&SpsSchedulingPolicy::EarliestDeadlineFirst)
1791            .expect("test: schedule_next should return Some");
1792        assert_eq!(first, 4); // deadline = 50
1793    }
1794
1795    #[test]
1796    fn test_wfq_no_eligible_returns_none() {
1797        let mut s = make_scheduler();
1798        s.add_stream(1, 10, 1)
1799            .expect("test: add_stream should succeed"); // no pending bytes
1800        let r = s.schedule_next(&SpsSchedulingPolicy::WeightedFairQueuing);
1801        assert!(r.is_none());
1802    }
1803
1804    #[test]
1805    fn test_drr_empty_active_list_rebuilt() {
1806        let mut s = make_scheduler();
1807        s.add_stream(1, 10, 1)
1808            .expect("test: add_stream should succeed");
1809        s.enqueue_bytes(1, 500)
1810            .expect("test: enqueue_bytes should succeed");
1811        // First call builds active list internally.
1812        let r1 = s.schedule_next(&SpsSchedulingPolicy::DeficitRoundRobin);
1813        assert!(r1.is_some());
1814    }
1815
1816    #[test]
1817    fn test_fairness_index_in_stats() {
1818        let mut s = make_scheduler();
1819        s.add_stream(1, 10, 1)
1820            .expect("test: add_stream 1 should succeed");
1821        s.add_stream(2, 10, 1)
1822            .expect("test: add_stream 2 should succeed");
1823        s.enqueue_bytes(1, 5000)
1824            .expect("test: enqueue_bytes for stream 1 should succeed");
1825        s.enqueue_bytes(2, 5000)
1826            .expect("test: enqueue_bytes for stream 2 should succeed");
1827        s.schedule_batch(&SpsSchedulingPolicy::WeightedFairQueuing, 50);
1828        let stats = s.scheduler_stats();
1829        assert!((0.0..=1.0 + 1e-9).contains(&stats.fairness_index));
1830    }
1831
1832    #[test]
1833    fn test_update_priority_no_pending_no_queue_change() {
1834        let mut s = make_scheduler();
1835        s.add_stream(1, 10, 1)
1836            .expect("test: add_stream should succeed");
1837        // No pending bytes, update priority should not error.
1838        s.update_priority(1, 50)
1839            .expect("test: update_priority should succeed");
1840        assert_eq!(
1841            s.get_stream(1)
1842                .expect("test: stream 1 should exist")
1843                .priority,
1844            50
1845        );
1846    }
1847
1848    #[test]
1849    fn test_scheduler_stats_avg_wait_zero_before_any_schedule() {
1850        let mut s = make_scheduler();
1851        s.add_stream(1, 10, 1)
1852            .expect("test: add_stream should succeed");
1853        let stats = s.scheduler_stats();
1854        assert_eq!(stats.avg_wait, 0.0);
1855    }
1856
1857    #[test]
1858    fn test_drr_run_round_with_no_streams() {
1859        let mut s = make_scheduler();
1860        let results = s.run_drr_round();
1861        assert!(results.is_empty());
1862    }
1863
1864    #[test]
1865    fn test_blocked_stream_not_scheduled() {
1866        let mut s = make_scheduler();
1867        s.add_stream(1, 100, 1)
1868            .expect("test: add_stream 1 should succeed");
1869        s.add_stream(2, 10, 1)
1870            .expect("test: add_stream 2 should succeed");
1871        s.enqueue_bytes(1, 5000)
1872            .expect("test: enqueue_bytes for stream 1 should succeed");
1873        s.enqueue_bytes(2, 5000)
1874            .expect("test: enqueue_bytes for stream 2 should succeed");
1875        s.block_stream(1)
1876            .expect("test: block_stream should succeed");
1877        let (id, _) = s
1878            .schedule_next(&SpsSchedulingPolicy::StrictPriority)
1879            .expect("test: schedule_next should return Some");
1880        assert_eq!(id, 2); // blocked stream 1 should not be chosen
1881    }
1882
1883    #[test]
1884    fn test_stream_is_eligible_after_enqueue() {
1885        let mut s = make_scheduler();
1886        s.add_stream(1, 10, 1)
1887            .expect("test: add_stream should succeed");
1888        assert!(!s
1889            .get_stream(1)
1890            .expect("test: stream 1 should exist")
1891            .is_eligible());
1892        s.enqueue_bytes(1, 100)
1893            .expect("test: enqueue_bytes should succeed");
1894        assert!(s
1895            .get_stream(1)
1896            .expect("test: stream 1 should exist")
1897            .is_eligible());
1898    }
1899
1900    #[test]
1901    fn test_stream_not_eligible_when_blocked_even_with_data() {
1902        let mut s = make_scheduler();
1903        s.add_stream(1, 10, 1)
1904            .expect("test: add_stream should succeed");
1905        s.enqueue_bytes(1, 100)
1906            .expect("test: enqueue_bytes should succeed");
1907        s.block_stream(1)
1908            .expect("test: block_stream should succeed");
1909        assert!(!s
1910            .get_stream(1)
1911            .expect("test: stream 1 should exist")
1912            .is_eligible());
1913    }
1914
1915    #[test]
1916    fn test_sps_stream_new_defaults() {
1917        let stream = SpsStream::new(42, 100, 5);
1918        assert_eq!(stream.id, 42);
1919        assert_eq!(stream.priority, 100);
1920        assert_eq!(stream.weight, 5);
1921        assert_eq!(stream.pending_bytes, 0);
1922        assert!(!stream.is_blocked);
1923    }
1924
1925    #[test]
1926    fn test_sps_error_display() {
1927        let e = SpsError::StreamNotFound(5);
1928        assert!(e.to_string().contains("5"));
1929    }
1930
1931    #[test]
1932    fn test_add_multiple_streams_different_priorities() {
1933        let mut s = make_scheduler();
1934        for i in 0u64..10 {
1935            s.add_stream(i, (i * 10) as u32, 1)
1936                .expect("test: add_stream should succeed");
1937        }
1938        assert_eq!(s.stream_count(), 10);
1939    }
1940
1941    #[test]
1942    fn test_remove_and_readd_same_id() {
1943        let mut s = make_scheduler();
1944        s.add_stream(1, 10, 1)
1945            .expect("test: add_stream should succeed");
1946        s.remove_stream(1)
1947            .expect("test: first remove_stream should succeed");
1948        s.add_stream(1, 20, 2)
1949            .expect("test: re-add_stream should succeed");
1950        assert_eq!(
1951            s.get_stream(1)
1952                .expect("test: stream 1 should exist after re-add")
1953                .priority,
1954            20
1955        );
1956    }
1957
1958    #[test]
1959    fn test_htb_fallback_with_no_tokens() {
1960        let mut s = make_scheduler();
1961        s.add_stream(1, 10, 1)
1962            .expect("test: add_stream 1 should succeed");
1963        s.add_stream(2, 20, 1)
1964            .expect("test: add_stream 2 should succeed");
1965        s.enqueue_bytes(1, 5000)
1966            .expect("test: enqueue_bytes for stream 1 should succeed");
1967        s.enqueue_bytes(2, 5000)
1968            .expect("test: enqueue_bytes for stream 2 should succeed");
1969        // No refill → falls back to strict priority.
1970        let (id, _) = s
1971            .schedule_next(&SpsSchedulingPolicy::HierarchicalToken)
1972            .expect("test: schedule_next should return Some");
1973        assert_eq!(id, 2); // higher priority wins in fallback
1974    }
1975}