ndn_discovery/strategy/
reactive.rs1use std::time::{Duration, Instant};
21
22use crate::config::DiscoveryConfig;
23use crate::strategy::{NeighborProbeStrategy, ProbeRequest, TriggerEvent};
24
25pub struct ReactiveScheduler {
29 min_interval: Duration,
31 last_sent: Option<Instant>,
33 pending: bool,
35}
36
37impl ReactiveScheduler {
38 pub fn from_discovery_config(cfg: &DiscoveryConfig) -> Self {
40 Self {
41 min_interval: cfg.hello_interval_base,
42 last_sent: None,
43 pending: true, }
45 }
46}
47
48impl NeighborProbeStrategy for ReactiveScheduler {
49 fn on_tick(&mut self, now: Instant) -> Vec<ProbeRequest> {
50 if !self.pending {
51 return Vec::new();
52 }
53
54 if let Some(last) = self.last_sent
56 && now.duration_since(last) < self.min_interval
57 {
58 return Vec::new(); }
60
61 self.pending = false;
62 self.last_sent = Some(now);
63 vec![ProbeRequest::Broadcast]
64 }
65
66 fn on_probe_success(&mut self, _rtt: Duration) {
67 }
69
70 fn on_probe_timeout(&mut self) {
71 self.pending = true;
73 }
74
75 fn trigger(&mut self, event: TriggerEvent) {
76 match event {
77 TriggerEvent::PassiveDetection => {
78 }
80 TriggerEvent::FaceUp
81 | TriggerEvent::ForwardingFailure
82 | TriggerEvent::NeighborStale => {
83 self.pending = true;
84 }
85 }
86 }
87}
88
89#[cfg(test)]
92mod tests {
93 use std::time::Duration;
94
95 use super::*;
96 use crate::config::{DiscoveryConfig, DiscoveryProfile};
97
98 fn mobile_sched() -> ReactiveScheduler {
99 ReactiveScheduler::from_discovery_config(&DiscoveryConfig::for_profile(
100 &DiscoveryProfile::Mobile,
101 ))
102 }
103
104 #[test]
105 fn fires_on_first_tick() {
106 let mut s = mobile_sched();
107 let reqs = s.on_tick(Instant::now());
108 assert_eq!(reqs, vec![ProbeRequest::Broadcast]);
109 }
110
111 #[test]
112 fn does_not_fire_without_trigger() {
113 let mut s = mobile_sched();
114 let now = Instant::now();
115 s.on_tick(now); let reqs = s.on_tick(now + Duration::from_secs(1));
119 assert!(reqs.is_empty());
120 }
121
122 #[test]
123 fn fires_after_trigger() {
124 let mut s = mobile_sched();
125 let now = Instant::now();
126 s.on_tick(now); s.trigger(TriggerEvent::ForwardingFailure);
129 let reqs = s.on_tick(now + Duration::from_secs(1));
130 assert_eq!(reqs, vec![ProbeRequest::Broadcast]);
131 }
132
133 #[test]
134 fn rate_limits_rapid_triggers() {
135 let mut s = mobile_sched();
136 let now = Instant::now();
137 s.on_tick(now); s.trigger(TriggerEvent::NeighborStale);
141 let reqs = s.on_tick(now); assert!(reqs.is_empty(), "should be rate-limited");
143
144 let later = now + s.min_interval + Duration::from_millis(1);
146 let reqs = s.on_tick(later);
147 assert_eq!(reqs, vec![ProbeRequest::Broadcast]);
148 }
149
150 #[test]
151 fn passive_detection_is_ignored() {
152 let mut s = mobile_sched();
153 let now = Instant::now();
154 s.on_tick(now); s.trigger(TriggerEvent::PassiveDetection);
157 let reqs = s.on_tick(now + Duration::from_secs(10));
158 assert!(reqs.is_empty());
159 }
160}