rustyclaw_core/retry/
policy.rs1use std::time::Duration;
2
3#[derive(Debug, Clone)]
5pub struct RetryPolicy {
6 pub max_attempts: u32,
8 pub base_delay: Duration,
10 pub max_delay: Duration,
12 pub jitter_ratio: f64,
14}
15
16impl RetryPolicy {
17 pub fn http_default() -> Self {
19 Self {
20 max_attempts: 4,
21 base_delay: Duration::from_millis(250),
22 max_delay: Duration::from_secs(8),
23 jitter_ratio: 0.20,
24 }
25 }
26
27 pub fn backoff_delay(&self, retry_index: u32) -> Duration {
29 let shift = retry_index.saturating_sub(1).min(31);
30 let multiplier = 1u32 << shift;
31 let base = self
32 .base_delay
33 .checked_mul(multiplier)
34 .unwrap_or(self.max_delay);
35 base.min(self.max_delay)
36 }
37
38 pub fn with_jitter(&self, delay: Duration) -> Duration {
40 if self.jitter_ratio <= 0.0 {
41 return delay;
42 }
43 let ratio = self.jitter_ratio.clamp(0.0, 1.0);
44 let millis = delay.as_millis() as f64;
45 let spread = millis * ratio;
46 let low = (millis - spread).max(0.0);
47 let high = millis + spread;
48 let sampled = if high <= low {
49 low
50 } else {
51 rand::random::<f64>() * (high - low) + low
52 };
53 Duration::from_millis(sampled.round() as u64)
54 }
55}
56
57#[cfg(test)]
58mod tests {
59 use super::*;
60
61 #[test]
62 fn backoff_grows_and_caps() {
63 let policy = RetryPolicy {
64 max_attempts: 5,
65 base_delay: Duration::from_millis(100),
66 max_delay: Duration::from_millis(500),
67 jitter_ratio: 0.0,
68 };
69 assert_eq!(policy.backoff_delay(1), Duration::from_millis(100));
70 assert_eq!(policy.backoff_delay(2), Duration::from_millis(200));
71 assert_eq!(policy.backoff_delay(3), Duration::from_millis(400));
72 assert_eq!(policy.backoff_delay(4), Duration::from_millis(500));
73 }
74}