1use std::str::FromStr;
12use std::time::Duration;
13
14use http::StatusCode;
15use qubit_config::{
16 ConfigReader,
17 ConfigResult,
18};
19use qubit_retry::{
20 RetryDelay,
21 RetryJitter,
22 RetryOptions,
23};
24
25use super::http_retry_method_policy::HttpRetryMethodPolicy;
26use super::HttpConfigError;
27use crate::{
28 HttpErrorKind,
29 HttpRequest,
30};
31
32const DEFAULT_RETRY_MAX_ATTEMPTS: u32 = 3;
33const DEFAULT_RETRY_INITIAL_DELAY: Duration = Duration::from_millis(200);
34const DEFAULT_RETRY_MAX_DELAY: Duration = Duration::from_secs(5);
35const DEFAULT_RETRY_MULTIPLIER: f64 = 2.0;
36const DEFAULT_RETRY_JITTER_FACTOR: f64 = 0.1;
37
38#[derive(Debug, Clone, PartialEq)]
40pub struct HttpRetryOptions {
41 pub enabled: bool,
43 pub max_attempts: u32,
45 pub max_duration: Option<Duration>,
47 pub delay_strategy: RetryDelay,
49 pub jitter_factor: f64,
51 pub method_policy: HttpRetryMethodPolicy,
53 pub retry_status_codes: Option<Vec<StatusCode>>,
57 pub retry_error_kinds: Option<Vec<HttpErrorKind>>,
61}
62
63fn is_retryable_status(status: StatusCode, retry_status_codes: Option<&[StatusCode]>) -> bool {
67 if let Some(status_codes) = retry_status_codes {
68 status_codes.contains(&status)
69 } else {
70 default_retryable_status(status)
71 }
72}
73
74fn is_retryable_error_kind(
78 kind: HttpErrorKind,
79 retry_error_kinds: Option<&[HttpErrorKind]>,
80) -> bool {
81 if let Some(error_kinds) = retry_error_kinds {
82 error_kinds.contains(&kind)
83 } else {
84 default_retryable_error_kind(kind)
85 }
86}
87
88impl HttpRetryOptions {
89 pub fn new() -> Self {
94 Self::default()
95 }
96
97 pub fn from_config<R>(config: &R) -> Result<Self, HttpConfigError>
105 where
106 R: ConfigReader + ?Sized,
107 {
108 let raw = Self::read_config(config).map_err(HttpConfigError::from)?;
109 let mut opts = Self::default();
110
111 if let Some(enabled) = raw.enabled {
112 opts.enabled = enabled;
113 }
114 if let Some(max_attempts) = raw.max_attempts {
115 opts.max_attempts = max_attempts;
116 }
117 opts.max_duration = raw.max_duration;
118 if let Some(jitter_factor) = raw.jitter_factor {
119 opts.jitter_factor = jitter_factor;
120 }
121 if let Some(method_policy) = raw.method_policy.as_ref() {
122 opts.method_policy = HttpRetryMethodPolicy::from_config_value(method_policy)?;
123 }
124 if let Some(status_codes) = raw.status_codes.as_ref() {
125 opts.retry_status_codes = Some(parse_retry_status_codes(status_codes)?);
126 }
127 if let Some(error_kinds) = raw.error_kinds.as_ref() {
128 opts.retry_error_kinds = Some(parse_retry_error_kinds(error_kinds)?);
129 }
130
131 if let Some(delay_strategy) = raw.delay_strategy.as_ref() {
132 opts.delay_strategy = parse_retry_delay_strategy(delay_strategy, &raw)?;
133 }
134
135 opts.validate()?;
136 Ok(opts)
137 }
138
139 fn read_config<R>(config: &R) -> ConfigResult<HttpRetryConfigInput>
148 where
149 R: ConfigReader + ?Sized,
150 {
151 Ok(HttpRetryConfigInput {
152 enabled: config.get_optional("enabled")?,
153 max_attempts: config.get_optional("max_attempts")?,
154 max_duration: config.get_optional("max_duration")?,
155 delay_strategy: config.get_optional_string("delay_strategy")?,
156 fixed_delay: config.get_optional("fixed_delay")?,
157 random_min_delay: config.get_optional("random_min_delay")?,
158 random_max_delay: config.get_optional("random_max_delay")?,
159 backoff_initial_delay: config.get_optional("backoff_initial_delay")?,
160 backoff_max_delay: config.get_optional("backoff_max_delay")?,
161 backoff_multiplier: config.get_optional("backoff_multiplier")?,
162 jitter_factor: config.get_optional("jitter_factor")?,
163 method_policy: config.get_optional_string("method_policy")?,
164 status_codes: config.get_optional_string_list("status_codes")?,
165 error_kinds: config.get_optional_string_list("error_kinds")?,
166 })
167 }
168
169 pub fn validate(&self) -> Result<(), HttpConfigError> {
174 if self.max_attempts == 0 {
175 return Err(HttpConfigError::invalid_value(
176 "max_attempts",
177 "Retry max_attempts must be greater than 0",
178 ));
179 }
180 if !(0.0..=1.0).contains(&self.jitter_factor) {
181 return Err(HttpConfigError::invalid_value(
182 "jitter_factor",
183 "Retry jitter_factor must be between 0.0 and 1.0",
184 ));
185 }
186 self.delay_strategy
187 .validate()
188 .map_err(|message| HttpConfigError::invalid_value("delay_strategy", message))?;
189 Ok(())
190 }
191
192 pub fn allows_method(&self, method: &http::Method) -> bool {
200 self.enabled && self.method_policy.allows_method(method)
201 }
202
203 pub fn should_retry(&self, request: &HttpRequest) -> bool {
212 self.max_attempts > 1 && self.allows_method(request.method())
213 }
214
215 pub fn resolve(&self, request: &HttpRequest) -> Self {
223 let mut options = self.clone();
224 options.enabled = request.retry_override().resolve_enabled(options.enabled);
225 options.method_policy = request
226 .retry_override()
227 .resolve_method_policy(options.method_policy);
228 options
229 }
230
231 pub fn is_retryable_status(&self, status: StatusCode) -> bool {
239 is_retryable_status(status, self.retry_status_codes.as_deref())
240 }
241
242 pub fn is_retryable_error_kind(&self, kind: HttpErrorKind) -> bool {
251 is_retryable_error_kind(kind, self.retry_error_kinds.as_deref())
252 }
253
254 pub(crate) fn to_executor_options(&self) -> RetryOptions {
265 RetryOptions::new(
266 self.max_attempts,
267 None,
268 self.max_duration,
269 self.delay_strategy.clone(),
270 RetryJitter::factor(self.jitter_factor),
271 )
272 .expect("validated HTTP retry options should convert to retry executor options")
273 }
274}
275
276impl Default for HttpRetryOptions {
277 fn default() -> Self {
278 Self {
279 enabled: false,
280 max_attempts: DEFAULT_RETRY_MAX_ATTEMPTS,
281 max_duration: None,
282 delay_strategy: RetryDelay::Exponential {
283 initial: DEFAULT_RETRY_INITIAL_DELAY,
284 max: DEFAULT_RETRY_MAX_DELAY,
285 multiplier: DEFAULT_RETRY_MULTIPLIER,
286 },
287 jitter_factor: DEFAULT_RETRY_JITTER_FACTOR,
288 method_policy: HttpRetryMethodPolicy::default(),
289 retry_status_codes: None,
290 retry_error_kinds: None,
291 }
292 }
293}
294
295struct HttpRetryConfigInput {
296 enabled: Option<bool>,
297 max_attempts: Option<u32>,
298 max_duration: Option<Duration>,
299 delay_strategy: Option<String>,
300 fixed_delay: Option<Duration>,
301 random_min_delay: Option<Duration>,
302 random_max_delay: Option<Duration>,
303 backoff_initial_delay: Option<Duration>,
304 backoff_max_delay: Option<Duration>,
305 backoff_multiplier: Option<f64>,
306 jitter_factor: Option<f64>,
307 method_policy: Option<String>,
308 status_codes: Option<Vec<String>>,
309 error_kinds: Option<Vec<String>>,
310}
311
312fn parse_retry_delay_strategy(
313 value: &str,
314 raw: &HttpRetryConfigInput,
315) -> Result<RetryDelay, HttpConfigError> {
316 let normalized = value.trim().to_ascii_lowercase().replace('-', "_");
317 match normalized.as_str() {
318 "none" => Ok(RetryDelay::None),
319 "fixed" => Ok(RetryDelay::Fixed(
320 raw.fixed_delay.unwrap_or(DEFAULT_RETRY_INITIAL_DELAY),
321 )),
322 "random" => Ok(RetryDelay::Random {
323 min: raw.random_min_delay.unwrap_or(DEFAULT_RETRY_INITIAL_DELAY),
324 max: raw.random_max_delay.unwrap_or(DEFAULT_RETRY_MAX_DELAY),
325 }),
326 "exponential_backoff" | "exponential" => Ok(RetryDelay::Exponential {
327 initial: raw
328 .backoff_initial_delay
329 .unwrap_or(DEFAULT_RETRY_INITIAL_DELAY),
330 max: raw.backoff_max_delay.unwrap_or(DEFAULT_RETRY_MAX_DELAY),
331 multiplier: raw.backoff_multiplier.unwrap_or(DEFAULT_RETRY_MULTIPLIER),
332 }),
333 _ => Err(HttpConfigError::invalid_value(
334 "delay_strategy",
335 format!("Unsupported retry delay strategy: {value}"),
336 )),
337 }
338}
339
340fn parse_retry_status_codes(values: &[String]) -> Result<Vec<StatusCode>, HttpConfigError> {
352 let mut result = Vec::<StatusCode>::new();
353 for value in values {
354 let trimmed = value.trim();
355 if trimmed.is_empty() {
356 return Err(HttpConfigError::invalid_value(
357 "status_codes",
358 "Retry status_codes cannot contain blank values",
359 ));
360 }
361 let raw_code = trimmed.parse::<u16>().map_err(|error| {
362 HttpConfigError::invalid_value(
363 "status_codes",
364 format!("Invalid retry status code '{trimmed}': {error}"),
365 )
366 })?;
367 if !(100..=599).contains(&raw_code) {
368 return Err(HttpConfigError::invalid_value(
369 "status_codes",
370 format!("Retry status code must be in range 100..=599, got {raw_code}"),
371 ));
372 }
373 let status = StatusCode::from_u16(raw_code)
374 .expect("retry status code range is pre-validated to 100..=599");
375 if !result.contains(&status) {
376 result.push(status);
377 }
378 }
379 result.sort_by_key(|status| status.as_u16());
380 Ok(result)
381}
382
383fn parse_retry_error_kinds(values: &[String]) -> Result<Vec<HttpErrorKind>, HttpConfigError> {
394 let mut result = Vec::<HttpErrorKind>::new();
395 for value in values {
396 let trimmed = value.trim();
397 if trimmed.is_empty() {
398 return Err(HttpConfigError::invalid_value(
399 "error_kinds",
400 "Retry error_kinds cannot contain blank values",
401 ));
402 }
403 let normalized = trimmed.replace('-', "_");
404 let kind = HttpErrorKind::from_str(&normalized).map_err(|_| {
405 HttpConfigError::invalid_value(
406 "error_kinds",
407 format!("Unsupported retry error kind: {trimmed}"),
408 )
409 })?;
410 if !result.contains(&kind) {
411 result.push(kind);
412 }
413 }
414 Ok(result)
415}
416
417fn default_retryable_status(status: StatusCode) -> bool {
426 status == StatusCode::TOO_MANY_REQUESTS || status.is_server_error()
427}
428
429fn default_retryable_error_kind(kind: HttpErrorKind) -> bool {
438 matches!(
439 kind,
440 HttpErrorKind::ConnectTimeout
441 | HttpErrorKind::ReadTimeout
442 | HttpErrorKind::WriteTimeout
443 | HttpErrorKind::RequestTimeout
444 | HttpErrorKind::Transport
445 )
446}