1use 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); const BACKOFF_MULTIPLIER: f64 = 2.0;
13const JITTER_RANGE: f64 = 0.1; #[derive(Debug, Clone)]
17pub struct BackoffEntry {
18 pub attempt_count: u32,
20 pub last_attempt: Instant,
22 pub next_attempt_after: Duration,
24}
25
26impl BackoffEntry {
27 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 pub fn can_retry(&self) -> bool {
38 self.last_attempt.elapsed() >= self.next_attempt_after
39 }
40
41 pub fn time_until_retry(&self) -> Duration {
43 self.next_attempt_after
44 .saturating_sub(self.last_attempt.elapsed())
45 }
46
47 pub fn record_attempt(&mut self) {
49 self.attempt_count += 1;
50 self.last_attempt = Instant::now();
51
52 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 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 pub fn record_success(&mut self) {
67 self.attempt_count = 0;
69 self.next_attempt_after = MIN_BACKOFF;
70 }
71
72 pub fn should_give_up(&self) -> bool {
74 self.attempt_count >= 10 || self.next_attempt_after >= MAX_BACKOFF
76 }
77}
78
79#[derive(Debug)]
81pub struct BackoffManager {
82 entries: HashMap<PeerId, BackoffEntry>,
83}
84
85impl BackoffManager {
86 pub fn new() -> Self {
88 Self {
89 entries: HashMap::new(),
90 }
91 }
92
93 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, }
99 }
100
101 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 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 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 pub fn record_failure(&mut self, peer_id: PeerId) {
128 self.record_attempt(peer_id);
130 }
131
132 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}