typesafe_systemone/
retry.rs1use std::time::{Duration, SystemTime, UNIX_EPOCH};
2
3#[derive(Clone, Debug, PartialEq)]
8pub struct RetryPolicy {
9 pub max_retries: u32,
11 pub backoff_initial: Duration,
13 pub backoff_max: Duration,
15 pub backoff_jitter: f64,
17 pub respect_retry_after: bool,
19}
20
21impl Default for RetryPolicy {
22 fn default() -> Self {
23 Self {
24 max_retries: 2,
25 backoff_initial: Duration::from_millis(500),
26 backoff_max: Duration::from_secs(5),
27 backoff_jitter: 0.25,
28 respect_retry_after: true,
29 }
30 }
31}
32
33impl RetryPolicy {
34 pub fn none() -> Self {
36 Self {
37 max_retries: 0,
38 ..Self::default()
39 }
40 }
41
42 pub(crate) fn is_retryable_status(status: u16) -> bool {
43 status == 408 || status == 429 || (500..=599).contains(&status)
44 }
45
46 pub(crate) fn delay(&self, retry: u32, retry_after: Option<Duration>) -> Duration {
48 if self.respect_retry_after {
49 if let Some(d) = retry_after {
50 return d.min(self.backoff_max.max(d));
51 }
52 }
53 let exp = self
54 .backoff_initial
55 .saturating_mul(2u32.saturating_pow(retry.saturating_sub(1)));
56 let base = exp.min(self.backoff_max);
57 let jitter = self.backoff_jitter.clamp(0.0, 1.0);
58 if jitter == 0.0 {
59 return base;
60 }
61 base.mul_f64(1.0 - jitter * unit_random())
62 }
63}
64
65fn unit_random() -> f64 {
67 let nanos = SystemTime::now()
68 .duration_since(UNIX_EPOCH)
69 .map(|d| d.subsec_nanos() as u64)
70 .unwrap_or(0);
71 let mut x = nanos ^ 0x9E37_79B9_7F4A_7C15;
72 x ^= x << 13;
73 x ^= x >> 7;
74 x ^= x << 17;
75 (x % 10_000) as f64 / 10_000.0
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[test]
83 fn retryable_statuses() {
84 for s in [408, 429, 500, 502, 529, 599] {
85 assert!(RetryPolicy::is_retryable_status(s), "{s}");
86 }
87 for s in [200, 400, 401, 403, 404, 422] {
88 assert!(!RetryPolicy::is_retryable_status(s), "{s}");
89 }
90 }
91
92 #[test]
93 fn backoff_doubles_and_caps() {
94 let p = RetryPolicy {
95 backoff_jitter: 0.0,
96 ..RetryPolicy::default()
97 };
98 assert_eq!(p.delay(1, None), Duration::from_millis(500));
99 assert_eq!(p.delay(2, None), Duration::from_millis(1000));
100 assert_eq!(p.delay(3, None), Duration::from_millis(2000));
101 assert_eq!(p.delay(10, None), Duration::from_secs(5));
102 }
103
104 #[test]
105 fn jitter_only_shortens() {
106 let p = RetryPolicy::default();
107 for _ in 0..50 {
108 let d = p.delay(1, None);
109 assert!(
110 d <= Duration::from_millis(500) && d >= Duration::from_millis(375),
111 "{d:?}"
112 );
113 }
114 }
115
116 #[test]
117 fn retry_after_wins_when_respected() {
118 let p = RetryPolicy::default();
119 assert_eq!(p.delay(1, Some(Duration::from_secs(3))), Duration::from_secs(3));
120 let p = RetryPolicy {
121 respect_retry_after: false,
122 backoff_jitter: 0.0,
123 ..RetryPolicy::default()
124 };
125 assert_eq!(p.delay(1, Some(Duration::from_secs(3))), Duration::from_millis(500));
126 }
127}