1use crate::custom_module::has_anycast_origin;
2use crate::custom_module::manycastr::WorkerStatus;
3use crate::custom_module::manycastr::WorkerStatus::Probing;
4use crate::custom_module::manycastr::{
5 Address, End, Instruction, LiveTarget, MeasurementType, Probe, ScheduleMeasurement, Task,
6 Tasks, Trace, instruction, task,
7};
8use crate::orchestrator::trace::seed_tracemap_sessions;
9use crate::orchestrator::{
10 LIVE_DISCOVERY_TIMEOUT_SECS, MeasurementHandle, PendingTarget, WorkerRegistry, WorkerSel,
11 wire_nprobes,
12};
13use crate::{ALL_ORIGINS, ALL_WORKERS};
14use log::{debug, info, warn};
15use std::collections::HashMap;
16use std::time::Duration;
17use tokio::spawn;
18use tokio::sync::mpsc;
19use tokio::time::{Instant, Interval, MissedTickBehavior};
20
21const REPLY_GRACE_SECS: u64 = 5;
23
24const STACK_HIGH_WATERMARK_SECS: usize = 5;
26const STACK_LOW_WATERMARK_SECS: usize = 1;
28
29pub enum DistributionStrategy {
31 Broadcast,
33 RoundRobin,
35 Discovery {
37 is_responsive: bool,
39 },
40 Tracemap,
42}
43
44impl DistributionStrategy {
45 pub fn select(m_def: &ScheduleMeasurement) -> Self {
56 match m_def.m_type() {
57 MeasurementType::Catchment => Self::RoundRobin,
58 MeasurementType::Tracemap => Self::Tracemap,
59 MeasurementType::AnycastTraceroute => Self::Discovery {
61 is_responsive: false,
62 },
63 MeasurementType::AnycastLatency if has_anycast_origin(&m_def.configurations) => {
65 Self::Discovery {
66 is_responsive: false,
67 }
68 }
69 MeasurementType::Feed | MeasurementType::FeedTrace => {
71 unreachable!("feed measurements use the live task distributor")
72 }
73 MeasurementType::AnycastLatency | MeasurementType::Laces => {
75 if m_def.is_responsive {
76 Self::Discovery {
77 is_responsive: true,
78 }
79 } else {
80 Self::Broadcast
81 }
82 }
83 }
84 }
85
86 pub fn is_gated_broadcast(&self) -> bool {
89 matches!(
90 self,
91 Self::Discovery {
92 is_responsive: true
93 }
94 )
95 }
96}
97
98pub struct TaskDistributorConfig {
99 pub m_id: u32,
101 pub hitlist: Vec<Address>,
103 pub measurement: MeasurementHandle,
105 pub workers: WorkerRegistry,
107 pub probing_rate: u32,
109 pub probing_rate_interval: Interval,
111 pub number_of_probing_workers: usize,
113 pub worker_interval: u64,
115 pub nprobes: u32,
117 pub probe_interval: u64,
119 pub is_prefix_hitlist: bool,
121}
122
123#[inline]
126fn make_task(
127 addr: Address,
128 is_discovery: bool,
129 origin_id: u32,
130 nprobes: u32,
131 session_id: u32,
132) -> Task {
133 Task {
134 task_type: Some(if is_discovery {
135 task::TaskType::Discovery(Probe { dst: Some(addr) })
136 } else {
137 task::TaskType::Probe(Probe { dst: Some(addr) })
138 }),
139 origin_id,
140 nprobes: wire_nprobes(nprobes),
141 session_id,
142 }
143}
144
145async fn send_to_workers(
153 workers: &WorkerRegistry,
154 worker_id: u32,
155 instruction: Instruction,
156 inter_worker_interval: u64,
157) {
158 if worker_id == ALL_WORKERS {
159 let probing_ids: Vec<u32> = workers
161 .lock()
162 .unwrap()
163 .iter()
164 .filter(|sender| *sender.status == Probing)
165 .map(|sender| sender.worker_id)
166 .collect();
167
168 send_staggered(workers, &probing_ids, instruction, inter_worker_interval);
169 } else {
170 let sender = {
172 let workers = workers.lock().unwrap();
173 workers.iter().find(|s| s.worker_id == worker_id).cloned()
174 };
175 if let Some(sender) = sender {
176 if sender.get_status() != WorkerStatus::Disconnected {
177 let _ = sender.send(Ok(instruction)).await;
178 }
179 } else {
180 warn!("[Orchestrator] No sender found for worker ID {worker_id}");
181 }
182 }
183}
184
185fn send_staggered(
187 workers: &WorkerRegistry,
188 worker_ids: &[u32],
189 instruction: Instruction,
190 inter_worker_interval: u64,
191) {
192 let senders: Vec<_> = {
193 let registry = workers.lock().unwrap();
194 worker_ids
195 .iter()
196 .filter_map(|id| {
197 let sender = registry.iter().find(|s| s.worker_id == *id).cloned();
198 if sender.is_none() {
199 warn!("[Orchestrator] No sender found for worker ID {id}");
200 }
201 sender
202 })
203 .collect()
204 };
205
206 for (probing_index, sender) in (0_u64..).zip(senders) {
207 let task_c = instruction.clone();
208 spawn(async move {
209 tokio::time::sleep(Duration::from_secs(probing_index * inter_worker_interval)).await;
211
212 let _ = sender.send(Ok(task_c)).await;
213 });
214 }
215}
216
217fn resolve_worker_sel(
224 mut worker_ids: Vec<u32>,
225 probing_workers: &[u32],
226 dst: Address,
227) -> Option<WorkerSel> {
228 if worker_ids.is_empty() {
229 return Some(WorkerSel::Any);
230 }
231 if worker_ids.contains(&ALL_WORKERS) {
232 return Some(WorkerSel::All);
233 }
234
235 worker_ids.sort_unstable();
236 worker_ids.dedup();
237 let requested = worker_ids.len();
238 worker_ids.retain(|id| probing_workers.contains(id));
239 match worker_ids.len() {
240 0 => {
241 warn!(
242 "[Orchestrator] Dropping target {dst}: none of its workers are probing in this measurement"
243 );
244 None
245 }
246 probing => {
247 if probing < requested {
248 warn!(
249 "[Orchestrator] Target {dst}: ignoring {} worker(s) not probing in this measurement",
250 requested - probing
251 );
252 }
253 Some(WorkerSel::Set(worker_ids))
254 }
255 }
256}
257
258async fn finalize_measurement(
265 workers: &WorkerRegistry,
266 measurement: &MeasurementHandle,
267 m_id: u32,
268) {
269 {
271 let mut lock = measurement.write().unwrap();
272 match lock.as_mut() {
273 Some(state) if state.m_id == m_id => state.is_finalizing = true,
274 _ => {
275 warn!("[Orchestrator] Measurement {m_id} already ended, skipping finalization.");
276 return;
277 }
278 }
279 }
280 info!("[Orchestrator] Task distribution finished.");
281
282 let end = Instruction {
284 instruction_type: Some(instruction::InstructionType::End(End { code: 0 })),
285 };
286 let senders: Vec<_> = workers.lock().unwrap().clone();
287 for sender in &senders {
288 let _ = sender.send(Ok(end.clone())).await;
289 sender.finished();
290 }
291
292 while matches!(*measurement.read().unwrap(), Some(ref state) if state.m_id == m_id) {
294 tokio::time::sleep(Duration::from_secs(1)).await;
295 }
296}
297
298pub async fn distribute_tasks(config: TaskDistributorConfig, strategy: DistributionStrategy) {
305 let strategy_name = match &strategy {
306 DistributionStrategy::Broadcast => "Broadcast",
307 DistributionStrategy::RoundRobin => "Round-Robin",
308 DistributionStrategy::Discovery { .. } => "Round-Robin Discovery",
309 DistributionStrategy::Tracemap => "Round-Robin Tracemap",
310 };
311 info!("[Orchestrator] Starting {strategy_name} Task Distributor.");
312
313 let is_broadcast = matches!(&strategy, DistributionStrategy::Broadcast);
314 let is_tracemap = matches!(&strategy, DistributionStrategy::Tracemap);
315 let is_discovery = matches!(&strategy, DistributionStrategy::Discovery { .. });
317 let has_follow_ups = is_discovery || is_tracemap;
319 let is_responsive = strategy.is_gated_broadcast();
320
321 let repeat_secs = (config.nprobes.saturating_sub(1)) as u64 * config.probe_interval;
323 let cooldown_secs = if is_broadcast || is_responsive {
324 (config.number_of_probing_workers as u64 * config.worker_interval) + repeat_secs + 1
326 } else {
327 repeat_secs + 1
328 };
329
330 let mut probing_rate_interval = config.probing_rate_interval;
331
332 let mut hitlist_iter = config.hitlist.into_iter();
333 let mut hitlist_exhausted = false;
334 let mut hitlist_exhausted_at: Option<Instant> = None;
336 let mut cooldown_timer: Option<Instant> = None;
337 let mut cooldown_announced = false;
339
340 let task_nprobes = if has_follow_ups { 1 } else { config.nprobes };
342 let inter_worker_interval = config.worker_interval;
343
344 let high_watermark = STACK_HIGH_WATERMARK_SECS * config.probing_rate as usize;
346 let low_watermark = STACK_LOW_WATERMARK_SECS * config.probing_rate as usize;
347
348 spawn(async move {
349 let mut current_index: usize = 0;
350 let mut discovery_paused = false;
351
352 loop {
353 let (worker_id, n_probing) = {
355 let lock = config.measurement.read().unwrap();
356 let state = match *lock {
357 Some(ref s) if s.m_id == config.m_id => s,
358 _ => {
359 warn!("[Orchestrator] Measurement no longer active");
360 break;
361 }
362 };
363
364 let workers = &state.probing_workers;
365 if workers.is_empty() {
366 warn!("[Orchestrator] No more probing workers available, ending measurement.");
367 break;
368 }
369
370 if is_broadcast {
372 (ALL_WORKERS, workers.len())
373 } else {
374 current_index %= workers.len();
375 let id = workers[current_index];
376 current_index = (current_index + 1) % workers.len();
377 (id, workers.len())
378 }
379 };
380
381 let follow_up_count = if has_follow_ups {
383 let (f_worker_id, f_budget) = if is_responsive {
385 (
386 ALL_WORKERS,
387 std::cmp::max(1, config.probing_rate as usize / n_probing),
388 )
389 } else {
390 (worker_id, config.probing_rate as usize)
391 };
392
393 let (follow_up_tasks, max_stack_depth): (Vec<Task>, usize) = {
394 let mut lock = config.measurement.write().unwrap();
395 match lock.as_mut() {
396 Some(state) if state.m_id == config.m_id => {
397 let tasks =
398 if let Some(queue) = state.worker_stacks.get_mut(&f_worker_id) {
399 let n = std::cmp::min(f_budget, queue.len());
400 queue.drain(..n).collect()
401 } else {
402 Vec::new()
403 };
404 let depth = state
405 .worker_stacks
406 .values()
407 .map(|q| q.len())
408 .max()
409 .unwrap_or(0);
410 (tasks, depth)
411 }
412 _ => (Vec::new(), 0),
413 }
414 };
415
416 if discovery_paused {
418 if max_stack_depth <= low_watermark {
419 info!("[Orchestrator] Follow-up backlog drained, resuming discovery.");
420 discovery_paused = false;
421 }
422 } else if max_stack_depth >= high_watermark {
423 info!(
424 "[Orchestrator] Follow-up backlog too large ({max_stack_depth} tasks), pausing discovery."
425 );
426 discovery_paused = true;
427 }
428
429 let count = follow_up_tasks.len();
430 if !follow_up_tasks.is_empty() {
431 send_to_workers(
432 &config.workers,
433 f_worker_id,
434 Instruction {
435 instruction_type: Some(instruction::InstructionType::Tasks(Tasks {
436 tasks: follow_up_tasks,
437 })),
438 },
439 inter_worker_interval,
440 )
441 .await;
442 }
443 count
444 } else {
445 0
446 };
447
448 let follow_up_cost = if is_responsive {
450 follow_up_count.saturating_mul(n_probing)
452 } else {
453 follow_up_count
454 };
455 let remainder = (config.probing_rate as usize).saturating_sub(follow_up_cost);
456
457 if remainder > 0 && !hitlist_exhausted && !discovery_paused {
458 let tasks: Vec<Task> = if is_tracemap {
460 let addrs: Vec<Address> = hitlist_iter.by_ref().take(remainder).collect();
462 let mut lock = config.measurement.write().unwrap();
463 match lock
464 .as_mut()
465 .filter(|state| state.m_id == config.m_id)
466 .and_then(|state| state.trace_config.as_mut())
467 {
468 Some(trace_config) => {
469 seed_tracemap_sessions(addrs, worker_id, trace_config)
470 }
471 None => {
472 warn!(
473 "[Orchestrator] No traceroute configuration for tracemap, ending measurement."
474 );
475 break;
476 }
477 }
478 } else if config.is_prefix_hitlist {
479 let lock = config.measurement.read().unwrap();
481 match *lock {
482 Some(ref state) if state.m_id == config.m_id => hitlist_iter
483 .by_ref()
484 .filter(|addr| !state.resolved_targets.contains(&addr.prefix_base()))
485 .take(remainder)
486 .map(|addr| make_task(addr, is_discovery, ALL_ORIGINS, task_nprobes, 0))
487 .collect(),
488 _ => break, }
490 } else {
491 hitlist_iter
493 .by_ref()
494 .take(remainder)
495 .map(|addr| make_task(addr, is_discovery, ALL_ORIGINS, task_nprobes, 0))
496 .collect()
497 };
498
499 if tasks.len() < remainder {
500 hitlist_exhausted = true;
501 hitlist_exhausted_at = Some(Instant::now());
502 if has_follow_ups {
503 info!(
504 "[Orchestrator] All discovery probes sent, awaiting follow-up probes."
505 );
506 }
507 }
508
509 if !tasks.is_empty() {
510 send_to_workers(
511 &config.workers,
512 worker_id,
513 Instruction {
514 instruction_type: Some(instruction::InstructionType::Tasks(Tasks {
515 tasks,
516 })),
517 },
518 inter_worker_interval,
519 )
520 .await;
521 }
522 }
523
524 if hitlist_exhausted {
526 if has_follow_ups {
527 let (stacks_empty, traces_active) = {
529 let lock = config.measurement.read().unwrap();
530 match *lock {
531 Some(ref state) if state.m_id == config.m_id => (
532 state.worker_stacks.values().all(|q| q.is_empty()),
533 state
534 .trace_config
535 .as_ref()
536 .is_some_and(|c| !c.session_tracker.sessions.is_empty()),
537 ),
538 _ => break, }
540 };
541
542 if stacks_empty && !traces_active {
543 if let Some(start_time) = cooldown_timer {
544 if start_time.elapsed() >= Duration::from_secs(cooldown_secs) {
545 break;
546 }
547 } else if hitlist_exhausted_at
548 .is_some_and(|t| t.elapsed() >= Duration::from_secs(REPLY_GRACE_SECS))
549 {
550 if cooldown_announced {
552 debug!(
553 "[Orchestrator] Idle again after late follow-ups. Restarting the {cooldown_secs}-second cooldown."
554 );
555 } else {
556 info!(
557 "[Orchestrator] No more tasks. Awaiting a {cooldown_secs}-second cooldown."
558 );
559 cooldown_announced = true;
560 }
561 cooldown_timer = Some(Instant::now());
562 }
563 } else {
564 cooldown_timer = None;
566 }
567 } else {
568 break;
570 }
571 }
572
573 probing_rate_interval.tick().await;
574 }
575
576 if !has_follow_ups {
578 info!("[Orchestrator] All tasks sent. Awaiting a {cooldown_secs}-second cooldown.");
579 tokio::time::sleep(Duration::from_secs(cooldown_secs)).await;
580 }
581
582 finalize_measurement(&config.workers, &config.measurement, config.m_id).await;
583 });
584}
585
586pub fn distribute_live_tasks(
600 mut feed: mpsc::Receiver<LiveTarget>,
601 measurement: MeasurementHandle,
602 workers: WorkerRegistry,
603 m_def: &ScheduleMeasurement,
604 m_id: u32,
605) {
606 info!("[Orchestrator] Starting Live Task Distributor.");
607
608 let probing_rate = m_def.probing_rate;
609 let worker_interval = m_def.worker_interval as u64;
610 let probe_interval = m_def.probe_interval as u64;
611 let is_responsive = m_def.is_responsive;
612 let is_trace = m_def.m_type() == MeasurementType::FeedTrace;
613
614 spawn(async move {
615 let mut tick_interval = tokio::time::interval(Duration::from_secs(1));
616 tick_interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
617 let mut current_index: usize = 0;
618 let batch_capacity = probing_rate as usize;
619 let mut feed_closed = false;
620
621 loop {
622 tick_interval.tick().await;
623
624 let (follow_ups, set_follow_ups, probing_workers, pending_count) = {
626 let mut lock = measurement.write().unwrap();
627 let Some(state) = lock.as_mut().filter(|s| s.m_id == m_id) else {
628 warn!("[Orchestrator] Measurement no longer active");
629 break;
630 };
631
632 if state.probing_workers.is_empty() {
633 warn!("[Orchestrator] No more probing workers available, ending measurement.");
634 break;
635 }
636
637 let mut follow_ups: Vec<(u32, Vec<Task>)> = Vec::new();
638 for (worker_id, stack) in state.worker_stacks.iter_mut() {
639 if !stack.is_empty() {
640 let n = stack.len().min(batch_capacity);
641 follow_ups.push((*worker_id, stack.drain(..n).collect()));
642 }
643 }
644
645 let mut set_follow_ups: Vec<(Vec<u32>, Vec<Task>)> = Vec::new();
646 if let Some(live) = state.live.as_mut() {
647 for (worker_ids, stack) in live.set_stacks.iter_mut() {
648 if !stack.is_empty() {
649 let n = stack.len().min(batch_capacity);
650 set_follow_ups.push((worker_ids.clone(), stack.drain(..n).collect()));
651 }
652 }
653
654 if is_trace {
656 let now = std::time::Instant::now();
657 live.trace_targets.retain(|_, deadline| *deadline > now);
658 }
659 }
660
661 let pending_count = state.live.as_ref().map_or(0, |live| live.pending.len());
662 (
663 follow_ups,
664 set_follow_ups,
665 state.probing_workers.clone(),
666 pending_count,
667 )
668 };
669
670 let follow_up_count: usize = follow_ups
671 .iter()
672 .map(|(_, tasks)| tasks.len())
673 .sum::<usize>()
674 + set_follow_ups
675 .iter()
676 .map(|(_, tasks)| tasks.len())
677 .sum::<usize>();
678 for (worker_id, tasks) in follow_ups {
679 send_to_workers(
681 &workers,
682 worker_id,
683 Instruction {
684 instruction_type: Some(instruction::InstructionType::Tasks(Tasks {
685 tasks,
686 })),
687 },
688 worker_interval,
689 )
690 .await;
691 }
692 for (worker_ids, tasks) in set_follow_ups {
694 send_staggered(
695 &workers,
696 &worker_ids,
697 Instruction {
698 instruction_type: Some(instruction::InstructionType::Tasks(Tasks {
699 tasks,
700 })),
701 },
702 worker_interval,
703 );
704 }
705
706 let remainder = batch_capacity.saturating_sub(follow_up_count);
708 let mut batch: Vec<LiveTarget> = Vec::new();
709 while batch.len() < remainder {
710 match feed.try_recv() {
711 Ok(target) => batch.push(target),
712 Err(mpsc::error::TryRecvError::Empty) => break,
713 Err(mpsc::error::TryRecvError::Disconnected) => {
714 if !feed_closed {
715 info!("[Orchestrator] Live feed closed, finishing outstanding tasks.");
716 feed_closed = true;
717 }
718 break;
719 }
720 }
721 }
722 let dispatched = batch.len();
723
724 let mut per_set: HashMap<Vec<u32>, Vec<Task>> = HashMap::new();
726 let mut broadcast: Vec<Task> = Vec::new();
727 {
728 let mut lock = measurement.write().unwrap();
729 let Some(state) = lock.as_mut().filter(|s| s.m_id == m_id) else {
730 warn!("[Orchestrator] Measurement no longer active");
731 break;
732 };
733
734 for mut target in batch {
735 let Some(dst) = target.dst else { continue };
736
737 let Some(sel) = resolve_worker_sel(
739 std::mem::take(&mut target.worker_ids),
740 &probing_workers,
741 dst,
742 ) else {
743 continue;
744 };
745
746 if is_trace {
748 if let Some(live) = state.live.as_mut() {
750 let window = probing_workers.len() as u64 * worker_interval
751 + target.nprobes.max(1).saturating_sub(1) as u64 * probe_interval
752 + REPLY_GRACE_SECS;
753 let deadline = std::time::Instant::now() + Duration::from_secs(window);
754 live.trace_targets.insert(dst, deadline);
755 }
756
757 let ttl = if target.ttl == 0 { 255 } else { target.ttl };
759 let task = Task {
760 task_type: Some(task::TaskType::Trace(Trace {
761 dst: Some(dst),
762 ttl,
763 })),
764 origin_id: target.origin_id,
765 nprobes: wire_nprobes(target.nprobes),
766 session_id: 0,
767 };
768 match sel {
769 WorkerSel::Any => {
770 current_index %= probing_workers.len();
772 per_set
773 .entry(vec![probing_workers[current_index]])
774 .or_default()
775 .push(task);
776 current_index += 1;
777 }
778 WorkerSel::All => broadcast.push(task),
779 WorkerSel::Set(ids) => {
780 per_set.entry(ids).or_default().push(task);
781 }
782 }
783 continue;
784 }
785
786 if is_responsive && sel.is_multi() {
788 let probe_worker = match &sel {
790 WorkerSel::Any | WorkerSel::All => {
791 current_index %= probing_workers.len();
792 let id = probing_workers[current_index];
793 current_index += 1;
794 id
795 }
796 WorkerSel::Set(ids) => {
797 let id = ids[current_index % ids.len()];
798 current_index += 1;
799 id
800 }
801 };
802
803 let Some(live) = state.live.as_mut() else {
804 continue;
805 };
806
807 live.pending.insert(
808 (dst, target.session_id),
809 PendingTarget {
810 worker_sel: sel,
811 discovery_worker: probe_worker,
812 nprobes: target.nprobes,
813 deadline: std::time::Instant::now()
814 + Duration::from_secs(LIVE_DISCOVERY_TIMEOUT_SECS),
815 },
816 );
817
818 per_set
820 .entry(vec![probe_worker])
821 .or_default()
822 .push(make_task(dst, true, target.origin_id, 1, target.session_id));
823 continue;
824 }
825
826 let task = make_task(
828 dst,
829 false,
830 target.origin_id,
831 target.nprobes,
832 target.session_id,
833 );
834 match sel {
835 WorkerSel::Any => {
836 current_index %= probing_workers.len();
838 per_set
839 .entry(vec![probing_workers[current_index]])
840 .or_default()
841 .push(task);
842 current_index += 1;
843 }
844 WorkerSel::All => broadcast.push(task),
845 WorkerSel::Set(ids) => {
846 per_set.entry(ids).or_default().push(task);
847 }
848 }
849 }
850 }
851
852 for (worker_ids, tasks) in per_set {
854 send_staggered(
855 &workers,
856 &worker_ids,
857 Instruction {
858 instruction_type: Some(instruction::InstructionType::Tasks(Tasks {
859 tasks,
860 })),
861 },
862 worker_interval,
863 );
864 }
865
866 if !broadcast.is_empty() {
868 send_to_workers(
869 &workers,
870 ALL_WORKERS,
871 Instruction {
872 instruction_type: Some(instruction::InstructionType::Tasks(Tasks {
873 tasks: broadcast,
874 })),
875 },
876 worker_interval,
877 )
878 .await;
879 }
880
881 if feed_closed && dispatched == 0 && follow_up_count == 0 && pending_count == 0 {
883 break;
884 }
885 }
886
887 finalize_measurement(&workers, &measurement, m_id).await;
889 });
890}