braid_http/client/
retry.rs1use std::time::Duration;
4
5#[derive(Debug, Clone)]
7pub struct RetryConfig {
8 pub max_retries: Option<u32>,
10 pub initial_backoff: Duration,
12 pub max_backoff: Duration,
14 pub retry_on_status: Vec<u16>,
16 pub respect_retry_after: bool,
18}
19
20impl Default for RetryConfig {
21 fn default() -> Self {
22 Self {
23 max_retries: None,
24 initial_backoff: Duration::from_secs(1),
25 max_backoff: Duration::from_secs(3),
26 retry_on_status: vec![408, 425, 429, 502, 503, 504],
27 respect_retry_after: true,
28 }
29 }
30}
31
32impl RetryConfig {
33 #[must_use]
34 pub fn new() -> Self {
35 Self::default()
36 }
37
38 #[must_use]
39 pub fn no_retry() -> Self {
40 Self {
41 max_retries: Some(0),
42 ..Default::default()
43 }
44 }
45
46 #[must_use]
47 pub fn with_max_retries(mut self, max: u32) -> Self {
48 self.max_retries = Some(max);
49 self
50 }
51
52 #[must_use]
53 pub fn with_initial_backoff(mut self, duration: Duration) -> Self {
54 self.initial_backoff = duration;
55 self
56 }
57
58 #[must_use]
59 pub fn with_max_backoff(mut self, duration: Duration) -> Self {
60 self.max_backoff = duration;
61 self
62 }
63
64 #[must_use]
65 pub fn with_retry_on_status(mut self, status: u16) -> Self {
66 if !self.retry_on_status.contains(&status) {
67 self.retry_on_status.push(status);
68 }
69 self
70 }
71
72 #[must_use]
73 pub fn with_respect_retry_after(mut self, respect: bool) -> Self {
74 self.respect_retry_after = respect;
75 self
76 }
77}
78
79#[derive(Debug, Clone, PartialEq)]
80pub enum RetryDecision {
81 Retry(Duration),
82 DontRetry,
83}
84
85#[derive(Debug, Clone)]
86pub struct RetryState {
87 pub attempts: u32,
88 pub current_backoff: Duration,
89 config: RetryConfig,
90}
91
92impl RetryState {
93 pub fn new(config: RetryConfig) -> Self {
94 Self {
95 attempts: 0,
96 current_backoff: config.initial_backoff,
97 config,
98 }
99 }
100
101 pub fn should_retry_error(&mut self, is_abort: bool) -> RetryDecision {
102 if is_abort {
103 return RetryDecision::DontRetry;
104 }
105 self.decide_retry(None)
106 }
107
108 pub fn should_retry_status(
109 &mut self,
110 status: u16,
111 retry_after: Option<Duration>,
112 ) -> RetryDecision {
113 if !self.config.retry_on_status.contains(&status) {
114 return RetryDecision::DontRetry;
115 }
116 self.decide_retry(retry_after)
117 }
118
119 pub fn should_retry_status_with_text(
120 &mut self,
121 status: u16,
122 status_text: Option<&str>,
123 retry_after: Option<Duration>,
124 ) -> RetryDecision {
125 if let Some(text) = status_text {
126 if text.to_lowercase().contains("missing parents") {
127 return self.decide_retry(retry_after);
128 }
129 }
130 self.should_retry_status(status, retry_after)
131 }
132
133 fn decide_retry(&mut self, retry_after: Option<Duration>) -> RetryDecision {
134 self.attempts += 1;
135 if let Some(max) = self.config.max_retries {
136 if self.attempts > max {
137 return RetryDecision::DontRetry;
138 }
139 }
140
141 let wait = if self.config.respect_retry_after {
142 retry_after.unwrap_or(self.current_backoff)
143 } else {
144 self.current_backoff
145 };
146
147 self.current_backoff = std::cmp::min(
148 self.current_backoff + Duration::from_secs(1),
149 self.config.max_backoff,
150 );
151
152 RetryDecision::Retry(wait)
153 }
154
155 pub fn reset(&mut self) {
156 self.attempts = 0;
157 self.current_backoff = self.config.initial_backoff;
158 }
159}
160
161pub fn parse_retry_after(value: &str) -> Option<Duration> {
162 if let Ok(seconds) = value.parse::<u64>() {
163 return Some(Duration::from_secs(seconds));
164 }
165 None
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn test_default_config() {
174 let config = RetryConfig::default();
175 assert_eq!(config.max_retries, None);
176 assert!(config.retry_on_status.contains(&503));
177 }
178
179 #[test]
180 fn test_retry_state_basic() {
181 let config = RetryConfig::default().with_max_retries(1);
182 let mut state = RetryState::new(config);
183 assert!(matches!(
184 state.should_retry_error(false),
185 RetryDecision::Retry(_)
186 ));
187 assert_eq!(state.should_retry_error(false), RetryDecision::DontRetry);
188 }
189}