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 tracing::debug!(%issue, "New path issue");
451
452 let issue_marker = IssueMarker {
453 target: applies_to,
454 timestamp,
455 penalty: issue.penalty(),
456 };
457
458 {
460 let mut issues_guard = self.0.issue_manager.lock().expect("lock poisoned");
461 issues_guard.add_issue(issue, issue_marker.clone());
462 }
463 }
464}
465
466impl<F: PathFetcher> ScmpErrorReceiver for MultiPathManager<F> {
467 fn report_scmp_error(&self, scmp_error: ScmpErrorMessage, _path: ScionDpPathViewRef) {
468 self.report_path_issue(SystemTime::now(), IssueKind::Scmp { error: scmp_error });
469 }
470}
471
472impl<F: PathFetcher> SendErrorReceiver for MultiPathManager<F> {
473 fn report_send_error(&self, error: &ScionSocketSendError) {
474 if let Some(send_error) = SendError::from_socket_send_error(error) {
475 self.report_path_issue(SystemTime::now(), IssueKind::Socket { err: send_error });
476 }
477 }
478}
479
480impl<F: PathFetcher> SyncPathManager for MultiPathManager<F> {
481 fn register_path(&self, _src: IsdAsn, _dst: IsdAsn, _now: SystemTime, _path: ScionPath) {
482 }
486
487 fn try_cached_path(
488 &self,
489 src: IsdAsn,
490 dst: IsdAsn,
491 now: SystemTime,
492 ) -> std::io::Result<Option<ScionPath>> {
493 Ok(self.cached_path(src, dst, now))
494 }
495}
496
497impl<F: PathFetcher> PathManager for MultiPathManager<F> {
498 fn path_wait(
499 &self,
500 src: IsdAsn,
501 dst: IsdAsn,
502 now: SystemTime,
503 ) -> impl std::future::Future<Output = Result<ScionPath, PathWaitError>> + Send + '_ {
504 async move {
505 match self.path(src, dst, now).await {
506 Ok(path) => Ok(path),
507 Err(e) => {
508 match &*e {
509 PathFetchError::NoPathsFound => Err(PathWaitError::NoPathFound),
510 _ => Err(PathWaitError::FetchFailed(e)),
511 }
512 }
513 }
514 }
515 }
516}
517
518impl<F: PathFetcher> PathPrefetcher for MultiPathManager<F> {
519 fn prefetch_path(&self, src: IsdAsn, dst: IsdAsn) {
520 self.ensure_managed_paths(src, dst);
521 }
522}
523
524pub struct MultiPathManagerRef<F: PathFetcher>(Weak<MultiPathManagerInner<F>>);
529
530impl<F: PathFetcher> Clone for MultiPathManagerRef<F> {
531 fn clone(&self) -> Self {
532 MultiPathManagerRef(self.0.clone())
533 }
534}
535
536impl<F: PathFetcher> MultiPathManagerRef<F> {
537 #[must_use]
539 pub fn upgrade(&self) -> Option<MultiPathManager<F>> {
540 self.0.upgrade().map(MultiPathManager)
541 }
542}
543
544struct PathIssueManager {
548 max_entries: usize,
550 deduplication_window: Duration,
551
552 cache: HashMap<u64, IssueMarker>,
555 fifo_issues: VecDeque<(u64, SystemTime)>,
557
558 issue_broadcast_tx: broadcast::Sender<(u64, IssueMarker)>,
560}
561
562impl PathIssueManager {
563 fn new(max_entries: usize, broadcast_buffer: usize, deduplication_window: Duration) -> Self {
564 let (issue_broadcast_tx, _) = broadcast::channel(broadcast_buffer);
565 PathIssueManager {
566 max_entries,
567 deduplication_window,
568 cache: HashMap::new(),
569 fifo_issues: VecDeque::new(),
570 issue_broadcast_tx,
571 }
572 }
573
574 pub fn issues_subscriber(&self) -> broadcast::Receiver<(u64, IssueMarker)> {
576 self.issue_broadcast_tx.subscribe()
577 }
578
579 pub fn add_issue(&mut self, issue: IssueKind, marker: IssueMarker) {
588 let id = issue.dedup_id(&marker.target);
589
590 if let Some(existing_marker) = self.cache.get(&id) {
592 let time_since_last_seen = marker
593 .timestamp
594 .duration_since(existing_marker.timestamp)
595 .unwrap_or_else(|_| Duration::from_secs(0));
596
597 if time_since_last_seen < self.deduplication_window {
598 tracing::trace!(%id, ?time_since_last_seen, ?marker, %issue, "Ignoring duplicate path issue");
599 return;
601 }
602 }
603
604 self.issue_broadcast_tx.send((id, marker.clone())).ok();
606
607 if self.cache.len() >= self.max_entries {
608 self.pop_front();
609 }
610
611 self.fifo_issues.push_back((id, marker.timestamp)); self.cache.insert(id, marker);
614 }
615
616 pub fn apply_cached_issues(&self, entry: &mut PathManagerPath, now: SystemTime) -> bool {
624 let mut applied = false;
625 for issue in self.cache.values() {
626 if issue
627 .target
628 .matches_path(&entry.path, &entry.scion_path().fingerprint())
629 {
630 entry.reliability.update(issue.decayed_penalty(now), now);
631 applied = true;
632 }
633 }
634 applied
635 }
636
637 fn pop_front(&mut self) -> Option<IssueMarker> {
639 let (issue_id, timestamp) = self.fifo_issues.pop_front()?;
640
641 match self.cache.entry(issue_id) {
642 hash_map::Entry::Occupied(occupied_entry) => {
643 if occupied_entry.get().timestamp == timestamp {
645 Some(occupied_entry.remove())
646 } else {
647 None
648 }
649 }
650 hash_map::Entry::Vacant(_) => {
651 debug_assert!(false, "Bad cache: issue ID not found in cache");
652 None
653 }
654 }
655 }
656}
657
658#[cfg(test)]
659mod tests {
660 use helpers::*;
661 use tokio::time::timeout;
662
663 use super::*;
664
665 #[tokio::test]
667 #[test_log::test]
668 async fn should_create_pathset_on_request() {
669 let cfg = base_config();
670 let fetcher = MockFetcher::new(generate_responses(5, 0, BASE_TIME, DEFAULT_EXP_UNITS));
671
672 let mgr = MultiPathManager::new(cfg, fetcher, PathStrategy::default())
673 .expect("Should create manager");
674
675 assert!(mgr.0.managed_paths.is_empty());
677
678 let path = mgr.cached_path(SRC_ADDR.isd_asn(), DST_ADDR.isd_asn(), BASE_TIME);
680 assert!(path.is_none());
682
683 assert!(
685 mgr.0
686 .managed_paths
687 .contains(&(SRC_ADDR.isd_asn(), DST_ADDR.isd_asn()))
688 );
689 }
690
691 #[tokio::test]
693 #[test_log::test]
694 async fn new_rejects_invalid_config() {
695 let mut cfg = base_config();
696 cfg.min_refetch_delay = cfg.refetch_interval + Duration::from_secs(1);
698 let fetcher = MockFetcher::new(generate_responses(1, 0, BASE_TIME, DEFAULT_EXP_UNITS));
699
700 let err = match MultiPathManager::new(cfg, fetcher, PathStrategy::default()) {
701 Ok(_) => panic!("invalid config should be rejected"),
702 Err(e) => e,
703 };
704 assert!(
705 err.to_string().contains("min_refetch_delay"),
706 "unexpected error message: {err}"
707 );
708 }
709
710 #[tokio::test]
712 #[test_log::test]
713 async fn should_remove_idle_pathsets() {
714 let mut cfg = base_config();
715 cfg.max_idle_period = Duration::from_millis(10); let fetcher = MockFetcher::new(generate_responses(5, 0, BASE_TIME, DEFAULT_EXP_UNITS));
718
719 let mgr = MultiPathManager::new(cfg, fetcher, PathStrategy::default())
720 .expect("Should create manager");
721
722 let handle = mgr.ensure_managed_paths(SRC_ADDR.isd_asn(), DST_ADDR.isd_asn());
724
725 assert!(
727 mgr.0
728 .managed_paths
729 .contains(&(SRC_ADDR.isd_asn(), DST_ADDR.isd_asn()))
730 );
731
732 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
734
735 let contains = mgr
737 .0
738 .managed_paths
739 .contains(&(SRC_ADDR.isd_asn(), DST_ADDR.isd_asn()));
740
741 assert!(!contains, "Idle path set should be removed");
742
743 let err = handle.current_error();
744 assert!(
745 err.is_some(),
746 "Handle should report error after path set removal"
747 );
748 println!("Error after idle removal: {err:?}");
749 assert!(
750 err.unwrap().to_string().contains("idle"),
751 "Error message should indicate idle removal"
752 );
753 }
754
755 #[tokio::test]
757 #[test_log::test]
758 async fn should_cancel_pathset_tasks_on_drop() {
759 let cfg: MultiPathManagerConfig = base_config();
760 let fetcher = MockFetcher::new(generate_responses(5, 0, BASE_TIME, DEFAULT_EXP_UNITS));
761
762 let mgr = MultiPathManager::new(cfg, fetcher, PathStrategy::default())
763 .expect("Should create manager");
764
765 let handle = mgr.ensure_managed_paths(SRC_ADDR.isd_asn(), DST_ADDR.isd_asn());
767 handle.wait_initialized().await;
768
769 let mut set_entry = mgr
770 .0
771 .managed_paths
772 .get_sync(&(SRC_ADDR.isd_asn(), DST_ADDR.isd_asn()))
773 .unwrap();
774
775 let task_handle = unsafe {
776 let swap_handle = tokio::spawn(async {});
779 std::mem::replace(&mut set_entry.get_mut().1.task, swap_handle)
780 };
781
782 let cancel_token = set_entry.get().1.cancel_token.clone();
783
784 let count = mgr.0.managed_paths.len();
785 assert_eq!(count, 1, "Should have 1 managed path set");
786
787 drop(mgr);
789 assert!(
791 cancel_token.is_cancelled(),
792 "Cancel token should be triggered"
793 );
794
795 timeout(Duration::from_millis(50), task_handle)
797 .await
798 .unwrap()
799 .unwrap();
800
801 let err = handle
802 .shared
803 .sync
804 .lock()
805 .unwrap()
806 .current_error
807 .clone()
808 .expect("Should have error after manager drop");
809
810 assert!(
813 err.to_string().contains("cancelled") || err.to_string().contains("dropped"),
814 "Error message should indicate cancellation or manager drop"
815 );
816 }
817
818 mod issue_handling {
819 use scc::HashIndex;
820 use sciparse::identifier::{asn::Asn, isd::Isd};
821
822 use super::*;
823 use crate::path::{
824 manager::{MultiPathManagerInner, PathIssueManager, reliability::ReliabilityScore},
825 types::Score,
826 };
827
828 #[tokio::test]
831 #[test_log::test]
832 async fn should_ingest_issues_and_apply_to_existing_paths() {
833 let cfg = base_config();
834 let fetcher = MockFetcher::new(generate_responses(5, 0, BASE_TIME, DEFAULT_EXP_UNITS));
835 let (mgr, mut path_set) = manual_pathset(BASE_TIME, fetcher.clone(), cfg, None);
836
837 path_set.maintain(BASE_TIME, &mgr).await;
838
839 let first_path = &path_set.internal.cached_paths[0];
841 let first_fp = first_path.scion_path().fingerprint();
842
843 let issue = IssueKind::Socket {
845 err: SendError::FirstHopUnreachable {
846 isd_asn: first_path.path.src_ia(),
847 interface_id: first_path.path.first_egress_interface().unwrap().id,
848 address: None,
849 msg: "test".into(),
850 },
851 };
852
853 let penalty = Score::new_clamped(-0.3);
854 let marker = IssueMarker {
855 target: issue.target_type().unwrap(),
856 timestamp: BASE_TIME,
857 penalty,
858 };
859
860 {
861 let mut issues_guard = mgr.0.issue_manager.lock().unwrap();
862 issues_guard.add_issue(issue, marker);
864 assert!(!issues_guard.cache.is_empty(), "Issue should be in cache");
866 }
867 let recv_result = path_set.internal.issue_rx.recv().await;
869 path_set.handle_issue_rx(BASE_TIME, recv_result, &mgr);
870
871 let updated_path = path_set
873 .internal
874 .cached_paths
875 .iter()
876 .find(|e| e.scion_path().fingerprint() == first_fp)
877 .expect("Path should still exist");
878
879 let updated_score = updated_path.reliability.score(BASE_TIME).value();
880
881 assert!(
882 updated_score == penalty.value(),
883 "Path score should be updated by penalty. Expected: {}, Got: {}",
884 penalty.value(),
885 updated_score
886 );
887
888 let later_time = BASE_TIME + Duration::from_secs(30);
890 let decayed_score = updated_path.reliability.score(later_time).value();
891 assert!(
892 decayed_score > updated_score,
893 "Path score should recover over time. Updated: {updated_score}, Decayed: {decayed_score}"
894 );
895 }
896
897 #[tokio::test]
898 #[test_log::test]
899 async fn should_deduplicate_issues_within_window() {
900 let cfg = base_config();
901 let mgr_inner = MultiPathManagerInner {
902 config: cfg,
903 fetcher: MockFetcher::new(Ok(vec![])),
904 path_strategy: PathStrategy::default(),
905 issue_manager: Mutex::new(PathIssueManager::new(64, 64, Duration::from_secs(10))),
906 managed_paths: HashIndex::new(),
907 };
908 let mgr = MultiPathManager(Arc::new(mgr_inner));
909
910 let issue_marker = IssueMarker {
911 target: IssueMarkerTarget::FirstHop {
912 isd_asn: SRC_ADDR.isd_asn(),
913 egress_interface: 1,
914 },
915 timestamp: BASE_TIME,
916 penalty: Score::new_clamped(-0.3),
917 };
918
919 let issue = IssueKind::Socket {
920 err: SendError::FirstHopUnreachable {
921 isd_asn: SRC_ADDR.isd_asn(),
922 interface_id: 1,
923 address: None,
924 msg: "test".into(),
925 },
926 };
927
928 mgr.0
930 .issue_manager
931 .lock()
932 .unwrap()
933 .add_issue(issue.clone(), issue_marker.clone());
934 let cache_size_1 = mgr.0.issue_manager.lock().unwrap().cache.len();
935 assert_eq!(cache_size_1, 1);
936
937 let issue_marker_2 = IssueMarker {
939 timestamp: BASE_TIME + Duration::from_secs(1), ..issue_marker.clone()
941 };
942 mgr.0
943 .issue_manager
944 .lock()
945 .unwrap()
946 .add_issue(issue.clone(), issue_marker_2);
947
948 let fifo_size = mgr.0.issue_manager.lock().unwrap().fifo_issues.len();
949 let cache_size_2 = mgr.0.issue_manager.lock().unwrap().cache.len();
950 assert_eq!(cache_size_2, 1, "Duplicate issue should be ignored");
951 assert_eq!(
952 fifo_size, 1,
953 "FIFO queue size should remain unchanged on duplicate issue"
954 );
955
956 let issue_marker_3 = IssueMarker {
958 timestamp: BASE_TIME + Duration::from_secs(11), ..issue_marker
960 };
961 mgr.0
962 .issue_manager
963 .lock()
964 .unwrap()
965 .add_issue(issue, issue_marker_3);
966
967 let fifo_size_3 = mgr.0.issue_manager.lock().unwrap().fifo_issues.len();
968 let cache_size_3 = mgr.0.issue_manager.lock().unwrap().cache.len();
969 assert_eq!(
970 cache_size_3, 1,
971 "Issue outside dedup window should update existing"
972 );
973 assert_eq!(
974 fifo_size_3, 2,
975 "FIFO queue size should increase for new issue outside dedup window"
976 );
977 }
978
979 #[tokio::test]
982 #[test_log::test]
983 async fn should_apply_issues_to_new_paths_on_fetch() {
984 let cfg = base_config();
985 let fetcher = MockFetcher::new(Ok(vec![]));
986 let (mgr, mut path_set) = manual_pathset(BASE_TIME, fetcher.clone(), cfg, None);
987
988 path_set.maintain(BASE_TIME, &mgr).await;
989
990 let issue_marker = IssueMarker {
992 target: IssueMarkerTarget::FirstHop {
993 isd_asn: SRC_ADDR.isd_asn(),
994 egress_interface: 1,
995 },
996 timestamp: BASE_TIME,
997 penalty: Score::new_clamped(-0.5),
998 };
999
1000 let issue = IssueKind::Socket {
1001 err: SendError::FirstHopUnreachable {
1002 isd_asn: SRC_ADDR.isd_asn(),
1003 interface_id: 1,
1004 address: None,
1005 msg: "test".into(),
1006 },
1007 };
1008
1009 mgr.0
1011 .issue_manager
1012 .lock()
1013 .unwrap()
1014 .add_issue(issue, issue_marker);
1015
1016 path_set.drain_and_apply_issue_channel(BASE_TIME);
1018
1019 fetcher.lock().unwrap().set_response(generate_responses(
1021 3,
1022 0,
1023 BASE_TIME + Duration::from_secs(1),
1024 DEFAULT_EXP_UNITS,
1025 ));
1026
1027 let next_refetch = path_set.internal.next_refetch;
1028 path_set.maintain(next_refetch, &mgr).await;
1029
1030 let affected_path = path_set
1032 .internal
1033 .cached_paths
1034 .first()
1035 .expect("Path should exist");
1036
1037 let score = affected_path
1038 .reliability
1039 .score(BASE_TIME + Duration::from_secs(1))
1040 .value();
1041 assert!(
1042 score < 0.0,
1043 "Newly fetched path should have cached issue applied. Score: {score}"
1044 );
1045 }
1046
1047 #[tokio::test]
1049 #[test_log::test]
1050 async fn should_trigger_active_path_reevaluation_on_issue() {
1051 let cfg = base_config();
1052 let fetcher = MockFetcher::new(generate_responses(5, 0, BASE_TIME, DEFAULT_EXP_UNITS));
1053 let (mgr, mut path_set) = manual_pathset(BASE_TIME, fetcher.clone(), cfg, None);
1054
1055 path_set.maintain(BASE_TIME, &mgr).await;
1056
1057 let active_fp = path_set.shared.active_path.load().as_ref().unwrap().1;
1058
1059 let issue_marker = IssueMarker {
1061 target: IssueMarkerTarget::FullPath {
1062 fingerprint: active_fp,
1063 },
1064 timestamp: BASE_TIME,
1065 penalty: Score::new_clamped(-1.0), };
1067
1068 let issue = IssueKind::Socket {
1069 err: SendError::FirstHopUnreachable {
1070 isd_asn: SRC_ADDR.isd_asn(),
1071 interface_id: 1,
1072 address: None,
1073 msg: "test".into(),
1074 },
1075 };
1076
1077 mgr.0
1079 .issue_manager
1080 .lock()
1081 .unwrap()
1082 .add_issue(issue, issue_marker);
1083
1084 let recv_result = path_set.internal.issue_rx.recv().await;
1086 path_set.handle_issue_rx(BASE_TIME, recv_result, &mgr);
1087
1088 let new_active_fp = path_set.shared.active_path.load().as_ref().unwrap().1;
1090 assert_ne!(
1091 active_fp, new_active_fp,
1092 "Active path should change when severely penalized"
1093 );
1094 }
1095
1096 #[tokio::test]
1097 #[test_log::test]
1098 async fn should_swap_to_better_path_if_one_appears() {
1099 let cfg = base_config();
1100 let fetcher = MockFetcher::new(generate_responses(1, 0, BASE_TIME, DEFAULT_EXP_UNITS));
1101 let (mgr, mut path_set) = manual_pathset(BASE_TIME, fetcher.clone(), cfg, None);
1102
1103 path_set.maintain(BASE_TIME, &mgr).await;
1104
1105 path_set
1107 .shared
1108 .was_used_in_idle_period
1109 .store(true, std::sync::atomic::Ordering::Relaxed);
1110
1111 let active_fp = path_set.shared.active_path.load().as_ref().unwrap().1;
1112
1113 let issue_marker = IssueMarker {
1115 target: IssueMarkerTarget::FullPath {
1116 fingerprint: active_fp,
1117 },
1118 timestamp: BASE_TIME,
1119 penalty: Score::new_clamped(-0.8),
1120 };
1121
1122 mgr.0.issue_manager.lock().unwrap().add_issue(
1123 IssueKind::Socket {
1124 err: SendError::FirstHopUnreachable {
1125 isd_asn: SRC_ADDR.isd_asn(),
1126 interface_id: 1,
1127 address: None,
1128 msg: "test".into(),
1129 },
1130 },
1131 issue_marker,
1132 );
1133
1134 let active_fp_after_issue = path_set.shared.active_path.load().as_ref().unwrap().1;
1136 assert_eq!(
1137 active_fp, active_fp_after_issue,
1138 "Active path should remain the same if no better path exists"
1139 );
1140
1141 fetcher.lock().unwrap().set_response(generate_responses(
1143 1,
1144 100,
1145 BASE_TIME + Duration::from_secs(1),
1146 DEFAULT_EXP_UNITS,
1147 ));
1148
1149 path_set
1150 .maintain(path_set.internal.next_refetch, &mgr)
1151 .await;
1152 path_set
1154 .shared
1155 .was_used_in_idle_period
1156 .store(true, std::sync::atomic::Ordering::Relaxed);
1157
1158 let new_active_fp = path_set.shared.active_path.load().as_ref().unwrap().1;
1160 assert_ne!(
1161 active_fp, new_active_fp,
1162 "Active path should change when a better path appears"
1163 );
1164
1165 let positive_score = Score::new_clamped(0.8);
1167 let mut reliability = ReliabilityScore::new_with_time(path_set.internal.next_refetch);
1168 reliability.update(positive_score, path_set.internal.next_refetch);
1169
1170 path_set
1172 .internal
1173 .cached_paths
1174 .iter_mut()
1175 .find(|e| e.scion_path().fingerprint() == active_fp)
1176 .unwrap()
1177 .reliability = reliability;
1178
1179 path_set
1180 .maintain(path_set.internal.next_refetch, &mgr)
1181 .await;
1182
1183 assert_eq!(
1184 active_fp,
1185 path_set.shared.active_path.load().as_ref().unwrap().1,
1186 "Active path should change on positive score diff"
1187 );
1188 }
1189
1190 #[tokio::test]
1191 #[test_log::test]
1192 async fn should_keep_max_issue_cache_size() {
1193 let max_size = 10;
1194 let mut issue_mgr = PathIssueManager::new(max_size, 64, Duration::from_secs(10));
1195
1196 for i in 0..20u16 {
1198 let issue_marker = IssueMarker {
1199 target: IssueMarkerTarget::FirstHop {
1200 isd_asn: IsdAsn::new(Isd(1), Asn(1)),
1201 egress_interface: i,
1202 },
1203 timestamp: BASE_TIME + Duration::from_secs(u64::from(i)),
1204 penalty: Score::new_clamped(-0.1),
1205 };
1206
1207 let issue = IssueKind::Socket {
1208 err: SendError::FirstHopUnreachable {
1209 isd_asn: IsdAsn::new(Isd(1), Asn(1)),
1210 interface_id: i,
1211 address: None,
1212 msg: "test".into(),
1213 },
1214 };
1215
1216 issue_mgr.add_issue(issue, issue_marker);
1217 }
1218
1219 assert!(
1221 issue_mgr.cache.len() <= max_size,
1222 "Cache size {} should not exceed max {}",
1223 issue_mgr.cache.len(),
1224 max_size
1225 );
1226
1227 assert_eq!(issue_mgr.cache.len(), issue_mgr.fifo_issues.len());
1229 }
1230 }
1231
1232 pub mod helpers {
1233 use std::{
1234 hash::{DefaultHasher, Hash, Hasher},
1235 net::{IpAddr, Ipv4Addr},
1236 sync::{Arc, Mutex},
1237 time::{Duration, SystemTime},
1238 };
1239
1240 use sciparse::{
1241 address::ip_addr::ScionIpAddr,
1242 identifier::{asn::Asn, isd::Isd},
1243 util::test_builder::TestPathBuilder,
1244 };
1245 use tokio::sync::Notify;
1246
1247 use super::*;
1248 use crate::path::manager::{MultiPathManagerInner, PathIssueManager, pathset::PathSet};
1249
1250 pub const SRC_ADDR: ScionIpAddr =
1251 ScionIpAddr::new(IsdAsn::new(Isd(1), Asn(1)), IpAddr::V4(Ipv4Addr::LOCALHOST));
1252 pub const DST_ADDR: ScionIpAddr = ScionIpAddr::new(
1253 IsdAsn::new(Isd(2), Asn(1)),
1254 IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2)),
1255 );
1256
1257 pub const DEFAULT_EXP_UNITS: u8 = 100;
1258 pub const BASE_TIME: SystemTime = SystemTime::UNIX_EPOCH;
1259
1260 pub fn dummy_path(hop_count: u16, timestamp: u32, exp_units: u8, seed: u32) -> ScionPath {
1261 let mut builder = TestPathBuilder::new(SRC_ADDR.into(), DST_ADDR.into())
1262 .using_info_timestamp(timestamp)
1263 .with_hop_expiry(exp_units)
1264 .up();
1265
1266 builder = builder.add_hop(0, 1);
1267
1268 for cnt in 0..hop_count {
1269 let mut hash = DefaultHasher::new();
1270 seed.hash(&mut hash);
1271 cnt.hash(&mut hash);
1272 let hash = hash.finish() as u32;
1273
1274 let hop = hash.saturating_sub(2) as u16; builder = builder.with_asn(hash).add_hop(hop + 1, hop + 2);
1276 }
1277
1278 builder = builder.add_hop(1, 0);
1279
1280 builder.build(timestamp).path()
1281 }
1282
1283 pub fn base_config() -> MultiPathManagerConfig {
1284 MultiPathManagerConfig {
1285 max_cached_paths_per_pair: 5,
1286 refetch_interval: Duration::from_secs(100),
1287 min_refetch_delay: Duration::from_secs(1),
1288 min_expiry_threshold: Duration::from_secs(5),
1289 max_idle_period: Duration::from_secs(30),
1290 fetch_failure_backoff: BackoffConfig {
1291 minimum_delay_secs: 1.0,
1292 maximum_delay_secs: 10.0,
1293 factor: 2.0,
1294 jitter_secs: 0.0,
1295 },
1296 issue_cache_size: 64,
1297 issue_broadcast_size: 64,
1298 issue_deduplication_window: Duration::from_secs(10),
1299 path_swap_score_threshold: 0.1,
1300 }
1301 }
1302
1303 pub fn generate_responses(
1304 path_count: u16,
1305 path_seed: u32,
1306 timestamp: SystemTime,
1307 exp_units: u8,
1308 ) -> Result<Vec<ScionPath>, String> {
1309 let mut paths = Vec::new();
1310 for resp_id in 0..path_count {
1311 paths.push(dummy_path(
1312 2,
1313 timestamp
1314 .duration_since(SystemTime::UNIX_EPOCH)
1315 .unwrap()
1316 .as_secs() as u32,
1317 exp_units,
1318 path_seed + u32::from(resp_id),
1319 ));
1320 }
1321
1322 Ok(paths)
1323 }
1324
1325 pub struct MockFetcher {
1326 next_response: Result<Vec<ScionPath>, String>,
1327 pub received_requests: usize,
1328 pub wait_till_notify: bool,
1329 pub notify_to_resolve: Arc<Notify>,
1330 }
1331 impl MockFetcher {
1332 pub fn new(response: Result<Vec<ScionPath>, String>) -> Arc<Mutex<Self>> {
1333 Arc::new(Mutex::new(Self {
1334 next_response: response,
1335 received_requests: 0,
1336 wait_till_notify: false,
1337 notify_to_resolve: Arc::new(Notify::new()),
1338 }))
1339 }
1340
1341 pub fn set_response(&mut self, response: Result<Vec<ScionPath>, String>) {
1342 self.next_response = response;
1343 }
1344
1345 pub fn wait_till_notify(&mut self, wait: bool) {
1346 self.wait_till_notify = wait;
1347 }
1348
1349 pub fn notify(&self) {
1350 self.notify_to_resolve.notify_waiters();
1351 }
1352 }
1353
1354 impl PathFetcher for Arc<Mutex<MockFetcher>> {
1355 async fn fetch_paths(
1356 &self,
1357 _src: IsdAsn,
1358 _dst: IsdAsn,
1359 ) -> Result<Vec<ScionPath>, PathFetchError> {
1360 let response;
1361 let notify = {
1363 let mut guard = self.lock().unwrap();
1364
1365 guard.received_requests += 1;
1366 response = guard.next_response.clone();
1367
1368 if guard.wait_till_notify {
1370 let notif = guard.notify_to_resolve.clone().notified_owned();
1371 Some(notif)
1372 } else {
1373 None
1374 }
1375 };
1376
1377 if let Some(notif) = notify {
1378 notif.await;
1379 }
1380
1381 match response {
1382 Ok(paths) if paths.is_empty() => Err(PathFetchError::NoPathsFound),
1383 Ok(paths) => Ok(paths),
1384 Err(e) => Err(PathFetchError::InternalError(e.into())),
1385 }
1386 }
1387 }
1388
1389 pub fn manual_pathset<F: PathFetcher>(
1390 now: SystemTime,
1391 fetcher: F,
1392 cfg: MultiPathManagerConfig,
1393 strategy: Option<PathStrategy>,
1394 ) -> (MultiPathManager<F>, PathSet<F>) {
1395 let mgr_inner = MultiPathManagerInner {
1396 config: cfg,
1397 fetcher,
1398 path_strategy: strategy.unwrap_or_else(|| {
1399 let mut ps = PathStrategy::default();
1400 ps.scoring.use_default_scorers();
1401 ps
1402 }),
1403 issue_manager: Mutex::new(PathIssueManager::new(64, 64, Duration::from_secs(10))),
1404 managed_paths: HashIndex::new(),
1405 };
1406 let mgr = MultiPathManager(Arc::new(mgr_inner));
1407 let issue_rx = mgr.0.issue_manager.lock().unwrap().issues_subscriber();
1408 let mgr_ref = mgr.weak_ref();
1409 (
1410 mgr,
1411 PathSet::new_with_time(
1412 SRC_ADDR.isd_asn(),
1413 DST_ADDR.isd_asn(),
1414 mgr_ref,
1415 cfg,
1416 issue_rx,
1417 now,
1418 ),
1419 )
1420 }
1421 }
1422}