wickra_exchange_core/
retry.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct Backoff {
13 base_ms: u64,
14 max_ms: u64,
15 max_retries: u32,
16}
17
18impl Backoff {
19 #[must_use]
25 pub const fn new(base_ms: u64, max_ms: u64, max_retries: u32) -> Self {
26 Self {
27 base_ms,
28 max_ms,
29 max_retries,
30 }
31 }
32
33 #[must_use]
36 pub fn delay_ms(&self, attempt: u32) -> u64 {
37 let factor = 2u64.saturating_pow(attempt);
38 self.base_ms.saturating_mul(factor).min(self.max_ms)
39 }
40
41 #[must_use]
44 pub fn jittered_delay_ms(&self, attempt: u32, rand01: f64) -> u64 {
45 let clamped = rand01.clamp(0.0, 1.0);
46 #[allow(clippy::cast_precision_loss, clippy::cast_sign_loss)]
47 let jittered = self.delay_ms(attempt) as f64 * clamped;
48 jittered as u64
49 }
50
51 #[must_use]
53 pub fn should_retry(&self, attempt: u32) -> bool {
54 attempt < self.max_retries
55 }
56
57 #[must_use]
59 pub fn max_retries(&self) -> u32 {
60 self.max_retries
61 }
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67
68 #[test]
69 fn delay_doubles_and_caps() {
70 let b = Backoff::new(100, 2_000, 5);
71 assert_eq!(b.delay_ms(0), 100);
72 assert_eq!(b.delay_ms(1), 200);
73 assert_eq!(b.delay_ms(2), 400);
74 assert_eq!(b.delay_ms(3), 800);
75 assert_eq!(b.delay_ms(4), 1_600);
76 assert_eq!(b.delay_ms(5), 2_000);
78 assert_eq!(b.delay_ms(60), 2_000);
79 }
80
81 #[test]
82 fn jitter_scales_within_the_delay() {
83 let b = Backoff::new(100, 10_000, 5);
84 assert_eq!(b.jittered_delay_ms(2, 0.0), 0);
85 assert_eq!(b.jittered_delay_ms(2, 0.5), 200); assert_eq!(b.jittered_delay_ms(2, 1.0), 400);
87 assert_eq!(b.jittered_delay_ms(2, -1.0), 0);
89 assert_eq!(b.jittered_delay_ms(2, 2.0), 400);
90 }
91
92 #[test]
93 fn retry_decision_respects_the_limit() {
94 let b = Backoff::new(100, 1_000, 3);
95 assert_eq!(b.max_retries(), 3);
96 assert!(b.should_retry(0));
97 assert!(b.should_retry(2));
98 assert!(!b.should_retry(3));
99 assert!(!b.should_retry(10));
100 }
101}