p2p_chat/sync/
backoff.rs

1//! This module provides an exponential backoff mechanism with jitter for retrying operations.
2//!
3//! It includes `BackoffEntry` to track individual peer backoff states and `BackoffManager`
4//! to manage multiple peer backoff entries.
5use libp2p::PeerId;
6use rand::Rng;
7use std::collections::HashMap;
8use std::time::{Duration, Instant};
9
10const MIN_BACKOFF: Duration = Duration::from_secs(1);
11const MAX_BACKOFF: Duration = Duration::from_secs(300); // 5 minutes max
12const BACKOFF_MULTIPLIER: f64 = 2.0;
13const JITTER_RANGE: f64 = 0.1; // 10% jitter
14
15/// Represents the backoff state for a single peer or operation.
16#[derive(Debug, Clone)]
17pub struct BackoffEntry {
18    /// The number of attempts made so far.
19    pub attempt_count: u32,
20    /// The `Instant` when the last attempt was made.
21    pub last_attempt: Instant,
22    /// The duration after which the next attempt can be made.
23    pub next_attempt_after: Duration,
24}
25
26impl BackoffEntry {
27    /// Creates a new `BackoffEntry` with initial values.
28    pub fn new() -> Self {
29        Self {
30            attempt_count: 0,
31            last_attempt: Instant::now(),
32            next_attempt_after: MIN_BACKOFF,
33        }
34    }
35
36    /// Checks if a retry attempt can be made now.
37    pub fn can_retry(&self) -> bool {
38        self.last_attempt.elapsed() >= self.next_attempt_after
39    }
40
41    /// Returns the time remaining until the next retry attempt is allowed.
42    pub fn time_until_retry(&self) -> Duration {
43        self.next_attempt_after
44            .saturating_sub(self.last_attempt.elapsed())
45    }
46
47    /// Records an attempt, updating the attempt count and calculating the next backoff duration.
48    pub fn record_attempt(&mut self) {
49        self.attempt_count += 1;
50        self.last_attempt = Instant::now();
51
52        // Calculate next backoff with exponential growth.
53        let base_backoff =
54            MIN_BACKOFF.as_secs_f64() * BACKOFF_MULTIPLIER.powi(self.attempt_count as i32 - 1);
55        let clamped_backoff = base_backoff.min(MAX_BACKOFF.as_secs_f64());
56
57        // Add jitter to prevent thundering herd.
58        let mut rng = rand::thread_rng();
59        let jitter_factor = 1.0 + rng.gen_range(-JITTER_RANGE..JITTER_RANGE);
60        let final_backoff = clamped_backoff * jitter_factor;
61
62        self.next_attempt_after = Duration::from_secs_f64(final_backoff);
63    }
64
65    /// Resets the backoff state to initial values, typically after a successful operation.
66    pub fn record_success(&mut self) {
67        // Reset backoff on success.
68        self.attempt_count = 0;
69        self.next_attempt_after = MIN_BACKOFF;
70    }
71
72    /// Checks if further retry attempts should be given up.
73    pub fn should_give_up(&self) -> bool {
74        // Give up after 10 attempts or 5 minutes of backoff.
75        self.attempt_count >= 10 || self.next_attempt_after >= MAX_BACKOFF
76    }
77}
78
79/// Manages backoff states for multiple peers or operations.
80#[derive(Debug)]
81pub struct BackoffManager {
82    entries: HashMap<PeerId, BackoffEntry>,
83}
84
85impl BackoffManager {
86    /// Creates a new `BackoffManager`.
87    pub fn new() -> Self {
88        Self {
89            entries: HashMap::new(),
90        }
91    }
92
93    /// Checks if an attempt can be made for a given peer.
94    pub fn can_attempt(&self, peer_id: &PeerId) -> bool {
95        match self.entries.get(peer_id) {
96            Some(entry) => entry.can_retry() && !entry.should_give_up(),
97            None => true, // First attempt is always allowed.
98        }
99    }
100
101    /// Returns the time remaining until a retry attempt is allowed for a given peer.
102    pub fn time_until_retry(&self, peer_id: &PeerId) -> Option<Duration> {
103        self.entries
104            .get(peer_id)
105            .map(|entry| entry.time_until_retry())
106    }
107
108    /// Records an attempt for a given peer, updating its backoff state.
109    pub fn record_attempt(&mut self, peer_id: PeerId) {
110        let entry = self
111            .entries
112            .entry(peer_id)
113            .or_insert_with(BackoffEntry::new);
114        entry.record_attempt();
115    }
116
117    /// Records a success for a given peer, resetting its backoff state.
118    pub fn record_success(&mut self, peer_id: &PeerId) {
119        if let Some(entry) = self.entries.get_mut(peer_id) {
120            entry.record_success();
121        }
122    }
123
124    /// Records a failure for a given peer, updating its backoff state.
125    ///
126    /// Currently, this behaves the same as `record_attempt`.
127    pub fn record_failure(&mut self, peer_id: PeerId) {
128        // Same as record_attempt for now, but could be extended with different logic.
129        self.record_attempt(peer_id);
130    }
131
132    /// Cleans up old backoff entries that have not been updated recently.
133    pub fn cleanup_old_entries(&mut self, max_age: Duration) {
134        let cutoff = Instant::now() - max_age;
135        self.entries.retain(|_, entry| entry.last_attempt >= cutoff);
136    }
137}
138
139impl Default for BackoffManager {
140    fn default() -> Self {
141        Self::new()
142    }
143}