Skip to main content

scion_stack/path/
manager.rs

1// Copyright 2025 Anapaya Systems
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//   http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Multipath manager for SCION path selection.
16//!
17//! Runs one task per (src,dst) pair. Each task fetches paths, filters them, applies issue
18//! penalties, ranks candidates, and picks an active path.
19//!
20//! Tasks track expiry, refetch intervals, backoff after failures, and drop entries that go
21//! idle. The Active path for a (src,dst) pair is exposed lock-free via `ArcSwap`.
22//!
23//! All path data comes from the provided `PathFetcher`. Issue reports feed
24//! into reliability scoring and can trigger immediate re-ranking.
25//!
26//! ## Issue Handling & Penalties
27//!
28//! Incoming issues are applied to cached paths immediately and can trigger an active-path
29//! switch. Issues are cached with a timestamp and applied to newly fetched paths.
30//!
31//! Penalties on individual paths and individual cached issues decay over time. Allowing paths
32//! to recover.
33//!
34//! ## Active Path Switching
35//!
36//! If no active path exists, the highest-ranked valid path is selected.
37//! Active path is replaced when it expires, nears expiry, or falls behind the best candidate
38//! by a configured score margin.
39
40// Internal:
41//
42// ## Core components
43//
44// MultiPathManager: Central entry point. Holds configuration, the global issue manager, and a
45// concurrent map from (src, dst) to worker. Spawns a worker on first access and provides lock-free
46// reads to workers.
47//
48// PathSet: Per-tuple worker. Fetches paths, filters them, applies issue penalties, ranks
49// candidates, and maintains an active path. Runs a periodic maintenance loop handling refetch,
50// backoff, and idle shutdown.
51//
52// PathIssueManager:  Global issue cache and broadcast system. Deduplicates issues and notifies all
53// workers of incoming issues.
54//
55// IssueKind / IssueMarker: Describe concrete path problems (SCMP, socket errors). Compute the
56// affected hop or full path, assign a penalty, and support deduplication and decay.
57
58use 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;
90/// Path issue definitions, including mapping issues to affected targets and their respective
91/// penalties
92mod issues;
93/// Pathsets manage paths for a specific src-dst pair.
94mod pathset;
95/// Path reliability tracking
96pub(crate) mod reliability;
97/// Path fetcher traits and types.
98pub mod traits;
99
100/// Configuration for the `MultiPathManager`.
101#[derive(Debug, Clone, Copy)]
102pub struct MultiPathManagerConfig {
103    /// Maximum number of cached paths per src-dst pair.
104    max_cached_paths_per_pair: usize,
105    /// Interval between path refetches
106    refetch_interval: Duration,
107    /// Minimum duration between path refetches.
108    min_refetch_delay: Duration,
109    /// Minimum remaining expiry before refetching paths.
110    min_expiry_threshold: Duration,
111    /// Maximum idle period before the managed paths are removed.
112    max_idle_period: Duration,
113    /// Backoff configuration for path fetch failures.
114    fetch_failure_backoff: BackoffConfig,
115    /// Count of issues to be cached
116    issue_cache_size: usize,
117    /// Size of the issue cache broadcast channel
118    issue_broadcast_size: usize,
119    /// Time window to ignore duplicate issues
120    issue_deduplication_window: Duration,
121    /// Score difference after which active path should be replaced
122    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), // 30 minutes
130            min_refetch_delay: Duration::from_secs(60),
131            min_expiry_threshold: Duration::from_secs(60 * 5), // 5 minutes
132            max_idle_period: Duration::from_secs(60 * 2),      // 2 minutes
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            // Same issue within 10s is duplicate
142            issue_deduplication_window: Duration::from_secs(10),
143            path_swap_score_threshold: 0.5,
144        }
145    }
146}
147
148impl MultiPathManagerConfig {
149    /// Sets the maximum number of cached paths per src-dst pair.
150    #[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    /// Sets the interval between path refetches.
157    #[must_use]
158    pub fn with_refetch_interval(mut self, interval: Duration) -> Self {
159        self.refetch_interval = interval;
160        self
161    }
162
163    /// Sets the minimum duration between path refetches.
164    #[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    /// Sets the minimum remaining expiry before refetching paths.
171    #[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    /// Sets the maximum idle period before managed paths are removed.
178    #[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    /// Sets the time window during which duplicate issues are ignored.
185    #[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    /// Sets the score difference after which the active path should be replaced.
192    #[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    /// Validates the configuration.
199    fn validate(&self) -> Result<(), MultiPathManagerConfigError> {
200        if self.min_refetch_delay > self.refetch_interval {
201            // Otherwise, refetch interval makes no sense.
202            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            // Otherwise, very unlikely, we have paths expiring before we can refetch.
209            return Err(MultiPathManagerConfigError(
210                "min_refetch_delay must be smaller than min_expiry_threshold",
211            ));
212        }
213
214        Ok(())
215    }
216}
217
218/// Error returned when a [`MultiPathManagerConfig`] is invalid.
219#[derive(Debug, thiserror::Error)]
220#[error("invalid path manager configuration: {0}")]
221#[non_exhaustive]
222pub struct MultiPathManagerConfigError(&'static str);
223
224/// Path manager managing multiple paths per src-dst pair.
225pub 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    /// Creates a new [`MultiPathManager`].
246    ///
247    /// # Errors
248    ///
249    /// Returns [`MultiPathManagerConfigError`] if `config` is invalid.
250    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    /// Returns the cached active path for the given src-dst pair, if one is available.
273    ///
274    /// If no active path is cached, returns `None`.
275    ///
276    /// If the src-dst pair is not yet managed, starts managing it.
277    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                // XXX(ake): Since the Paths are actively managed, they should never be expired
289                // here.
290                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                // Start managing paths for the src-dst pair
303                self.fast_ensure_managed_paths(src, dst);
304                None
305            }
306        }
307    }
308
309    /// Gets the active path for the given src-dst pair.
310    ///
311    /// If the src-dst pair is not yet managed, starts managing it, possibly waiting for the first
312    /// path fetch.
313    ///
314    /// Returns an error if no path is available after waiting.
315    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                // Ensure paths are being managed
343                let path_set = self.ensure_managed_paths(src, dst);
344
345                // Try to get active path, possibly waiting for initialization/update
346                let active = path_set.active_path().await.as_ref().map(|p| p.0.clone());
347
348                // Check active path after waiting
349                match active {
350                    Some(active) => Ok(active),
351                    None => {
352                        // No active path even after waiting, return last error if any
353                        let last_error = path_set.current_error();
354                        match last_error {
355                            Some(e) => Err(e),
356                            None => {
357                                // There is a chance for a race here, where the error was cleared
358                                // between the wait and now. In that case, we assume no paths were
359                                // found.
360                                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            // XXX(ake): Since the Paths are actively managed, they should never be expired
375            // here.
376            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    /// Creates a weak reference to this [`MultiPathManager`].
384    ///
385    /// Upgrade it back to a strong handle with [`MultiPathManagerRef::upgrade`].
386    pub fn weak_ref(&self) -> MultiPathManagerRef<F> {
387        MultiPathManagerRef(Arc::downgrade(&self.0))
388    }
389
390    /// Quickly ensures that paths are being managed for the given src-dst pair.
391    ///
392    /// Does nothing if paths are already being managed.
393    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    /// Starts managing paths for the given src-dst pair.
402    ///
403    /// Returns a reference to the managed paths.
404    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    /// Stops managing paths for the given src-dst pair.
432    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    /// Reports a path issue to the issue manager.
439    pub(crate) fn report_path_issue(&self, timestamp: SystemTime, issue: IssueKind) {
440        let Some(applies_to) = issue.target_type() else {
441            // Not a path issue we care about
442            return;
443        };
444
445        if matches!(applies_to, IssueMarkerTarget::DestinationNetwork { .. }) {
446            // We can't handle dst network issues in a global path manager
447            return;
448        }
449
450        let issue_marker = IssueMarker {
451            target: applies_to,
452            timestamp,
453            penalty: issue.penalty(),
454        };
455
456        // Push to issues cache
457        {
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        // No-op
492        // Based on discussions we do not support externally registered paths in the PathManager
493        // Likely we will handle path mirroring in Connection Based Protocols instead
494    }
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
533/// Weak reference to a [`MultiPathManager`].
534///
535/// Can be upgraded to a strong reference using [`upgrade`](Self::upgrade), mirroring
536/// [`std::sync::Weak::upgrade`].
537pub 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    /// Attempts to upgrade the weak reference to a strong reference.
547    #[must_use]
548    pub fn upgrade(&self) -> Option<MultiPathManager<F>> {
549        self.0.upgrade().map(MultiPathManager)
550    }
551}
552
553/// Path Issue manager
554///
555/// Receives reported issues, deduplicates them, and broadcasts them to all path sets.
556struct PathIssueManager {
557    // Config
558    max_entries: usize,
559    deduplication_window: Duration,
560
561    // Mutable
562    /// Map of issue ID to issue marker
563    cache: HashMap<u64, IssueMarker>,
564    // FiFo queue of issue IDs and their timestamps
565    fifo_issues: VecDeque<(u64, SystemTime)>,
566
567    /// Channel for broadcasting issues
568    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    /// Returns a subscriber to the issue broadcast channel.
584    pub fn issues_subscriber(&self) -> broadcast::Receiver<(u64, IssueMarker)> {
585        self.issue_broadcast_tx.subscribe()
586    }
587
588    /// Adds a new issue to the manager.
589    ///
590    /// Issues might cause the Active path to change immediately.
591    ///
592    /// All issues get cached to be applied to newly fetched paths.
593    ///
594    /// If a similar issue, applying to the same Path is seen in the deduplication window, it will
595    /// be ignored.
596    pub fn add_issue(&mut self, issue: IssueKind, marker: IssueMarker) {
597        let id = issue.dedup_id(&marker.target);
598
599        // Check if we already have this issue
600        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                // Too soon since last seen, ignore
609                return;
610            }
611        }
612
613        tracing::debug!(%id, %issue, "New path issue");
614
615        // Broadcast issue
616        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        // Insert issue
623        self.fifo_issues.push_back((id, marker.timestamp)); // Store timestamp for matching on removal
624        self.cache.insert(id, marker);
625    }
626
627    /// Applies all cached issues to the given path.
628    ///
629    /// This is called when a path is fetched, to ensure that issues affecting it are applied.
630    /// Should only be called on fresh paths.
631    ///
632    /// Returns true if any issues were applied.
633    /// Returns the max
634    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    /// Pops the oldest issue from the cache.
649    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                // Only remove if timestamps match
655                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    // The manager should create path sets on request
677    #[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        // Initially no managed paths
687        assert!(mgr.0.managed_paths.is_empty());
688
689        // Request a path - should create path set
690        let path = mgr.cached_path(SRC_ADDR.isd_asn(), DST_ADDR.isd_asn(), BASE_TIME);
691        // First call returns None (not yet initialized)
692        assert!(path.is_none());
693
694        // But path set should be created
695        assert!(
696            mgr.0
697                .managed_paths
698                .contains(&(SRC_ADDR.isd_asn(), DST_ADDR.isd_asn()))
699        );
700    }
701
702    // An invalid configuration is rejected by `MultiPathManager::new`.
703    #[tokio::test]
704    #[test_log::test]
705    async fn new_rejects_invalid_config() {
706        let mut cfg = base_config();
707        // `min_refetch_delay` must not exceed `refetch_interval`; violate that invariant.
708        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    // The manager should remove idle path sets
722    #[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); // Short idle period for testing
727
728        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        // Create path set
734        let handle = mgr.ensure_managed_paths(SRC_ADDR.isd_asn(), DST_ADDR.isd_asn());
735
736        // Should exist
737        assert!(
738            mgr.0
739                .managed_paths
740                .contains(&(SRC_ADDR.isd_asn(), DST_ADDR.isd_asn()))
741        );
742
743        // Wait for idle timeout plus some margin
744        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
745
746        // Path set should be removed by idle check
747        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    // Dropping the manager should cancel all path set maintenance tasks
767    #[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        // ensure path set exists and initialized
777        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            // swap join handle with a fake one, only possible since the manager doesn't use
788            // the handle
789            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 the manager
799        drop(mgr);
800        // Cancel token should be triggered
801        assert!(
802            cancel_token.is_cancelled(),
803            "Cancel token should be triggered"
804        );
805
806        // Give tasks time to detect manager drop and exit
807        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        // XXX(ake): exit reason may vary between "cancelled" and "manager dropped" because
822        // of select!
823        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        // When an issue is ingested, affected paths should have their reliability scores
840        // updated appropriately The issue should be in the issue cache
841        #[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            // Get the first path to create an issue for
851            let first_path = &path_set.internal.cached_paths[0];
852            let first_fp = first_path.scion_path().fingerprint();
853
854            // Create an issue targeting the first hop of the first path
855            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                // Add issue to manager
874                issues_guard.add_issue(issue, marker);
875                // Check issue is in cache
876                assert!(!issues_guard.cache.is_empty(), "Issue should be in cache");
877            }
878            // Handle the issue in path_set
879            let recv_result = path_set.internal.issue_rx.recv().await;
880            path_set.handle_issue_rx(BASE_TIME, recv_result, &mgr);
881
882            // Check that the path's score was updated
883            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            // Should decay over time
900            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            // Add issue first time
940            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            // Add same issue within dedup window (should be ignored)
949            let issue_marker_2 = IssueMarker {
950                timestamp: BASE_TIME + Duration::from_secs(1), // Within 10s window
951                ..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            // Add same issue outside dedup window (should be added)
968            let issue_marker_3 = IssueMarker {
969                timestamp: BASE_TIME + Duration::from_secs(11), // Outside 10s window
970                ..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        // When new paths are fetched, existing issues in the issue cache should be applied to
991        // them
992        #[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            // Create an issue
1002            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            // Add to manager's issue cache
1021            mgr.0
1022                .issue_manager
1023                .lock()
1024                .unwrap()
1025                .add_issue(issue, issue_marker);
1026
1027            // Drain issue channel so no issues are pending
1028            path_set.drain_and_apply_issue_channel(BASE_TIME);
1029
1030            // Now fetch paths again - the issue should be applied to the newly fetched path
1031            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            // The newly fetched path should have the penalty applied
1042            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        // If the active path is affected by an issue, it should be re-evaluated
1059        #[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            // Create a severe issue targeting the active path
1071            let issue_marker = IssueMarker {
1072                target: IssueMarkerTarget::FullPath {
1073                    fingerprint: active_fp,
1074                },
1075                timestamp: BASE_TIME,
1076                penalty: Score::new_clamped(-1.0), // Severe penalty
1077            };
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            // Add issue
1089            mgr.0
1090                .issue_manager
1091                .lock()
1092                .unwrap()
1093                .add_issue(issue, issue_marker);
1094
1095            // Handle issue
1096            let recv_result = path_set.internal.issue_rx.recv().await;
1097            path_set.handle_issue_rx(BASE_TIME, recv_result, &mgr);
1098
1099            // Active path should have changed
1100            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            // mark as used to prevent idle removal
1117            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            // add issue to active path to lower its score
1125            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            // active path should be the same
1146            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            // Now fetch a better path
1153            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            // mark as used to prevent idle removal
1164            path_set
1165                .shared
1166                .was_used_in_idle_period
1167                .store(true, std::sync::atomic::Ordering::Relaxed);
1168
1169            // Active path should have changed
1170            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            // Should also work for positive score changes
1177            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            // Change old paths reliability to be better
1182            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            // Add more issues than max_size
1208            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            // Cache should not exceed max_size
1231            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            // FIFO queue should match cache size
1239            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; // ensure no underflow or overflow
1286                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                // Wait for notification if needed
1373                let notify = {
1374                    let mut guard = self.lock().unwrap();
1375
1376                    guard.received_requests += 1;
1377                    response = guard.next_response.clone();
1378
1379                    // maybe wait till notified
1380                    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}