1use std::sync::Arc;
31use std::time::Duration;
32
33use crate::error::GraphError;
34
35#[derive(Clone)]
37pub enum RetryOn {
38 Any,
44 Timeout,
46 Custom(Arc<dyn Fn(&GraphError) -> bool + Send + Sync>),
48}
49
50impl std::fmt::Debug for RetryOn {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 match self {
53 Self::Any => f.write_str("Any"),
54 Self::Timeout => f.write_str("Timeout"),
55 Self::Custom(_) => f.write_str("Custom(..)"),
56 }
57 }
58}
59
60impl RetryOn {
61 pub fn should_retry(&self, error: &GraphError) -> bool {
63 if matches!(error, GraphError::Interrupted(_)) {
66 return false;
67 }
68 match self {
69 Self::Any => true,
70 Self::Timeout => matches!(error, GraphError::NodeTimedOut { .. }),
71 Self::Custom(predicate) => predicate(error),
72 }
73 }
74}
75
76#[derive(Debug, Clone)]
78pub struct RetryPolicy {
79 pub max_attempts: u32,
81 pub initial_delay: Duration,
83 pub max_delay: Duration,
85 pub backoff_factor: f64,
87 pub jitter: f64,
90 pub retry_on: RetryOn,
92}
93
94impl Default for RetryPolicy {
103 fn default() -> Self {
104 Self {
105 max_attempts: 10,
106 initial_delay: Duration::from_secs(1),
107 max_delay: Duration::from_secs(60),
108 backoff_factor: 2.0,
109 jitter: 1.0,
110 retry_on: RetryOn::Any,
111 }
112 }
113}
114
115impl RetryPolicy {
116 pub fn new(max_attempts: u32) -> Self {
118 Self { max_attempts: max_attempts.max(1), ..Default::default() }
119 }
120
121 pub fn with_initial_delay(mut self, delay: Duration) -> Self {
123 self.initial_delay = delay;
124 self
125 }
126
127 pub fn with_max_delay(mut self, delay: Duration) -> Self {
129 self.max_delay = delay;
130 self
131 }
132
133 pub fn with_backoff_factor(mut self, factor: f64) -> Self {
135 self.backoff_factor = factor;
136 self
137 }
138
139 pub fn with_jitter(mut self, jitter: f64) -> Self {
141 self.jitter = jitter.clamp(0.0, 1.0);
142 self
143 }
144
145 pub fn with_retry_on(mut self, retry_on: RetryOn) -> Self {
147 self.retry_on = retry_on;
148 self
149 }
150
151 pub fn allows_another_attempt(&self, attempts_so_far: u32) -> bool {
153 attempts_so_far < self.max_attempts
154 }
155
156 pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
168 if attempt == 0 {
169 return Duration::ZERO;
170 }
171 let factor = if self.backoff_factor <= 0.0 { 1.0 } else { self.backoff_factor };
172 let mut millis = self.initial_delay.as_millis() as f64;
173 for _ in 1..attempt {
174 millis *= factor;
175 }
176 let capped = millis.min(self.max_delay.as_millis() as f64);
177
178 if self.jitter <= 0.0 {
179 return Duration::from_millis(capped as u64);
180 }
181 let spread = capped * self.jitter;
183 let offset = pseudo_random_unit() * 2.0 * spread - spread;
184 Duration::from_millis((capped + offset).max(0.0) as u64)
185 }
186}
187
188fn pseudo_random_unit() -> f64 {
193 use std::time::{SystemTime, UNIX_EPOCH};
194 let nanos = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.subsec_nanos()).unwrap_or(0);
195 f64::from(nanos % 1_000_000) / 1_000_000.0
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
205 fn a_default_policy_retries_ten_times() {
206 let policy = RetryPolicy::default();
207 assert_eq!(policy.max_attempts, 10);
208 assert!(policy.allows_another_attempt(1), "a first failure is retried");
209 assert!(policy.allows_another_attempt(9), "and so is a ninth");
210 assert!(!policy.allows_another_attempt(10), "the tenth is the last");
211 }
212
213 #[test]
216 fn the_default_gives_up_after_about_four_minutes() {
217 let policy = RetryPolicy::default().with_jitter(0.0);
218 let total: Duration = (1..policy.max_attempts).map(|n| policy.delay_for_attempt(n)).sum();
219 assert_eq!(total, Duration::from_secs(243));
220 }
221
222 #[test]
223 fn delays_grow_by_the_backoff_factor() {
224 let policy = RetryPolicy::new(5)
225 .with_initial_delay(Duration::from_millis(100))
226 .with_backoff_factor(3.0)
227 .with_jitter(0.0);
228 assert_eq!(policy.delay_for_attempt(1), Duration::from_millis(100));
229 assert_eq!(policy.delay_for_attempt(2), Duration::from_millis(300));
230 assert_eq!(policy.delay_for_attempt(3), Duration::from_millis(900));
231 }
232
233 #[test]
234 fn a_delay_is_capped() {
235 let policy = RetryPolicy::new(10)
236 .with_initial_delay(Duration::from_millis(100))
237 .with_max_delay(Duration::from_millis(250))
238 .with_backoff_factor(10.0)
239 .with_jitter(0.0);
240 assert_eq!(policy.delay_for_attempt(3), Duration::from_millis(250));
241 }
242
243 #[test]
244 fn a_non_positive_backoff_factor_gives_a_constant_delay() {
245 let policy = RetryPolicy::new(4)
246 .with_initial_delay(Duration::from_millis(50))
247 .with_backoff_factor(0.0)
248 .with_jitter(0.0);
249 assert_eq!(policy.delay_for_attempt(1), Duration::from_millis(50));
250 assert_eq!(policy.delay_for_attempt(3), Duration::from_millis(50));
251 }
252
253 #[test]
254 fn an_interrupt_is_never_retried() {
255 let interrupt = GraphError::Interrupted(Box::new(crate::error::InterruptedExecution::new(
256 "t".to_string(),
257 "c".to_string(),
258 crate::interrupt::Interrupt::Before("n".to_string()),
259 Default::default(),
260 0,
261 )));
262 assert!(!RetryOn::Any.should_retry(&interrupt));
263 let always = RetryOn::Custom(Arc::new(|_| true));
264 assert!(!always.should_retry(&interrupt), "even a permissive predicate must not retry it");
265 }
266
267 #[test]
268 fn timeout_only_retries_a_timeout() {
269 let timeout =
270 GraphError::NodeTimedOut { node: "slow".to_string(), elapsed: Duration::from_secs(1) };
271 let other =
272 GraphError::NodeExecutionFailed { node: "n".to_string(), message: "boom".to_string() };
273 assert!(RetryOn::Timeout.should_retry(&timeout));
274 assert!(!RetryOn::Timeout.should_retry(&other));
275 assert!(RetryOn::Any.should_retry(&other));
276 }
277}