Skip to main content

hooksmith_core/
retry.rs

1use std::time::Duration;
2
3/// Policy controlling how [`HttpClient::post_json_with_retry`] retries failed
4/// requests.
5///
6/// Successive retries use **exponential backoff**: the delay before attempt `n`
7/// (0-indexed) is `base_delay × 2ⁿ`. When `jitter` is enabled a random
8/// fraction of the current step is added on top, which reduces thundering-herd
9/// bursts when many senders retry simultaneously.
10///
11/// # Example
12///
13/// ```rust,ignore
14/// use std::time::Duration;
15/// use hooksmith_core::RetryPolicy;
16///
17/// let policy = RetryPolicy {
18///     max_attempts: 4,
19///     base_delay:   Duration::from_millis(250),
20///     jitter:       true,
21/// };
22///
23/// client.post_json_with_retry(url, &payload, &policy).await?;
24/// ```
25#[derive(Debug, Clone)]
26pub struct RetryPolicy {
27    /// Maximum total attempts (including the first try).  Clamped to at least 1.
28    pub max_attempts: u32,
29
30    /// Delay before the first retry.  Each subsequent retry doubles this value.
31    pub base_delay: Duration,
32
33    /// When `true`, add a random sub-delay ≤ the current step to spread out
34    /// concurrent retries.
35    pub jitter: bool,
36}
37
38impl Default for RetryPolicy {
39    /// Returns a sensible default: 3 attempts, 500 ms base delay, jitter on.
40    fn default() -> Self {
41        Self {
42            max_attempts: 3,
43            base_delay: Duration::from_millis(500),
44            jitter: true,
45        }
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52
53    #[test]
54    fn default_values_are_sensible() {
55        let p = RetryPolicy::default();
56        assert_eq!(p.max_attempts, 3);
57        assert_eq!(p.base_delay, Duration::from_millis(500));
58        assert!(p.jitter, "jitter should be on by default");
59    }
60
61    #[test]
62    fn clone_is_independent() {
63        let a = RetryPolicy {
64            max_attempts: 5,
65            base_delay: Duration::from_millis(100),
66            jitter: false,
67        };
68        let b = a.clone();
69        assert_eq!(b.max_attempts, 5);
70        assert_eq!(b.base_delay, Duration::from_millis(100));
71        assert!(!b.jitter);
72    }
73
74    #[test]
75    fn custom_values_are_preserved() {
76        let p = RetryPolicy {
77            max_attempts: 10,
78            base_delay: Duration::from_secs(2),
79            ..Default::default()
80        };
81        assert_eq!(p.max_attempts, 10);
82        assert_eq!(p.base_delay, Duration::from_secs(2));
83        assert!(p.jitter); // inherited from Default
84    }
85}