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}