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.expect("test: fallback strategy should be Some when alternative peer exists")
399 {
400 FallbackStrategy::AlternativePeers { alternatives } => {
401 assert_eq!(alternatives.len(), 1);
402 assert_eq!(alternatives[0], alt_peer);
403 }
404 _ => panic!("Expected AlternativePeers strategy"),
405 }
406 }
407
408 #[test]
409 fn test_fallback_strategy_relay() {
410 let manager = FallbackManager::default();
411 let peer = test_peer_id();
412 let relay = test_peer_id();
413
414 manager.add_relay_peer(relay);
415
416 let strategy = manager.get_fallback_strategy(peer, None);
417 assert!(strategy.is_some());
418
419 match strategy.expect("test: fallback strategy should be Some when relay peer is available")
420 {
421 FallbackStrategy::RelayFallback {
422 relay_peer,
423 target_peer,
424 } => {
425 assert_eq!(relay_peer, relay);
426 assert_eq!(target_peer, peer);
427 }
428 _ => panic!("Expected RelayFallback strategy"),
429 }
430 }
431
432 #[test]
433 fn test_retry_state_reset() {
434 let manager = FallbackManager::default();
435 let peer = test_peer_id();
436
437 let _strategy = manager.get_fallback_strategy(peer, None);
439
440 manager.reset_retry_state(&peer);
442
443 let stats = manager.retry_stats();
445 assert_eq!(stats.total_peers_with_retries, 0);
446 }
447
448 #[test]
449 fn test_degraded_mode() {
450 let manager = FallbackManager::default();
451
452 assert!(!manager.is_degraded());
453
454 manager.enter_degraded_mode("Test reason");
455 assert!(manager.is_degraded());
456
457 manager.exit_degraded_mode();
458 assert!(!manager.is_degraded());
459 }
460
461 #[test]
462 fn test_retry_stats() {
463 let manager = FallbackManager::default();
464 let peer = test_peer_id();
465
466 let _strategy = manager.get_fallback_strategy(peer, None);
468
469 let stats = manager.retry_stats();
470 assert!(stats.total_peers_with_retries > 0);
471 }
472
473 #[test]
474 fn test_fallback_strategy_description() {
475 let strategy = FallbackStrategy::AlternativePeers {
476 alternatives: vec![test_peer_id()],
477 };
478 assert!(strategy.description().contains("alternative"));
479
480 let strategy = FallbackStrategy::DegradedMode {
481 reason: "test".to_string(),
482 };
483 assert!(strategy.description().contains("degraded"));
484 }
485
486 #[test]
487 fn test_fallback_result_success() {
488 let result: FallbackResult<i32> = FallbackResult::Success(42);
489 assert!(result.is_success());
490 assert!(!result.has_fallback());
491 assert_eq!(result.unwrap(), 42);
492 }
493
494 #[test]
495 fn test_fallback_result_with_fallback() {
496 let strategy = FallbackStrategy::DegradedMode {
497 reason: "test".to_string(),
498 };
499 let result: FallbackResult<i32> = FallbackResult::FallbackAvailable(strategy);
500 assert!(!result.is_success());
501 assert!(result.has_fallback());
502 assert_eq!(result.unwrap_or(0), 0);
503 }
504
505 #[test]
506 fn test_clear() {
507 let manager = FallbackManager::default();
508 let peer = test_peer_id();
509
510 manager.add_alternatives("test", vec![peer]);
511 manager.add_relay_peer(peer);
512 manager.enter_degraded_mode("test");
513
514 manager.clear();
515
516 assert!(!manager.is_degraded());
517 assert_eq!(manager.retry_stats().total_peers_with_retries, 0);
518 assert_eq!(manager.get_next_alternative("test"), None);
519 }
520}