deepstrike_core/signals/
router.rs1use std::collections::HashSet;
2
3use compact_str::CompactString;
4
5use super::attention::UrgencyBasedPolicy;
6use super::queue::SignalQueue;
7use crate::types::policy::SignalDisposition;
8use crate::types::signal::RuntimeSignal;
9
10pub struct SignalRouter {
12 seen: HashSet<CompactString>,
13 queue: SignalQueue,
14 attention: UrgencyBasedPolicy,
15}
16
17impl SignalRouter {
18 pub fn new(max_queue_size: usize) -> Self {
19 Self {
20 seen: HashSet::new(),
21 queue: SignalQueue::new(max_queue_size),
22 attention: UrgencyBasedPolicy,
23 }
24 }
25
26 pub fn ingest(&mut self, signal: RuntimeSignal, is_running: bool) -> SignalDisposition {
31 if let Some(ref key) = signal.dedupe_key {
32 if !self.seen.insert(key.clone()) {
33 return SignalDisposition::Ignore;
34 }
35 }
36
37 let disposition = self.attention.evaluate(&signal, is_running);
38
39 if disposition == SignalDisposition::Queue {
40 if !self.queue.push(signal) {
41 return SignalDisposition::Dropped;
42 }
43 }
44
45 disposition
46 }
47
48 pub fn next(&mut self) -> Option<RuntimeSignal> {
50 self.queue.pop()
51 }
52
53 pub fn depth(&self) -> usize {
55 self.queue.len()
56 }
57
58 pub fn clear_dedup(&mut self) {
60 self.seen.clear();
61 }
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67 use crate::types::signal::{SignalSource, SignalType, Urgency};
68
69 #[test]
70 fn deduplicates_signals() {
71 let mut router = SignalRouter::new(100);
72 let sig = RuntimeSignal::new(
73 SignalSource::Cron,
74 SignalType::Event,
75 Urgency::Normal,
76 "tick",
77 )
78 .with_dedupe("cron-tick-1");
79
80 let d1 = router.ingest(sig.clone(), false);
81 assert_ne!(d1, SignalDisposition::Ignore);
82
83 let d2 = router.ingest(sig, false);
84 assert_eq!(d2, SignalDisposition::Ignore);
85 }
86
87 #[test]
88 fn normal_signal_queued() {
89 let mut router = SignalRouter::new(100);
90 let sig = RuntimeSignal::new(
91 SignalSource::Cron,
92 SignalType::Event,
93 Urgency::Normal,
94 "job",
95 );
96
97 let d = router.ingest(sig, false);
98 assert_eq!(d, SignalDisposition::Queue);
99 assert_eq!(router.depth(), 1);
100 assert!(router.next().is_some());
101 }
102
103 #[test]
104 fn interrupt_signals_not_queued() {
105 let mut router = SignalRouter::new(100);
106 let sig = RuntimeSignal::new(
107 SignalSource::Gateway,
108 SignalType::Alert,
109 Urgency::Critical,
110 "fire",
111 );
112
113 let d = router.ingest(sig, true);
114 assert_eq!(d, SignalDisposition::InterruptNow);
115 assert_eq!(router.depth(), 0);
116 }
117
118 #[test]
119 fn full_queue_drops_signal() {
120 let mut router = SignalRouter::new(1);
121 let s1 = RuntimeSignal::new(
122 SignalSource::Cron,
123 SignalType::Event,
124 Urgency::Normal,
125 "first",
126 );
127 let s2 = RuntimeSignal::new(
128 SignalSource::Cron,
129 SignalType::Event,
130 Urgency::Normal,
131 "second",
132 );
133
134 assert_eq!(router.ingest(s1, false), SignalDisposition::Queue);
135 assert_eq!(router.ingest(s2, false), SignalDisposition::Dropped);
136 }
137
138 #[test]
139 fn clear_dedup_allows_reingest() {
140 let mut router = SignalRouter::new(100);
141 let sig = RuntimeSignal::new(
142 SignalSource::Cron,
143 SignalType::Event,
144 Urgency::Normal,
145 "tick",
146 )
147 .with_dedupe("key-1");
148
149 router.ingest(sig.clone(), false);
150 assert_eq!(router.ingest(sig.clone(), false), SignalDisposition::Ignore);
151
152 router.clear_dedup();
153 assert_ne!(router.ingest(sig, false), SignalDisposition::Ignore);
154 }
155}