1use std::collections::HashMap;
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
25pub enum MessagePriority {
26 Background = 0,
28 Normal = 1,
30 High = 2,
32 Urgent = 3,
34}
35
36impl MessagePriority {
37 fn promote(self) -> Self {
39 match self {
40 Self::Background => Self::Normal,
41 Self::Normal => Self::High,
42 Self::High => Self::Urgent,
43 Self::Urgent => Self::Urgent,
44 }
45 }
46}
47
48#[derive(Clone, Debug, PartialEq, Eq)]
52pub struct PrioritizedMessage {
53 pub msg_id: u64,
55 pub peer_id: String,
57 pub priority: MessagePriority,
59 pub payload_bytes: u64,
61 pub enqueued_tick: u64,
63 pub effective_priority: MessagePriority,
65}
66
67#[derive(Clone, Debug)]
71pub struct AgingConfig {
72 pub promote_after_ticks: u64,
75 pub max_queue_size: usize,
77}
78
79impl Default for AgingConfig {
80 fn default() -> Self {
81 Self {
82 promote_after_ticks: 50,
83 max_queue_size: 256,
84 }
85 }
86}
87
88#[derive(Clone, Debug, Default)]
92pub struct PrioritizerStats {
93 pub total_enqueued: u64,
96 pub total_dequeued: u64,
98 pub total_promoted: u64,
100 pub total_dropped: u64,
102}
103
104impl PrioritizerStats {
105 pub fn throughput_ratio(&self) -> f64 {
109 self.total_dequeued as f64 / self.total_enqueued.max(1) as f64
110 }
111}
112
113pub struct PeerMessagePrioritizer {
124 pub queues: HashMap<String, Vec<PrioritizedMessage>>,
126 pub config: AgingConfig,
128 pub stats: PrioritizerStats,
130 pub current_tick: u64,
132 pub next_id: u64,
134}
135
136impl PeerMessagePrioritizer {
137 pub fn new(config: AgingConfig) -> Self {
139 Self {
140 queues: HashMap::new(),
141 config,
142 stats: PrioritizerStats::default(),
143 current_tick: 0,
144 next_id: 1,
145 }
146 }
147
148 fn insert_sorted(queue: &mut Vec<PrioritizedMessage>, msg: PrioritizedMessage) {
155 let pos = queue.partition_point(|m| {
157 m.effective_priority > msg.effective_priority
159 || (m.effective_priority == msg.effective_priority
160 && m.enqueued_tick <= msg.enqueued_tick)
161 });
162 queue.insert(pos, msg);
163 }
164
165 fn drop_lowest(queue: &mut Vec<PrioritizedMessage>) -> bool {
169 if queue.is_empty() {
170 return false;
171 }
172 let lowest_prio = queue.last().map(|m| m.effective_priority);
176 if let Some(lp) = lowest_prio {
177 let victim = queue
180 .iter()
181 .enumerate()
182 .rev() .take_while(|(_, m)| m.effective_priority == lp)
184 .max_by_key(|(_, m)| m.enqueued_tick)
185 .map(|(i, _)| i);
186
187 if let Some(idx) = victim {
188 queue.remove(idx);
189 return true;
190 }
191 }
192 false
193 }
194
195 pub fn enqueue(
206 &mut self,
207 peer_id: String,
208 priority: MessagePriority,
209 payload_bytes: u64,
210 ) -> Option<u64> {
211 let queue = self.queues.entry(peer_id.clone()).or_default();
212
213 if queue.len() >= self.config.max_queue_size {
214 if Self::drop_lowest(queue) {
216 self.stats.total_dropped += 1;
217 }
218 if queue.len() >= self.config.max_queue_size {
220 return None;
221 }
222 }
223
224 let msg_id = self.next_id;
225 self.next_id += 1;
226
227 let msg = PrioritizedMessage {
228 msg_id,
229 peer_id,
230 priority,
231 payload_bytes,
232 enqueued_tick: self.current_tick,
233 effective_priority: priority,
234 };
235
236 Self::insert_sorted(queue, msg);
237 self.stats.total_enqueued += 1;
238 Some(msg_id)
239 }
240
241 pub fn dequeue(&mut self, peer_id: &str) -> Option<PrioritizedMessage> {
244 let queue = self.queues.get_mut(peer_id)?;
245 if queue.is_empty() {
246 return None;
247 }
248 let msg = queue.remove(0);
249 self.stats.total_dequeued += 1;
250 Some(msg)
251 }
252
253 pub fn advance_tick(&mut self) {
260 self.current_tick += 1;
261
262 let promote_after = self.config.promote_after_ticks;
263 let current_tick = self.current_tick;
264
265 for queue in self.queues.values_mut() {
266 let mut any_promoted = false;
267
268 for msg in queue.iter_mut() {
269 if msg.effective_priority < MessagePriority::Urgent
270 && current_tick.saturating_sub(msg.enqueued_tick) >= promote_after
271 {
272 msg.effective_priority = msg.effective_priority.promote();
273 self.stats.total_promoted += 1;
274 any_promoted = true;
275 }
276 }
277
278 if any_promoted {
279 queue.sort_by(|a, b| {
281 b.effective_priority
282 .cmp(&a.effective_priority)
283 .then_with(|| a.enqueued_tick.cmp(&b.enqueued_tick))
284 });
285 }
286 }
287 }
288
289 pub fn stats(&self) -> &PrioritizerStats {
291 &self.stats
292 }
293
294 pub fn queue_depth(&self, peer_id: &str) -> usize {
296 self.queues.get(peer_id).map_or(0, |q| q.len())
297 }
298}
299
300#[cfg(test)]
303mod tests {
304 use super::*;
305
306 fn default_prioritizer() -> PeerMessagePrioritizer {
307 PeerMessagePrioritizer::new(AgingConfig::default())
308 }
309
310 #[test]
313 fn enqueue_returns_msg_id() {
314 let mut p = default_prioritizer();
315 let id = p.enqueue("peer-a".to_string(), MessagePriority::Normal, 128);
316 assert!(id.is_some(), "enqueue should return Some(id)");
317 assert_eq!(id.expect("test: enqueue should return msg_id"), 1u64);
318 }
319
320 #[test]
323 fn msg_ids_are_monotonically_increasing() {
324 let mut p = default_prioritizer();
325 let id1 = p
326 .enqueue("peer-a".to_string(), MessagePriority::Normal, 64)
327 .expect("test: first enqueue should return msg_id");
328 let id2 = p
329 .enqueue("peer-a".to_string(), MessagePriority::Normal, 64)
330 .expect("test: second enqueue should return msg_id");
331 let id3 = p
332 .enqueue("peer-b".to_string(), MessagePriority::High, 64)
333 .expect("test: third enqueue should return msg_id");
334 assert!(id1 < id2);
335 assert!(id2 < id3);
336 }
337
338 #[test]
341 fn dequeue_highest_priority_first() {
342 let mut p = default_prioritizer();
343 p.enqueue("peer-a".to_string(), MessagePriority::Background, 10)
344 .expect("test: enqueue Background should succeed");
345 p.enqueue("peer-a".to_string(), MessagePriority::Urgent, 10)
346 .expect("test: enqueue Urgent should succeed");
347 p.enqueue("peer-a".to_string(), MessagePriority::Normal, 10)
348 .expect("test: enqueue Normal should succeed");
349
350 let first = p
351 .dequeue("peer-a")
352 .expect("test: first dequeue should return Urgent message");
353 assert_eq!(first.effective_priority, MessagePriority::Urgent);
354
355 let second = p
356 .dequeue("peer-a")
357 .expect("test: second dequeue should return Normal message");
358 assert_eq!(second.effective_priority, MessagePriority::Normal);
359
360 let third = p
361 .dequeue("peer-a")
362 .expect("test: third dequeue should return Background message");
363 assert_eq!(third.effective_priority, MessagePriority::Background);
364 }
365
366 #[test]
369 fn fifo_within_same_priority() {
370 let mut p = default_prioritizer();
371 let id1 = p
372 .enqueue("peer-a".to_string(), MessagePriority::Normal, 10)
373 .expect("test: first enqueue should succeed");
374 let id2 = p
375 .enqueue("peer-a".to_string(), MessagePriority::Normal, 20)
376 .expect("test: second enqueue should succeed");
377 let id3 = p
378 .enqueue("peer-a".to_string(), MessagePriority::Normal, 30)
379 .expect("test: third enqueue should succeed");
380
381 assert_eq!(
382 p.dequeue("peer-a")
383 .expect("test: first dequeue should return message")
384 .msg_id,
385 id1
386 );
387 assert_eq!(
388 p.dequeue("peer-a")
389 .expect("test: second dequeue should return message")
390 .msg_id,
391 id2
392 );
393 assert_eq!(
394 p.dequeue("peer-a")
395 .expect("test: third dequeue should return message")
396 .msg_id,
397 id3
398 );
399 }
400
401 #[test]
404 fn fifo_stable_across_ticks() {
405 let config = AgingConfig {
406 promote_after_ticks: 1000, max_queue_size: 256,
408 };
409 let mut p = PeerMessagePrioritizer::new(config);
410 let id1 = p
411 .enqueue("peer-a".to_string(), MessagePriority::High, 10)
412 .expect("test: first enqueue at tick 0 should succeed");
413 p.advance_tick();
414 let id2 = p
415 .enqueue("peer-a".to_string(), MessagePriority::High, 10)
416 .expect("test: second enqueue at tick 1 should succeed");
417 p.advance_tick();
418 let id3 = p
419 .enqueue("peer-a".to_string(), MessagePriority::High, 10)
420 .expect("test: third enqueue at tick 2 should succeed");
421
422 assert_eq!(
423 p.dequeue("peer-a")
424 .expect("test: first dequeue should return message")
425 .msg_id,
426 id1
427 );
428 assert_eq!(
429 p.dequeue("peer-a")
430 .expect("test: second dequeue should return message")
431 .msg_id,
432 id2
433 );
434 assert_eq!(
435 p.dequeue("peer-a")
436 .expect("test: third dequeue should return message")
437 .msg_id,
438 id3
439 );
440 }
441
442 #[test]
445 fn advance_tick_promotes_after_threshold() {
446 let config = AgingConfig {
447 promote_after_ticks: 3,
448 max_queue_size: 256,
449 };
450 let mut p = PeerMessagePrioritizer::new(config);
451 p.enqueue("peer-a".to_string(), MessagePriority::Background, 10)
452 .expect("test: enqueue Background should succeed");
453
454 p.advance_tick();
456 p.advance_tick();
457 assert_eq!(p.stats().total_promoted, 0);
458
459 p.advance_tick();
461 assert_eq!(p.stats().total_promoted, 1);
462
463 let msg = p
464 .dequeue("peer-a")
465 .expect("test: dequeue should return promoted message");
466 assert_eq!(msg.effective_priority, MessagePriority::Normal);
467 assert_eq!(msg.priority, MessagePriority::Background); }
469
470 #[test]
473 fn promotion_capped_at_urgent() {
474 let config = AgingConfig {
475 promote_after_ticks: 1,
476 max_queue_size: 256,
477 };
478 let mut p = PeerMessagePrioritizer::new(config);
479 p.enqueue("peer-a".to_string(), MessagePriority::High, 10)
480 .expect("test: enqueue High should succeed");
481
482 for _ in 0..10 {
484 p.advance_tick();
485 }
486
487 let msg = p
488 .dequeue("peer-a")
489 .expect("test: dequeue should return Urgent message after promotion");
490 assert_eq!(msg.effective_priority, MessagePriority::Urgent);
491 }
492
493 #[test]
496 fn total_promoted_counter() {
497 let config = AgingConfig {
498 promote_after_ticks: 2,
499 max_queue_size: 256,
500 };
501 let mut p = PeerMessagePrioritizer::new(config);
502 p.enqueue("peer-a".to_string(), MessagePriority::Background, 10)
504 .expect("test: first enqueue Background should succeed");
505 p.enqueue("peer-a".to_string(), MessagePriority::Background, 10)
506 .expect("test: second enqueue Background should succeed");
507
508 p.advance_tick(); assert_eq!(p.stats().total_promoted, 0);
510
511 p.advance_tick(); assert_eq!(p.stats().total_promoted, 2);
513 }
514
515 #[test]
518 fn drop_lowest_priority_when_full() {
519 let config = AgingConfig {
520 promote_after_ticks: 9999,
521 max_queue_size: 3,
522 };
523 let mut p = PeerMessagePrioritizer::new(config);
524
525 p.enqueue("peer-a".to_string(), MessagePriority::Normal, 10)
526 .expect("test: enqueue Normal should succeed");
527 p.enqueue("peer-a".to_string(), MessagePriority::Background, 10)
528 .expect("test: enqueue Background should succeed");
529 p.enqueue("peer-a".to_string(), MessagePriority::High, 10)
530 .expect("test: enqueue High should succeed");
531 let result = p.enqueue("peer-a".to_string(), MessagePriority::Urgent, 10);
535 assert!(
536 result.is_some(),
537 "should succeed by evicting lowest-priority message"
538 );
539 assert_eq!(p.stats().total_dropped, 1);
540 assert_eq!(p.queue_depth("peer-a"), 3);
541
542 let priorities: Vec<_> = p.queues["peer-a"]
544 .iter()
545 .map(|m| m.effective_priority)
546 .collect();
547 for prio in &priorities {
548 assert!(
549 *prio >= MessagePriority::Normal,
550 "Background should have been evicted"
551 );
552 }
553 }
554
555 #[test]
558 fn total_dropped_counter() {
559 let config = AgingConfig {
560 promote_after_ticks: 9999,
561 max_queue_size: 2,
562 };
563 let mut p = PeerMessagePrioritizer::new(config);
564 p.enqueue("peer-a".to_string(), MessagePriority::Background, 10)
565 .expect("test: first enqueue Background should succeed");
566 p.enqueue("peer-a".to_string(), MessagePriority::Background, 10)
567 .expect("test: second enqueue Background should succeed");
568 p.enqueue("peer-a".to_string(), MessagePriority::Normal, 10)
570 .expect("test: enqueue Normal should succeed");
571 assert_eq!(p.stats().total_dropped, 1);
572 }
573
574 #[test]
577 fn returns_none_when_still_full_after_drop() {
578 let config = AgingConfig {
579 promote_after_ticks: 9999,
580 max_queue_size: 2,
581 };
582 let mut p = PeerMessagePrioritizer::new(config);
583 p.enqueue("peer-a".to_string(), MessagePriority::Urgent, 10)
584 .expect("test: first enqueue Urgent should succeed");
585 p.enqueue("peer-a".to_string(), MessagePriority::Urgent, 10)
586 .expect("test: second enqueue Urgent should succeed");
587 let third = p.enqueue("peer-a".to_string(), MessagePriority::Urgent, 10);
591 assert!(third.is_some());
593 assert_eq!(p.stats().total_dropped, 1);
594 assert_eq!(p.queue_depth("peer-a"), 2);
595 }
596
597 #[test]
600 fn empty_dequeue_returns_none() {
601 let mut p = default_prioritizer();
602 assert!(p.dequeue("nonexistent-peer").is_none());
603 }
604
605 #[test]
608 fn dequeue_exhausted_queue_returns_none() {
609 let mut p = default_prioritizer();
610 p.enqueue("peer-a".to_string(), MessagePriority::Normal, 10)
611 .expect("test: enqueue Normal should succeed");
612 p.dequeue("peer-a");
613 assert!(p.dequeue("peer-a").is_none());
614 }
615
616 #[test]
619 fn queue_depth_reflects_state() {
620 let mut p = default_prioritizer();
621 assert_eq!(p.queue_depth("peer-a"), 0);
622 p.enqueue("peer-a".to_string(), MessagePriority::Normal, 10)
623 .expect("test: first enqueue should succeed");
624 assert_eq!(p.queue_depth("peer-a"), 1);
625 p.enqueue("peer-a".to_string(), MessagePriority::High, 10)
626 .expect("test: second enqueue should succeed");
627 assert_eq!(p.queue_depth("peer-a"), 2);
628 p.dequeue("peer-a");
629 assert_eq!(p.queue_depth("peer-a"), 1);
630 }
631
632 #[test]
635 fn throughput_ratio_correct() {
636 let mut p = default_prioritizer();
637 assert!((p.stats().throughput_ratio() - 0.0).abs() < f64::EPSILON);
639
640 p.enqueue("peer-a".to_string(), MessagePriority::Normal, 10)
641 .expect("test: first enqueue should succeed");
642 p.enqueue("peer-a".to_string(), MessagePriority::Normal, 10)
643 .expect("test: second enqueue should succeed");
644 p.enqueue("peer-a".to_string(), MessagePriority::Normal, 10)
645 .expect("test: third enqueue should succeed");
646 p.dequeue("peer-a");
647 let ratio = p.stats().throughput_ratio();
649 assert!((ratio - 1.0 / 3.0).abs() < 1e-9, "ratio = {ratio}");
650 }
651
652 #[test]
655 fn enqueued_and_dequeued_counters() {
656 let mut p = default_prioritizer();
657 for _ in 0..5 {
658 p.enqueue("peer-a".to_string(), MessagePriority::Normal, 10)
659 .expect("test: enqueue in loop should succeed");
660 }
661 assert_eq!(p.stats().total_enqueued, 5);
662 for _ in 0..3 {
663 p.dequeue("peer-a");
664 }
665 assert_eq!(p.stats().total_dequeued, 3);
666 }
667
668 #[test]
671 fn multi_peer_isolation() {
672 let mut p = default_prioritizer();
673 p.enqueue("peer-a".to_string(), MessagePriority::Urgent, 10)
674 .expect("test: enqueue Urgent for peer-a should succeed");
675 p.enqueue("peer-b".to_string(), MessagePriority::Background, 10)
676 .expect("test: enqueue Background for peer-b should succeed");
677
678 let msg_a = p
679 .dequeue("peer-a")
680 .expect("test: dequeue peer-a should return Urgent message");
681 assert_eq!(msg_a.effective_priority, MessagePriority::Urgent);
682 assert_eq!(msg_a.peer_id, "peer-a");
683
684 let msg_b = p
685 .dequeue("peer-b")
686 .expect("test: dequeue peer-b should return Background message");
687 assert_eq!(msg_b.effective_priority, MessagePriority::Background);
688 assert_eq!(msg_b.peer_id, "peer-b");
689 }
690
691 #[test]
694 fn advance_tick_does_not_promote_urgent() {
695 let config = AgingConfig {
696 promote_after_ticks: 1,
697 max_queue_size: 256,
698 };
699 let mut p = PeerMessagePrioritizer::new(config);
700 p.enqueue("peer-a".to_string(), MessagePriority::Urgent, 10)
701 .expect("test: enqueue Urgent should succeed");
702
703 p.advance_tick();
704
705 assert_eq!(p.stats().total_promoted, 0);
706 let msg = p
707 .dequeue("peer-a")
708 .expect("test: dequeue should return Urgent message");
709 assert_eq!(msg.effective_priority, MessagePriority::Urgent);
710 }
711
712 #[test]
715 fn payload_bytes_preserved() {
716 let mut p = default_prioritizer();
717 p.enqueue("peer-a".to_string(), MessagePriority::Normal, 4096)
718 .expect("test: enqueue 4096 bytes should succeed");
719 let msg = p
720 .dequeue("peer-a")
721 .expect("test: dequeue should return message with payload");
722 assert_eq!(msg.payload_bytes, 4096);
723 }
724
725 #[test]
728 fn promote_at_exact_threshold() {
729 let config = AgingConfig {
730 promote_after_ticks: 5,
731 max_queue_size: 256,
732 };
733 let mut p = PeerMessagePrioritizer::new(config);
734 p.enqueue("peer-a".to_string(), MessagePriority::Background, 10)
735 .expect("test: enqueue Background should succeed");
736
737 for _ in 0..4 {
738 p.advance_tick();
739 }
740 assert_eq!(
741 p.stats().total_promoted,
742 0,
743 "should not promote before threshold"
744 );
745
746 p.advance_tick(); assert_eq!(
748 p.stats().total_promoted,
749 1,
750 "should promote at exact threshold"
751 );
752 }
753}