1use std::{
59 collections::{HashMap, VecDeque, hash_map},
60 sync::{Arc, Mutex, Weak},
61 time::{Duration, SystemTime},
62};
63
64use scc::HashIndex;
65use scion_sdk_utils::backoff::BackoffConfig;
66use sciparse::{
67 dataplane_path::view::ScionDpPathViewRef, identifier::isd_asn::IsdAsn, path::ScionPath,
68 payload::scmp::model::ScmpErrorMessage,
69};
70use tokio::sync::broadcast::{self};
71
72use crate::{
73 path::{
74 PathStrategy,
75 fetcher::{
76 PathFetcherImpl,
77 traits::{PathFetchError, PathFetcher},
78 },
79 manager::{
80 issues::{IssueKind, IssueMarker, IssueMarkerTarget, SendError},
81 pathset::{PathSet, PathSetHandle, PathSetTask},
82 traits::{PathManager, PathPrefetcher, PathWaitError, SyncPathManager},
83 },
84 types::PathManagerPath,
85 },
86 stack::{ScionSocketSendError, scmp_handler::ScmpErrorReceiver, socket::SendErrorReceiver},
87};
88
89mod algo;
90mod issues;
93mod pathset;
95pub(crate) mod reliability;
97pub mod traits;
99
100#[derive(Debug, Clone, Copy)]
102pub struct MultiPathManagerConfig {
103 max_cached_paths_per_pair: usize,
105 refetch_interval: Duration,
107 min_refetch_delay: Duration,
109 min_expiry_threshold: Duration,
111 max_idle_period: Duration,
113 fetch_failure_backoff: BackoffConfig,
115 issue_cache_size: usize,
117 issue_broadcast_size: usize,
119 issue_deduplication_window: Duration,
121 path_swap_score_threshold: f32,
123}
124
125impl Default for MultiPathManagerConfig {
126 fn default() -> Self {
127 MultiPathManagerConfig {
128 max_cached_paths_per_pair: 50,
129 refetch_interval: Duration::from_secs(60 * 30), min_refetch_delay: Duration::from_secs(60),
131 min_expiry_threshold: Duration::from_secs(60 * 5), max_idle_period: Duration::from_secs(60 * 2), fetch_failure_backoff: BackoffConfig {
134 minimum_delay_secs: 60.0,
135 maximum_delay_secs: 300.0,
136 factor: 1.5,
137 jitter_secs: 5.0,
138 },
139 issue_cache_size: 100,
140 issue_broadcast_size: 10,
141 issue_deduplication_window: Duration::from_secs(10),
143 path_swap_score_threshold: 0.5,
144 }
145 }
146}
147
148impl MultiPathManagerConfig {
149 #[must_use]
151 pub fn with_max_cached_paths_per_pair(mut self, max: usize) -> Self {
152 self.max_cached_paths_per_pair = max;
153 self
154 }
155
156 #[must_use]
158 pub fn with_refetch_interval(mut self, interval: Duration) -> Self {
159 self.refetch_interval = interval;
160 self
161 }
162
163 #[must_use]
165 pub fn with_min_refetch_delay(mut self, delay: Duration) -> Self {
166 self.min_refetch_delay = delay;
167 self
168 }
169
170 #[must_use]
172 pub fn with_min_expiry_threshold(mut self, threshold: Duration) -> Self {
173 self.min_expiry_threshold = threshold;
174 self
175 }
176
177 #[must_use]
179 pub fn with_max_idle_period(mut self, period: Duration) -> Self {
180 self.max_idle_period = period;
181 self
182 }
183
184 #[must_use]
186 pub fn with_issue_deduplication_window(mut self, window: Duration) -> Self {
187 self.issue_deduplication_window = window;
188 self
189 }
190
191 #[must_use]
193 pub fn with_path_swap_score_threshold(mut self, threshold: f32) -> Self {
194 self.path_swap_score_threshold = threshold;
195 self
196 }
197
198 fn validate(&self) -> Result<(), MultiPathManagerConfigError> {
200 if self.min_refetch_delay > self.refetch_interval {
201 return Err(MultiPathManagerConfigError(
203 "min_refetch_delay must be smaller than refetch_interval",
204 ));
205 }
206
207 if self.min_refetch_delay > self.min_expiry_threshold {
208 return Err(MultiPathManagerConfigError(
210 "min_refetch_delay must be smaller than min_expiry_threshold",
211 ));
212 }
213
214 Ok(())
215 }
216}
217
218#[derive(Debug, thiserror::Error)]
220#[error("invalid path manager configuration: {0}")]
221#[non_exhaustive]
222pub struct MultiPathManagerConfigError(&'static str);
223
224pub struct MultiPathManager<F: PathFetcher = PathFetcherImpl>(Arc<MultiPathManagerInner<F>>);
226
227impl<F> Clone for MultiPathManager<F>
228where
229 F: PathFetcher,
230{
231 fn clone(&self) -> Self {
232 MultiPathManager(self.0.clone())
233 }
234}
235
236struct MultiPathManagerInner<F: PathFetcher> {
237 config: MultiPathManagerConfig,
238 fetcher: F,
239 path_strategy: PathStrategy,
240 issue_manager: Mutex<PathIssueManager>,
241 managed_paths: HashIndex<(IsdAsn, IsdAsn), (PathSetHandle, PathSetTask)>,
242}
243
244impl<F: PathFetcher> MultiPathManager<F> {
245 pub fn new(
251 config: MultiPathManagerConfig,
252 fetcher: F,
253 path_strategy: PathStrategy,
254 ) -> Result<Self, MultiPathManagerConfigError> {
255 config.validate()?;
256
257 let issue_manager = Mutex::new(PathIssueManager::new(
258 config.issue_cache_size,
259 config.issue_broadcast_size,
260 config.issue_deduplication_window,
261 ));
262
263 Ok(MultiPathManager(Arc::new(MultiPathManagerInner {
264 config,
265 fetcher,
266 issue_manager,
267 path_strategy,
268 managed_paths: HashIndex::new(),
269 })))
270 }
271
272 pub fn cached_path(&self, src: IsdAsn, dst: IsdAsn, now: SystemTime) -> Option<ScionPath> {
278 let try_path = self
279 .0
280 .managed_paths
281 .peek_with(&(src, dst), |_, (handle, _)| {
282 handle.try_active_path().as_deref().map(|p| p.0.clone())
283 })
284 .flatten();
285
286 match try_path {
287 Some(active) => {
288 let timestamp = now
291 .duration_since(SystemTime::UNIX_EPOCH)
292 .unwrap_or_default()
293 .as_secs() as u32;
294
295 let expired = active.is_expired(timestamp).unwrap_or(false);
296
297 debug_assert!(!expired, "Returned expired path from try_get_path");
298
299 Some(active)
300 }
301 None => {
302 self.fast_ensure_managed_paths(src, dst);
304 None
305 }
306 }
307 }
308
309 pub async fn path(
316 &self,
317 src: IsdAsn,
318 dst: IsdAsn,
319 now: SystemTime,
320 ) -> Result<ScionPath, Arc<PathFetchError>> {
321 if src.is_wildcard() || dst.is_wildcard() {
322 return Err(Arc::new(PathFetchError::InternalError(
323 "Wildcard src or dst is not supported".into(),
324 )));
325 }
326
327 if src == dst {
328 return Ok(ScionPath::local(src).expect("Checked for wildcard above"));
329 }
330
331 let try_path = self
332 .0
333 .managed_paths
334 .peek_with(&(src, dst), |_, (handle, _)| {
335 handle.try_active_path().as_deref().map(|p| p.0.clone())
336 })
337 .flatten();
338
339 let res = match try_path {
340 Some(active) => Ok(active),
341 None => {
342 let path_set = self.ensure_managed_paths(src, dst);
344
345 let active = path_set.active_path().await.as_ref().map(|p| p.0.clone());
347
348 match active {
350 Some(active) => Ok(active),
351 None => {
352 let last_error = path_set.current_error();
354 match last_error {
355 Some(e) => Err(e),
356 None => {
357 Err(Arc::new(PathFetchError::NoPathsFound))
361 }
362 }
363 }
364 }
365 }
366 };
367
368 if let Ok(active) = &res {
369 let timestamp = now
370 .duration_since(SystemTime::UNIX_EPOCH)
371 .unwrap_or_default()
372 .as_secs() as u32;
373
374 let expired = active.is_expired(timestamp).unwrap_or(false);
377 debug_assert!(!expired, "Returned expired path from get_path");
378 }
379
380 res
381 }
382
383 pub fn weak_ref(&self) -> MultiPathManagerRef<F> {
387 MultiPathManagerRef(Arc::downgrade(&self.0))
388 }
389
390 fn fast_ensure_managed_paths(&self, src: IsdAsn, dst: IsdAsn) {
394 if self.0.managed_paths.contains(&(src, dst)) {
395 return;
396 }
397
398 self.ensure_managed_paths(src, dst);
399 }
400
401 fn ensure_managed_paths(&self, src: IsdAsn, dst: IsdAsn) -> PathSetHandle {
405 let entry = match self.0.managed_paths.entry_sync((src, dst)) {
406 scc::hash_index::Entry::Occupied(occupied) => {
407 tracing::trace!(%src, %dst, "Already managing paths for src-dst pair");
408 occupied
409 }
410 scc::hash_index::Entry::Vacant(vacant) => {
411 tracing::info!(%src, %dst, "Starting to manage paths for src-dst pair");
412 let managed = PathSet::new(
413 src,
414 dst,
415 self.weak_ref(),
416 self.0.config,
417 self.0
418 .issue_manager
419 .lock()
420 .expect("lock poisoned")
421 .issues_subscriber(),
422 );
423
424 vacant.insert_entry(managed.manage())
425 }
426 };
427
428 entry.get().0.clone()
429 }
430
431 pub fn stop_managing_paths(&self, src: IsdAsn, dst: IsdAsn) {
433 if self.0.managed_paths.remove_sync(&(src, dst)) {
434 tracing::info!(%src, %dst, "Stopped managing paths for src-dst pair");
435 }
436 }
437
438 pub(crate) fn report_path_issue(&self, timestamp: SystemTime, issue: IssueKind) {
440 let Some(applies_to) = issue.target_type() else {
441 return;
443 };
444
445 if matches!(applies_to, IssueMarkerTarget::DestinationNetwork { .. }) {
446 return;
448 }
449
450 let issue_marker = IssueMarker {
451 target: applies_to,
452 timestamp,
453 penalty: issue.penalty(),
454 };
455
456 {
458 let mut issues_guard = self.0.issue_manager.lock().expect("lock poisoned");
459 issues_guard.add_issue(issue, issue_marker.clone());
460 }
461 }
462}
463
464impl<F: PathFetcher> ScmpErrorReceiver for MultiPathManager<F> {
465 fn report_scmp_error(
466 &self,
467 src_ia: IsdAsn,
468 scmp_error: ScmpErrorMessage,
469 _path: ScionDpPathViewRef,
470 ) {
471 self.report_path_issue(
472 SystemTime::now(),
473 IssueKind::Scmp {
474 src_ia,
475 error: scmp_error,
476 },
477 );
478 }
479}
480
481impl<F: PathFetcher> SendErrorReceiver for MultiPathManager<F> {
482 fn report_send_error(&self, error: &ScionSocketSendError) {
483 if let Some(send_error) = SendError::from_socket_send_error(error) {
484 self.report_path_issue(SystemTime::now(), IssueKind::Socket { err: send_error });
485 }
486 }
487}
488
489impl<F: PathFetcher> SyncPathManager for MultiPathManager<F> {
490 fn register_path(&self, _src: IsdAsn, _dst: IsdAsn, _now: SystemTime, _path: ScionPath) {
491 }
495
496 fn try_cached_path(
497 &self,
498 src: IsdAsn,
499 dst: IsdAsn,
500 now: SystemTime,
501 ) -> std::io::Result<Option<ScionPath>> {
502 Ok(self.cached_path(src, dst, now))
503 }
504}
505
506impl<F: PathFetcher> PathManager for MultiPathManager<F> {
507 fn path_wait(
508 &self,
509 src: IsdAsn,
510 dst: IsdAsn,
511 now: SystemTime,
512 ) -> impl std::future::Future<Output = Result<ScionPath, PathWaitError>> + Send + '_ {
513 async move {
514 match self.path(src, dst, now).await {
515 Ok(path) => Ok(path),
516 Err(e) => {
517 match &*e {
518 PathFetchError::NoPathsFound => Err(PathWaitError::NoPathFound),
519 _ => Err(PathWaitError::FetchFailed(e)),
520 }
521 }
522 }
523 }
524 }
525}
526
527impl<F: PathFetcher> PathPrefetcher for MultiPathManager<F> {
528 fn prefetch_path(&self, src: IsdAsn, dst: IsdAsn) {
529 self.ensure_managed_paths(src, dst);
530 }
531}
532
533pub struct MultiPathManagerRef<F: PathFetcher>(Weak<MultiPathManagerInner<F>>);
538
539impl<F: PathFetcher> Clone for MultiPathManagerRef<F> {
540 fn clone(&self) -> Self {
541 MultiPathManagerRef(self.0.clone())
542 }
543}
544
545impl<F: PathFetcher> MultiPathManagerRef<F> {
546 #[must_use]
548 pub fn upgrade(&self) -> Option<MultiPathManager<F>> {
549 self.0.upgrade().map(MultiPathManager)
550 }
551}
552
553struct PathIssueManager {
557 max_entries: usize,
559 deduplication_window: Duration,
560
561 cache: HashMap<u64, IssueMarker>,
564 fifo_issues: VecDeque<(u64, SystemTime)>,
566
567 issue_broadcast_tx: broadcast::Sender<(u64, IssueMarker)>,
569}
570
571impl PathIssueManager {
572 fn new(max_entries: usize, broadcast_buffer: usize, deduplication_window: Duration) -> Self {
573 let (issue_broadcast_tx, _) = broadcast::channel(broadcast_buffer);
574 PathIssueManager {
575 max_entries,
576 deduplication_window,
577 cache: HashMap::new(),
578 fifo_issues: VecDeque::new(),
579 issue_broadcast_tx,
580 }
581 }
582
583 pub fn issues_subscriber(&self) -> broadcast::Receiver<(u64, IssueMarker)> {
585 self.issue_broadcast_tx.subscribe()
586 }
587
588 pub fn add_issue(&mut self, issue: IssueKind, marker: IssueMarker) {
597 let id = issue.dedup_id(&marker.target);
598
599 if let Some(existing_marker) = self.cache.get(&id) {
601 let time_since_last_seen = marker
602 .timestamp
603 .duration_since(existing_marker.timestamp)
604 .unwrap_or_else(|_| Duration::from_secs(0));
605
606 if time_since_last_seen < self.deduplication_window {
607 tracing::trace!(%id, ?time_since_last_seen, ?marker, %issue, "Ignoring duplicate path issue");
608 return;
610 }
611 }
612
613 tracing::debug!(%id, %issue, "New path issue");
614
615 self.issue_broadcast_tx.send((id, marker.clone())).ok();
617
618 if self.cache.len() >= self.max_entries {
619 self.pop_front();
620 }
621
622 self.fifo_issues.push_back((id, marker.timestamp)); self.cache.insert(id, marker);
625 }
626
627 pub fn apply_cached_issues(&self, entry: &mut PathManagerPath, now: SystemTime) -> bool {
635 let mut applied = false;
636 for issue in self.cache.values() {
637 if issue
638 .target
639 .matches_path(&entry.path, &entry.scion_path().fingerprint())
640 {
641 entry.reliability.update(issue.decayed_penalty(now), now);
642 applied = true;
643 }
644 }
645 applied
646 }
647
648 fn pop_front(&mut self) -> Option<IssueMarker> {
650 let (issue_id, timestamp) = self.fifo_issues.pop_front()?;
651
652 match self.cache.entry(issue_id) {
653 hash_map::Entry::Occupied(occupied_entry) => {
654 if occupied_entry.get().timestamp == timestamp {
656 Some(occupied_entry.remove())
657 } else {
658 None
659 }
660 }
661 hash_map::Entry::Vacant(_) => {
662 debug_assert!(false, "Bad cache: issue ID not found in cache");
663 None
664 }
665 }
666 }
667}
668
669#[cfg(test)]
670mod tests {
671 use helpers::*;
672 use tokio::time::timeout;
673
674 use super::*;
675
676 #[tokio::test]
678 #[test_log::test]
679 async fn should_create_pathset_on_request() {
680 let cfg = base_config();
681 let fetcher = MockFetcher::new(generate_responses(5, 0, BASE_TIME, DEFAULT_EXP_UNITS));
682
683 let mgr = MultiPathManager::new(cfg, fetcher, PathStrategy::default())
684 .expect("Should create manager");
685
686 assert!(mgr.0.managed_paths.is_empty());
688
689 let path = mgr.cached_path(SRC_ADDR.isd_asn(), DST_ADDR.isd_asn(), BASE_TIME);
691 assert!(path.is_none());
693
694 assert!(
696 mgr.0
697 .managed_paths
698 .contains(&(SRC_ADDR.isd_asn(), DST_ADDR.isd_asn()))
699 );
700 }
701
702 #[tokio::test]
704 #[test_log::test]
705 async fn new_rejects_invalid_config() {
706 let mut cfg = base_config();
707 cfg.min_refetch_delay = cfg.refetch_interval + Duration::from_secs(1);
709 let fetcher = MockFetcher::new(generate_responses(1, 0, BASE_TIME, DEFAULT_EXP_UNITS));
710
711 let err = match MultiPathManager::new(cfg, fetcher, PathStrategy::default()) {
712 Ok(_) => panic!("invalid config should be rejected"),
713 Err(e) => e,
714 };
715 assert!(
716 err.to_string().contains("min_refetch_delay"),
717 "unexpected error message: {err}"
718 );
719 }
720
721 #[tokio::test]
723 #[test_log::test]
724 async fn should_remove_idle_pathsets() {
725 let mut cfg = base_config();
726 cfg.max_idle_period = Duration::from_millis(10); let fetcher = MockFetcher::new(generate_responses(5, 0, BASE_TIME, DEFAULT_EXP_UNITS));
729
730 let mgr = MultiPathManager::new(cfg, fetcher, PathStrategy::default())
731 .expect("Should create manager");
732
733 let handle = mgr.ensure_managed_paths(SRC_ADDR.isd_asn(), DST_ADDR.isd_asn());
735
736 assert!(
738 mgr.0
739 .managed_paths
740 .contains(&(SRC_ADDR.isd_asn(), DST_ADDR.isd_asn()))
741 );
742
743 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
745
746 let contains = mgr
748 .0
749 .managed_paths
750 .contains(&(SRC_ADDR.isd_asn(), DST_ADDR.isd_asn()));
751
752 assert!(!contains, "Idle path set should be removed");
753
754 let err = handle.current_error();
755 assert!(
756 err.is_some(),
757 "Handle should report error after path set removal"
758 );
759 println!("Error after idle removal: {err:?}");
760 assert!(
761 err.unwrap().to_string().contains("idle"),
762 "Error message should indicate idle removal"
763 );
764 }
765
766 #[tokio::test]
768 #[test_log::test]
769 async fn should_cancel_pathset_tasks_on_drop() {
770 let cfg: MultiPathManagerConfig = base_config();
771 let fetcher = MockFetcher::new(generate_responses(5, 0, BASE_TIME, DEFAULT_EXP_UNITS));
772
773 let mgr = MultiPathManager::new(cfg, fetcher, PathStrategy::default())
774 .expect("Should create manager");
775
776 let handle = mgr.ensure_managed_paths(SRC_ADDR.isd_asn(), DST_ADDR.isd_asn());
778 handle.wait_initialized().await;
779
780 let mut set_entry = mgr
781 .0
782 .managed_paths
783 .get_sync(&(SRC_ADDR.isd_asn(), DST_ADDR.isd_asn()))
784 .unwrap();
785
786 let task_handle = unsafe {
787 let swap_handle = tokio::spawn(async {});
790 std::mem::replace(&mut set_entry.get_mut().1.task, swap_handle)
791 };
792
793 let cancel_token = set_entry.get().1.cancel_token.clone();
794
795 let count = mgr.0.managed_paths.len();
796 assert_eq!(count, 1, "Should have 1 managed path set");
797
798 drop(mgr);
800 assert!(
802 cancel_token.is_cancelled(),
803 "Cancel token should be triggered"
804 );
805
806 timeout(Duration::from_millis(50), task_handle)
808 .await
809 .unwrap()
810 .unwrap();
811
812 let err = handle
813 .shared
814 .sync
815 .lock()
816 .unwrap()
817 .current_error
818 .clone()
819 .expect("Should have error after manager drop");
820
821 assert!(
824 err.to_string().contains("cancelled") || err.to_string().contains("dropped"),
825 "Error message should indicate cancellation or manager drop"
826 );
827 }
828
829 mod issue_handling {
830 use scc::HashIndex;
831 use sciparse::identifier::{asn::Asn, isd::Isd};
832
833 use super::*;
834 use crate::path::{
835 manager::{MultiPathManagerInner, PathIssueManager, reliability::ReliabilityScore},
836 types::Score,
837 };
838
839 #[tokio::test]
842 #[test_log::test]
843 async fn should_ingest_issues_and_apply_to_existing_paths() {
844 let cfg = base_config();
845 let fetcher = MockFetcher::new(generate_responses(5, 0, BASE_TIME, DEFAULT_EXP_UNITS));
846 let (mgr, mut path_set) = manual_pathset(BASE_TIME, fetcher.clone(), cfg, None);
847
848 path_set.maintain(BASE_TIME, &mgr).await;
849
850 let first_path = &path_set.internal.cached_paths[0];
852 let first_fp = first_path.scion_path().fingerprint();
853
854 let issue = IssueKind::Socket {
856 err: SendError::FirstHopUnreachable {
857 isd_asn: first_path.path.src_ia(),
858 interface_id: first_path.path.first_egress_interface().unwrap().id,
859 address: None,
860 msg: "test".into(),
861 },
862 };
863
864 let penalty = Score::new_clamped(-0.3);
865 let marker = IssueMarker {
866 target: issue.target_type().unwrap(),
867 timestamp: BASE_TIME,
868 penalty,
869 };
870
871 {
872 let mut issues_guard = mgr.0.issue_manager.lock().unwrap();
873 issues_guard.add_issue(issue, marker);
875 assert!(!issues_guard.cache.is_empty(), "Issue should be in cache");
877 }
878 let recv_result = path_set.internal.issue_rx.recv().await;
880 path_set.handle_issue_rx(BASE_TIME, recv_result, &mgr);
881
882 let updated_path = path_set
884 .internal
885 .cached_paths
886 .iter()
887 .find(|e| e.scion_path().fingerprint() == first_fp)
888 .expect("Path should still exist");
889
890 let updated_score = updated_path.reliability.score(BASE_TIME).value();
891
892 assert!(
893 updated_score == penalty.value(),
894 "Path score should be updated by penalty. Expected: {}, Got: {}",
895 penalty.value(),
896 updated_score
897 );
898
899 let later_time = BASE_TIME + Duration::from_secs(30);
901 let decayed_score = updated_path.reliability.score(later_time).value();
902 assert!(
903 decayed_score > updated_score,
904 "Path score should recover over time. Updated: {updated_score}, Decayed: {decayed_score}"
905 );
906 }
907
908 #[tokio::test]
909 #[test_log::test]
910 async fn should_deduplicate_issues_within_window() {
911 let cfg = base_config();
912 let mgr_inner = MultiPathManagerInner {
913 config: cfg,
914 fetcher: MockFetcher::new(Ok(vec![])),
915 path_strategy: PathStrategy::default(),
916 issue_manager: Mutex::new(PathIssueManager::new(64, 64, Duration::from_secs(10))),
917 managed_paths: HashIndex::new(),
918 };
919 let mgr = MultiPathManager(Arc::new(mgr_inner));
920
921 let issue_marker = IssueMarker {
922 target: IssueMarkerTarget::FirstHop {
923 isd_asn: SRC_ADDR.isd_asn(),
924 egress_interface: 1,
925 },
926 timestamp: BASE_TIME,
927 penalty: Score::new_clamped(-0.3),
928 };
929
930 let issue = IssueKind::Socket {
931 err: SendError::FirstHopUnreachable {
932 isd_asn: SRC_ADDR.isd_asn(),
933 interface_id: 1,
934 address: None,
935 msg: "test".into(),
936 },
937 };
938
939 mgr.0
941 .issue_manager
942 .lock()
943 .unwrap()
944 .add_issue(issue.clone(), issue_marker.clone());
945 let cache_size_1 = mgr.0.issue_manager.lock().unwrap().cache.len();
946 assert_eq!(cache_size_1, 1);
947
948 let issue_marker_2 = IssueMarker {
950 timestamp: BASE_TIME + Duration::from_secs(1), ..issue_marker.clone()
952 };
953 mgr.0
954 .issue_manager
955 .lock()
956 .unwrap()
957 .add_issue(issue.clone(), issue_marker_2);
958
959 let fifo_size = mgr.0.issue_manager.lock().unwrap().fifo_issues.len();
960 let cache_size_2 = mgr.0.issue_manager.lock().unwrap().cache.len();
961 assert_eq!(cache_size_2, 1, "Duplicate issue should be ignored");
962 assert_eq!(
963 fifo_size, 1,
964 "FIFO queue size should remain unchanged on duplicate issue"
965 );
966
967 let issue_marker_3 = IssueMarker {
969 timestamp: BASE_TIME + Duration::from_secs(11), ..issue_marker
971 };
972 mgr.0
973 .issue_manager
974 .lock()
975 .unwrap()
976 .add_issue(issue, issue_marker_3);
977
978 let fifo_size_3 = mgr.0.issue_manager.lock().unwrap().fifo_issues.len();
979 let cache_size_3 = mgr.0.issue_manager.lock().unwrap().cache.len();
980 assert_eq!(
981 cache_size_3, 1,
982 "Issue outside dedup window should update existing"
983 );
984 assert_eq!(
985 fifo_size_3, 2,
986 "FIFO queue size should increase for new issue outside dedup window"
987 );
988 }
989
990 #[tokio::test]
993 #[test_log::test]
994 async fn should_apply_issues_to_new_paths_on_fetch() {
995 let cfg = base_config();
996 let fetcher = MockFetcher::new(Ok(vec![]));
997 let (mgr, mut path_set) = manual_pathset(BASE_TIME, fetcher.clone(), cfg, None);
998
999 path_set.maintain(BASE_TIME, &mgr).await;
1000
1001 let issue_marker = IssueMarker {
1003 target: IssueMarkerTarget::FirstHop {
1004 isd_asn: SRC_ADDR.isd_asn(),
1005 egress_interface: 1,
1006 },
1007 timestamp: BASE_TIME,
1008 penalty: Score::new_clamped(-0.5),
1009 };
1010
1011 let issue = IssueKind::Socket {
1012 err: SendError::FirstHopUnreachable {
1013 isd_asn: SRC_ADDR.isd_asn(),
1014 interface_id: 1,
1015 address: None,
1016 msg: "test".into(),
1017 },
1018 };
1019
1020 mgr.0
1022 .issue_manager
1023 .lock()
1024 .unwrap()
1025 .add_issue(issue, issue_marker);
1026
1027 path_set.drain_and_apply_issue_channel(BASE_TIME);
1029
1030 fetcher.lock().unwrap().set_response(generate_responses(
1032 3,
1033 0,
1034 BASE_TIME + Duration::from_secs(1),
1035 DEFAULT_EXP_UNITS,
1036 ));
1037
1038 let next_refetch = path_set.internal.next_refetch;
1039 path_set.maintain(next_refetch, &mgr).await;
1040
1041 let affected_path = path_set
1043 .internal
1044 .cached_paths
1045 .first()
1046 .expect("Path should exist");
1047
1048 let score = affected_path
1049 .reliability
1050 .score(BASE_TIME + Duration::from_secs(1))
1051 .value();
1052 assert!(
1053 score < 0.0,
1054 "Newly fetched path should have cached issue applied. Score: {score}"
1055 );
1056 }
1057
1058 #[tokio::test]
1060 #[test_log::test]
1061 async fn should_trigger_active_path_reevaluation_on_issue() {
1062 let cfg = base_config();
1063 let fetcher = MockFetcher::new(generate_responses(5, 0, BASE_TIME, DEFAULT_EXP_UNITS));
1064 let (mgr, mut path_set) = manual_pathset(BASE_TIME, fetcher.clone(), cfg, None);
1065
1066 path_set.maintain(BASE_TIME, &mgr).await;
1067
1068 let active_fp = path_set.shared.active_path.load().as_ref().unwrap().1;
1069
1070 let issue_marker = IssueMarker {
1072 target: IssueMarkerTarget::FullPath {
1073 fingerprint: active_fp,
1074 },
1075 timestamp: BASE_TIME,
1076 penalty: Score::new_clamped(-1.0), };
1078
1079 let issue = IssueKind::Socket {
1080 err: SendError::FirstHopUnreachable {
1081 isd_asn: SRC_ADDR.isd_asn(),
1082 interface_id: 1,
1083 address: None,
1084 msg: "test".into(),
1085 },
1086 };
1087
1088 mgr.0
1090 .issue_manager
1091 .lock()
1092 .unwrap()
1093 .add_issue(issue, issue_marker);
1094
1095 let recv_result = path_set.internal.issue_rx.recv().await;
1097 path_set.handle_issue_rx(BASE_TIME, recv_result, &mgr);
1098
1099 let new_active_fp = path_set.shared.active_path.load().as_ref().unwrap().1;
1101 assert_ne!(
1102 active_fp, new_active_fp,
1103 "Active path should change when severely penalized"
1104 );
1105 }
1106
1107 #[tokio::test]
1108 #[test_log::test]
1109 async fn should_swap_to_better_path_if_one_appears() {
1110 let cfg = base_config();
1111 let fetcher = MockFetcher::new(generate_responses(1, 0, BASE_TIME, DEFAULT_EXP_UNITS));
1112 let (mgr, mut path_set) = manual_pathset(BASE_TIME, fetcher.clone(), cfg, None);
1113
1114 path_set.maintain(BASE_TIME, &mgr).await;
1115
1116 path_set
1118 .shared
1119 .was_used_in_idle_period
1120 .store(true, std::sync::atomic::Ordering::Relaxed);
1121
1122 let active_fp = path_set.shared.active_path.load().as_ref().unwrap().1;
1123
1124 let issue_marker = IssueMarker {
1126 target: IssueMarkerTarget::FullPath {
1127 fingerprint: active_fp,
1128 },
1129 timestamp: BASE_TIME,
1130 penalty: Score::new_clamped(-0.8),
1131 };
1132
1133 mgr.0.issue_manager.lock().unwrap().add_issue(
1134 IssueKind::Socket {
1135 err: SendError::FirstHopUnreachable {
1136 isd_asn: SRC_ADDR.isd_asn(),
1137 interface_id: 1,
1138 address: None,
1139 msg: "test".into(),
1140 },
1141 },
1142 issue_marker,
1143 );
1144
1145 let active_fp_after_issue = path_set.shared.active_path.load().as_ref().unwrap().1;
1147 assert_eq!(
1148 active_fp, active_fp_after_issue,
1149 "Active path should remain the same if no better path exists"
1150 );
1151
1152 fetcher.lock().unwrap().set_response(generate_responses(
1154 1,
1155 100,
1156 BASE_TIME + Duration::from_secs(1),
1157 DEFAULT_EXP_UNITS,
1158 ));
1159
1160 path_set
1161 .maintain(path_set.internal.next_refetch, &mgr)
1162 .await;
1163 path_set
1165 .shared
1166 .was_used_in_idle_period
1167 .store(true, std::sync::atomic::Ordering::Relaxed);
1168
1169 let new_active_fp = path_set.shared.active_path.load().as_ref().unwrap().1;
1171 assert_ne!(
1172 active_fp, new_active_fp,
1173 "Active path should change when a better path appears"
1174 );
1175
1176 let positive_score = Score::new_clamped(0.8);
1178 let mut reliability = ReliabilityScore::new_with_time(path_set.internal.next_refetch);
1179 reliability.update(positive_score, path_set.internal.next_refetch);
1180
1181 path_set
1183 .internal
1184 .cached_paths
1185 .iter_mut()
1186 .find(|e| e.scion_path().fingerprint() == active_fp)
1187 .unwrap()
1188 .reliability = reliability;
1189
1190 path_set
1191 .maintain(path_set.internal.next_refetch, &mgr)
1192 .await;
1193
1194 assert_eq!(
1195 active_fp,
1196 path_set.shared.active_path.load().as_ref().unwrap().1,
1197 "Active path should change on positive score diff"
1198 );
1199 }
1200
1201 #[tokio::test]
1202 #[test_log::test]
1203 async fn should_keep_max_issue_cache_size() {
1204 let max_size = 10;
1205 let mut issue_mgr = PathIssueManager::new(max_size, 64, Duration::from_secs(10));
1206
1207 for i in 0..20u16 {
1209 let issue_marker = IssueMarker {
1210 target: IssueMarkerTarget::FirstHop {
1211 isd_asn: IsdAsn::new(Isd(1), Asn(1)),
1212 egress_interface: i,
1213 },
1214 timestamp: BASE_TIME + Duration::from_secs(u64::from(i)),
1215 penalty: Score::new_clamped(-0.1),
1216 };
1217
1218 let issue = IssueKind::Socket {
1219 err: SendError::FirstHopUnreachable {
1220 isd_asn: IsdAsn::new(Isd(1), Asn(1)),
1221 interface_id: i,
1222 address: None,
1223 msg: "test".into(),
1224 },
1225 };
1226
1227 issue_mgr.add_issue(issue, issue_marker);
1228 }
1229
1230 assert!(
1232 issue_mgr.cache.len() <= max_size,
1233 "Cache size {} should not exceed max {}",
1234 issue_mgr.cache.len(),
1235 max_size
1236 );
1237
1238 assert_eq!(issue_mgr.cache.len(), issue_mgr.fifo_issues.len());
1240 }
1241 }
1242
1243 pub mod helpers {
1244 use std::{
1245 hash::{DefaultHasher, Hash, Hasher},
1246 net::{IpAddr, Ipv4Addr},
1247 sync::{Arc, Mutex},
1248 time::{Duration, SystemTime},
1249 };
1250
1251 use sciparse::{
1252 address::ip_addr::ScionIpAddr,
1253 identifier::{asn::Asn, isd::Isd},
1254 util::test_builder::TestPathBuilder,
1255 };
1256 use tokio::sync::Notify;
1257
1258 use super::*;
1259 use crate::path::manager::{MultiPathManagerInner, PathIssueManager, pathset::PathSet};
1260
1261 pub const SRC_ADDR: ScionIpAddr =
1262 ScionIpAddr::new(IsdAsn::new(Isd(1), Asn(1)), IpAddr::V4(Ipv4Addr::LOCALHOST));
1263 pub const DST_ADDR: ScionIpAddr = ScionIpAddr::new(
1264 IsdAsn::new(Isd(2), Asn(1)),
1265 IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)),
1266 );
1267
1268 pub const DEFAULT_EXP_UNITS: u8 = 100;
1269 pub const BASE_TIME: SystemTime = SystemTime::UNIX_EPOCH;
1270
1271 pub fn dummy_path(hop_count: u16, timestamp: u32, exp_units: u8, seed: u32) -> ScionPath {
1272 let mut builder = TestPathBuilder::new(SRC_ADDR.into(), DST_ADDR.into())
1273 .using_info_timestamp(timestamp)
1274 .with_hop_expiry(exp_units)
1275 .up();
1276
1277 builder = builder.add_hop(0, 1);
1278
1279 for cnt in 0..hop_count {
1280 let mut hash = DefaultHasher::new();
1281 seed.hash(&mut hash);
1282 cnt.hash(&mut hash);
1283 let hash = hash.finish() as u32;
1284
1285 let hop = hash.saturating_sub(2) as u16; builder = builder.with_asn(hash).add_hop(hop + 1, hop + 2);
1287 }
1288
1289 builder = builder.add_hop(1, 0);
1290
1291 builder.build(timestamp).path()
1292 }
1293
1294 pub fn base_config() -> MultiPathManagerConfig {
1295 MultiPathManagerConfig {
1296 max_cached_paths_per_pair: 5,
1297 refetch_interval: Duration::from_secs(100),
1298 min_refetch_delay: Duration::from_secs(1),
1299 min_expiry_threshold: Duration::from_secs(5),
1300 max_idle_period: Duration::from_secs(30),
1301 fetch_failure_backoff: BackoffConfig {
1302 minimum_delay_secs: 1.0,
1303 maximum_delay_secs: 10.0,
1304 factor: 2.0,
1305 jitter_secs: 0.0,
1306 },
1307 issue_cache_size: 64,
1308 issue_broadcast_size: 64,
1309 issue_deduplication_window: Duration::from_secs(10),
1310 path_swap_score_threshold: 0.1,
1311 }
1312 }
1313
1314 pub fn generate_responses(
1315 path_count: u16,
1316 path_seed: u32,
1317 timestamp: SystemTime,
1318 exp_units: u8,
1319 ) -> Result<Vec<ScionPath>, String> {
1320 let mut paths = Vec::new();
1321 for resp_id in 0..path_count {
1322 paths.push(dummy_path(
1323 2,
1324 timestamp
1325 .duration_since(SystemTime::UNIX_EPOCH)
1326 .unwrap()
1327 .as_secs() as u32,
1328 exp_units,
1329 path_seed + u32::from(resp_id),
1330 ));
1331 }
1332
1333 Ok(paths)
1334 }
1335
1336 pub struct MockFetcher {
1337 next_response: Result<Vec<ScionPath>, String>,
1338 pub received_requests: usize,
1339 pub wait_till_notify: bool,
1340 pub notify_to_resolve: Arc<Notify>,
1341 }
1342 impl MockFetcher {
1343 pub fn new(response: Result<Vec<ScionPath>, String>) -> Arc<Mutex<Self>> {
1344 Arc::new(Mutex::new(Self {
1345 next_response: response,
1346 received_requests: 0,
1347 wait_till_notify: false,
1348 notify_to_resolve: Arc::new(Notify::new()),
1349 }))
1350 }
1351
1352 pub fn set_response(&mut self, response: Result<Vec<ScionPath>, String>) {
1353 self.next_response = response;
1354 }
1355
1356 pub fn wait_till_notify(&mut self, wait: bool) {
1357 self.wait_till_notify = wait;
1358 }
1359
1360 pub fn notify(&self) {
1361 self.notify_to_resolve.notify_waiters();
1362 }
1363 }
1364
1365 impl PathFetcher for Arc<Mutex<MockFetcher>> {
1366 async fn fetch_paths(
1367 &self,
1368 _src: IsdAsn,
1369 _dst: IsdAsn,
1370 ) -> Result<Vec<ScionPath>, PathFetchError> {
1371 let response;
1372 let notify = {
1374 let mut guard = self.lock().unwrap();
1375
1376 guard.received_requests += 1;
1377 response = guard.next_response.clone();
1378
1379 if guard.wait_till_notify {
1381 let notif = guard.notify_to_resolve.clone().notified_owned();
1382 Some(notif)
1383 } else {
1384 None
1385 }
1386 };
1387
1388 if let Some(notif) = notify {
1389 notif.await;
1390 }
1391
1392 match response {
1393 Ok(paths) if paths.is_empty() => Err(PathFetchError::NoPathsFound),
1394 Ok(paths) => Ok(paths),
1395 Err(e) => Err(PathFetchError::InternalError(e.into())),
1396 }
1397 }
1398 }
1399
1400 pub fn manual_pathset<F: PathFetcher>(
1401 now: SystemTime,
1402 fetcher: F,
1403 cfg: MultiPathManagerConfig,
1404 strategy: Option<PathStrategy>,
1405 ) -> (MultiPathManager<F>, PathSet<F>) {
1406 let mgr_inner = MultiPathManagerInner {
1407 config: cfg,
1408 fetcher,
1409 path_strategy: strategy.unwrap_or_else(|| {
1410 let mut ps = PathStrategy::default();
1411 ps.scoring.use_default_scorers();
1412 ps
1413 }),
1414 issue_manager: Mutex::new(PathIssueManager::new(64, 64, Duration::from_secs(10))),
1415 managed_paths: HashIndex::new(),
1416 };
1417 let mgr = MultiPathManager(Arc::new(mgr_inner));
1418 let issue_rx = mgr.0.issue_manager.lock().unwrap().issues_subscriber();
1419 let mgr_ref = mgr.weak_ref();
1420 (
1421 mgr,
1422 PathSet::new_with_time(
1423 SRC_ADDR.isd_asn(),
1424 DST_ADDR.isd_asn(),
1425 mgr_ref,
1426 cfg,
1427 issue_rx,
1428 now,
1429 ),
1430 )
1431 }
1432 }
1433}