Skip to main content

rustyclaw_core/retry/
policy.rs

1use std::time::Duration;
2
3/// Strategy for retrying transient failures with exponential backoff.
4#[derive(Debug, Clone)]
5pub struct RetryPolicy {
6    /// Maximum attempts including the first request.
7    pub max_attempts: u32,
8    /// Base delay for the first retry.
9    pub base_delay: Duration,
10    /// Maximum delay cap for later retries.
11    pub max_delay: Duration,
12    /// Jitter ratio (0.0..=1.0) applied to delay.
13    pub jitter_ratio: f64,
14}
15
16impl RetryPolicy {
17    /// Default policy for outbound HTTP provider requests.
18    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    /// Exponential backoff delay for the given retry index (1-based).
28    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    /// Apply jitter to a delay using a symmetric random range.
39    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}