1use std::str::FromStr;
11use std::time::Duration;
12
13use http::StatusCode;
14use qubit_config::{ConfigReader, ConfigResult};
15use qubit_retry::{RetryDelay, RetryJitter, RetryOptions};
16
17use super::http_retry_method_policy::HttpRetryMethodPolicy;
18use super::HttpConfigError;
19use crate::{HttpError, HttpErrorKind, HttpRequest, HttpResult};
20
21const DEFAULT_RETRY_MAX_ATTEMPTS: u32 = 3;
22const DEFAULT_RETRY_INITIAL_DELAY: Duration = Duration::from_millis(200);
23const DEFAULT_RETRY_MAX_DELAY: Duration = Duration::from_secs(5);
24const DEFAULT_RETRY_MULTIPLIER: f64 = 2.0;
25const DEFAULT_RETRY_JITTER_FACTOR: f64 = 0.1;
26
27#[derive(Debug, Clone, PartialEq)]
29pub struct HttpRetryOptions {
30 pub enabled: bool,
32 pub max_attempts: u32,
34 pub max_duration: Option<Duration>,
36 pub delay_strategy: RetryDelay,
38 pub jitter_factor: f64,
40 pub method_policy: HttpRetryMethodPolicy,
42 pub retry_status_codes: Option<Vec<StatusCode>>,
46 pub retry_error_kinds: Option<Vec<HttpErrorKind>>,
50}
51
52fn is_retryable_status(status: StatusCode, retry_status_codes: Option<&[StatusCode]>) -> bool {
56 if let Some(status_codes) = retry_status_codes {
57 status_codes.contains(&status)
58 } else {
59 default_retryable_status(status)
60 }
61}
62
63fn is_retryable_error_kind(
67 kind: HttpErrorKind,
68 retry_error_kinds: Option<&[HttpErrorKind]>,
69) -> bool {
70 if let Some(error_kinds) = retry_error_kinds {
71 error_kinds.contains(&kind)
72 } else {
73 default_retryable_error_kind(kind)
74 }
75}
76
77impl HttpRetryOptions {
78 pub fn new() -> Self {
83 Self::default()
84 }
85
86 pub fn from_config<R>(config: &R) -> Result<Self, HttpConfigError>
94 where
95 R: ConfigReader + ?Sized,
96 {
97 let raw = Self::read_config(config).map_err(HttpConfigError::from)?;
98 let mut opts = Self::default();
99
100 if let Some(enabled) = raw.enabled {
101 opts.enabled = enabled;
102 }
103 if let Some(max_attempts) = raw.max_attempts {
104 opts.max_attempts = max_attempts;
105 }
106 opts.max_duration = raw.max_duration;
107 if let Some(jitter_factor) = raw.jitter_factor {
108 opts.jitter_factor = jitter_factor;
109 }
110 if let Some(method_policy) = raw.method_policy.as_ref() {
111 opts.method_policy = HttpRetryMethodPolicy::from_config_value(method_policy)?;
112 }
113 if let Some(status_codes) = raw.status_codes.as_ref() {
114 opts.retry_status_codes = Some(parse_retry_status_codes(status_codes)?);
115 }
116 if let Some(error_kinds) = raw.error_kinds.as_ref() {
117 opts.retry_error_kinds = Some(parse_retry_error_kinds(error_kinds)?);
118 }
119
120 if let Some(delay_strategy) = raw.delay_strategy.as_ref() {
121 opts.delay_strategy = parse_retry_delay_strategy(delay_strategy, &raw)?;
122 }
123
124 opts.validate()?;
125 Ok(opts)
126 }
127
128 fn read_config<R>(config: &R) -> ConfigResult<HttpRetryConfigInput>
137 where
138 R: ConfigReader + ?Sized,
139 {
140 Ok(HttpRetryConfigInput {
141 enabled: config.get_optional("enabled")?,
142 max_attempts: config.get_optional("max_attempts")?,
143 max_duration: config.get_optional("max_duration")?,
144 delay_strategy: config.get_optional_string("delay_strategy")?,
145 fixed_delay: config.get_optional("fixed_delay")?,
146 random_min_delay: config.get_optional("random_min_delay")?,
147 random_max_delay: config.get_optional("random_max_delay")?,
148 backoff_initial_delay: config.get_optional("backoff_initial_delay")?,
149 backoff_max_delay: config.get_optional("backoff_max_delay")?,
150 backoff_multiplier: config.get_optional("backoff_multiplier")?,
151 jitter_factor: config.get_optional("jitter_factor")?,
152 method_policy: config.get_optional_string("method_policy")?,
153 status_codes: config.get_optional_string_list("status_codes")?,
154 error_kinds: config.get_optional_string_list("error_kinds")?,
155 })
156 }
157
158 pub fn validate(&self) -> Result<(), HttpConfigError> {
163 if self.max_attempts == 0 {
164 return Err(HttpConfigError::invalid_value(
165 "max_attempts",
166 "Retry max_attempts must be greater than 0",
167 ));
168 }
169 if !(0.0..=1.0).contains(&self.jitter_factor) {
170 return Err(HttpConfigError::invalid_value(
171 "jitter_factor",
172 "Retry jitter_factor must be between 0.0 and 1.0",
173 ));
174 }
175 self.delay_strategy
176 .validate()
177 .map_err(|message| HttpConfigError::invalid_value("delay_strategy", message))?;
178 Ok(())
179 }
180
181 pub fn allows_method(&self, method: &http::Method) -> bool {
189 self.enabled && self.method_policy.allows_method(method)
190 }
191
192 pub fn should_retry(&self, request: &HttpRequest) -> bool {
201 self.max_attempts > 1 && self.allows_method(request.method())
202 }
203
204 pub fn resolve(&self, request: &HttpRequest) -> Self {
212 let mut options = self.clone();
213 options.enabled = request.retry_override().resolve_enabled(options.enabled);
214 options.method_policy = request
215 .retry_override()
216 .resolve_method_policy(options.method_policy);
217 options
218 }
219
220 pub fn is_retryable_status(&self, status: StatusCode) -> bool {
228 is_retryable_status(status, self.retry_status_codes.as_deref())
229 }
230
231 pub fn is_retryable_error_kind(&self, kind: HttpErrorKind) -> bool {
240 is_retryable_error_kind(kind, self.retry_error_kinds.as_deref())
241 }
242
243 pub(crate) fn to_executor_options(&self) -> HttpResult<RetryOptions> {
253 RetryOptions::new(
254 self.max_attempts,
255 None,
256 self.max_duration,
257 self.delay_strategy.clone(),
258 RetryJitter::factor(self.jitter_factor),
259 )
260 .map_err(|error| HttpError::other(format!("Invalid HTTP retry options: {error}")))
261 }
262}
263
264impl Default for HttpRetryOptions {
265 fn default() -> Self {
266 Self {
267 enabled: false,
268 max_attempts: DEFAULT_RETRY_MAX_ATTEMPTS,
269 max_duration: None,
270 delay_strategy: RetryDelay::Exponential {
271 initial: DEFAULT_RETRY_INITIAL_DELAY,
272 max: DEFAULT_RETRY_MAX_DELAY,
273 multiplier: DEFAULT_RETRY_MULTIPLIER,
274 },
275 jitter_factor: DEFAULT_RETRY_JITTER_FACTOR,
276 method_policy: HttpRetryMethodPolicy::default(),
277 retry_status_codes: None,
278 retry_error_kinds: None,
279 }
280 }
281}
282
283struct HttpRetryConfigInput {
284 enabled: Option<bool>,
285 max_attempts: Option<u32>,
286 max_duration: Option<Duration>,
287 delay_strategy: Option<String>,
288 fixed_delay: Option<Duration>,
289 random_min_delay: Option<Duration>,
290 random_max_delay: Option<Duration>,
291 backoff_initial_delay: Option<Duration>,
292 backoff_max_delay: Option<Duration>,
293 backoff_multiplier: Option<f64>,
294 jitter_factor: Option<f64>,
295 method_policy: Option<String>,
296 status_codes: Option<Vec<String>>,
297 error_kinds: Option<Vec<String>>,
298}
299
300fn parse_retry_delay_strategy(
301 value: &str,
302 raw: &HttpRetryConfigInput,
303) -> Result<RetryDelay, HttpConfigError> {
304 let normalized = value.trim().to_ascii_lowercase().replace('-', "_");
305 match normalized.as_str() {
306 "none" => Ok(RetryDelay::None),
307 "fixed" => Ok(RetryDelay::Fixed(
308 raw.fixed_delay.unwrap_or(DEFAULT_RETRY_INITIAL_DELAY),
309 )),
310 "random" => Ok(RetryDelay::Random {
311 min: raw.random_min_delay.unwrap_or(DEFAULT_RETRY_INITIAL_DELAY),
312 max: raw.random_max_delay.unwrap_or(DEFAULT_RETRY_MAX_DELAY),
313 }),
314 "exponential_backoff" | "exponential" => Ok(RetryDelay::Exponential {
315 initial: raw
316 .backoff_initial_delay
317 .unwrap_or(DEFAULT_RETRY_INITIAL_DELAY),
318 max: raw.backoff_max_delay.unwrap_or(DEFAULT_RETRY_MAX_DELAY),
319 multiplier: raw.backoff_multiplier.unwrap_or(DEFAULT_RETRY_MULTIPLIER),
320 }),
321 _ => Err(HttpConfigError::invalid_value(
322 "delay_strategy",
323 format!("Unsupported retry delay strategy: {value}"),
324 )),
325 }
326}
327
328fn parse_retry_status_codes(values: &[String]) -> Result<Vec<StatusCode>, HttpConfigError> {
340 let mut result = Vec::<StatusCode>::new();
341 for value in values {
342 let trimmed = value.trim();
343 if trimmed.is_empty() {
344 return Err(HttpConfigError::invalid_value(
345 "status_codes",
346 "Retry status_codes cannot contain blank values",
347 ));
348 }
349 let raw_code = trimmed.parse::<u16>().map_err(|error| {
350 HttpConfigError::invalid_value(
351 "status_codes",
352 format!("Invalid retry status code '{trimmed}': {error}"),
353 )
354 })?;
355 if !(100..=599).contains(&raw_code) {
356 return Err(HttpConfigError::invalid_value(
357 "status_codes",
358 format!("Retry status code must be in range 100..=599, got {raw_code}"),
359 ));
360 }
361 let status = StatusCode::from_u16(raw_code)
362 .expect("retry status code range is pre-validated to 100..=599");
363 if !result.contains(&status) {
364 result.push(status);
365 }
366 }
367 result.sort_by_key(|status| status.as_u16());
368 Ok(result)
369}
370
371fn parse_retry_error_kinds(values: &[String]) -> Result<Vec<HttpErrorKind>, HttpConfigError> {
382 let mut result = Vec::<HttpErrorKind>::new();
383 for value in values {
384 let trimmed = value.trim();
385 if trimmed.is_empty() {
386 return Err(HttpConfigError::invalid_value(
387 "error_kinds",
388 "Retry error_kinds cannot contain blank values",
389 ));
390 }
391 let normalized = trimmed.replace('-', "_");
392 let kind = HttpErrorKind::from_str(&normalized).map_err(|_| {
393 HttpConfigError::invalid_value(
394 "error_kinds",
395 format!("Unsupported retry error kind: {trimmed}"),
396 )
397 })?;
398 if !result.contains(&kind) {
399 result.push(kind);
400 }
401 }
402 Ok(result)
403}
404
405fn default_retryable_status(status: StatusCode) -> bool {
414 status == StatusCode::TOO_MANY_REQUESTS || status.is_server_error()
415}
416
417fn default_retryable_error_kind(kind: HttpErrorKind) -> bool {
426 matches!(
427 kind,
428 HttpErrorKind::ConnectTimeout
429 | HttpErrorKind::ReadTimeout
430 | HttpErrorKind::WriteTimeout
431 | HttpErrorKind::RequestTimeout
432 | HttpErrorKind::Transport
433 )
434}
435
436#[cfg(coverage)]
441#[doc(hidden)]
442pub(crate) fn coverage_exercise_retry_option_paths() -> String {
443 let options = HttpRetryOptions {
444 jitter_factor: 2.0,
445 ..HttpRetryOptions::default()
446 };
447 options
448 .to_executor_options()
449 .expect_err("invalid jitter should fail executor option conversion")
450 .message
451}