1use crate::error_category::{ErrorCategory, classify_anyhow_error};
11
12#[derive(Debug, Clone)]
14pub struct RetryPolicy {
15 pub max_retries: u32,
17 pub base_delay_ms: u64,
19 pub max_delay_ms: u64,
21}
22
23impl Default for RetryPolicy {
24 fn default() -> Self {
25 Self {
26 max_retries: 3,
27 base_delay_ms: 1000,
28 max_delay_ms: 30_000,
29 }
30 }
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct RetryDecision {
36 pub retryable: bool,
38 pub category: ErrorCategory,
40}
41
42impl RetryPolicy {
43 pub fn classify_anyhow(&self, error: &anyhow::Error) -> RetryDecision {
48 let category = classify_anyhow_error(error);
49 RetryDecision {
50 retryable: category.is_retryable(),
51 category,
52 }
53 }
54
55 pub fn classify_status(&self, status: u16) -> RetryDecision {
57 let category = match status {
58 429 => ErrorCategory::RateLimit,
59 500 | 502 | 504 => ErrorCategory::Network,
60 503 => ErrorCategory::ServiceUnavailable,
61 401 | 403 => ErrorCategory::Authentication,
62 _ => ErrorCategory::ExecutionError,
63 };
64 RetryDecision {
65 retryable: category.is_retryable(),
66 category,
67 }
68 }
69
70 pub fn delay_ms_for_attempt(&self, attempt: u32) -> u64 {
74 let delay = self.base_delay_ms.saturating_mul(1u64 << attempt.min(16));
75 delay.min(self.max_delay_ms)
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn default_policy_has_reasonable_values() {
85 let policy = RetryPolicy::default();
86 assert_eq!(policy.max_retries, 3);
87 assert_eq!(policy.base_delay_ms, 1000);
88 assert_eq!(policy.max_delay_ms, 30_000);
89 }
90
91 #[test]
92 fn classify_status_rate_limit() {
93 let policy = RetryPolicy::default();
94 let decision = policy.classify_status(429);
95 assert!(decision.retryable);
96 assert_eq!(decision.category, ErrorCategory::RateLimit);
97 }
98
99 #[test]
100 fn classify_status_server_error() {
101 let policy = RetryPolicy::default();
102 let decision = policy.classify_status(503);
103 assert!(decision.retryable);
104 assert_eq!(decision.category, ErrorCategory::ServiceUnavailable);
105 }
106
107 #[test]
108 fn classify_status_auth_not_retryable() {
109 let policy = RetryPolicy::default();
110 let decision = policy.classify_status(401);
111 assert!(!decision.retryable);
112 assert_eq!(decision.category, ErrorCategory::Authentication);
113 }
114
115 #[test]
116 fn classify_anyhow_network_error() {
117 let policy = RetryPolicy::default();
118 let err = anyhow::anyhow!("connection refused");
119 let decision = policy.classify_anyhow(&err);
120 assert!(decision.retryable);
121 }
122
123 #[test]
124 fn delay_capped_at_max() {
125 let policy = RetryPolicy {
126 max_retries: 10,
127 base_delay_ms: 1000,
128 max_delay_ms: 5000,
129 };
130 assert_eq!(policy.delay_ms_for_attempt(0), 1000);
131 assert_eq!(policy.delay_ms_for_attempt(1), 2000);
132 assert_eq!(policy.delay_ms_for_attempt(2), 4000);
133 assert_eq!(policy.delay_ms_for_attempt(3), 5000); assert_eq!(policy.delay_ms_for_attempt(10), 5000); }
136}