1use crate::custom_module::has_anycast_origin;
2use crate::custom_module::manycastr::WorkerStatus::{Disconnected, Idle, Listening, Probing};
3use crate::custom_module::manycastr::controller_server::Controller;
4use crate::custom_module::manycastr::reply::ReplyData;
5use crate::custom_module::manycastr::{
6 Ack, CliMessage, DiscoveryReply, Empty, Finished, Init, Instruction, LiveTarget,
7 MeasurementType, Probe, Reply, ReplyBatch, ScheduleMeasurement, Start, Task, TraceOptions,
8 TraceReply, Worker, WorkerStatus, cli_message, instruction, task,
9};
10use crate::orchestrator::cli::CLIReceiver;
11use crate::orchestrator::result_handler::{
12 SessionTracker, discovery_handler, trace_discovery_handler, trace_replies_handler,
13};
14use crate::orchestrator::task_distributor::{
15 DistributionStrategy, TaskDistributorConfig, distribute_live_tasks, distribute_tasks,
16};
17use crate::orchestrator::trace::check_trace_timeouts;
18use crate::orchestrator::worker::{WorkerReceiver, WorkerSender};
19use crate::orchestrator::{
20 ControllerService, LiveState, MeasurementHandle, MeasurementState, Participant,
21 TracerouteConfig, WorkerRegistry, WorkerSel, wire_nprobes,
22};
23use crate::{ALL_ORIGINS, ALL_WORKERS, custom_module};
24use log::{error, info, warn};
25
26use std::collections::{HashMap, HashSet};
27use std::sync::{Arc, Mutex};
28use std::time::{Duration, Instant};
29use tokio::sync::mpsc;
30use tokio::time::MissedTickBehavior;
31use tonic::{Request, Response, Status};
32
33const FEED_BUFFER_SECS: usize = 5;
36
37struct ClassifiedWorkers {
39 participating_ids: Vec<u32>,
40 probing_ids: Vec<u32>,
41}
42
43#[tonic::async_trait]
46impl Controller for ControllerService {
47 async fn measurement_finished(
56 &self,
57 request: Request<Finished>,
58 ) -> Result<Response<Ack>, Status> {
59 self.check_worker_access("measurement_finished")?;
60
61 let finished_measurement = request.into_inner();
62 let m_id: u32 = finished_measurement.m_id;
63 let finished_worker_id = finished_measurement.worker_id;
64
65 let mut should_notify = false;
67
68 {
69 let mut lock = self.measurement.write().unwrap();
70 if let Some(ref mut state) = *lock {
71 if state.m_id != m_id {
72 warn!(
74 "[Orchestrator] Worker {finished_worker_id} finished measurement {m_id}, but the active measurement is {}",
75 state.m_id
76 );
77 return Err(Status::not_found("Measurement ID mismatch"));
78 }
79
80 if state.participants.remove(&finished_worker_id).is_none() {
82 warn!(
83 "[Orchestrator] Received finished signal from non-participant worker {finished_worker_id}"
84 );
85 return Ok(Response::new(Ack::ok()));
86 }
87 state.start_instructions.remove(&finished_worker_id);
88 state.probing_workers.retain(|&id| id != finished_worker_id);
89
90 {
92 let workers = self.saved_workers.lock().unwrap();
93 if let Some(w) = workers.iter().find(|w| w.worker_id == finished_worker_id) {
94 w.finished();
95 }
96 }
97
98 if state.active_workers() == 0 {
99 info!(
101 "[Orchestrator] All workers finished for measurement {m_id}. Notifying CLI"
102 );
103 should_notify = true;
104
105 *lock = None;
107 }
108 } else {
109 warn!(
111 "[Orchestrator] Received measurement finished signal for worker {finished_worker_id}, but no measurement is active."
112 );
113 return Err(Status::not_found("No active measurement found"));
114 }
115 }
116
117 if should_notify {
119 let cli_tx = { self.cli_sender.lock().unwrap().clone() };
120 if let Some(tx) = cli_tx
121 && tx.send(Ok(ReplyBatch::default())).await.is_err()
122 {
123 warn!("[Orchestrator] CLI disconnected, cannot send measurement-finished signal.");
124 }
125 }
126
127 Ok(Response::new(Ack::ok()))
129 }
130
131 type WorkerConnectStream = WorkerReceiver<Result<Instruction, Status>>;
132
133 async fn worker_connect(
140 &self,
141 request: Request<Worker>,
142 ) -> Result<Response<Self::WorkerConnectStream>, Status> {
143 self.check_worker_access("worker_connect")?;
144
145 let worker = request.into_inner();
146 let hostname = worker.hostname;
147 let unicast_v4 = worker.unicast_v4;
148 let unicast_v6 = worker.unicast_v6;
149 let (tx, rx) = mpsc::channel::<Result<Instruction, Status>>(1000);
150 let (worker_id, is_reconnect) = self.get_worker_id(&hostname)?;
152
153 if is_reconnect {
154 info!("[Orchestrator] Reconnecting worker: {hostname}");
155 } else {
156 info!("[Orchestrator] New worker connected: {hostname}");
157 }
158
159 tx.send(Ok(Instruction {
161 instruction_type: Some(instruction::InstructionType::Init(Init { worker_id })),
162 }))
163 .await
164 .expect("Unable to send task");
165
166 let worker_status = Arc::new(Mutex::new(Idle));
167
168 let worker_tx = WorkerSender {
169 inner: tx.clone(),
170 worker_id,
171 hostname: hostname.clone(),
172 status: worker_status.clone(),
173 unicast_v4,
174 unicast_v6,
175 };
176
177 if is_reconnect {
179 let mut senders = self.saved_workers.lock().unwrap();
180 senders.retain(|sender| sender.worker_id != worker_id);
181 }
182
183 self.saved_workers.lock().unwrap().push(worker_tx);
185
186 if is_reconnect {
188 self.try_rejoin(worker_id, &hostname, &tx, &worker_status);
189 }
190
191 let worker_rx = WorkerReceiver {
193 inner: rx,
194 measurement: self.measurement.clone(),
195 cli_sender: self.cli_sender.clone(),
196 hostname,
197 status: worker_status,
198 worker_id,
199 };
200
201 Ok(Response::new(worker_rx))
203 }
204
205 type DoMeasurementStream = CLIReceiver<Result<ReplyBatch, Status>>;
206
207 async fn do_measurement(
219 &self,
220 request: Request<ScheduleMeasurement>,
221 ) -> Result<Response<Self::DoMeasurementStream>, Status> {
222 self.check_cli_access("do_measurement")?;
223
224 info!("[Orchestrator] Received CLI measurement request for measurement");
225 let mut m_def = request.into_inner();
226
227 self.validate_rate(m_def.probing_rate)?;
229 self.validate_origins(&m_def)?;
230 let worker_interval = m_def.worker_interval as u64;
231 let probe_interval = m_def.probe_interval as u64;
232 let nprobes = m_def.number_of_probes;
233 let probing_rate = m_def.probing_rate;
234 let m_type = m_def.m_type();
235
236 if m_type.is_feed() {
238 return Err(Status::invalid_argument(
239 "Feed measurements must use the live measurement stream",
240 ));
241 }
242
243 let ClassifiedWorkers {
245 participating_ids,
246 probing_ids,
247 } = self.classify_workers(&m_def)?;
248 let probing_workers_count = probing_ids.len();
249 let m_id = self.next_m_id();
250
251 let strategy = DistributionStrategy::select(&m_def);
253
254 self.init_measurement(
255 &m_def,
256 m_id,
257 &participating_ids,
258 &probing_ids,
259 strategy.is_gated_broadcast(),
260 )?;
261
262 info!(
263 "[Orchestrator] {} participating workers, {} will probe ({worker_interval} seconds between probing workers)",
264 participating_ids.len(),
265 probing_workers_count,
266 );
267
268 let (cli_tx, cli_rx) = mpsc::channel::<Result<ReplyBatch, Status>>(1000);
270 let _ = self.cli_sender.lock().unwrap().insert(cli_tx);
271
272 send_start_instructions(&self.saved_workers, &self.measurement, &m_def, m_id).await;
274 tokio::time::sleep(Duration::from_secs(1)).await;
275
276 if let Some(trace_options) = m_def.trace_options.take() {
278 let trace_origin_id = m_def
280 .configurations
281 .first()
282 .and_then(|c| c.origin)
283 .map(|o| o.origin_id)
284 .unwrap_or(ALL_ORIGINS);
285 self.setup_traceroute(trace_options, trace_origin_id);
286 }
287
288 let mut probing_rate_interval = if matches!(strategy, DistributionStrategy::Broadcast) {
290 tokio::time::interval(Duration::from_secs(1))
291 } else {
292 tokio::time::interval(Duration::from_secs(1) / probing_workers_count as u32)
293 };
294 probing_rate_interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
296
297 let task_config = TaskDistributorConfig {
299 m_id,
300 hitlist: std::mem::take(&mut m_def.hitlist),
301 measurement: self.measurement.clone(),
302 workers: Arc::clone(&self.saved_workers),
303 probing_rate,
304 probing_rate_interval,
305 number_of_probing_workers: probing_workers_count,
306 worker_interval,
307 nprobes,
308 probe_interval,
309 is_prefix_hitlist: m_def.is_prefix_hitlist,
310 };
311
312 distribute_tasks(task_config, strategy).await;
313
314 Ok(Response::new(CLIReceiver {
316 inner: cli_rx,
317 measurement: self.measurement.clone(),
318 workers: Arc::clone(&self.saved_workers),
319 m_id,
320 }))
321 }
322
323 type LiveMeasurementStream = CLIReceiver<Result<ReplyBatch, Status>>;
324
325 async fn live_measurement(
335 &self,
336 request: Request<tonic::Streaming<CliMessage>>,
337 ) -> Result<Response<Self::LiveMeasurementStream>, Status> {
338 self.check_cli_access("live_measurement")?;
339
340 let mut inbound = request.into_inner();
341
342 let m_def = match inbound.message().await? {
344 Some(CliMessage {
345 message: Some(cli_message::Message::Start(m_def)),
346 }) => m_def,
347 _ => {
348 return Err(Status::invalid_argument(
349 "First message on a live stream must be the measurement definition",
350 ));
351 }
352 };
353
354 if !m_def.m_type().is_feed() {
355 return Err(Status::invalid_argument(
356 "Live measurements require a feed measurement type (-m feed or -m feed-trace)",
357 ));
358 }
359
360 let mut origin_workers: HashMap<u32, HashSet<u32>> = HashMap::new();
362 for config in &m_def.configurations {
363 if let Some(origin) = &config.origin {
364 origin_workers
365 .entry(origin.origin_id)
366 .or_default()
367 .insert(config.worker_id);
368 }
369 }
370 let all_assignments: HashSet<u32> =
371 m_def.configurations.iter().map(|c| c.worker_id).collect();
372 for (origin_id, assigned) in &origin_workers {
373 if !assigned.contains(&ALL_WORKERS) && *assigned != all_assignments {
374 return Err(Status::invalid_argument(format!(
375 "Live measurements require origins shared among all probing workers (origin {origin_id} is not)"
376 )));
377 }
378 }
379
380 self.validate_rate(m_def.probing_rate)?;
382 self.validate_origins(&m_def)?;
383 let probing_rate = m_def.probing_rate;
384
385 info!(
386 "[Orchestrator] Received CLI live measurement request (rate {probing_rate} per worker)"
387 );
388
389 let ClassifiedWorkers {
391 participating_ids,
392 probing_ids,
393 } = self.classify_workers(&m_def)?;
394 let probing_workers_count = probing_ids.len();
395 if probing_workers_count == 0 {
396 return Err(Status::new(
397 tonic::Code::Cancelled,
398 "No probing workers available",
399 ));
400 }
401
402 let m_id = self.next_m_id();
404 self.init_measurement(&m_def, m_id, &participating_ids, &probing_ids, false)?;
406
407 if let Some(state) = self.measurement.write().unwrap().as_mut() {
408 state.live = Some(LiveState {
409 pending: HashMap::new(),
410 set_stacks: HashMap::new(),
411 trace_targets: HashMap::new(),
412 });
413 }
414
415 self.spawn_discovery_sweeper();
417
418 info!(
419 "[Orchestrator] {} participating workers, {} will probe",
420 participating_ids.len(),
421 probing_workers_count,
422 );
423
424 let (cli_tx, cli_rx) = mpsc::channel::<Result<ReplyBatch, Status>>(1000);
426 let _ = self.cli_sender.lock().unwrap().insert(cli_tx);
427
428 send_start_instructions(&self.saved_workers, &self.measurement, &m_def, m_id).await;
430 tokio::time::sleep(Duration::from_secs(1)).await;
431
432 let capacity = (probing_rate as usize * FEED_BUFFER_SECS).max(1000);
434 let (feed_tx, feed_rx) = mpsc::channel::<LiveTarget>(capacity);
435
436 tokio::spawn(async move {
438 loop {
439 match inbound.message().await {
440 Ok(Some(CliMessage {
441 message: Some(cli_message::Message::Targets(batch)),
442 })) => {
443 for target in batch.targets {
444 if feed_tx.send(target).await.is_err() {
445 return; }
447 }
448 }
449 Ok(Some(_)) => {
450 warn!("[Orchestrator] Ignoring unexpected message on live stream");
451 }
452 Ok(None) => return, Err(e) => {
454 warn!("[Orchestrator] Live stream error: {e}");
455 return;
456 }
457 }
458 }
459 });
460
461 distribute_live_tasks(
462 feed_rx,
463 self.measurement.clone(),
464 Arc::clone(&self.saved_workers),
465 &m_def,
466 m_id,
467 );
468
469 Ok(Response::new(CLIReceiver {
471 inner: cli_rx,
472 measurement: self.measurement.clone(),
473 workers: Arc::clone(&self.saved_workers),
474 m_id,
475 }))
476 }
477
478 async fn list_workers(
482 &self,
483 _request: Request<Empty>,
484 ) -> Result<Response<custom_module::manycastr::Status>, Status> {
485 self.check_cli_access("list_workers")?;
486
487 let workers_list = self.saved_workers.lock().unwrap();
489 let mut workers = Vec::new();
490 for worker in workers_list.iter() {
491 workers.push(Worker {
492 worker_id: worker.worker_id,
493 hostname: worker.hostname.clone(),
494 status: worker.get_status() as i32,
495 unicast_v4: worker.unicast_v4,
496 unicast_v6: worker.unicast_v6,
497 });
498 }
499
500 let status = custom_module::manycastr::Status { workers };
501 Ok(Response::new(status))
502 }
503
504 async fn send_result(&self, request: Request<ReplyBatch>) -> Result<Response<Ack>, Status> {
512 self.check_worker_access("send_result")?;
513
514 let task_result = request.into_inner();
516 let catcher_id = task_result.rx_id;
517 let origin_id = task_result.origin_id;
518
519 let mut results_bucket: Vec<Reply> = Vec::new();
521 let mut trace_bucket: Vec<TraceReply> = Vec::new();
522 let mut discovery_bucket: Vec<DiscoveryReply> = Vec::new();
523
524 for result_wrapper in task_result.results {
525 match result_wrapper.reply_data {
526 Some(ReplyData::Trace(t)) => {
527 trace_bucket.push(t);
528 }
529 Some(ReplyData::Discovery(d)) => {
530 discovery_bucket.push(d);
531 }
532 Some(inner_data) => {
533 results_bucket.push(Reply {
534 reply_data: Some(inner_data),
535 });
536 }
537 None => {}
538 }
539 }
540
541 if !discovery_bucket.is_empty() || !trace_bucket.is_empty() {
543 let mut lock = self.measurement.write().unwrap();
544 let Some(state) = lock.as_mut() else {
545 warn!(
547 "[Orchestrator] Dropping {} late replies from worker {catcher_id} (no active measurement)",
548 discovery_bucket.len() + trace_bucket.len()
549 );
550 return Ok(Response::new(Ack::ok()));
551 };
552
553 if let Some(live) = state.live.as_mut() {
555 for reply in discovery_bucket.drain(..) {
556 let Some(src) = reply.src else { continue };
557 let Some((session_id, pending)) = live.remove_pending(src, reply.session_id)
558 else {
559 continue; };
561
562 let task = Task {
564 task_type: Some(task::TaskType::Probe(Probe { dst: Some(src) })),
565 origin_id,
566 nprobes: wire_nprobes(pending.nprobes), session_id,
568 };
569 match pending.worker_sel {
570 WorkerSel::Any => state
572 .worker_stacks
573 .entry(pending.discovery_worker)
574 .or_default()
575 .push_back(task),
576 WorkerSel::All => state
577 .worker_stacks
578 .entry(ALL_WORKERS)
579 .or_default()
580 .push_back(task),
581 WorkerSel::Set(worker_ids) => live
583 .set_stacks
584 .entry(worker_ids)
585 .or_default()
586 .push_back(task),
587 }
588 }
589 }
590
591 discovery_bucket.retain(|reply| match reply.src {
593 Some(addr) if state.is_prefix_hitlist => {
594 state.resolved_targets.insert(addr.prefix_base())
596 }
597 Some(addr) => state.resolved_targets.insert(addr),
598 None => false,
599 });
600
601 if !discovery_bucket.is_empty() {
602 match state.m_type {
603 MeasurementType::Laces | MeasurementType::AnycastLatency => {
605 let follow_up_id = if state.is_gated_broadcast {
607 ALL_WORKERS
608 } else {
609 catcher_id
610 };
611 discovery_handler(
612 discovery_bucket,
613 follow_up_id,
614 &mut state.worker_stacks,
615 origin_id,
616 state.nprobes,
617 );
618 }
619
620 MeasurementType::AnycastTraceroute => {
622 if let Some(config) = state.trace_config.as_mut() {
623 trace_discovery_handler(
624 discovery_bucket,
625 catcher_id,
626 &mut state.worker_stacks,
627 config,
628 origin_id,
629 );
630 }
631 }
632
633 MeasurementType::Catchment
634 | MeasurementType::Tracemap
635 | MeasurementType::Feed
636 | MeasurementType::FeedTrace => warn!(
637 "[Orchestrator] Received discovery results for Origin {origin_id}, from Worker {catcher_id}, for unsupported mode: {}",
638 state.m_type
639 ),
640 }
641 }
642
643 if !trace_bucket.is_empty() {
644 if let Some(config) = state.trace_config.as_mut() {
645 let matched_replies = trace_replies_handler(
647 trace_bucket,
648 &mut state.worker_stacks,
649 config,
650 origin_id,
651 );
652
653 for t in matched_replies {
654 results_bucket.push(Reply {
655 reply_data: Some(ReplyData::Trace(t)),
656 });
657 }
658 } else if state.m_type == MeasurementType::FeedTrace {
659 let now = std::time::Instant::now();
661 if let Some(live) = state.live.as_ref() {
662 for t in trace_bucket {
663 let is_probed_target = t.trace_dst.is_some_and(|dst| {
664 live.trace_targets
665 .get(&dst)
666 .is_some_and(|deadline| *deadline > now)
667 });
668 if is_probed_target && state.participants.contains_key(&t.tx_id) {
669 results_bucket.push(Reply {
670 reply_data: Some(ReplyData::Trace(t)),
671 });
672 }
673 }
674 }
675 }
676 }
677 }
678
679 if !results_bucket.is_empty() {
680 let is_prefix_catchment = {
682 let lock = self.measurement.read().unwrap();
683 matches!(*lock, Some(ref state) if state.is_prefix_hitlist && state.m_type == MeasurementType::Catchment)
684 };
685 if is_prefix_catchment {
686 let mut lock = self.measurement.write().unwrap();
688 if let Some(state) = lock.as_mut() {
689 for reply in &results_bucket {
690 if let Some(ReplyData::Measurement(m)) = &reply.reply_data
691 && let Some(src) = m.src
692 {
693 state.resolved_targets.insert(src.prefix_base());
694 }
695 }
696 }
697 }
698 }
699
700 if !results_bucket.is_empty() {
701 let tx = self.cli_sender.lock().unwrap().clone();
703
704 if let Some(tx) = tx
705 && tx
706 .send(Ok(ReplyBatch {
707 rx_id: catcher_id,
708 results: results_bucket,
709 origin_id,
710 }))
711 .await
712 .is_err()
713 {
714 warn!("[Orchestrator] CLI disconnected, dropping result batch.");
715 }
716 }
717
718 Ok(Response::new(Ack::ok()))
719 }
720}
721
722impl ControllerService {
723 fn validate_rate(&self, probing_rate: u32) -> Result<(), Status> {
729 let Some(max_rate) = self.max_rate else {
730 return Ok(()); };
732 if probing_rate > max_rate {
733 warn!(
734 "[Orchestrator] Refusing measurement: probing rate {probing_rate} exceeds the configured maximum of {max_rate}"
735 );
736 return Err(Status::invalid_argument(format!(
737 "Probing rate {probing_rate} exceeds this orchestrator's maximum rate of {max_rate} (probes per second, per worker)"
738 )));
739 }
740 Ok(())
741 }
742
743 fn validate_origins(&self, m_def: &ScheduleMeasurement) -> Result<(), Status> {
751 let Some(allowed_origins) = &self.allowed_origins else {
752 return Ok(()); };
754
755 for origin in m_def
756 .configurations
757 .iter()
758 .filter_map(|c| c.origin.as_ref())
759 {
760 let Some(src) = &origin.src else { continue };
761 let p_type = origin.p_type();
762
763 if allowed_origins
764 .iter()
765 .any(|rule| rule.src == *src && rule.allows(p_type))
766 {
767 continue;
768 }
769
770 let reason = if allowed_origins.iter().any(|rule| rule.src == *src) {
772 format!(
773 "protocol {} is not allowed for source address {src}",
774 p_type.as_str()
775 )
776 } else {
777 format!("source address {src} is not available")
778 };
779 warn!("[Orchestrator] Refusing measurement: {reason}");
780
781 let available: Vec<String> = allowed_origins
782 .iter()
783 .map(|rule| rule.to_string())
784 .collect();
785 return Err(Status::permission_denied(format!(
786 "Origin not allowed by this orchestrator: {reason}. Available origins: {}",
787 available.join(", ")
788 )));
789 }
790
791 Ok(())
792 }
793
794 fn classify_workers(&self, m_def: &ScheduleMeasurement) -> Result<ClassifiedWorkers, Status> {
800 let mut participating_worker_ids = Vec::new();
801 let mut probing_worker_ids = Vec::new();
802
803 let is_anycast = has_anycast_origin(&m_def.configurations);
805
806 let workers = self.saved_workers.lock().unwrap();
807
808 for worker in workers.iter() {
809 let mut status_lock = worker.status.lock().unwrap();
810
811 if *status_lock == Disconnected {
813 warn!("[Orchestrator] Worker {} unavailable.", worker.hostname);
814 continue;
815 }
816
817 let is_probing = m_def.configurations.iter().any(|config| {
819 config.worker_id == worker.worker_id || config.worker_id == ALL_WORKERS
820 });
821
822 if is_probing {
823 *status_lock = Probing;
824 probing_worker_ids.push(worker.worker_id);
825 participating_worker_ids.push(worker.worker_id);
826 } else if is_anycast {
827 *status_lock = Listening;
828 participating_worker_ids.push(worker.worker_id);
829 } else {
830 *status_lock = Idle;
831 };
832 }
833
834 if participating_worker_ids.is_empty() {
836 error!("[Orchestrator] No connected workers available for this configuration.");
837 return Err(Status::new(tonic::Code::Cancelled, "No connected workers"));
838 }
839
840 if m_def.configurations.iter().any(|conf| {
842 conf.worker_id != ALL_WORKERS && !workers.iter().any(|w| w.worker_id == conf.worker_id)
843 }) {
844 error!("[Orchestrator] Configuration contains unknown worker IDs.");
845 return Err(Status::new(
846 tonic::Code::Cancelled,
847 "Unknown worker in configuration",
848 ));
849 }
850
851 Ok(ClassifiedWorkers {
852 participating_ids: participating_worker_ids,
853 probing_ids: probing_worker_ids,
854 })
855 }
856
857 fn try_rejoin(
863 &self,
864 worker_id: u32,
865 hostname: &str,
866 tx: &mpsc::Sender<Result<Instruction, Status>>,
867 status: &Arc<Mutex<WorkerStatus>>,
868 ) {
869 let mut lock = self.measurement.write().unwrap();
870 let Some(state) = lock.as_mut() else {
871 return; };
873 if !state.participants.contains_key(&worker_id) {
874 return; }
876 if state.is_finalizing {
877 return; }
879 let Some(start) = state.start_instructions.get(&worker_id) else {
880 return; };
882
883 let start_instruction = Instruction {
885 instruction_type: Some(instruction::InstructionType::Start(start.clone())),
886 };
887 if tx.try_send(Ok(start_instruction)).is_err() {
888 warn!(
890 "[Orchestrator] Could not queue Start instruction for rejoining worker {hostname}"
891 );
892 return;
893 }
894
895 let role = state.participants[&worker_id].role;
897 *status.lock().unwrap() = role;
898 if role == Probing && !state.probing_workers.contains(&worker_id) {
899 state.probing_workers.push(worker_id);
900 }
901 if let Some(participant) = state.participants.get_mut(&worker_id) {
903 participant.is_counted = true;
904 }
905
906 info!(
907 "[Orchestrator] Worker {hostname} rejoined measurement {} ({})",
908 state.m_id,
909 role.as_str_name()
910 );
911 }
912
913 fn init_measurement(
915 &self,
916 m_def: &ScheduleMeasurement,
917 m_id: u32,
918 participating_ids: &[u32],
919 probing_ids: &[u32],
920 is_gated_broadcast: bool,
921 ) -> Result<(), Status> {
922 let mut lock = self.measurement.write().unwrap();
923 if lock.is_some() {
924 error!("[Orchestrator] There is already an active measurement, returning");
925 return Err(Status::new(
926 tonic::Code::Cancelled,
927 "There is already an active measurement",
928 ));
929 }
930
931 let participants = participating_ids
933 .iter()
934 .map(|&id| {
935 let role = if probing_ids.contains(&id) {
936 Probing
937 } else {
938 Listening
939 };
940 (
941 id,
942 Participant {
943 role,
944 is_counted: true,
945 },
946 )
947 })
948 .collect();
949
950 *lock = Some(MeasurementState {
951 m_id,
952 probing_workers: probing_ids.to_vec(),
953 participants,
954 start_instructions: HashMap::new(),
955 is_finalizing: false,
956 m_type: m_def.m_type(),
957 is_gated_broadcast,
958 is_prefix_hitlist: m_def.is_prefix_hitlist,
959 nprobes: m_def.number_of_probes,
960 worker_stacks: HashMap::new(),
961 trace_config: None,
962 resolved_targets: HashSet::new(),
963 live: None,
964 });
965
966 Ok(())
967 }
968
969 fn spawn_discovery_sweeper(&self) {
974 let measurement = self.measurement.clone();
975 tokio::spawn(async move {
976 let mut interval = tokio::time::interval(Duration::from_secs(1));
977 loop {
978 interval.tick().await;
979
980 let mut lock = measurement.write().unwrap();
981 let Some(state) = lock.as_mut() else {
982 break; };
984 let Some(live) = state.live.as_mut() else {
985 break;
986 };
987
988 let now = Instant::now();
989 live.pending.retain(|_, pending| pending.deadline > now);
990 }
991 });
992 }
993
994 fn setup_traceroute(&self, trace_options: TraceOptions, origin_id: u32) {
997 {
998 let mut lock = self.measurement.write().unwrap();
999 if let Some(ref mut state) = *lock {
1000 state.trace_config = Some(TracerouteConfig {
1001 session_tracker: SessionTracker::new(),
1002 origin_id,
1003 timeout: trace_options.timeout as u64,
1004 max_hops: trace_options.max_hops,
1005 initial_hop: trace_options.initial_hop,
1006 max_failures: trace_options.max_failures,
1007 star_unresponsive: trace_options.star_unresponsive,
1008 });
1009 }
1010 }
1011
1012 let measurement_clone = self.measurement.clone();
1013 let cli_sender_clone = self.cli_sender.clone();
1014 std::thread::spawn(move || {
1015 check_trace_timeouts(measurement_clone, cli_sender_clone);
1016 });
1017 }
1018}
1019
1020async fn send_start_instructions(
1023 workers: &WorkerRegistry,
1024 measurement: &MeasurementHandle,
1025 m_def: &ScheduleMeasurement,
1026 m_id: u32,
1027) {
1028 let mut seen_origins = HashSet::new();
1030 let mut anycast_rx_origins = vec![];
1031 for configuration in m_def.configurations.iter() {
1032 if let Some(origin) = &configuration.origin
1033 && !origin.is_unicast()
1034 && seen_origins.insert(origin.origin_id)
1035 {
1036 anycast_rx_origins.push(*origin);
1037 }
1038 }
1039
1040 let participants: Vec<_> = workers
1042 .lock()
1043 .unwrap()
1044 .iter()
1045 .filter(|w| w.is_participating())
1046 .cloned()
1047 .collect();
1048
1049 for worker in participants {
1050 let worker_id = worker.worker_id;
1051
1052 let mut tx_origins = vec![];
1054 for configuration in &m_def.configurations {
1055 if (configuration.worker_id == worker_id || configuration.worker_id == ALL_WORKERS)
1056 && let Some(origin) = &configuration.origin
1057 {
1058 tx_origins.push(*origin);
1059 }
1060 }
1061
1062 let mut rx_origins = anycast_rx_origins.clone();
1064 rx_origins.extend(tx_origins.iter().filter(|o| o.is_unicast()).copied());
1065
1066 let start = Start {
1067 rate: m_def.probing_rate,
1068 m_id,
1069 tx_origins,
1070 rx_origins,
1071 record: m_def.record.clone(),
1072 url: m_def.url.clone(),
1073 m_type: m_def.m_type,
1074 probe_interval: m_def.probe_interval,
1075 };
1076
1077 if let Some(state) = measurement.write().unwrap().as_mut() {
1079 state.start_instructions.insert(worker_id, start.clone());
1080 }
1081
1082 let start_instruction = Instruction {
1083 instruction_type: Some(instruction::InstructionType::Start(start)),
1084 };
1085
1086 worker
1087 .send(Ok(start_instruction))
1088 .await
1089 .expect("Failed to send Start instruction to worker");
1090 }
1091}