1use std::time::Duration;
42
43use crate::error::{AgentError, OperationError};
44
45const DEFAULT_INITIAL_BACKOFF: Duration = Duration::from_millis(200);
47
48const DEFAULT_MAX_BACKOFF: Duration = Duration::from_secs(30);
50
51const DEFAULT_MULTIPLIER: f64 = 2.0;
53
54#[derive(Debug, Clone)]
81pub struct RetryPolicy {
82 pub(crate) max_retries: u32,
83 pub(crate) initial_backoff: Duration,
84 pub(crate) max_backoff: Duration,
85 pub(crate) multiplier: f64,
86}
87
88impl RetryPolicy {
89 pub fn new(max_retries: u32) -> Self {
107 assert!(max_retries > 0, "max_retries must be greater than 0");
108 Self {
109 max_retries,
110 initial_backoff: DEFAULT_INITIAL_BACKOFF,
111 max_backoff: DEFAULT_MAX_BACKOFF,
112 multiplier: DEFAULT_MULTIPLIER,
113 }
114 }
115
116 pub fn backoff(mut self, duration: Duration) -> Self {
124 assert!(!duration.is_zero(), "initial backoff must not be zero");
125 self.initial_backoff = duration;
126 self
127 }
128
129 pub fn max_backoff(mut self, duration: Duration) -> Self {
138 assert!(!duration.is_zero(), "max backoff must not be zero");
139 self.max_backoff = duration;
140 self
141 }
142
143 pub fn multiplier(mut self, multiplier: f64) -> Self {
154 assert!(
155 multiplier >= 1.0 && multiplier.is_finite(),
156 "multiplier must be >= 1.0 and finite, got {multiplier}"
157 );
158 self.multiplier = multiplier;
159 self
160 }
161
162 pub fn max_retries(&self) -> u32 {
164 self.max_retries
165 }
166
167 pub(crate) fn delay_for_attempt(&self, attempt: u32) -> Duration {
171 let delay = self.initial_backoff.as_secs_f64() * self.multiplier.powi(attempt as i32);
172 let capped = delay.min(self.max_backoff.as_secs_f64());
173 Duration::from_secs_f64(capped)
174 }
175}
176
177pub fn is_retryable(error: &OperationError) -> bool {
193 match error {
194 OperationError::Http { status, .. } => match status {
195 None => true,
196 Some(code) => *code >= 500 || *code == 429,
197 },
198 OperationError::Agent(agent_err) => match agent_err {
199 AgentError::ProcessFailed { .. }
200 | AgentError::Timeout { .. }
201 | AgentError::SchemaValidation { .. }
202 | AgentError::RateLimited { .. } => true,
203 AgentError::HttpProvider { status_code, .. } => {
204 *status_code == 0 || *status_code >= 500
205 }
206 AgentError::PromptTooLarge { .. } => false,
207 },
208 OperationError::Timeout { .. } => true,
209 OperationError::Shell { .. } | OperationError::Deserialize { .. } => false,
210 }
211}
212
213pub(crate) fn is_retryable_status(status: u16) -> bool {
216 status >= 500 || status == 429
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222 use std::time::Duration;
223
224 #[test]
227 fn new_creates_policy_with_defaults() {
228 let policy = RetryPolicy::new(3);
229 assert_eq!(policy.max_retries, 3);
230 assert_eq!(policy.initial_backoff, DEFAULT_INITIAL_BACKOFF);
231 assert_eq!(policy.max_backoff, DEFAULT_MAX_BACKOFF);
232 assert!((policy.multiplier - DEFAULT_MULTIPLIER).abs() < f64::EPSILON);
233 }
234
235 #[test]
236 #[should_panic(expected = "max_retries must be greater than 0")]
237 fn new_zero_retries_panics() {
238 let _ = RetryPolicy::new(0);
239 }
240
241 #[test]
242 fn backoff_sets_initial_backoff() {
243 let policy = RetryPolicy::new(1).backoff(Duration::from_secs(1));
244 assert_eq!(policy.initial_backoff, Duration::from_secs(1));
245 }
246
247 #[test]
248 #[should_panic(expected = "initial backoff must not be zero")]
249 fn backoff_zero_panics() {
250 let _ = RetryPolicy::new(1).backoff(Duration::ZERO);
251 }
252
253 #[test]
254 fn max_backoff_sets_cap() {
255 let policy = RetryPolicy::new(1).max_backoff(Duration::from_secs(60));
256 assert_eq!(policy.max_backoff, Duration::from_secs(60));
257 }
258
259 #[test]
260 #[should_panic(expected = "max backoff must not be zero")]
261 fn max_backoff_zero_panics() {
262 let _ = RetryPolicy::new(1).max_backoff(Duration::ZERO);
263 }
264
265 #[test]
266 fn multiplier_sets_value() {
267 let policy = RetryPolicy::new(1).multiplier(3.0);
268 assert!((policy.multiplier - 3.0).abs() < f64::EPSILON);
269 }
270
271 #[test]
272 #[should_panic(expected = "multiplier must be >= 1.0")]
273 fn multiplier_below_one_panics() {
274 let _ = RetryPolicy::new(1).multiplier(0.5);
275 }
276
277 #[test]
278 #[should_panic(expected = "multiplier must be >= 1.0 and finite")]
279 fn multiplier_nan_panics() {
280 let _ = RetryPolicy::new(1).multiplier(f64::NAN);
281 }
282
283 #[test]
284 #[should_panic(expected = "multiplier must be >= 1.0 and finite")]
285 fn multiplier_infinity_panics() {
286 let _ = RetryPolicy::new(1).multiplier(f64::INFINITY);
287 }
288
289 #[test]
290 fn max_retries_accessor() {
291 assert_eq!(RetryPolicy::new(5).max_retries(), 5);
292 }
293
294 #[test]
297 fn delay_for_attempt_zero_is_initial_backoff() {
298 let policy = RetryPolicy::new(3).backoff(Duration::from_millis(100));
299 let delay = policy.delay_for_attempt(0);
300 assert_eq!(delay, Duration::from_millis(100));
301 }
302
303 #[test]
304 fn delay_for_attempt_grows_exponentially() {
305 let policy = RetryPolicy::new(5)
306 .backoff(Duration::from_millis(100))
307 .multiplier(2.0);
308
309 assert_eq!(policy.delay_for_attempt(0), Duration::from_millis(100));
310 assert_eq!(policy.delay_for_attempt(1), Duration::from_millis(200));
311 assert_eq!(policy.delay_for_attempt(2), Duration::from_millis(400));
312 assert_eq!(policy.delay_for_attempt(3), Duration::from_millis(800));
313 }
314
315 #[test]
316 fn delay_for_attempt_capped_at_max_backoff() {
317 let policy = RetryPolicy::new(10)
318 .backoff(Duration::from_secs(1))
319 .max_backoff(Duration::from_secs(5))
320 .multiplier(10.0);
321
322 assert_eq!(policy.delay_for_attempt(0), Duration::from_secs(1));
324 assert_eq!(policy.delay_for_attempt(1), Duration::from_secs(5));
325 assert_eq!(policy.delay_for_attempt(2), Duration::from_secs(5));
326 }
327
328 #[test]
329 fn delay_for_attempt_with_multiplier_one_is_constant() {
330 let policy = RetryPolicy::new(3)
331 .backoff(Duration::from_millis(500))
332 .multiplier(1.0);
333
334 assert_eq!(policy.delay_for_attempt(0), Duration::from_millis(500));
335 assert_eq!(policy.delay_for_attempt(1), Duration::from_millis(500));
336 assert_eq!(policy.delay_for_attempt(2), Duration::from_millis(500));
337 }
338
339 #[test]
342 fn http_transport_error_is_retryable() {
343 let err = OperationError::Http {
344 status: None,
345 message: "connection refused".to_string(),
346 };
347 assert!(is_retryable(&err));
348 }
349
350 #[test]
351 fn http_500_is_retryable() {
352 let err = OperationError::Http {
353 status: Some(500),
354 message: "internal server error".to_string(),
355 };
356 assert!(is_retryable(&err));
357 }
358
359 #[test]
360 fn http_502_is_retryable() {
361 let err = OperationError::Http {
362 status: Some(502),
363 message: "bad gateway".to_string(),
364 };
365 assert!(is_retryable(&err));
366 }
367
368 #[test]
369 fn http_503_is_retryable() {
370 let err = OperationError::Http {
371 status: Some(503),
372 message: "service unavailable".to_string(),
373 };
374 assert!(is_retryable(&err));
375 }
376
377 #[test]
378 fn http_429_is_retryable() {
379 let err = OperationError::Http {
380 status: Some(429),
381 message: "too many requests".to_string(),
382 };
383 assert!(is_retryable(&err));
384 }
385
386 #[test]
387 fn http_400_is_not_retryable() {
388 let err = OperationError::Http {
389 status: Some(400),
390 message: "bad request".to_string(),
391 };
392 assert!(!is_retryable(&err));
393 }
394
395 #[test]
396 fn http_404_is_not_retryable() {
397 let err = OperationError::Http {
398 status: Some(404),
399 message: "not found".to_string(),
400 };
401 assert!(!is_retryable(&err));
402 }
403
404 #[test]
405 fn agent_process_failed_is_retryable() {
406 let err = OperationError::Agent(AgentError::ProcessFailed {
407 exit_code: 1,
408 stderr: "crash".to_string(),
409 });
410 assert!(is_retryable(&err));
411 }
412
413 #[test]
414 fn agent_timeout_is_retryable() {
415 let err = OperationError::Agent(AgentError::Timeout {
416 limit: Duration::from_secs(60),
417 });
418 assert!(is_retryable(&err));
419 }
420
421 #[test]
422 fn agent_prompt_too_large_is_not_retryable() {
423 let err = OperationError::Agent(AgentError::PromptTooLarge {
424 chars: 1_000_000,
425 estimated_tokens: 250_000,
426 model_limit: 200_000,
427 });
428 assert!(!is_retryable(&err));
429 }
430
431 #[test]
432 fn agent_schema_validation_is_retryable() {
433 let err = OperationError::Agent(AgentError::SchemaValidation {
434 expected: "object".to_string(),
435 got: "string".to_string(),
436 debug_messages: Vec::new(),
437 partial_usage: Box::default(),
438 raw_response: None,
439 });
440 assert!(is_retryable(&err));
441 }
442
443 #[test]
444 fn operation_timeout_is_retryable() {
445 let err = OperationError::Timeout {
446 step: "fetch".to_string(),
447 limit: Duration::from_secs(30),
448 };
449 assert!(is_retryable(&err));
450 }
451
452 #[test]
453 fn shell_error_is_not_retryable() {
454 let err = OperationError::Shell {
455 exit_code: 1,
456 stderr: "fail".to_string(),
457 };
458 assert!(!is_retryable(&err));
459 }
460
461 #[test]
462 fn deserialize_error_is_not_retryable() {
463 let err = OperationError::Deserialize {
464 target_type: "MyStruct".to_string(),
465 reason: "missing field".to_string(),
466 };
467 assert!(!is_retryable(&err));
468 }
469
470 #[test]
473 fn retryable_status_codes() {
474 assert!(is_retryable_status(500));
475 assert!(is_retryable_status(502));
476 assert!(is_retryable_status(503));
477 assert!(is_retryable_status(504));
478 assert!(is_retryable_status(429));
479 }
480
481 #[test]
482 fn non_retryable_status_codes() {
483 assert!(!is_retryable_status(200));
484 assert!(!is_retryable_status(201));
485 assert!(!is_retryable_status(301));
486 assert!(!is_retryable_status(400));
487 assert!(!is_retryable_status(401));
488 assert!(!is_retryable_status(403));
489 assert!(!is_retryable_status(404));
490 assert!(!is_retryable_status(422));
491 assert!(!is_retryable_status(428));
492 }
493
494 #[test]
497 fn builder_chain_all_methods() {
498 let policy = RetryPolicy::new(5)
499 .backoff(Duration::from_millis(100))
500 .max_backoff(Duration::from_secs(10))
501 .multiplier(3.0);
502
503 assert_eq!(policy.max_retries, 5);
504 assert_eq!(policy.initial_backoff, Duration::from_millis(100));
505 assert_eq!(policy.max_backoff, Duration::from_secs(10));
506 assert!((policy.multiplier - 3.0).abs() < f64::EPSILON);
507 }
508
509 #[test]
510 fn clone_produces_independent_copy() {
511 let policy = RetryPolicy::new(3).backoff(Duration::from_millis(100));
512 let cloned = policy.clone();
513 assert_eq!(policy.max_retries, cloned.max_retries);
514 assert_eq!(policy.initial_backoff, cloned.initial_backoff);
515 }
516
517 #[test]
518 fn debug_does_not_panic() {
519 let policy = RetryPolicy::new(1);
520 let debug = format!("{:?}", policy);
521 assert!(debug.contains("RetryPolicy"));
522 }
523}