1use ipfrs_core::error::Error;
10use libp2p::PeerId;
11use std::collections::{HashMap, VecDeque};
12use std::time::{Duration, Instant};
13
14#[derive(Debug, Clone)]
16pub struct FallbackConfig {
17 pub max_alternatives: usize,
19 pub enable_relay_fallback: bool,
21 pub enable_degraded_mode: bool,
23 pub initial_retry_delay: Duration,
25 pub max_retry_delay: Duration,
27 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#[derive(Debug, Clone)]
46pub enum FallbackStrategy {
47 AlternativePeers {
49 alternatives: Vec<PeerId>,
51 },
52 RelayFallback {
54 relay_peer: PeerId,
56 target_peer: PeerId,
58 },
59 DegradedMode {
61 reason: String,
63 },
64 RetryWithBackoff {
66 attempt: usize,
68 delay: Duration,
70 },
71}
72
73impl FallbackStrategy {
74 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
96pub struct FallbackManager {
98 config: FallbackConfig,
99 alternatives: parking_lot::RwLock<HashMap<String, VecDeque<PeerId>>>,
101 relay_peers: parking_lot::RwLock<Vec<PeerId>>,
103 retry_state: parking_lot::RwLock<HashMap<PeerId, RetryState>>,
105 degraded_mode: parking_lot::RwLock<bool>,
107}
108
109#[derive(Debug, Clone)]
111struct RetryState {
112 attempts: usize,
113 last_attempt: Instant,
114 next_delay: Duration,
115}
116
117impl FallbackManager {
118 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 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 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 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 pub fn get_relay_peers(&self) -> Vec<PeerId> {
158 self.relay_peers.read().clone()
159 }
160
161 pub fn get_fallback_strategy(
163 &self,
164 peer_id: PeerId,
165 key: Option<&str>,
166 ) -> Option<FallbackStrategy> {
167 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 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 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 if state.last_attempt.elapsed() >= state.next_delay {
196 state.attempts += 1;
197 state.last_attempt = Instant::now();
198
199 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 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 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 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 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 pub fn is_degraded(&self) -> bool {
245 *self.degraded_mode.read()
246 }
247
248 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 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#[derive(Debug, Clone, serde::Serialize)]
280pub struct RetryStats {
281 pub total_peers_with_retries: usize,
283 pub total_retry_attempts: usize,
285 pub peers_in_backoff: usize,
287}
288
289pub enum FallbackResult<T> {
291 Success(T),
293 FallbackAvailable(FallbackStrategy),
295 Failed(Error),
297}
298
299impl<T> FallbackResult<T> {
300 pub fn is_success(&self) -> bool {
302 matches!(self, Self::Success(_))
303 }
304
305 pub fn has_fallback(&self) -> bool {
307 matches!(self, Self::FallbackAvailable(_))
308 }
309
310 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 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 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 let _strategy = manager.get_fallback_strategy(peer, None);
437
438 manager.reset_retry_state(&peer);
440
441 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 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}