manycastr/orchestrator/trace.rs
1use crate::custom_module::manycastr::reply::ReplyData;
2use crate::custom_module::manycastr::{Address, Reply, ReplyBatch, Task, Trace, TraceReply, task};
3use crate::orchestrator::{CliHandle, MeasurementHandle, TracerouteConfig};
4use log::warn;
5use std::collections::{HashMap, VecDeque};
6use std::thread;
7use std::time::{Duration, Instant};
8
9/// Session Tracker for fast lookups (based on expiration queue)
10#[derive(Debug)]
11pub struct SessionTracker {
12 pub sessions: HashMap<TraceIdentifier, TraceSession>,
13 pub expiration_queue: VecDeque<(TraceIdentifier, Instant)>,
14}
15
16impl SessionTracker {
17 pub fn new() -> Self {
18 Self {
19 sessions: HashMap::new(),
20 expiration_queue: VecDeque::new(),
21 }
22 }
23}
24
25#[derive(Debug)]
26pub struct TraceSession {
27 /// Worker from which the traceroute is being performed
28 pub worker_id: u32,
29 /// Target destination address to which the traceroute is being performed
30 pub target: Option<Address>,
31 /// Origin used for the traceroute (source address, port mappings) [None if a single origin is used]
32 pub origin_id: u32,
33 /// How this session advances through TTLs
34 pub progress: TraceProgress,
35 /// Time at which last trace was performed
36 pub last_updated: Instant,
37}
38
39/// TTL advancement strategy of a trace session
40#[derive(Debug)]
41pub enum TraceProgress {
42 /// Hop-by-hop walk from initial_hop upward (anycast-traceroute)
43 Linear {
44 /// Current TTL being traced
45 current_ttl: u8,
46 /// Consecutive failures counter
47 consecutive_failures: u8,
48 },
49 /// Binary search for the deepest hop that replies with Time Exceeded (tracemap)
50 Binary {
51 /// Lower search bound: every responding hop found so far is below this TTL
52 lo: u8,
53 /// Upper search bound: the deepest TTL that may still respond
54 hi: u8,
55 /// Midpoint TTL at which the current confirmation window started
56 mid: u8,
57 /// TTL of the probe currently in flight (mid, or a confirmation probe above it)
58 probing_ttl: u8,
59 /// Remaining confirmation probes before concluding the silent tail starts at mid
60 window_left: u8,
61 },
62}
63
64/// Midpoint of two TTLs without u8 overflow
65#[inline]
66pub fn ttl_midpoint(lo: u8, hi: u8) -> u8 {
67 ((lo as u16 + hi as u16) / 2) as u8
68}
69
70/// First-probe TTL for the tracemap binary search.
71const TRACEMAP_FIRST_TTL: u8 = 12;
72
73/// Create tracemap binary-search sessions for a batch of (unresponsive) targets and
74/// return the initial `Trace` tasks (probing [`TRACEMAP_FIRST_TTL`]) for the probing worker.
75///
76/// # Arguments
77/// * `targets` - Target addresses to map
78/// * `worker_id` - Worker that will probe these targets (with the anycast source)
79/// * `config` - Traceroute parameters (including the seed origin) and session tracker
80pub fn seed_tracemap_sessions(
81 targets: Vec<Address>,
82 worker_id: u32,
83 config: &mut TracerouteConfig,
84) -> Vec<Task> {
85 let origin_id = config.origin_id;
86 let lo = config.initial_hop as u8;
87 let hi = config.max_hops as u8;
88 let mid = TRACEMAP_FIRST_TTL.clamp(lo, hi);
89 let now = Instant::now();
90 let deadline = now + Duration::from_secs(config.timeout);
91
92 targets
93 .into_iter()
94 .map(|target| {
95 let identifier = TraceIdentifier {
96 worker_id,
97 target,
98 origin_id,
99 };
100
101 config.session_tracker.sessions.insert(
102 identifier.clone(),
103 TraceSession {
104 worker_id,
105 target: Some(target),
106 origin_id,
107 progress: TraceProgress::Binary {
108 lo,
109 hi,
110 mid,
111 probing_ttl: mid,
112 window_left: config.max_failures as u8,
113 },
114 last_updated: now,
115 },
116 );
117 config
118 .session_tracker
119 .expiration_queue
120 .push_back((identifier, deadline));
121
122 Task {
123 task_type: Some(task::TaskType::Trace(Trace {
124 dst: Some(target),
125 ttl: mid as u32,
126 })),
127 origin_id,
128 nprobes: 0, // single send
129 session_id: 0, // trace probes carry no session
130 }
131 })
132 .collect()
133}
134
135/// Identify unique TraceSession
136#[derive(Hash, PartialEq, Eq, Clone, Debug)]
137pub struct TraceIdentifier {
138 pub worker_id: u32,
139 pub target: Address,
140 pub origin_id: u32,
141}
142
143/// Check ongoing Trace tasks that have timed out (i.e., a hop didn't respond within the timeout)
144///
145/// - **Linear** sessions follow up with TTL + 1, terminating after `max_failures`
146/// consecutive unresponsive hops
147/// - **Binary** sessions (tracemap) first extend the confirmation window past the
148/// silent midpoint (to rule out an interior unresponsive hop); once the window is
149/// exhausted the silent tail is assumed to start at the midpoint and the search
150/// continues in the lower half
151///
152/// # Arguments
153/// * `measurement` - Shared measurement state containing worker_stacks and trace_config
154/// * `cli_sender` - Sender handle for forwarding '*' hops to the CLI
155pub fn check_trace_timeouts(measurement: MeasurementHandle, cli_sender: CliHandle) {
156 // Get traceroute parameters (read once at start — they don't change during a measurement)
157 let (timeout, max_hops, max_failures, star_unresponsive) = {
158 let lock = measurement.read().unwrap();
159 let Some(config) = lock.as_ref().and_then(|state| state.trace_config.as_ref()) else {
160 // The measurement was torn down before this thread started
161 warn!("[Orchestrator] No active traceroute measurement, stopping timeout checker");
162 return;
163 };
164 (
165 config.timeout,
166 config.max_hops,
167 config.max_failures,
168 config.star_unresponsive,
169 )
170 };
171
172 loop {
173 // Check if measurement is finished
174 if measurement.read().unwrap().is_none() {
175 break;
176 }
177
178 // Keep track of tasks to send to the workers
179 let mut tasks_to_send = Vec::new();
180 // `*` (no-reply) hops to forward to the CLI for timed-out hops: (rx_id, origin_id, reply)
181 let mut star_replies: Vec<(u32, u32, TraceReply)> = Vec::new();
182 let now = Instant::now();
183
184 {
185 // Lock measurement state
186 let mut lock = measurement.write().unwrap();
187 if let Some(ref mut state) = *lock
188 && let Some(ref mut config) = state.trace_config
189 {
190 let session_tracker = &mut config.session_tracker;
191
192 // Iteratively check top of the stack (oldest sessions) to see if they timed out
193 while let Some((_id, deadline)) = session_tracker.expiration_queue.front() {
194 // Deadline is in the future
195 if *deadline > now {
196 break;
197 }
198 // Pop candidate
199 let (id, _old_deadline) = session_tracker.expiration_queue.pop_front().unwrap();
200
201 // The session may have ended in the meantime (drop from the queue)
202 let Some(session) = session_tracker.sessions.get_mut(&id) else {
203 continue;
204 };
205
206 // Verify the session is still timed out (might have been updated)
207 let expiration = session.last_updated + Duration::from_secs(timeout);
208 if expiration > now {
209 // Still alive (received update during check) -> re-queue with its new deadline
210 session_tracker.expiration_queue.push_back((id, expiration));
211 continue;
212 }
213
214 // Hop timed out: emit a '*' hop to the CLI for it, if enabled
215 if star_unresponsive {
216 let timed_out_ttl = match &session.progress {
217 TraceProgress::Linear { current_ttl, .. } => *current_ttl,
218 TraceProgress::Binary { probing_ttl, .. } => *probing_ttl,
219 };
220 star_replies.push((
221 session.worker_id,
222 session.origin_id,
223 TraceReply {
224 tx_id: session.worker_id,
225 trace_dst: session.target,
226 hop_count: timed_out_ttl as u32,
227 ..Default::default() // unresponsive -> None fields
228 },
229 ));
230 }
231
232 session.last_updated = now;
233
234 // Advance the session; None means it is finished
235 let next_ttl = match &mut session.progress {
236 TraceProgress::Linear {
237 current_ttl,
238 consecutive_failures,
239 } => {
240 *consecutive_failures += 1;
241 *current_ttl += 1;
242
243 if *consecutive_failures > max_failures as u8
244 || *current_ttl > max_hops as u8
245 {
246 None
247 } else {
248 Some(*current_ttl)
249 }
250 }
251 TraceProgress::Binary {
252 lo,
253 hi,
254 mid,
255 probing_ttl,
256 window_left,
257 } => {
258 if *window_left > 0 && *probing_ttl < *hi {
259 // Probe the next TTL to rule out an interior unresponsive hop
260 *probing_ttl += 1;
261 *window_left -= 1;
262 Some(*probing_ttl)
263 } else {
264 // Window exhausted: [mid, probing_ttl] is silent → the tail starts at or before mid
265 *hi = mid.saturating_sub(1);
266 if *hi < *lo {
267 None // Search converged: deepest responder found
268 } else {
269 *mid = ttl_midpoint(*lo, *hi);
270 *probing_ttl = *mid;
271 *window_left = max_failures as u8;
272 Some(*probing_ttl)
273 }
274 }
275 }
276 };
277
278 let Some(next_ttl) = next_ttl else {
279 session_tracker.sessions.remove(&id);
280 continue;
281 };
282
283 // Measure the next hop and re-queue the session with a fresh deadline
284 tasks_to_send.push((
285 session.worker_id,
286 Task {
287 task_type: Some(task::TaskType::Trace(Trace {
288 dst: session.target,
289 ttl: next_ttl as u32,
290 })),
291 origin_id: session.origin_id,
292 nprobes: 0, // single send
293 session_id: 0, // trace probes carry no session
294 },
295 ));
296 session_tracker
297 .expiration_queue
298 .push_back((id, now + Duration::from_secs(timeout)));
299 }
300
301 // Put tasks in worker stacks (while we still hold the write lock)
302 for (worker_id, task_to_send) in tasks_to_send {
303 state
304 .worker_stacks
305 .entry(worker_id)
306 .or_default()
307 .push_back(task_to_send);
308 }
309 }
310 }
311
312 // Forward '*' hops for timed-out hops to the CLI
313 if !star_replies.is_empty() {
314 let tx_opt = cli_sender.lock().unwrap().clone();
315 if let Some(tx) = tx_opt {
316 for (rx_id, origin_id, reply) in star_replies {
317 let _ = tx.blocking_send(Ok(ReplyBatch {
318 rx_id,
319 results: vec![Reply {
320 reply_data: Some(ReplyData::Trace(reply)),
321 }],
322 origin_id,
323 }));
324 }
325 }
326 }
327
328 // Sleep for the timeout interval before checking timeouts again
329 thread::sleep(Duration::from_secs(timeout));
330 }
331}