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