ndn_discovery/strategy/
passive.rs1use std::time::{Duration, Instant};
21
22use ndn_transport::FaceId;
23
24use crate::backoff::{BackoffConfig, BackoffState};
25use crate::config::DiscoveryConfig;
26use crate::strategy::{NeighborProbeStrategy, ProbeRequest, TriggerEvent};
27
28pub struct PassiveScheduler {
32 backoff_cfg: BackoffConfig,
34 backoff_state: BackoffState,
36 next_fallback_at: Option<Instant>,
38 passive_idle_timeout: Duration,
40 last_passive: Option<Instant>,
42 pending_unicast: Vec<FaceId>,
44 pending_broadcast: bool,
46}
47
48impl PassiveScheduler {
49 pub fn from_discovery_config(cfg: &DiscoveryConfig) -> Self {
51 let backoff_cfg = BackoffConfig {
52 initial_interval: cfg.hello_interval_base,
53 max_interval: cfg.hello_interval_max,
54 jitter_fraction: cfg.hello_jitter as f64,
55 };
56 let passive_idle_timeout = cfg.hello_interval_max * 3;
59 Self {
60 backoff_state: BackoffState::new(seed_from_now()),
61 backoff_cfg,
62 next_fallback_at: None,
63 passive_idle_timeout,
64 last_passive: None,
65 pending_unicast: Vec::new(),
66 pending_broadcast: true, }
68 }
69
70 fn is_passive_active(&self, now: Instant) -> bool {
71 match self.last_passive {
72 None => false,
73 Some(t) => now.duration_since(t) < self.passive_idle_timeout,
74 }
75 }
76}
77
78impl NeighborProbeStrategy for PassiveScheduler {
79 fn on_tick(&mut self, now: Instant) -> Vec<ProbeRequest> {
80 let mut reqs: Vec<ProbeRequest> = Vec::new();
81
82 for face_id in self.pending_unicast.drain(..) {
84 reqs.push(ProbeRequest::Unicast(face_id));
85 }
86
87 if self.pending_broadcast {
89 self.pending_broadcast = false;
90 reqs.push(ProbeRequest::Broadcast);
91 }
92
93 if !self.is_passive_active(now) {
95 let fire_fallback = self.next_fallback_at.map(|t| now >= t).unwrap_or(true);
96 if fire_fallback {
97 let interval = self.backoff_state.next_failure(&self.backoff_cfg);
98 self.next_fallback_at = Some(now + interval);
99 reqs.push(ProbeRequest::Broadcast);
100 }
101 }
102
103 reqs
104 }
105
106 fn on_probe_success(&mut self, _rtt: Duration) {
107 self.backoff_state.reset(&self.backoff_cfg);
108 let next = self.backoff_cfg.initial_interval;
109 self.next_fallback_at = Some(Instant::now() + next);
110 }
111
112 fn on_probe_timeout(&mut self) {
113 }
115
116 fn trigger(&mut self, event: TriggerEvent) {
117 match event {
118 TriggerEvent::PassiveDetection => {
119 self.last_passive = Some(Instant::now());
121 }
125 TriggerEvent::FaceUp => {
126 self.pending_broadcast = true;
127 }
128 TriggerEvent::ForwardingFailure | TriggerEvent::NeighborStale => {
129 self.pending_broadcast = true;
130 self.backoff_state.reset(&self.backoff_cfg);
132 }
133 }
134 }
135}
136
137impl PassiveScheduler {
142 pub fn enqueue_unicast(&mut self, face_id: FaceId) {
143 if !self.pending_unicast.contains(&face_id) {
144 self.pending_unicast.push(face_id);
145 }
146 }
147}
148
149fn seed_from_now() -> u32 {
152 let ns = Instant::now().elapsed().subsec_nanos();
153 if ns == 0 { 0xdeadbeef } else { ns }
154}
155
156#[cfg(test)]
159mod tests {
160 use std::time::Duration;
161
162 use ndn_transport::FaceId;
163
164 use super::*;
165 use crate::config::{DiscoveryConfig, DiscoveryProfile};
166
167 fn high_mob_sched() -> PassiveScheduler {
168 PassiveScheduler::from_discovery_config(&DiscoveryConfig::for_profile(
169 &DiscoveryProfile::HighMobility,
170 ))
171 }
172
173 #[test]
174 fn fires_broadcast_on_first_tick() {
175 let mut s = high_mob_sched();
176 let reqs = s.on_tick(Instant::now());
177 assert!(reqs.contains(&ProbeRequest::Broadcast));
178 }
179
180 #[test]
181 fn unicast_after_passive_detection_enqueue() {
182 let mut s = high_mob_sched();
183 let now = Instant::now();
184 s.on_tick(now); s.trigger(TriggerEvent::PassiveDetection);
187 s.enqueue_unicast(FaceId(3));
188 let reqs = s.on_tick(now + Duration::from_millis(10));
189 assert!(reqs.contains(&ProbeRequest::Unicast(FaceId(3))));
190 }
191
192 #[test]
193 fn fallback_fires_when_passive_idle() {
194 let mut s = high_mob_sched();
195 let now = Instant::now();
196 s.on_tick(now); let future = now + Duration::from_secs(3600);
200 let reqs = s.on_tick(future);
201 assert!(reqs.contains(&ProbeRequest::Broadcast));
202 }
203
204 #[test]
205 fn fallback_suppressed_when_passive_active() {
206 let mut s = high_mob_sched();
207 let now = Instant::now();
208 s.on_tick(now); s.trigger(TriggerEvent::PassiveDetection);
212 let soon = now + Duration::from_millis(100);
214 let reqs = s.on_tick(soon);
215 let broadcasts: Vec<_> = reqs
217 .iter()
218 .filter(|r| **r == ProbeRequest::Broadcast)
219 .collect();
220 assert!(
221 broadcasts.is_empty(),
222 "fallback should be suppressed: {reqs:?}"
223 );
224 }
225
226 #[test]
227 fn face_up_trigger_broadcasts() {
228 let mut s = high_mob_sched();
229 let now = Instant::now();
230 s.on_tick(now); s.trigger(TriggerEvent::FaceUp);
233 let reqs = s.on_tick(now + Duration::from_millis(10));
234 assert!(reqs.contains(&ProbeRequest::Broadcast));
235 }
236}