ipfrs_network/
fallback.rs

1//! Fallback strategies for network error handling
2//!
3//! Provides comprehensive fallback mechanisms including:
4//! - Alternative peer selection
5//! - Relay fallback for NAT traversal
6//! - Degraded mode operation
7//! - Automatic retry with exponential backoff
8
9use ipfrs_core::error::Error;
10use libp2p::PeerId;
11use std::collections::{HashMap, VecDeque};
12use std::time::{Duration, Instant};
13
14/// Fallback strategy configuration
15#[derive(Debug, Clone)]
16pub struct FallbackConfig {
17    /// Maximum number of alternative peers to try
18    pub max_alternatives: usize,
19    /// Enable relay fallback
20    pub enable_relay_fallback: bool,
21    /// Enable degraded mode
22    pub enable_degraded_mode: bool,
23    /// Initial retry delay
24    pub initial_retry_delay: Duration,
25    /// Maximum retry delay
26    pub max_retry_delay: Duration,
27    /// Retry backoff multiplier
28    pub backoff_multiplier: f64,
29}
30
31impl Default for FallbackConfig {
32    fn default() -> Self {
33        Self {
34            max_alternatives: 5,
35            enable_relay_fallback: true,
36            enable_degraded_mode: true,
37            initial_retry_delay: Duration::from_millis(100),
38            max_retry_delay: Duration::from_secs(30),
39            backoff_multiplier: 2.0,
40        }
41    }
42}
43
44/// Fallback strategy for peer connections
45#[derive(Debug, Clone)]
46pub enum FallbackStrategy {
47    /// Try alternative peers
48    AlternativePeers {
49        /// List of alternative peers to try
50        alternatives: Vec<PeerId>,
51    },
52    /// Use relay connection
53    RelayFallback {
54        /// Relay peer ID
55        relay_peer: PeerId,
56        /// Target peer ID
57        target_peer: PeerId,
58    },
59    /// Enter degraded mode
60    DegradedMode {
61        /// Reason for degraded mode
62        reason: String,
63    },
64    /// Retry with exponential backoff
65    RetryWithBackoff {
66        /// Number of retries attempted
67        attempt: usize,
68        /// Next retry delay
69        delay: Duration,
70    },
71}
72
73impl FallbackStrategy {
74    /// Get a description of the fallback strategy
75    pub fn description(&self) -> String {
76        match self {
77            Self::AlternativePeers { alternatives } => {
78                format!("Try {} alternative peer(s)", alternatives.len())
79            }
80            Self::RelayFallback {
81                relay_peer,
82                target_peer,
83            } => {
84                format!("Connect to {} via relay {}", target_peer, relay_peer)
85            }
86            Self::DegradedMode { reason } => {
87                format!("Enter degraded mode: {}", reason)
88            }
89            Self::RetryWithBackoff { attempt, delay } => {
90                format!("Retry attempt {} after {:?}", attempt, delay)
91            }
92        }
93    }
94}
95
96/// Fallback manager for coordinating fallback strategies
97pub struct FallbackManager {
98    config: FallbackConfig,
99    /// Alternative peers per content ID or operation
100    alternatives: parking_lot::RwLock<HashMap<String, VecDeque<PeerId>>>,
101    /// Available relay peers
102    relay_peers: parking_lot::RwLock<Vec<PeerId>>,
103    /// Retry state per peer
104    retry_state: parking_lot::RwLock<HashMap<PeerId, RetryState>>,
105    /// Degraded mode state
106    degraded_mode: parking_lot::RwLock<bool>,
107}
108
109/// Retry state for a peer
110#[derive(Debug, Clone)]
111struct RetryState {
112    attempts: usize,
113    last_attempt: Instant,
114    next_delay: Duration,
115}
116
117impl FallbackManager {
118    /// Create a new fallback manager
119    pub fn new(config: FallbackConfig) -> Self {
120        Self {
121            config,
122            alternatives: parking_lot::RwLock::new(HashMap::new()),
123            relay_peers: parking_lot::RwLock::new(Vec::new()),
124            retry_state: parking_lot::RwLock::new(HashMap::new()),
125            degraded_mode: parking_lot::RwLock::new(false),
126        }
127    }
128
129    /// Add alternative peers for a key (content ID or operation)
130    pub fn add_alternatives(&self, key: &str, peers: Vec<PeerId>) {
131        let mut alternatives = self.alternatives.write();
132        alternatives
133            .entry(key.to_string())
134            .or_default()
135            .extend(peers);
136    }
137
138    /// Get next alternative peer for a key
139    pub fn get_next_alternative(&self, key: &str) -> Option<PeerId> {
140        let mut alternatives = self.alternatives.write();
141        if let Some(peers) = alternatives.get_mut(key) {
142            peers.pop_front()
143        } else {
144            None
145        }
146    }
147
148    /// Add a relay peer
149    pub fn add_relay_peer(&self, peer: PeerId) {
150        let mut relay_peers = self.relay_peers.write();
151        if !relay_peers.contains(&peer) {
152            relay_peers.push(peer);
153        }
154    }
155
156    /// Get available relay peers
157    pub fn get_relay_peers(&self) -> Vec<PeerId> {
158        self.relay_peers.read().clone()
159    }
160
161    /// Get fallback strategy for a failed connection
162    pub fn get_fallback_strategy(
163        &self,
164        peer_id: PeerId,
165        key: Option<&str>,
166    ) -> Option<FallbackStrategy> {
167        // 1. Try alternative peers first
168        if let Some(key) = key {
169            if let Some(alternative) = self.get_next_alternative(key) {
170                let alternatives = vec![alternative];
171                return Some(FallbackStrategy::AlternativePeers { alternatives });
172            }
173        }
174
175        // 2. Try relay fallback
176        if self.config.enable_relay_fallback {
177            let relay_peers = self.get_relay_peers();
178            if let Some(relay_peer) = relay_peers.first() {
179                return Some(FallbackStrategy::RelayFallback {
180                    relay_peer: *relay_peer,
181                    target_peer: peer_id,
182                });
183            }
184        }
185
186        // 3. Try retry with backoff
187        let mut retry_state = self.retry_state.write();
188        let state = retry_state.entry(peer_id).or_insert_with(|| RetryState {
189            attempts: 0,
190            last_attempt: Instant::now(),
191            next_delay: self.config.initial_retry_delay,
192        });
193
194        // Check if we should retry
195        if state.last_attempt.elapsed() >= state.next_delay {
196            state.attempts += 1;
197            state.last_attempt = Instant::now();
198
199            // Calculate next delay with exponential backoff
200            let next_delay = Duration::from_secs_f64(
201                state.next_delay.as_secs_f64() * self.config.backoff_multiplier,
202            )
203            .min(self.config.max_retry_delay);
204            state.next_delay = next_delay;
205
206            return Some(FallbackStrategy::RetryWithBackoff {
207                attempt: state.attempts,
208                delay: state.next_delay,
209            });
210        }
211
212        // 4. Enter degraded mode if enabled
213        if self.config.enable_degraded_mode {
214            self.enter_degraded_mode("All fallback strategies exhausted");
215            return Some(FallbackStrategy::DegradedMode {
216                reason: "All fallback strategies exhausted".to_string(),
217            });
218        }
219
220        None
221    }
222
223    /// Reset retry state for a peer (after successful connection)
224    pub fn reset_retry_state(&self, peer_id: &PeerId) {
225        let mut retry_state = self.retry_state.write();
226        retry_state.remove(peer_id);
227    }
228
229    /// Enter degraded mode
230    pub fn enter_degraded_mode(&self, reason: &str) {
231        let mut degraded = self.degraded_mode.write();
232        *degraded = true;
233        tracing::warn!("Entering degraded mode: {}", reason);
234    }
235
236    /// Exit degraded mode
237    pub fn exit_degraded_mode(&self) {
238        let mut degraded = self.degraded_mode.write();
239        *degraded = false;
240        tracing::info!("Exiting degraded mode");
241    }
242
243    /// Check if in degraded mode
244    pub fn is_degraded(&self) -> bool {
245        *self.degraded_mode.read()
246    }
247
248    /// Get retry statistics
249    pub fn retry_stats(&self) -> RetryStats {
250        let retry_state = self.retry_state.read();
251        let total_peers = retry_state.len();
252        let total_attempts: usize = retry_state.values().map(|s| s.attempts).sum();
253
254        RetryStats {
255            total_peers_with_retries: total_peers,
256            total_retry_attempts: total_attempts,
257            peers_in_backoff: retry_state
258                .values()
259                .filter(|s| s.last_attempt.elapsed() < s.next_delay)
260                .count(),
261        }
262    }
263
264    /// Clear all fallback state
265    pub fn clear(&self) {
266        self.alternatives.write().clear();
267        self.retry_state.write().clear();
268        self.exit_degraded_mode();
269    }
270}
271
272impl Default for FallbackManager {
273    fn default() -> Self {
274        Self::new(FallbackConfig::default())
275    }
276}
277
278/// Retry statistics
279#[derive(Debug, Clone, serde::Serialize)]
280pub struct RetryStats {
281    /// Number of peers with retry state
282    pub total_peers_with_retries: usize,
283    /// Total retry attempts across all peers
284    pub total_retry_attempts: usize,
285    /// Number of peers currently in backoff
286    pub peers_in_backoff: usize,
287}
288
289/// Fallback result wrapper
290pub enum FallbackResult<T> {
291    /// Operation succeeded
292    Success(T),
293    /// Operation failed, fallback strategy available
294    FallbackAvailable(FallbackStrategy),
295    /// Operation failed, no fallback available
296    Failed(Error),
297}
298
299impl<T> FallbackResult<T> {
300    /// Check if the result is successful
301    pub fn is_success(&self) -> bool {
302        matches!(self, Self::Success(_))
303    }
304
305    /// Check if fallback is available
306    pub fn has_fallback(&self) -> bool {
307        matches!(self, Self::FallbackAvailable(_))
308    }
309
310    /// Unwrap the success value or panic
311    pub fn unwrap(self) -> T {
312        match self {
313            Self::Success(value) => value,
314            Self::FallbackAvailable(strategy) => {
315                panic!("Called unwrap on FallbackAvailable: {:?}", strategy)
316            }
317            Self::Failed(error) => panic!("Called unwrap on Failed: {}", error),
318        }
319    }
320
321    /// Get the success value or a default
322    pub fn unwrap_or(self, default: T) -> T {
323        match self {
324            Self::Success(value) => value,
325            _ => default,
326        }
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333
334    fn test_peer_id() -> PeerId {
335        PeerId::random()
336    }
337
338    #[test]
339    fn test_fallback_config_default() {
340        let config = FallbackConfig::default();
341        assert_eq!(config.max_alternatives, 5);
342        assert!(config.enable_relay_fallback);
343        assert!(config.enable_degraded_mode);
344    }
345
346    #[test]
347    fn test_fallback_manager_creation() {
348        let manager = FallbackManager::default();
349        assert!(!manager.is_degraded());
350        assert_eq!(manager.get_relay_peers().len(), 0);
351    }
352
353    #[test]
354    fn test_add_and_get_alternatives() {
355        let manager = FallbackManager::default();
356        let peer1 = test_peer_id();
357        let peer2 = test_peer_id();
358
359        manager.add_alternatives("test_key", vec![peer1, peer2]);
360
361        let alt1 = manager.get_next_alternative("test_key");
362        assert_eq!(alt1, Some(peer1));
363
364        let alt2 = manager.get_next_alternative("test_key");
365        assert_eq!(alt2, Some(peer2));
366
367        let alt3 = manager.get_next_alternative("test_key");
368        assert_eq!(alt3, None);
369    }
370
371    #[test]
372    fn test_relay_peer_management() {
373        let manager = FallbackManager::default();
374        let relay = test_peer_id();
375
376        manager.add_relay_peer(relay);
377        let relays = manager.get_relay_peers();
378
379        assert_eq!(relays.len(), 1);
380        assert_eq!(relays[0], relay);
381
382        // Adding same relay again should not duplicate
383        manager.add_relay_peer(relay);
384        assert_eq!(manager.get_relay_peers().len(), 1);
385    }
386
387    #[test]
388    fn test_fallback_strategy_alternative_peers() {
389        let manager = FallbackManager::default();
390        let peer = test_peer_id();
391        let alt_peer = test_peer_id();
392
393        manager.add_alternatives("test_key", vec![alt_peer]);
394
395        let strategy = manager.get_fallback_strategy(peer, Some("test_key"));
396        assert!(strategy.is_some());
397
398        match strategy.unwrap() {
399            FallbackStrategy::AlternativePeers { alternatives } => {
400                assert_eq!(alternatives.len(), 1);
401                assert_eq!(alternatives[0], alt_peer);
402            }
403            _ => panic!("Expected AlternativePeers strategy"),
404        }
405    }
406
407    #[test]
408    fn test_fallback_strategy_relay() {
409        let manager = FallbackManager::default();
410        let peer = test_peer_id();
411        let relay = test_peer_id();
412
413        manager.add_relay_peer(relay);
414
415        let strategy = manager.get_fallback_strategy(peer, None);
416        assert!(strategy.is_some());
417
418        match strategy.unwrap() {
419            FallbackStrategy::RelayFallback {
420                relay_peer,
421                target_peer,
422            } => {
423                assert_eq!(relay_peer, relay);
424                assert_eq!(target_peer, peer);
425            }
426            _ => panic!("Expected RelayFallback strategy"),
427        }
428    }
429
430    #[test]
431    fn test_retry_state_reset() {
432        let manager = FallbackManager::default();
433        let peer = test_peer_id();
434
435        // Get a retry strategy to create state
436        let _strategy = manager.get_fallback_strategy(peer, None);
437
438        // Reset the state
439        manager.reset_retry_state(&peer);
440
441        // Stats should show no retries
442        let stats = manager.retry_stats();
443        assert_eq!(stats.total_peers_with_retries, 0);
444    }
445
446    #[test]
447    fn test_degraded_mode() {
448        let manager = FallbackManager::default();
449
450        assert!(!manager.is_degraded());
451
452        manager.enter_degraded_mode("Test reason");
453        assert!(manager.is_degraded());
454
455        manager.exit_degraded_mode();
456        assert!(!manager.is_degraded());
457    }
458
459    #[test]
460    fn test_retry_stats() {
461        let manager = FallbackManager::default();
462        let peer = test_peer_id();
463
464        // Trigger a retry
465        let _strategy = manager.get_fallback_strategy(peer, None);
466
467        let stats = manager.retry_stats();
468        assert!(stats.total_peers_with_retries > 0);
469    }
470
471    #[test]
472    fn test_fallback_strategy_description() {
473        let strategy = FallbackStrategy::AlternativePeers {
474            alternatives: vec![test_peer_id()],
475        };
476        assert!(strategy.description().contains("alternative"));
477
478        let strategy = FallbackStrategy::DegradedMode {
479            reason: "test".to_string(),
480        };
481        assert!(strategy.description().contains("degraded"));
482    }
483
484    #[test]
485    fn test_fallback_result_success() {
486        let result: FallbackResult<i32> = FallbackResult::Success(42);
487        assert!(result.is_success());
488        assert!(!result.has_fallback());
489        assert_eq!(result.unwrap(), 42);
490    }
491
492    #[test]
493    fn test_fallback_result_with_fallback() {
494        let strategy = FallbackStrategy::DegradedMode {
495            reason: "test".to_string(),
496        };
497        let result: FallbackResult<i32> = FallbackResult::FallbackAvailable(strategy);
498        assert!(!result.is_success());
499        assert!(result.has_fallback());
500        assert_eq!(result.unwrap_or(0), 0);
501    }
502
503    #[test]
504    fn test_clear() {
505        let manager = FallbackManager::default();
506        let peer = test_peer_id();
507
508        manager.add_alternatives("test", vec![peer]);
509        manager.add_relay_peer(peer);
510        manager.enter_degraded_mode("test");
511
512        manager.clear();
513
514        assert!(!manager.is_degraded());
515        assert_eq!(manager.retry_stats().total_peers_with_retries, 0);
516        assert_eq!(manager.get_next_alternative("test"), None);
517    }
518}