1use crate::error::FaucetError;
10use std::future::Future;
11use std::sync::atomic::{AtomicU64, Ordering};
12use std::time::Duration;
13
14const MAX_BACKOFF: Duration = Duration::from_secs(60);
19
20pub async fn execute_with_retry<F, Fut, T>(
25 max_retries: u32,
26 base_backoff: Duration,
27 operation: F,
28) -> Result<T, FaucetError>
29where
30 F: FnMut() -> Fut,
31 Fut: Future<Output = Result<T, FaucetError>>,
32{
33 let policy = crate::resilience::RetryPolicy {
34 max_attempts: max_retries.saturating_add(1),
36 backoff: crate::resilience::BackoffKind::Exponential,
37 base: base_backoff,
38 max: MAX_BACKOFF,
39 jitter: true,
40 retry_on: crate::resilience::RetryClassSet::default(),
41 };
42 crate::resilience::execute_with_policy(&policy, None, operation).await
43}
44
45pub fn backoff_with_jitter(base: Duration, attempt: u32) -> Duration {
53 let exp = base
54 .saturating_mul(2u32.saturating_pow(attempt))
55 .min(MAX_BACKOFF);
56 let nanos = exp.as_nanos() as u64;
57 Duration::from_nanos((nanos as f64 * pseudo_random_factor()) as u64)
58}
59
60fn pseudo_random_factor() -> f64 {
62 static COUNTER: AtomicU64 = AtomicU64::new(0);
63 let nanos = std::time::SystemTime::now()
64 .duration_since(std::time::UNIX_EPOCH)
65 .unwrap_or_default()
66 .subsec_nanos();
67 let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
68 jitter_factor(decorrelate(nanos, counter))
69}
70
71fn decorrelate(nanos: u32, counter: u64) -> u32 {
78 let mut x = (nanos as u64) ^ counter.wrapping_mul(0x9E37_79B9_7F4A_7C15);
79 x ^= x >> 30;
80 x = x.wrapping_mul(0xBF58_476D_1CE4_E5B9);
81 x ^= x >> 27;
82 x = x.wrapping_mul(0x94D0_49BB_1331_11EB);
83 x ^= x >> 31;
84 (x % 1_000_000_000) as u32
85}
86
87fn jitter_factor(nanos: u32) -> f64 {
92 0.5 + (nanos as f64 / 1_000_000_000.0)
93}
94
95pub(crate) fn apply_jitter(delay: std::time::Duration) -> std::time::Duration {
99 let nanos = delay.as_nanos() as u64;
100 std::time::Duration::from_nanos((nanos as f64 * pseudo_random_factor()) as u64)
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use std::sync::Arc;
107 use std::sync::atomic::{AtomicU32, Ordering};
108
109 #[tokio::test]
110 async fn returns_immediately_on_success() {
111 let calls = Arc::new(AtomicU32::new(0));
112 let c = calls.clone();
113 let r = execute_with_retry(3, Duration::from_millis(1), move || {
114 c.fetch_add(1, Ordering::SeqCst);
115 async { Ok::<_, FaucetError>(7) }
116 })
117 .await;
118 assert_eq!(r.unwrap(), 7);
119 assert_eq!(calls.load(Ordering::SeqCst), 1);
120 }
121
122 #[tokio::test]
123 async fn retries_then_succeeds_on_transient_5xx() {
124 let calls = Arc::new(AtomicU32::new(0));
125 let c = calls.clone();
126 let r = execute_with_retry(3, Duration::from_millis(1), move || {
127 let n = c.fetch_add(1, Ordering::SeqCst);
128 async move {
129 if n < 2 {
130 Err::<i32, _>(FaucetError::HttpStatus {
131 status: 503,
132 url: "http://t".into(),
133 body: "x".into(),
134 })
135 } else {
136 Ok(42)
137 }
138 }
139 })
140 .await;
141 assert_eq!(r.unwrap(), 42);
142 assert_eq!(calls.load(Ordering::SeqCst), 3);
143 }
144
145 #[test]
146 fn jitter_factor_spans_documented_half_to_one_and_a_half_range() {
147 assert_eq!(jitter_factor(0), 0.5);
151 let mid = jitter_factor(500_000_000);
152 assert!((mid - 1.0).abs() < 1e-6, "midpoint factor was {mid}");
153 let hi = jitter_factor(999_999_999);
154 assert!(
155 (1.4..1.5).contains(&hi),
156 "factor at max sub-second nanos was {hi}, expected ~1.5"
157 );
158 }
159
160 #[test]
161 fn backoff_is_capped_for_large_attempt() {
162 let d = backoff_with_jitter(Duration::from_secs(1), 60);
166 assert!(d < Duration::from_secs(90), "backoff not capped: {d:?}");
167 assert!(
169 d >= Duration::from_secs(30),
170 "backoff unexpectedly tiny: {d:?}"
171 );
172 }
173
174 #[test]
175 fn decorrelate_diverges_for_same_nanos_concurrent_calls() {
176 let a = decorrelate(123_456_789, 0);
180 let b = decorrelate(123_456_789, 1);
181 let c = decorrelate(123_456_789, 2);
182 assert_ne!(a, b);
183 assert_ne!(b, c);
184 assert_ne!(a, c);
185 for v in [a, b, c] {
186 assert!(
187 v < 1_000_000_000,
188 "decorrelate out of jitter_factor range: {v}"
189 );
190 }
191 }
192
193 #[tokio::test]
194 async fn non_retriable_fails_immediately() {
195 let calls = Arc::new(AtomicU32::new(0));
196 let c = calls.clone();
197 let r = execute_with_retry(3, Duration::from_millis(1), move || {
198 c.fetch_add(1, Ordering::SeqCst);
199 async { Err::<i32, _>(FaucetError::Auth("nope".into())) }
200 })
201 .await;
202 assert!(r.is_err());
203 assert_eq!(calls.load(Ordering::SeqCst), 1);
204 }
205}