Skip to main content

ipfrs_network/
priority_queue.rs

1//! Multi-level priority queue for outbound peer messages.
2//!
3//! Provides [`PeerPriorityQueue`] with three priority tiers (Urgent, Normal,
4//! Background), per-peer fairness enforcement, and a global byte-budget cap.
5//!
6//! # Design
7//!
8//! Messages are placed into one of three FIFO sub-queues based on their
9//! [`MessagePriority`].  On dequeue, the implementation drains the highest
10//! non-empty tier first (Urgent → Normal → Background), guaranteeing strict
11//! priority ordering while still allowing lower-priority traffic to be sent
12//! whenever the higher tiers are empty.
13//!
14//! Backpressure is enforced in two independent dimensions:
15//!
16//! * **Global byte budget** — the sum of all `payload_bytes` across all
17//!   enqueued messages must not exceed [`QueueConfig::max_total_bytes`].
18//! * **Per-peer message cap** — a single peer may not have more than
19//!   [`QueueConfig::max_per_peer_messages`] messages pending (across all
20//!   priority levels).
21//!
22//! When either limit would be exceeded the incoming message is dropped and
23//! [`QueueStats::total_dropped`] is incremented.
24
25use std::collections::HashMap;
26
27// ─── MessagePriority ──────────────────────────────────────────────────────────
28
29/// Priority tier for a queued peer message.
30///
31/// The discriminants are chosen so that `Urgent < Normal < Background` via the
32/// derived `Ord` implementation; however the *dequeue* logic treats `Urgent` as
33/// the **highest** priority (it is drained first).  The ascending-integer
34/// convention mirrors C-style enum tables where 0 == most important.
35#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
36#[repr(u8)]
37pub enum MessagePriority {
38    /// Highest priority — dequeued before all others.
39    Urgent = 0,
40    /// Mid-level priority.
41    Normal = 1,
42    /// Lowest priority — only sent when Urgent and Normal are empty.
43    Background = 2,
44}
45
46// ─── QueuedMessage ────────────────────────────────────────────────────────────
47
48/// A message waiting to be sent to a remote peer.
49#[derive(Clone, Debug, PartialEq, Eq)]
50pub struct QueuedMessage {
51    /// Unique identifier for the message.
52    pub message_id: u64,
53    /// Destination peer identifier.
54    pub peer_id: String,
55    /// Priority tier that determines the sub-queue this message is placed in.
56    pub priority: MessagePriority,
57    /// Size of the message payload in bytes, used for byte-budget accounting.
58    pub payload_bytes: u64,
59    /// Logical clock value at the time the message was enqueued.
60    pub enqueued_at_tick: u64,
61}
62
63// ─── QueueConfig ─────────────────────────────────────────────────────────────
64
65/// Configuration for [`PeerPriorityQueue`].
66#[derive(Clone, Debug)]
67pub struct QueueConfig {
68    /// Maximum total payload bytes that may be buffered at once (default 10 MB).
69    pub max_total_bytes: u64,
70    /// Maximum number of pending messages per peer across all priority levels
71    /// (default 100).
72    pub max_per_peer_messages: usize,
73    /// Relative dequeue weight for the Urgent tier (default 4).
74    pub urgent_weight: u32,
75    /// Relative dequeue weight for the Normal tier (default 2).
76    pub normal_weight: u32,
77    /// Relative dequeue weight for the Background tier (default 1).
78    pub background_weight: u32,
79}
80
81impl Default for QueueConfig {
82    fn default() -> Self {
83        Self {
84            max_total_bytes: 10_485_760, // 10 MB
85            max_per_peer_messages: 100,
86            urgent_weight: 4,
87            normal_weight: 2,
88            background_weight: 1,
89        }
90    }
91}
92
93// ─── QueueStats ──────────────────────────────────────────────────────────────
94
95/// Running statistics for a [`PeerPriorityQueue`].
96#[derive(Clone, Debug, Default)]
97pub struct QueueStats {
98    /// Total messages successfully enqueued over the lifetime of the queue.
99    pub total_enqueued: u64,
100    /// Total messages successfully dequeued over the lifetime of the queue.
101    pub total_dequeued: u64,
102    /// Total messages dropped due to budget exhaustion or per-peer limits.
103    pub total_dropped: u64,
104    /// Current total payload bytes held across all sub-queues.
105    pub current_bytes: u64,
106    /// Current total message count across all sub-queues.
107    pub current_message_count: usize,
108    /// Current message count per priority tier.
109    pub by_priority: HashMap<MessagePriority, usize>,
110}
111
112// ─── PeerPriorityQueue ───────────────────────────────────────────────────────
113
114/// Multi-level priority queue for outbound peer messages.
115///
116/// See the [module documentation](self) for a full description of the
117/// backpressure strategy and dequeue ordering.
118pub struct PeerPriorityQueue {
119    /// FIFO sub-queue for [`MessagePriority::Urgent`] messages.
120    pub urgent: Vec<QueuedMessage>,
121    /// FIFO sub-queue for [`MessagePriority::Normal`] messages.
122    pub normal: Vec<QueuedMessage>,
123    /// FIFO sub-queue for [`MessagePriority::Background`] messages.
124    pub background: Vec<QueuedMessage>,
125    /// Current pending message count keyed by peer identifier.
126    pub peer_counts: HashMap<String, usize>,
127    /// Sum of `payload_bytes` for all currently queued messages.
128    pub current_bytes: u64,
129    /// Configuration controlling limits and weights.
130    pub config: QueueConfig,
131    /// Cumulative statistics.
132    pub stats: QueueStats,
133}
134
135impl PeerPriorityQueue {
136    /// Create a new queue with the supplied configuration.
137    pub fn new(config: QueueConfig) -> Self {
138        Self {
139            urgent: Vec::new(),
140            normal: Vec::new(),
141            background: Vec::new(),
142            peer_counts: HashMap::new(),
143            current_bytes: 0,
144            config,
145            stats: QueueStats::default(),
146        }
147    }
148
149    /// Attempt to enqueue `msg`.
150    ///
151    /// Returns `true` when the message was accepted, `false` when it was
152    /// dropped because either the global byte budget or the per-peer message
153    /// cap would be exceeded.
154    pub fn enqueue(&mut self, msg: QueuedMessage) -> bool {
155        // ── backpressure checks ──────────────────────────────────────────────
156        let would_exceed_bytes = self
157            .current_bytes
158            .checked_add(msg.payload_bytes)
159            .is_none_or(|total| total > self.config.max_total_bytes);
160
161        let peer_count = self
162            .peer_counts
163            .get(msg.peer_id.as_str())
164            .copied()
165            .unwrap_or(0);
166        let would_exceed_peer = peer_count >= self.config.max_per_peer_messages;
167
168        if would_exceed_bytes || would_exceed_peer {
169            self.stats.total_dropped += 1;
170            return false;
171        }
172
173        // ── accept ───────────────────────────────────────────────────────────
174        self.current_bytes += msg.payload_bytes;
175        *self.peer_counts.entry(msg.peer_id.clone()).or_insert(0) += 1;
176
177        // Update by_priority counter.
178        *self.stats.by_priority.entry(msg.priority).or_insert(0) += 1;
179
180        self.stats.total_enqueued += 1;
181        self.stats.current_bytes = self.current_bytes;
182        self.stats.current_message_count += 1;
183
184        match msg.priority {
185            MessagePriority::Urgent => self.urgent.push(msg),
186            MessagePriority::Normal => self.normal.push(msg),
187            MessagePriority::Background => self.background.push(msg),
188        }
189
190        true
191    }
192
193    /// Dequeue the highest-priority available message.
194    ///
195    /// Priority order is Urgent → Normal → Background.  Returns `None` when
196    /// all sub-queues are empty.
197    pub fn dequeue(&mut self) -> Option<QueuedMessage> {
198        // Pick the highest non-empty tier.
199        let msg = if !self.urgent.is_empty() {
200            self.urgent.remove(0)
201        } else if !self.normal.is_empty() {
202            self.normal.remove(0)
203        } else if !self.background.is_empty() {
204            self.background.remove(0)
205        } else {
206            return None;
207        };
208
209        // ── bookkeeping ──────────────────────────────────────────────────────
210        self.current_bytes = self.current_bytes.saturating_sub(msg.payload_bytes);
211
212        // Decrement peer count; remove the key when it hits zero to keep the
213        // map tidy and let later limit checks work correctly.
214        let peer_count = self.peer_counts.entry(msg.peer_id.clone()).or_insert(0);
215        if *peer_count > 0 {
216            *peer_count -= 1;
217        }
218        if *peer_count == 0 {
219            self.peer_counts.remove(&msg.peer_id);
220        }
221
222        // Decrement by_priority counter.
223        if let Some(cnt) = self.stats.by_priority.get_mut(&msg.priority) {
224            *cnt = cnt.saturating_sub(1);
225        }
226
227        self.stats.total_dequeued += 1;
228        self.stats.current_bytes = self.current_bytes;
229        self.stats.current_message_count = self.stats.current_message_count.saturating_sub(1);
230
231        Some(msg)
232    }
233
234    /// Dequeue up to `n` messages, returning however many were available.
235    pub fn dequeue_n(&mut self, n: usize) -> Vec<QueuedMessage> {
236        let mut out = Vec::with_capacity(n);
237        for _ in 0..n {
238            match self.dequeue() {
239                Some(msg) => out.push(msg),
240                None => break,
241            }
242        }
243        out
244    }
245
246    /// Total number of messages across all sub-queues.
247    pub fn len(&self) -> usize {
248        self.urgent.len() + self.normal.len() + self.background.len()
249    }
250
251    /// Returns `true` when all sub-queues are empty.
252    pub fn is_empty(&self) -> bool {
253        self.urgent.is_empty() && self.normal.is_empty() && self.background.is_empty()
254    }
255
256    /// Reference to the current queue statistics.
257    pub fn stats(&self) -> &QueueStats {
258        &self.stats
259    }
260
261    /// Remove all pending messages for `peer_id`.
262    ///
263    /// Returns the number of messages removed.  Updates `current_bytes`,
264    /// `peer_counts`, and the relevant `stats` fields.
265    pub fn remove_peer(&mut self, peer_id: &str) -> usize {
266        let mut removed = 0usize;
267
268        // Helper closure to drain a sub-queue and accumulate byte/priority stats.
269        macro_rules! drain_queue {
270            ($queue:expr) => {{
271                let before = $queue.len();
272                let mut freed_bytes = 0u64;
273                let mut by_prio: HashMap<MessagePriority, usize> = HashMap::new();
274
275                $queue.retain(|msg| {
276                    if msg.peer_id == peer_id {
277                        freed_bytes += msg.payload_bytes;
278                        *by_prio.entry(msg.priority).or_insert(0) += 1;
279                        false
280                    } else {
281                        true
282                    }
283                });
284
285                let count = before - $queue.len();
286                removed += count;
287                self.current_bytes = self.current_bytes.saturating_sub(freed_bytes);
288
289                for (prio, n) in by_prio {
290                    if let Some(c) = self.stats.by_priority.get_mut(&prio) {
291                        *c = c.saturating_sub(n);
292                    }
293                }
294            }};
295        }
296
297        drain_queue!(self.urgent);
298        drain_queue!(self.normal);
299        drain_queue!(self.background);
300
301        // Remove the peer from the per-peer map entirely.
302        self.peer_counts.remove(peer_id);
303
304        // Sync aggregate stats.
305        self.stats.current_bytes = self.current_bytes;
306        self.stats.current_message_count = self.stats.current_message_count.saturating_sub(removed);
307
308        removed
309    }
310}
311
312// ─── Tests ───────────────────────────────────────────────────────────────────
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317
318    // ── helpers ──────────────────────────────────────────────────────────────
319
320    fn default_queue() -> PeerPriorityQueue {
321        PeerPriorityQueue::new(QueueConfig::default())
322    }
323
324    fn msg(id: u64, peer: &str, priority: MessagePriority, bytes: u64) -> QueuedMessage {
325        QueuedMessage {
326            message_id: id,
327            peer_id: peer.to_string(),
328            priority,
329            payload_bytes: bytes,
330            enqueued_at_tick: id,
331        }
332    }
333
334    // ── enqueue / sub-queue placement ────────────────────────────────────────
335
336    #[test]
337    fn enqueue_urgent_goes_to_urgent_queue() {
338        let mut q = default_queue();
339        assert!(q.enqueue(msg(1, "peer-a", MessagePriority::Urgent, 100)));
340        assert_eq!(q.urgent.len(), 1);
341        assert_eq!(q.normal.len(), 0);
342        assert_eq!(q.background.len(), 0);
343    }
344
345    #[test]
346    fn enqueue_normal_goes_to_normal_queue() {
347        let mut q = default_queue();
348        assert!(q.enqueue(msg(1, "peer-a", MessagePriority::Normal, 100)));
349        assert_eq!(q.normal.len(), 1);
350        assert_eq!(q.urgent.len(), 0);
351        assert_eq!(q.background.len(), 0);
352    }
353
354    #[test]
355    fn enqueue_background_goes_to_background_queue() {
356        let mut q = default_queue();
357        assert!(q.enqueue(msg(1, "peer-a", MessagePriority::Background, 100)));
358        assert_eq!(q.background.len(), 1);
359        assert_eq!(q.urgent.len(), 0);
360        assert_eq!(q.normal.len(), 0);
361    }
362
363    #[test]
364    fn enqueue_increments_stats() {
365        let mut q = default_queue();
366        q.enqueue(msg(1, "peer-a", MessagePriority::Normal, 256));
367        assert_eq!(q.stats().total_enqueued, 1);
368        assert_eq!(q.stats().current_message_count, 1);
369        assert_eq!(q.stats().current_bytes, 256);
370    }
371
372    // ── byte budget enforcement ───────────────────────────────────────────────
373
374    #[test]
375    fn enqueue_drops_when_byte_budget_exceeded() {
376        let config = QueueConfig {
377            max_total_bytes: 500,
378            ..Default::default()
379        };
380        let mut q = PeerPriorityQueue::new(config);
381
382        // Fill up to exactly 500 bytes.
383        assert!(q.enqueue(msg(1, "peer-a", MessagePriority::Normal, 500)));
384        // Next message would exceed budget.
385        assert!(!q.enqueue(msg(2, "peer-a", MessagePriority::Normal, 1)));
386        assert_eq!(q.stats().total_dropped, 1);
387        assert_eq!(q.len(), 1);
388    }
389
390    #[test]
391    fn enqueue_accepts_when_byte_budget_exactly_full() {
392        let config = QueueConfig {
393            max_total_bytes: 1000,
394            ..Default::default()
395        };
396        let mut q = PeerPriorityQueue::new(config);
397        // Two messages totalling exactly 1000 bytes should both be accepted.
398        assert!(q.enqueue(msg(1, "peer-a", MessagePriority::Normal, 600)));
399        assert!(q.enqueue(msg(2, "peer-a", MessagePriority::Normal, 400)));
400        assert_eq!(q.len(), 2);
401        assert_eq!(q.stats().total_dropped, 0);
402    }
403
404    #[test]
405    fn enqueue_byte_budget_recovers_after_dequeue() {
406        let config = QueueConfig {
407            max_total_bytes: 500,
408            ..Default::default()
409        };
410        let mut q = PeerPriorityQueue::new(config);
411        assert!(q.enqueue(msg(1, "peer-a", MessagePriority::Normal, 400)));
412        // Would exceed budget.
413        assert!(!q.enqueue(msg(2, "peer-a", MessagePriority::Normal, 200)));
414        // Dequeue to free space.
415        let _ = q.dequeue();
416        // Now there's room.
417        assert!(q.enqueue(msg(3, "peer-a", MessagePriority::Normal, 200)));
418        assert_eq!(q.len(), 1);
419    }
420
421    // ── per-peer message limit ────────────────────────────────────────────────
422
423    #[test]
424    fn enqueue_drops_when_per_peer_limit_exceeded() {
425        let config = QueueConfig {
426            max_per_peer_messages: 3,
427            max_total_bytes: u64::MAX,
428            ..Default::default()
429        };
430        let mut q = PeerPriorityQueue::new(config);
431        assert!(q.enqueue(msg(1, "peer-a", MessagePriority::Normal, 10)));
432        assert!(q.enqueue(msg(2, "peer-a", MessagePriority::Normal, 10)));
433        assert!(q.enqueue(msg(3, "peer-a", MessagePriority::Normal, 10)));
434        // 4th message for same peer should be dropped.
435        assert!(!q.enqueue(msg(4, "peer-a", MessagePriority::Normal, 10)));
436        assert_eq!(q.stats().total_dropped, 1);
437    }
438
439    #[test]
440    fn per_peer_limit_is_per_peer_not_global() {
441        let config = QueueConfig {
442            max_per_peer_messages: 2,
443            max_total_bytes: u64::MAX,
444            ..Default::default()
445        };
446        let mut q = PeerPriorityQueue::new(config);
447        assert!(q.enqueue(msg(1, "peer-a", MessagePriority::Normal, 10)));
448        assert!(q.enqueue(msg(2, "peer-a", MessagePriority::Normal, 10)));
449        // peer-a is at limit but peer-b is not.
450        assert!(!q.enqueue(msg(3, "peer-a", MessagePriority::Normal, 10)));
451        assert!(q.enqueue(msg(4, "peer-b", MessagePriority::Normal, 10)));
452        assert_eq!(q.stats().total_dropped, 1);
453        assert_eq!(q.len(), 3);
454    }
455
456    // ── dequeue priority ordering ─────────────────────────────────────────────
457
458    #[test]
459    fn dequeue_returns_urgent_before_normal() {
460        let mut q = default_queue();
461        q.enqueue(msg(1, "peer-a", MessagePriority::Normal, 10));
462        q.enqueue(msg(2, "peer-a", MessagePriority::Urgent, 10));
463        let first = q.dequeue().expect("should have a message");
464        assert_eq!(first.priority, MessagePriority::Urgent);
465    }
466
467    #[test]
468    fn dequeue_returns_urgent_before_background() {
469        let mut q = default_queue();
470        q.enqueue(msg(1, "peer-a", MessagePriority::Background, 10));
471        q.enqueue(msg(2, "peer-a", MessagePriority::Urgent, 10));
472        let first = q.dequeue().expect("should have a message");
473        assert_eq!(first.priority, MessagePriority::Urgent);
474    }
475
476    #[test]
477    fn dequeue_returns_normal_when_urgent_empty() {
478        let mut q = default_queue();
479        q.enqueue(msg(1, "peer-a", MessagePriority::Background, 10));
480        q.enqueue(msg(2, "peer-a", MessagePriority::Normal, 10));
481        let first = q.dequeue().expect("should have a message");
482        assert_eq!(first.priority, MessagePriority::Normal);
483    }
484
485    #[test]
486    fn dequeue_returns_background_last() {
487        let mut q = default_queue();
488        q.enqueue(msg(1, "peer-a", MessagePriority::Background, 10));
489        q.enqueue(msg(2, "peer-a", MessagePriority::Normal, 10));
490        q.enqueue(msg(3, "peer-a", MessagePriority::Urgent, 10));
491        let m1 = q.dequeue().expect("msg");
492        let m2 = q.dequeue().expect("msg");
493        let m3 = q.dequeue().expect("msg");
494        assert_eq!(m1.priority, MessagePriority::Urgent);
495        assert_eq!(m2.priority, MessagePriority::Normal);
496        assert_eq!(m3.priority, MessagePriority::Background);
497    }
498
499    #[test]
500    fn dequeue_returns_none_when_all_empty() {
501        let mut q = default_queue();
502        assert!(q.dequeue().is_none());
503    }
504
505    #[test]
506    fn dequeue_updates_stats() {
507        let mut q = default_queue();
508        q.enqueue(msg(1, "peer-a", MessagePriority::Normal, 100));
509        let _ = q.dequeue();
510        assert_eq!(q.stats().total_dequeued, 1);
511        assert_eq!(q.stats().current_message_count, 0);
512        assert_eq!(q.stats().current_bytes, 0);
513    }
514
515    // ── FIFO ordering within a priority tier ─────────────────────────────────
516
517    #[test]
518    fn fifo_ordering_within_urgent() {
519        let mut q = default_queue();
520        q.enqueue(msg(1, "peer-a", MessagePriority::Urgent, 10));
521        q.enqueue(msg(2, "peer-a", MessagePriority::Urgent, 10));
522        q.enqueue(msg(3, "peer-a", MessagePriority::Urgent, 10));
523        assert_eq!(
524            q.dequeue()
525                .expect("test: dequeue should return urgent message with id 1")
526                .message_id,
527            1
528        );
529        assert_eq!(
530            q.dequeue()
531                .expect("test: dequeue should return urgent message with id 2")
532                .message_id,
533            2
534        );
535        assert_eq!(
536            q.dequeue()
537                .expect("test: dequeue should return urgent message with id 3")
538                .message_id,
539            3
540        );
541    }
542
543    #[test]
544    fn fifo_ordering_within_normal() {
545        let mut q = default_queue();
546        for id in [10u64, 20, 30] {
547            q.enqueue(msg(id, "peer-b", MessagePriority::Normal, 5));
548        }
549        assert_eq!(
550            q.dequeue()
551                .expect("test: dequeue should return normal message with id 10")
552                .message_id,
553            10
554        );
555        assert_eq!(
556            q.dequeue()
557                .expect("test: dequeue should return normal message with id 20")
558                .message_id,
559            20
560        );
561        assert_eq!(
562            q.dequeue()
563                .expect("test: dequeue should return normal message with id 30")
564                .message_id,
565            30
566        );
567    }
568
569    #[test]
570    fn fifo_ordering_within_background() {
571        let mut q = default_queue();
572        for id in [100u64, 200, 300] {
573            q.enqueue(msg(id, "peer-c", MessagePriority::Background, 5));
574        }
575        assert_eq!(
576            q.dequeue()
577                .expect("test: dequeue should return background message with id 100")
578                .message_id,
579            100
580        );
581        assert_eq!(
582            q.dequeue()
583                .expect("test: dequeue should return background message with id 200")
584                .message_id,
585            200
586        );
587        assert_eq!(
588            q.dequeue()
589                .expect("test: dequeue should return background message with id 300")
590                .message_id,
591            300
592        );
593    }
594
595    // ── dequeue_n ────────────────────────────────────────────────────────────
596
597    #[test]
598    fn dequeue_n_returns_up_to_n() {
599        let mut q = default_queue();
600        for id in 1u64..=5 {
601            q.enqueue(msg(id, "peer-a", MessagePriority::Normal, 10));
602        }
603        let drained = q.dequeue_n(3);
604        assert_eq!(drained.len(), 3);
605        assert_eq!(q.len(), 2);
606    }
607
608    #[test]
609    fn dequeue_n_returns_fewer_when_queue_has_less() {
610        let mut q = default_queue();
611        q.enqueue(msg(1, "peer-a", MessagePriority::Normal, 10));
612        q.enqueue(msg(2, "peer-a", MessagePriority::Normal, 10));
613        let drained = q.dequeue_n(10);
614        assert_eq!(drained.len(), 2);
615        assert!(q.is_empty());
616    }
617
618    #[test]
619    fn dequeue_n_zero_returns_empty_vec() {
620        let mut q = default_queue();
621        q.enqueue(msg(1, "peer-a", MessagePriority::Normal, 10));
622        let drained = q.dequeue_n(0);
623        assert!(drained.is_empty());
624        assert_eq!(q.len(), 1);
625    }
626
627    // ── remove_peer ───────────────────────────────────────────────────────────
628
629    #[test]
630    fn remove_peer_removes_all_messages_for_peer() {
631        let mut q = default_queue();
632        q.enqueue(msg(1, "peer-a", MessagePriority::Urgent, 10));
633        q.enqueue(msg(2, "peer-a", MessagePriority::Normal, 20));
634        q.enqueue(msg(3, "peer-a", MessagePriority::Background, 30));
635        q.enqueue(msg(4, "peer-b", MessagePriority::Normal, 40));
636
637        let removed = q.remove_peer("peer-a");
638        assert_eq!(removed, 3);
639        assert_eq!(q.len(), 1);
640        assert_eq!(
641            q.dequeue()
642                .expect("test: dequeue should return peer-b message after remove_peer")
643                .peer_id,
644            "peer-b"
645        );
646    }
647
648    #[test]
649    fn remove_peer_updates_current_bytes() {
650        let mut q = default_queue();
651        q.enqueue(msg(1, "peer-a", MessagePriority::Normal, 300));
652        q.enqueue(msg(2, "peer-b", MessagePriority::Normal, 200));
653        q.remove_peer("peer-a");
654        assert_eq!(q.current_bytes, 200);
655        assert_eq!(q.stats().current_bytes, 200);
656    }
657
658    #[test]
659    fn remove_peer_for_unknown_peer_returns_zero() {
660        let mut q = default_queue();
661        q.enqueue(msg(1, "peer-a", MessagePriority::Normal, 10));
662        let removed = q.remove_peer("peer-x");
663        assert_eq!(removed, 0);
664        assert_eq!(q.len(), 1);
665    }
666
667    // ── stats.by_priority ────────────────────────────────────────────────────
668
669    #[test]
670    fn by_priority_tracks_enqueued_counts() {
671        let mut q = default_queue();
672        q.enqueue(msg(1, "peer-a", MessagePriority::Urgent, 10));
673        q.enqueue(msg(2, "peer-a", MessagePriority::Urgent, 10));
674        q.enqueue(msg(3, "peer-a", MessagePriority::Normal, 10));
675        let stats = q.stats();
676        assert_eq!(
677            stats
678                .by_priority
679                .get(&MessagePriority::Urgent)
680                .copied()
681                .unwrap_or(0),
682            2
683        );
684        assert_eq!(
685            stats
686                .by_priority
687                .get(&MessagePriority::Normal)
688                .copied()
689                .unwrap_or(0),
690            1
691        );
692        assert_eq!(
693            stats
694                .by_priority
695                .get(&MessagePriority::Background)
696                .copied()
697                .unwrap_or(0),
698            0
699        );
700    }
701
702    #[test]
703    fn by_priority_decrements_on_dequeue() {
704        let mut q = default_queue();
705        q.enqueue(msg(1, "peer-a", MessagePriority::Urgent, 10));
706        q.enqueue(msg(2, "peer-a", MessagePriority::Urgent, 10));
707        q.dequeue();
708        let stats = q.stats();
709        assert_eq!(
710            stats
711                .by_priority
712                .get(&MessagePriority::Urgent)
713                .copied()
714                .unwrap_or(0),
715            1
716        );
717    }
718
719    // ── current_bytes tracking ────────────────────────────────────────────────
720
721    #[test]
722    fn current_bytes_increases_on_enqueue() {
723        let mut q = default_queue();
724        q.enqueue(msg(1, "peer-a", MessagePriority::Normal, 128));
725        q.enqueue(msg(2, "peer-a", MessagePriority::Normal, 256));
726        assert_eq!(q.current_bytes, 384);
727    }
728
729    #[test]
730    fn current_bytes_decreases_on_dequeue() {
731        let mut q = default_queue();
732        q.enqueue(msg(1, "peer-a", MessagePriority::Normal, 512));
733        q.enqueue(msg(2, "peer-a", MessagePriority::Normal, 256));
734        q.dequeue();
735        assert_eq!(q.current_bytes, 256);
736    }
737
738    #[test]
739    fn current_bytes_zero_after_all_dequeued() {
740        let mut q = default_queue();
741        q.enqueue(msg(1, "peer-a", MessagePriority::Urgent, 100));
742        q.enqueue(msg(2, "peer-b", MessagePriority::Normal, 200));
743        q.dequeue();
744        q.dequeue();
745        assert_eq!(q.current_bytes, 0);
746    }
747
748    // ── len / is_empty ───────────────────────────────────────────────────────
749
750    #[test]
751    fn len_sums_all_sub_queues() {
752        let mut q = default_queue();
753        q.enqueue(msg(1, "peer-a", MessagePriority::Urgent, 10));
754        q.enqueue(msg(2, "peer-a", MessagePriority::Normal, 10));
755        q.enqueue(msg(3, "peer-a", MessagePriority::Background, 10));
756        assert_eq!(q.len(), 3);
757    }
758
759    #[test]
760    fn is_empty_true_when_no_messages() {
761        let q = default_queue();
762        assert!(q.is_empty());
763    }
764
765    #[test]
766    fn is_empty_false_after_enqueue() {
767        let mut q = default_queue();
768        q.enqueue(msg(1, "peer-a", MessagePriority::Normal, 10));
769        assert!(!q.is_empty());
770    }
771}