1use std::cmp::Ordering;
14use std::collections::{BinaryHeap, HashMap};
15use std::sync::{
16 atomic::{AtomicU64, Ordering as AOrdering},
17 Arc, Mutex,
18};
19use std::time::{Duration, Instant};
20
21use thiserror::Error;
22
23#[derive(Error, Debug, Clone, PartialEq, Eq)]
27pub enum TopicError {
28 #[error("Topic not found: {0}")]
30 TopicNotFound(String),
31
32 #[error("Topic already registered: {0}")]
34 AlreadyRegistered(String),
35
36 #[error("Queue full for topic '{topic}' (max {max})")]
38 QueueFull { topic: String, max: usize },
39
40 #[error("Message priority {priority} is below threshold for topic '{topic}'")]
42 BelowThreshold { topic: String, priority: u8 },
43}
44
45#[derive(Debug, Clone)]
49pub struct TopicConfig {
50 pub max_queue_depth: usize,
53
54 pub priority_threshold: u8,
57
58 pub ttl: Duration,
60}
61
62impl Default for TopicConfig {
63 fn default() -> Self {
64 Self {
65 max_queue_depth: 1000,
66 priority_threshold: 0,
67 ttl: Duration::from_secs(60),
68 }
69 }
70}
71
72#[derive(Debug, Clone)]
80pub struct PrioritizedMessage {
81 pub payload: Vec<u8>,
83
84 pub priority: u8,
86
87 pub enqueued_at: Instant,
89
90 pub topic: String,
92}
93
94impl PrioritizedMessage {
95 #[must_use]
97 pub fn is_expired(&self, ttl: Duration) -> bool {
98 self.enqueued_at.elapsed() >= ttl
99 }
100}
101
102impl PartialEq for PrioritizedMessage {
103 fn eq(&self, other: &Self) -> bool {
104 self.priority == other.priority && self.enqueued_at == other.enqueued_at
105 }
106}
107
108impl Eq for PrioritizedMessage {}
109
110impl PartialOrd for PrioritizedMessage {
111 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
112 Some(self.cmp(other))
113 }
114}
115
116impl Ord for PrioritizedMessage {
117 fn cmp(&self, other: &Self) -> Ordering {
118 self.priority
120 .cmp(&other.priority)
121 .then_with(|| other.enqueued_at.cmp(&self.enqueued_at))
122 }
123}
124
125#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct TopicRouterStatsSnapshot {
130 pub total_topics: usize,
132 pub total_enqueued: u64,
134 pub total_dropped: u64,
136 pub total_dequeued: u64,
138}
139
140#[derive(Debug)]
144pub struct TopicRouterStats {
145 total_enqueued: Arc<AtomicU64>,
146 total_dropped: Arc<AtomicU64>,
147 total_dequeued: Arc<AtomicU64>,
148}
149
150impl Default for TopicRouterStats {
151 fn default() -> Self {
152 Self {
153 total_enqueued: Arc::new(AtomicU64::new(0)),
154 total_dropped: Arc::new(AtomicU64::new(0)),
155 total_dequeued: Arc::new(AtomicU64::new(0)),
156 }
157 }
158}
159
160impl TopicRouterStats {
161 #[must_use]
163 pub fn new() -> Self {
164 Self::default()
165 }
166
167 #[must_use]
169 pub fn clone_handle(&self) -> Self {
170 Self {
171 total_enqueued: Arc::clone(&self.total_enqueued),
172 total_dropped: Arc::clone(&self.total_dropped),
173 total_dequeued: Arc::clone(&self.total_dequeued),
174 }
175 }
176
177 pub(crate) fn record_enqueue(&self) {
179 self.total_enqueued.fetch_add(1, AOrdering::Relaxed);
180 }
181
182 pub(crate) fn record_drop(&self) {
184 self.total_dropped.fetch_add(1, AOrdering::Relaxed);
185 }
186
187 pub(crate) fn record_dequeue(&self) {
189 self.total_dequeued.fetch_add(1, AOrdering::Relaxed);
190 }
191
192 #[must_use]
194 pub fn snapshot(&self, total_topics: usize) -> TopicRouterStatsSnapshot {
195 TopicRouterStatsSnapshot {
196 total_topics,
197 total_enqueued: self.total_enqueued.load(AOrdering::Relaxed),
198 total_dropped: self.total_dropped.load(AOrdering::Relaxed),
199 total_dequeued: self.total_dequeued.load(AOrdering::Relaxed),
200 }
201 }
202}
203
204pub struct TopicRouter {
230 topics: Mutex<HashMap<String, TopicConfig>>,
232
233 message_counts: Mutex<HashMap<String, Arc<AtomicU64>>>,
235
236 priority_queues: Mutex<HashMap<String, BinaryHeap<PrioritizedMessage>>>,
238
239 stats: TopicRouterStats,
241}
242
243impl Default for TopicRouter {
244 fn default() -> Self {
245 Self::new()
246 }
247}
248
249impl TopicRouter {
250 #[must_use]
252 pub fn new() -> Self {
253 Self {
254 topics: Mutex::new(HashMap::new()),
255 message_counts: Mutex::new(HashMap::new()),
256 priority_queues: Mutex::new(HashMap::new()),
257 stats: TopicRouterStats::new(),
258 }
259 }
260
261 pub fn register_topic(&self, name: &str, config: TopicConfig) -> Result<(), TopicError> {
269 let mut topics = self.topics.lock().unwrap_or_else(|e| e.into_inner());
270
271 if topics.contains_key(name) {
272 return Err(TopicError::AlreadyRegistered(name.to_string()));
273 }
274
275 topics.insert(name.to_string(), config);
276 drop(topics);
277
278 self.message_counts
280 .lock()
281 .unwrap_or_else(|e| e.into_inner())
282 .insert(name.to_string(), Arc::new(AtomicU64::new(0)));
283
284 self.priority_queues
285 .lock()
286 .unwrap_or_else(|e| e.into_inner())
287 .insert(name.to_string(), BinaryHeap::new());
288
289 Ok(())
290 }
291
292 pub fn unregister_topic(&self, name: &str) -> Result<(), TopicError> {
298 let removed = self
299 .topics
300 .lock()
301 .unwrap_or_else(|e| e.into_inner())
302 .remove(name)
303 .is_some();
304
305 if !removed {
306 return Err(TopicError::TopicNotFound(name.to_string()));
307 }
308
309 self.message_counts
310 .lock()
311 .unwrap_or_else(|e| e.into_inner())
312 .remove(name);
313
314 self.priority_queues
315 .lock()
316 .unwrap_or_else(|e| e.into_inner())
317 .remove(name);
318
319 Ok(())
320 }
321
322 pub fn enqueue(&self, topic: &str, msg: PrioritizedMessage) -> Result<(), TopicError> {
332 let (max_depth, threshold) = {
334 let topics = self.topics.lock().unwrap_or_else(|e| e.into_inner());
335 match topics.get(topic) {
336 Some(cfg) => (cfg.max_queue_depth, cfg.priority_threshold),
337 None => return Err(TopicError::TopicNotFound(topic.to_string())),
338 }
339 };
340
341 if msg.priority < threshold {
343 self.stats.record_drop();
344 return Err(TopicError::BelowThreshold {
345 topic: topic.to_string(),
346 priority: msg.priority,
347 });
348 }
349
350 let mut queues = self
352 .priority_queues
353 .lock()
354 .unwrap_or_else(|e| e.into_inner());
355
356 let queue = queues.get_mut(topic).ok_or_else(|| {
357 TopicError::TopicNotFound(topic.to_string())
359 })?;
360
361 if queue.len() >= max_depth {
362 drop(queues);
363 self.stats.record_drop();
364 return Err(TopicError::QueueFull {
365 topic: topic.to_string(),
366 max: max_depth,
367 });
368 }
369
370 queue.push(msg);
371 drop(queues);
372
373 if let Some(counter) = self
375 .message_counts
376 .lock()
377 .unwrap_or_else(|e| e.into_inner())
378 .get(topic)
379 {
380 counter.fetch_add(1, AOrdering::Relaxed);
381 }
382
383 self.stats.record_enqueue();
384 Ok(())
385 }
386
387 #[must_use]
391 pub fn dequeue(&self, topic: &str) -> Option<PrioritizedMessage> {
392 let msg = self
393 .priority_queues
394 .lock()
395 .unwrap_or_else(|e| e.into_inner())
396 .get_mut(topic)?
397 .pop()?;
398
399 self.stats.record_dequeue();
400 Some(msg)
401 }
402
403 #[must_use]
409 pub fn queue_depth(&self, topic: &str) -> usize {
410 self.priority_queues
411 .lock()
412 .unwrap_or_else(|e| e.into_inner())
413 .get(topic)
414 .map(|q| q.len())
415 .unwrap_or(0)
416 }
417
418 #[must_use]
423 pub fn message_count(&self, topic: &str) -> u64 {
424 self.message_counts
425 .lock()
426 .unwrap_or_else(|e| e.into_inner())
427 .get(topic)
428 .map(|c| c.load(AOrdering::Relaxed))
429 .unwrap_or(0)
430 }
431
432 #[must_use]
434 pub fn all_topics(&self) -> Vec<String> {
435 let mut names: Vec<String> = self
436 .topics
437 .lock()
438 .unwrap_or_else(|e| e.into_inner())
439 .keys()
440 .cloned()
441 .collect();
442 names.sort();
443 names
444 }
445
446 #[must_use]
448 pub fn stats(&self) -> TopicRouterStatsSnapshot {
449 let total_topics = self.topics.lock().unwrap_or_else(|e| e.into_inner()).len();
450 self.stats.snapshot(total_topics)
451 }
452}
453
454#[cfg(test)]
457mod tests {
458 use super::*;
459 use std::time::{Duration, Instant};
460
461 fn make_msg(topic: &str, priority: u8, payload: &[u8]) -> PrioritizedMessage {
462 PrioritizedMessage {
463 payload: payload.to_vec(),
464 priority,
465 enqueued_at: Instant::now(),
466 topic: topic.to_string(),
467 }
468 }
469
470 fn default_router_with_topic(topic: &str) -> TopicRouter {
471 let r = TopicRouter::new();
472 r.register_topic(topic, TopicConfig::default())
473 .expect("test: register_topic should succeed for new topic");
474 r
475 }
476
477 #[test]
479 fn test_register_topic_ok() {
480 let router = TopicRouter::new();
481 let result = router.register_topic("alpha", TopicConfig::default());
482 assert!(result.is_ok(), "Expected successful registration");
483 assert!(router.all_topics().contains(&"alpha".to_string()));
484 }
485
486 #[test]
488 fn test_register_topic_duplicate() {
489 let router = TopicRouter::new();
490 router
491 .register_topic("beta", TopicConfig::default())
492 .expect("test: first registration of beta should succeed");
493 let err = router
494 .register_topic("beta", TopicConfig::default())
495 .expect_err("test: duplicate registration should fail");
496 assert!(
497 matches!(err, TopicError::AlreadyRegistered(ref n) if n == "beta"),
498 "Expected AlreadyRegistered, got {err:?}"
499 );
500 }
501
502 #[test]
504 fn test_unregister_topic() {
505 let router = default_router_with_topic("gamma");
506 router
507 .unregister_topic("gamma")
508 .expect("test: unregister_topic should succeed for registered topic");
509 assert!(!router.all_topics().contains(&"gamma".to_string()));
510 }
511
512 #[test]
514 fn test_unregister_nonexistent_topic() {
515 let router = TopicRouter::new();
516 let err = router
517 .unregister_topic("ghost")
518 .expect_err("test: unregister_topic should fail for unknown topic");
519 assert!(
520 matches!(err, TopicError::TopicNotFound(ref n) if n == "ghost"),
521 "Expected TopicNotFound"
522 );
523 }
524
525 #[test]
527 fn test_enqueue_unknown_topic() {
528 let router = TopicRouter::new();
529 let msg = make_msg("unknown", 100, b"payload");
530 let err = router
531 .enqueue("unknown", msg)
532 .expect_err("test: enqueue on unknown topic should fail");
533 assert!(matches!(err, TopicError::TopicNotFound(_)));
534 }
535
536 #[test]
538 fn test_priority_ordering() {
539 let router = default_router_with_topic("prio");
540 router
541 .enqueue("prio", make_msg("prio", 10, b"low"))
542 .expect("test: enqueue should succeed for valid priority message");
543 router
544 .enqueue("prio", make_msg("prio", 200, b"high"))
545 .expect("test: enqueue should succeed for valid priority message");
546 router
547 .enqueue("prio", make_msg("prio", 100, b"mid"))
548 .expect("test: enqueue should succeed for valid priority message");
549
550 let first = router
551 .dequeue("prio")
552 .expect("test: dequeue should return a message from non-empty queue");
553 let second = router
554 .dequeue("prio")
555 .expect("test: dequeue should return a message from non-empty queue");
556 let third = router
557 .dequeue("prio")
558 .expect("test: dequeue should return a message from non-empty queue");
559
560 assert_eq!(
561 first.payload, b"high",
562 "First dequeued should be highest priority"
563 );
564 assert_eq!(second.payload, b"mid");
565 assert_eq!(third.payload, b"low");
566 }
567
568 #[test]
570 fn test_dequeue_empty() {
571 let router = default_router_with_topic("empty");
572 assert!(router.dequeue("empty").is_none());
573 }
574
575 #[test]
577 fn test_queue_depth_limit() {
578 let router = TopicRouter::new();
579 let config = TopicConfig {
580 max_queue_depth: 3,
581 ..Default::default()
582 };
583 router
584 .register_topic("bounded", config)
585 .expect("test: register bounded topic should succeed");
586
587 for i in 0_u8..3 {
588 router
589 .enqueue("bounded", make_msg("bounded", i, b"x"))
590 .expect("test: enqueue should succeed when queue is not full");
591 }
592 assert_eq!(router.queue_depth("bounded"), 3);
593
594 let err = router
595 .enqueue("bounded", make_msg("bounded", 1, b"overflow"))
596 .expect_err("test: enqueue beyond max_queue_depth should fail");
597 assert!(
598 matches!(err, TopicError::QueueFull { ref topic, max: 3 } if topic == "bounded"),
599 "Expected QueueFull"
600 );
601 }
602
603 #[test]
605 fn test_priority_threshold_drop() {
606 let router = TopicRouter::new();
607 let config = TopicConfig {
608 priority_threshold: 100,
609 ..Default::default()
610 };
611 router
612 .register_topic("thresh", config)
613 .expect("test: register thresh topic should succeed");
614
615 let err = router
616 .enqueue("thresh", make_msg("thresh", 50, b"low"))
617 .expect_err("test: enqueue below priority threshold should fail");
618 assert!(
619 matches!(err, TopicError::BelowThreshold { ref topic, priority: 50 } if topic == "thresh"),
620 "Expected BelowThreshold"
621 );
622
623 router
625 .enqueue("thresh", make_msg("thresh", 100, b"ok"))
626 .expect("test: enqueue at exact priority threshold should succeed");
627 assert_eq!(router.queue_depth("thresh"), 1);
628 }
629
630 #[test]
632 fn test_stats_accumulation() {
633 let router = TopicRouter::new();
634 let config = TopicConfig {
635 priority_threshold: 50,
636 max_queue_depth: 2,
637 ..Default::default()
638 };
639 router
640 .register_topic("stats_topic", config)
641 .expect("test: register stats_topic should succeed");
642
643 router
645 .enqueue("stats_topic", make_msg("stats_topic", 100, b"a"))
646 .expect("test: enqueue into stats_topic should succeed");
647 router
648 .enqueue("stats_topic", make_msg("stats_topic", 200, b"b"))
649 .expect("test: enqueue into stats_topic should succeed");
650 let _ = router.enqueue("stats_topic", make_msg("stats_topic", 10, b"c"));
652 let _ = router.enqueue("stats_topic", make_msg("stats_topic", 200, b"d"));
654
655 let _ = router.dequeue("stats_topic");
657
658 let snap = router.stats();
659 assert_eq!(snap.total_enqueued, 2);
660 assert_eq!(snap.total_dropped, 2);
661 assert_eq!(snap.total_dequeued, 1);
662 assert_eq!(snap.total_topics, 1);
663 }
664
665 #[test]
667 fn test_ttl_expiry() {
668 let old = PrioritizedMessage {
670 payload: vec![],
671 priority: 100,
672 enqueued_at: Instant::now(),
675 topic: "ttl_topic".to_string(),
676 };
677 assert!(!old.is_expired(Duration::from_secs(10)));
679
680 assert!(old.is_expired(Duration::from_nanos(0)));
682 }
683
684 #[test]
686 fn test_all_topics_listing() {
687 let router = TopicRouter::new();
688 router
689 .register_topic("zebra", TopicConfig::default())
690 .expect("test: register_topic should succeed for unique topic name");
691 router
692 .register_topic("apple", TopicConfig::default())
693 .expect("test: register_topic should succeed for unique topic name");
694 router
695 .register_topic("mango", TopicConfig::default())
696 .expect("test: register_topic should succeed for unique topic name");
697
698 let topics = router.all_topics();
699 assert_eq!(topics, vec!["apple", "mango", "zebra"]);
700 }
701
702 #[test]
704 fn test_message_count_excludes_drops() {
705 let router = TopicRouter::new();
706 let config = TopicConfig {
707 priority_threshold: 100,
708 ..Default::default()
709 };
710 router
711 .register_topic("cnt", config)
712 .expect("test: register cnt topic should succeed");
713
714 router
715 .enqueue("cnt", make_msg("cnt", 200, b"good"))
716 .expect("test: enqueue good message into cnt should succeed");
717 let _ = router.enqueue("cnt", make_msg("cnt", 50, b"bad")); assert_eq!(router.message_count("cnt"), 1);
720 }
721
722 #[test]
724 fn test_queue_depth_tracking() {
725 let router = default_router_with_topic("depth");
726 assert_eq!(router.queue_depth("depth"), 0);
727
728 router
729 .enqueue("depth", make_msg("depth", 1, b"a"))
730 .expect("test: enqueue into depth topic should succeed");
731 router
732 .enqueue("depth", make_msg("depth", 2, b"b"))
733 .expect("test: enqueue into depth topic should succeed");
734 assert_eq!(router.queue_depth("depth"), 2);
735
736 let _ = router.dequeue("depth");
737 assert_eq!(router.queue_depth("depth"), 1);
738
739 let _ = router.dequeue("depth");
740 assert_eq!(router.queue_depth("depth"), 0);
741 }
742
743 #[test]
745 fn test_stats_total_topics() {
746 let router = TopicRouter::new();
747 assert_eq!(router.stats().total_topics, 0);
748
749 router
750 .register_topic("t1", TopicConfig::default())
751 .expect("test: register topic t1 should succeed");
752 assert_eq!(router.stats().total_topics, 1);
753
754 router
755 .register_topic("t2", TopicConfig::default())
756 .expect("test: register topic t2 should succeed");
757 assert_eq!(router.stats().total_topics, 2);
758
759 router
760 .unregister_topic("t1")
761 .expect("test: unregister_topic t1 should succeed");
762 assert_eq!(router.stats().total_topics, 1);
763 }
764}