1use dashmap::DashMap;
47use parking_lot::RwLock;
48use std::collections::VecDeque;
49use std::net::SocketAddr;
50use std::sync::Arc;
51use std::time::{Duration, Instant};
52use thiserror::Error;
53use tracing::{debug, info, warn};
54
55#[derive(Debug, Error)]
57pub enum MultipathError {
58 #[error("No paths available")]
59 NoPathsAvailable,
60
61 #[error("Path not found: {0}")]
62 PathNotFound(u64),
63
64 #[error("Path quality too low: {0}")]
65 PathQualityTooLow(f64),
66
67 #[error("Maximum paths reached: {0}")]
68 MaxPathsReached(usize),
69
70 #[error("Invalid configuration: {0}")]
71 InvalidConfig(String),
72}
73
74pub type Result<T> = std::result::Result<T, MultipathError>;
76
77pub type PathId = u64;
79
80#[derive(Debug, Clone)]
82pub struct NetworkPath {
83 pub id: PathId,
85
86 pub local_addr: SocketAddr,
88
89 pub remote_addr: SocketAddr,
91
92 pub state: PathState,
94
95 pub quality: PathQuality,
97
98 pub created_at: Instant,
100
101 pub last_used: Instant,
103
104 pub bytes_sent: u64,
106
107 pub bytes_received: u64,
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum PathState {
114 Validating,
116
117 Active,
119
120 Standby,
122
123 Degraded,
125
126 Failed,
128
129 Closing,
131}
132
133#[derive(Debug, Clone)]
135pub struct PathQuality {
136 pub rtt_ms: f64,
138
139 pub bandwidth_bps: u64,
141
142 pub loss_rate: f64,
144
145 pub jitter_ms: f64,
147
148 pub score: f64,
150
151 pub sample_count: usize,
153
154 pub last_updated: Instant,
156}
157
158impl Default for PathQuality {
159 fn default() -> Self {
160 Self {
161 rtt_ms: 0.0,
162 bandwidth_bps: 0,
163 loss_rate: 0.0,
164 jitter_ms: 0.0,
165 score: 0.5, sample_count: 0,
167 last_updated: Instant::now(),
168 }
169 }
170}
171
172impl PathQuality {
173 pub fn calculate_score(&mut self) {
175 const RTT_WEIGHT: f64 = 0.3;
177 const BANDWIDTH_WEIGHT: f64 = 0.3;
178 const LOSS_WEIGHT: f64 = 0.3;
179 const JITTER_WEIGHT: f64 = 0.1;
180
181 let rtt_score = (1.0 - (self.rtt_ms / 500.0).clamp(0.0, 1.0)).max(0.0);
183
184 let bandwidth_mbps = self.bandwidth_bps as f64 / 125_000.0;
186 let bandwidth_score = (bandwidth_mbps / 100.0).clamp(0.0, 1.0);
187
188 let loss_score = (1.0 - self.loss_rate).max(0.0);
190
191 let jitter_score = (1.0 - (self.jitter_ms / 50.0).clamp(0.0, 1.0)).max(0.0);
193
194 self.score = rtt_score * RTT_WEIGHT
196 + bandwidth_score * BANDWIDTH_WEIGHT
197 + loss_score * LOSS_WEIGHT
198 + jitter_score * JITTER_WEIGHT;
199 }
200
201 pub fn update(&mut self, rtt_ms: f64, bandwidth_bps: u64, loss_rate: f64, jitter_ms: f64) {
203 const ALPHA: f64 = 0.8; if self.sample_count == 0 {
206 self.rtt_ms = rtt_ms;
208 self.bandwidth_bps = bandwidth_bps;
209 self.loss_rate = loss_rate;
210 self.jitter_ms = jitter_ms;
211 } else {
212 self.rtt_ms = self.rtt_ms * (1.0 - ALPHA) + rtt_ms * ALPHA;
214 self.bandwidth_bps = ((self.bandwidth_bps as f64) * (1.0 - ALPHA)
215 + (bandwidth_bps as f64) * ALPHA) as u64;
216 self.loss_rate = self.loss_rate * (1.0 - ALPHA) + loss_rate * ALPHA;
217 self.jitter_ms = self.jitter_ms * (1.0 - ALPHA) + jitter_ms * ALPHA;
218 }
219
220 self.sample_count += 1;
221 self.last_updated = Instant::now();
222 self.calculate_score();
223 }
224}
225
226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
228pub enum PathSelectionStrategy {
229 RoundRobin,
231
232 QualityBased,
234
235 LowestLatency,
237
238 HighestBandwidth,
240
241 Redundant,
243
244 WeightedRoundRobin,
246}
247
248#[derive(Debug, Clone)]
250pub struct MultipathConfig {
251 pub max_paths: usize,
253
254 pub strategy: PathSelectionStrategy,
256
257 pub enable_redundancy: bool,
259
260 pub min_quality_threshold: f64,
262
263 pub quality_check_interval: Duration,
265
266 pub path_idle_timeout: Duration,
268
269 pub enable_auto_migration: bool,
271
272 pub migration_quality_threshold: f64,
274}
275
276impl Default for MultipathConfig {
277 fn default() -> Self {
278 Self {
279 max_paths: 4,
280 strategy: PathSelectionStrategy::QualityBased,
281 enable_redundancy: false,
282 min_quality_threshold: 0.3,
283 quality_check_interval: Duration::from_secs(5),
284 path_idle_timeout: Duration::from_secs(60),
285 enable_auto_migration: true,
286 migration_quality_threshold: 0.2, }
288 }
289}
290
291impl MultipathConfig {
292 pub fn low_latency() -> Self {
294 Self {
295 max_paths: 2,
296 strategy: PathSelectionStrategy::LowestLatency,
297 enable_redundancy: false,
298 min_quality_threshold: 0.4,
299 quality_check_interval: Duration::from_secs(1),
300 path_idle_timeout: Duration::from_secs(30),
301 enable_auto_migration: true,
302 migration_quality_threshold: 0.15,
303 }
304 }
305
306 pub fn high_bandwidth() -> Self {
308 Self {
309 max_paths: 4,
310 strategy: PathSelectionStrategy::HighestBandwidth,
311 enable_redundancy: false,
312 min_quality_threshold: 0.3,
313 quality_check_interval: Duration::from_secs(5),
314 path_idle_timeout: Duration::from_secs(60),
315 enable_auto_migration: true,
316 migration_quality_threshold: 0.25,
317 }
318 }
319
320 pub fn high_reliability() -> Self {
322 Self {
323 max_paths: 4,
324 strategy: PathSelectionStrategy::Redundant,
325 enable_redundancy: true,
326 min_quality_threshold: 0.2,
327 quality_check_interval: Duration::from_secs(3),
328 path_idle_timeout: Duration::from_secs(90),
329 enable_auto_migration: false, migration_quality_threshold: 0.3,
331 }
332 }
333
334 pub fn mobile() -> Self {
336 Self {
337 max_paths: 2, strategy: PathSelectionStrategy::QualityBased,
339 enable_redundancy: false,
340 min_quality_threshold: 0.25,
341 quality_check_interval: Duration::from_secs(2),
342 path_idle_timeout: Duration::from_secs(45),
343 enable_auto_migration: true,
344 migration_quality_threshold: 0.2,
345 }
346 }
347}
348
349#[derive(Debug, Clone, Default)]
351pub struct MultipathStats {
352 pub paths_created: usize,
354
355 pub active_paths: usize,
357
358 pub total_bytes_sent: u64,
360
361 pub total_bytes_received: u64,
363
364 pub migrations_count: usize,
366
367 pub path_failures: usize,
369
370 pub avg_quality_score: f64,
372
373 pub best_path_quality: f64,
375
376 pub redundant_packets: usize,
378}
379
380pub struct MultipathQuicManager {
382 config: MultipathConfig,
384
385 paths: Arc<DashMap<PathId, NetworkPath>>,
387
388 next_path_id: Arc<RwLock<PathId>>,
390
391 round_robin_index: Arc<RwLock<usize>>,
393
394 stats: Arc<RwLock<MultipathStats>>,
396
397 quality_history: Arc<RwLock<VecDeque<(Instant, f64)>>>,
399}
400
401impl MultipathQuicManager {
402 pub fn new(config: MultipathConfig) -> Self {
404 info!(
405 "Creating multipath QUIC manager with strategy: {:?}",
406 config.strategy
407 );
408
409 Self {
410 config,
411 paths: Arc::new(DashMap::new()),
412 next_path_id: Arc::new(RwLock::new(0)),
413 round_robin_index: Arc::new(RwLock::new(0)),
414 stats: Arc::new(RwLock::new(MultipathStats::default())),
415 quality_history: Arc::new(RwLock::new(VecDeque::with_capacity(100))),
416 }
417 }
418
419 pub fn add_path(&self, local_addr: SocketAddr, remote_addr: SocketAddr) -> Result<PathId> {
421 if self.paths.len() >= self.config.max_paths {
422 warn!(
423 "Maximum paths reached: {}, cannot add new path",
424 self.config.max_paths
425 );
426 return Err(MultipathError::MaxPathsReached(self.config.max_paths));
427 }
428
429 let path_id = {
430 let mut id = self.next_path_id.write();
431 let current_id = *id;
432 *id += 1;
433 current_id
434 };
435
436 let path = NetworkPath {
437 id: path_id,
438 local_addr,
439 remote_addr,
440 state: PathState::Validating,
441 quality: PathQuality::default(),
442 created_at: Instant::now(),
443 last_used: Instant::now(),
444 bytes_sent: 0,
445 bytes_received: 0,
446 };
447
448 self.paths.insert(path_id, path);
449
450 let mut stats = self.stats.write();
451 stats.paths_created += 1;
452 stats.active_paths = self.paths.len();
453
454 info!(
455 "Added new path {}: {} -> {}",
456 path_id, local_addr, remote_addr
457 );
458
459 Ok(path_id)
460 }
461
462 pub fn remove_path(&self, path_id: PathId) -> Result<()> {
464 if self.paths.remove(&path_id).is_some() {
465 let mut stats = self.stats.write();
466 stats.active_paths = self.paths.len();
467
468 info!("Removed path {}", path_id);
469 Ok(())
470 } else {
471 Err(MultipathError::PathNotFound(path_id))
472 }
473 }
474
475 pub fn update_path_state(&self, path_id: PathId, state: PathState) -> Result<()> {
477 if let Some(mut path) = self.paths.get_mut(&path_id) {
478 let old_state = path.state;
479 path.state = state;
480
481 debug!(
482 "Path {} state changed: {:?} -> {:?}",
483 path_id, old_state, state
484 );
485
486 if state == PathState::Failed {
487 let mut stats = self.stats.write();
488 stats.path_failures += 1;
489 }
490
491 Ok(())
492 } else {
493 Err(MultipathError::PathNotFound(path_id))
494 }
495 }
496
497 pub fn update_path_quality(
499 &self,
500 path_id: PathId,
501 rtt_ms: f64,
502 bandwidth_bps: u64,
503 loss_rate: f64,
504 jitter_ms: f64,
505 ) -> Result<()> {
506 if let Some(mut path) = self.paths.get_mut(&path_id) {
507 path.quality
508 .update(rtt_ms, bandwidth_bps, loss_rate, jitter_ms);
509
510 debug!(
511 "Path {} quality updated: score={:.2}, rtt={:.1}ms, bandwidth={}Mbps, loss={:.2}%",
512 path_id,
513 path.quality.score,
514 path.quality.rtt_ms,
515 path.quality.bandwidth_bps / 125_000,
516 path.quality.loss_rate * 100.0
517 );
518
519 if path.quality.score < self.config.min_quality_threshold
521 && path.state == PathState::Active
522 {
523 warn!(
524 "Path {} quality too low: {:.2}, marking as degraded",
525 path_id, path.quality.score
526 );
527 path.state = PathState::Degraded;
528 }
529
530 let mut history = self.quality_history.write();
532 history.push_back((Instant::now(), path.quality.score));
533 if history.len() > 100 {
534 history.pop_front();
535 }
536
537 Ok(())
538 } else {
539 Err(MultipathError::PathNotFound(path_id))
540 }
541 }
542
543 pub fn select_path(&self) -> Result<PathId> {
545 let active_paths: Vec<_> = self
546 .paths
547 .iter()
548 .filter(|entry| entry.value().state == PathState::Active)
549 .collect();
550
551 if active_paths.is_empty() {
552 return Err(MultipathError::NoPathsAvailable);
553 }
554
555 match self.config.strategy {
556 PathSelectionStrategy::RoundRobin => {
557 let mut index = self.round_robin_index.write();
558 let selected = &active_paths[*index % active_paths.len()];
559 *index = (*index + 1) % active_paths.len();
560 Ok(selected.value().id)
561 }
562
563 PathSelectionStrategy::QualityBased | PathSelectionStrategy::WeightedRoundRobin => {
564 let best = active_paths
566 .iter()
567 .max_by(|a, b| {
568 a.value()
569 .quality
570 .score
571 .partial_cmp(&b.value().quality.score)
572 .unwrap_or(std::cmp::Ordering::Equal)
573 })
574 .ok_or(MultipathError::NoPathsAvailable)?;
575
576 Ok(best.value().id)
577 }
578
579 PathSelectionStrategy::LowestLatency => {
580 let best = active_paths
582 .iter()
583 .min_by(|a, b| {
584 a.value()
585 .quality
586 .rtt_ms
587 .partial_cmp(&b.value().quality.rtt_ms)
588 .unwrap_or(std::cmp::Ordering::Equal)
589 })
590 .ok_or(MultipathError::NoPathsAvailable)?;
591
592 Ok(best.value().id)
593 }
594
595 PathSelectionStrategy::HighestBandwidth => {
596 let best = active_paths
598 .iter()
599 .max_by(|a, b| {
600 a.value()
601 .quality
602 .bandwidth_bps
603 .cmp(&b.value().quality.bandwidth_bps)
604 })
605 .ok_or(MultipathError::NoPathsAvailable)?;
606
607 Ok(best.value().id)
608 }
609
610 PathSelectionStrategy::Redundant => {
611 Ok(active_paths[0].value().id)
614 }
615 }
616 }
617
618 pub fn select_all_paths(&self) -> Vec<PathId> {
620 self.paths
621 .iter()
622 .filter(|entry| entry.value().state == PathState::Active)
623 .map(|entry| entry.value().id)
624 .collect()
625 }
626
627 pub fn record_sent(&self, path_id: PathId, bytes: u64) {
629 if let Some(mut path) = self.paths.get_mut(&path_id) {
630 path.bytes_sent += bytes;
631 path.last_used = Instant::now();
632
633 let mut stats = self.stats.write();
634 stats.total_bytes_sent += bytes;
635 }
636 }
637
638 pub fn record_received(&self, path_id: PathId, bytes: u64) {
640 if let Some(mut path) = self.paths.get_mut(&path_id) {
641 path.bytes_received += bytes;
642 path.last_used = Instant::now();
643
644 let mut stats = self.stats.write();
645 stats.total_bytes_received += bytes;
646 }
647 }
648
649 pub fn get_path(&self, path_id: PathId) -> Option<NetworkPath> {
651 self.paths.get(&path_id).map(|entry| entry.value().clone())
652 }
653
654 pub fn get_active_paths(&self) -> Vec<NetworkPath> {
656 self.paths
657 .iter()
658 .filter(|entry| entry.value().state == PathState::Active)
659 .map(|entry| entry.value().clone())
660 .collect()
661 }
662
663 pub fn get_all_paths(&self) -> Vec<NetworkPath> {
665 self.paths
666 .iter()
667 .map(|entry| entry.value().clone())
668 .collect()
669 }
670
671 pub fn should_migrate(&self, current_path_id: PathId) -> Option<PathId> {
673 if !self.config.enable_auto_migration {
674 return None;
675 }
676
677 let current_path = self.paths.get(¤t_path_id)?;
678 let current_quality = current_path.quality.score;
679
680 let best_path = self
682 .paths
683 .iter()
684 .filter(|entry| {
685 entry.value().id != current_path_id && entry.value().state == PathState::Active
686 })
687 .max_by(|a, b| {
688 a.value()
689 .quality
690 .score
691 .partial_cmp(&b.value().quality.score)
692 .unwrap_or(std::cmp::Ordering::Equal)
693 })?;
694
695 let best_quality = best_path.quality.score;
696
697 if best_quality - current_quality >= self.config.migration_quality_threshold {
699 info!(
700 "Migration recommended: path {} (quality={:.2}) -> path {} (quality={:.2})",
701 current_path_id,
702 current_quality,
703 best_path.value().id,
704 best_quality
705 );
706
707 let mut stats = self.stats.write();
708 stats.migrations_count += 1;
709
710 return Some(best_path.value().id);
711 }
712
713 None
714 }
715
716 pub fn cleanup_stale_paths(&self) {
718 let now = Instant::now();
719 let timeout = self.config.path_idle_timeout;
720
721 let stale_paths: Vec<PathId> = self
722 .paths
723 .iter()
724 .filter(|entry| {
725 let path = entry.value();
726 now.duration_since(path.last_used) > timeout && path.state != PathState::Active
727 })
728 .map(|entry| entry.value().id)
729 .collect();
730
731 for path_id in stale_paths {
732 info!("Removing stale path {}", path_id);
733 let _ = self.remove_path(path_id);
734 }
735 }
736
737 pub fn stats(&self) -> MultipathStats {
739 let mut stats = self.stats.read().clone();
740
741 let active_paths: Vec<_> = self
743 .paths
744 .iter()
745 .filter(|entry| entry.value().state == PathState::Active)
746 .collect();
747
748 if !active_paths.is_empty() {
749 let total_quality: f64 = active_paths
750 .iter()
751 .map(|entry| entry.value().quality.score)
752 .sum();
753 stats.avg_quality_score = total_quality / active_paths.len() as f64;
754
755 stats.best_path_quality = active_paths
756 .iter()
757 .map(|entry| entry.value().quality.score)
758 .fold(0.0, f64::max);
759 }
760
761 stats.active_paths = active_paths.len();
762
763 stats
764 }
765
766 pub fn config(&self) -> &MultipathConfig {
768 &self.config
769 }
770}
771
772#[cfg(test)]
773mod tests {
774 use super::*;
775
776 #[test]
777 fn test_path_quality_calculation() {
778 let mut quality = PathQuality::default();
779
780 quality.update(10.0, 10_000_000, 0.01, 2.0);
782
783 assert!(quality.score > 0.7, "Good quality should have high score");
784 assert_eq!(quality.sample_count, 1);
785 }
786
787 #[test]
788 fn test_path_quality_ema() {
789 let mut quality = PathQuality::default();
790
791 quality.update(100.0, 1_000_000, 0.1, 10.0);
793 let first_rtt = quality.rtt_ms;
794
795 quality.update(50.0, 1_000_000, 0.1, 10.0);
797
798 assert!(
799 quality.rtt_ms < first_rtt,
800 "RTT should decrease with better sample"
801 );
802 assert!(
803 quality.rtt_ms > 50.0,
804 "RTT should be smoothed with EMA, not exact"
805 );
806 assert_eq!(quality.sample_count, 2);
807 }
808
809 #[test]
810 fn test_manager_creation() {
811 let config = MultipathConfig::default();
812 let manager = MultipathQuicManager::new(config);
813
814 let stats = manager.stats();
815 assert_eq!(stats.active_paths, 0);
816 assert_eq!(stats.paths_created, 0);
817 }
818
819 #[test]
820 fn test_add_path() {
821 let config = MultipathConfig::default();
822 let manager = MultipathQuicManager::new(config);
823
824 let local = "127.0.0.1:8080".parse().unwrap();
825 let remote = "192.168.1.1:9090".parse().unwrap();
826
827 let path_id = manager.add_path(local, remote).unwrap();
828
829 assert_eq!(path_id, 0);
830
831 let stats = manager.stats();
833 assert_eq!(stats.active_paths, 0);
834 assert_eq!(stats.paths_created, 1);
835
836 manager
838 .update_path_state(path_id, PathState::Active)
839 .unwrap();
840
841 let stats = manager.stats();
843 assert_eq!(stats.active_paths, 1);
844 }
845
846 #[test]
847 fn test_max_paths_limit() {
848 let config = MultipathConfig {
849 max_paths: 2,
850 ..Default::default()
851 };
852 let manager = MultipathQuicManager::new(config);
853
854 let local = "127.0.0.1:8080".parse().unwrap();
855 let remote = "192.168.1.1:9090".parse().unwrap();
856
857 manager.add_path(local, remote).unwrap();
859 manager.add_path(local, remote).unwrap();
860
861 let result = manager.add_path(local, remote);
863 assert!(result.is_err());
864 assert!(matches!(result, Err(MultipathError::MaxPathsReached(2))));
865 }
866
867 #[test]
868 fn test_remove_path() {
869 let config = MultipathConfig::default();
870 let manager = MultipathQuicManager::new(config);
871
872 let local = "127.0.0.1:8080".parse().unwrap();
873 let remote = "192.168.1.1:9090".parse().unwrap();
874
875 let path_id = manager.add_path(local, remote).unwrap();
876 manager.remove_path(path_id).unwrap();
877
878 let stats = manager.stats();
879 assert_eq!(stats.active_paths, 0);
880 }
881
882 #[test]
883 fn test_update_path_state() {
884 let config = MultipathConfig::default();
885 let manager = MultipathQuicManager::new(config);
886
887 let local = "127.0.0.1:8080".parse().unwrap();
888 let remote = "192.168.1.1:9090".parse().unwrap();
889
890 let path_id = manager.add_path(local, remote).unwrap();
891
892 manager
893 .update_path_state(path_id, PathState::Active)
894 .unwrap();
895
896 let path = manager.get_path(path_id).unwrap();
897 assert_eq!(path.state, PathState::Active);
898 }
899
900 #[test]
901 fn test_path_selection_round_robin() {
902 let config = MultipathConfig {
903 strategy: PathSelectionStrategy::RoundRobin,
904 ..Default::default()
905 };
906 let manager = MultipathQuicManager::new(config);
907
908 let local = "127.0.0.1:8080".parse().unwrap();
909 let remote = "192.168.1.1:9090".parse().unwrap();
910
911 let path1 = manager.add_path(local, remote).unwrap();
912 let path2 = manager.add_path(local, remote).unwrap();
913
914 manager.update_path_state(path1, PathState::Active).unwrap();
915 manager.update_path_state(path2, PathState::Active).unwrap();
916
917 let selected1 = manager.select_path().unwrap();
918 let selected2 = manager.select_path().unwrap();
919
920 assert_ne!(selected1, selected2, "Round robin should alternate paths");
921 }
922
923 #[test]
924 fn test_path_selection_quality_based() {
925 let config = MultipathConfig {
926 strategy: PathSelectionStrategy::QualityBased,
927 ..Default::default()
928 };
929 let manager = MultipathQuicManager::new(config);
930
931 let local = "127.0.0.1:8080".parse().unwrap();
932 let remote = "192.168.1.1:9090".parse().unwrap();
933
934 let path1 = manager.add_path(local, remote).unwrap();
935 let path2 = manager.add_path(local, remote).unwrap();
936
937 manager.update_path_state(path1, PathState::Active).unwrap();
938 manager.update_path_state(path2, PathState::Active).unwrap();
939
940 manager
942 .update_path_quality(path1, 100.0, 1_000_000, 0.1, 10.0)
943 .unwrap();
944 manager
945 .update_path_quality(path2, 10.0, 10_000_000, 0.01, 2.0)
946 .unwrap();
947
948 let selected = manager.select_path().unwrap();
949 assert_eq!(selected, path2, "Should select higher quality path");
950 }
951
952 #[test]
953 fn test_path_selection_lowest_latency() {
954 let config = MultipathConfig {
955 strategy: PathSelectionStrategy::LowestLatency,
956 ..Default::default()
957 };
958 let manager = MultipathQuicManager::new(config);
959
960 let local = "127.0.0.1:8080".parse().unwrap();
961 let remote = "192.168.1.1:9090".parse().unwrap();
962
963 let path1 = manager.add_path(local, remote).unwrap();
964 let path2 = manager.add_path(local, remote).unwrap();
965
966 manager.update_path_state(path1, PathState::Active).unwrap();
967 manager.update_path_state(path2, PathState::Active).unwrap();
968
969 manager
971 .update_path_quality(path1, 10.0, 1_000_000, 0.1, 5.0)
972 .unwrap();
973 manager
974 .update_path_quality(path2, 100.0, 10_000_000, 0.01, 2.0)
975 .unwrap();
976
977 let selected = manager.select_path().unwrap();
978 assert_eq!(selected, path1, "Should select lowest latency path");
979 }
980
981 #[test]
982 fn test_record_sent_received() {
983 let config = MultipathConfig::default();
984 let manager = MultipathQuicManager::new(config);
985
986 let local = "127.0.0.1:8080".parse().unwrap();
987 let remote = "192.168.1.1:9090".parse().unwrap();
988
989 let path_id = manager.add_path(local, remote).unwrap();
990
991 manager.record_sent(path_id, 1000);
992 manager.record_received(path_id, 500);
993
994 let path = manager.get_path(path_id).unwrap();
995 assert_eq!(path.bytes_sent, 1000);
996 assert_eq!(path.bytes_received, 500);
997
998 let stats = manager.stats();
999 assert_eq!(stats.total_bytes_sent, 1000);
1000 assert_eq!(stats.total_bytes_received, 500);
1001 }
1002
1003 #[test]
1004 fn test_auto_migration() {
1005 let config = MultipathConfig {
1006 enable_auto_migration: true,
1007 migration_quality_threshold: 0.2,
1008 ..Default::default()
1009 };
1010 let manager = MultipathQuicManager::new(config);
1011
1012 let local = "127.0.0.1:8080".parse().unwrap();
1013 let remote = "192.168.1.1:9090".parse().unwrap();
1014
1015 let path1 = manager.add_path(local, remote).unwrap();
1016 let path2 = manager.add_path(local, remote).unwrap();
1017
1018 manager.update_path_state(path1, PathState::Active).unwrap();
1019 manager.update_path_state(path2, PathState::Active).unwrap();
1020
1021 manager
1023 .update_path_quality(path1, 200.0, 500_000, 0.2, 20.0)
1024 .unwrap();
1025 manager
1026 .update_path_quality(path2, 10.0, 10_000_000, 0.01, 2.0)
1027 .unwrap();
1028
1029 let migration = manager.should_migrate(path1);
1030 assert_eq!(migration, Some(path2), "Should recommend migration");
1031
1032 let stats = manager.stats();
1033 assert_eq!(stats.migrations_count, 1);
1034 }
1035
1036 #[test]
1037 fn test_config_presets() {
1038 let low_latency = MultipathConfig::low_latency();
1039 assert_eq!(low_latency.strategy, PathSelectionStrategy::LowestLatency);
1040 assert_eq!(low_latency.max_paths, 2);
1041
1042 let high_bandwidth = MultipathConfig::high_bandwidth();
1043 assert_eq!(
1044 high_bandwidth.strategy,
1045 PathSelectionStrategy::HighestBandwidth
1046 );
1047
1048 let high_reliability = MultipathConfig::high_reliability();
1049 assert_eq!(high_reliability.strategy, PathSelectionStrategy::Redundant);
1050 assert!(high_reliability.enable_redundancy);
1051
1052 let mobile = MultipathConfig::mobile();
1053 assert_eq!(mobile.max_paths, 2);
1054 assert!(mobile.enable_auto_migration);
1055 }
1056
1057 #[test]
1058 fn test_select_all_paths() {
1059 let config = MultipathConfig::default();
1060 let manager = MultipathQuicManager::new(config);
1061
1062 let local = "127.0.0.1:8080".parse().unwrap();
1063 let remote = "192.168.1.1:9090".parse().unwrap();
1064
1065 let path1 = manager.add_path(local, remote).unwrap();
1066 let path2 = manager.add_path(local, remote).unwrap();
1067
1068 manager.update_path_state(path1, PathState::Active).unwrap();
1069 manager.update_path_state(path2, PathState::Active).unwrap();
1070
1071 let all_paths = manager.select_all_paths();
1072 assert_eq!(all_paths.len(), 2);
1073 assert!(all_paths.contains(&path1));
1074 assert!(all_paths.contains(&path2));
1075 }
1076
1077 #[test]
1078 fn test_quality_threshold_degradation() {
1079 let config = MultipathConfig {
1080 min_quality_threshold: 0.5,
1081 ..Default::default()
1082 };
1083 let manager = MultipathQuicManager::new(config);
1084
1085 let local = "127.0.0.1:8080".parse().unwrap();
1086 let remote = "192.168.1.1:9090".parse().unwrap();
1087
1088 let path_id = manager.add_path(local, remote).unwrap();
1089 manager
1090 .update_path_state(path_id, PathState::Active)
1091 .unwrap();
1092
1093 manager
1095 .update_path_quality(path_id, 300.0, 100_000, 0.5, 50.0)
1096 .unwrap();
1097
1098 let path = manager.get_path(path_id).unwrap();
1099 assert_eq!(
1100 path.state,
1101 PathState::Degraded,
1102 "Path should be marked degraded due to low quality"
1103 );
1104 }
1105}