Skip to main content

qubit_http/options/
http_retry_options.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2025 - 2026.
4 *    Haixing Hu, Qubit Co. Ltd.
5 *
6 *    All rights reserved.
7 *
8 ******************************************************************************/
9
10use std::time::Duration;
11
12use qubit_config::{ConfigReader, ConfigResult};
13use qubit_retry::Delay;
14
15use super::http_retry_method_policy::HttpRetryMethodPolicy;
16use super::HttpConfigError;
17
18const DEFAULT_RETRY_MAX_ATTEMPTS: u32 = 3;
19const DEFAULT_RETRY_INITIAL_DELAY: Duration = Duration::from_millis(200);
20const DEFAULT_RETRY_MAX_DELAY: Duration = Duration::from_secs(5);
21const DEFAULT_RETRY_MULTIPLIER: f64 = 2.0;
22const DEFAULT_RETRY_JITTER_FACTOR: f64 = 0.1;
23
24/// Retry settings for [`crate::HttpClient`].
25#[derive(Debug, Clone, PartialEq)]
26pub struct HttpRetryOptions {
27    /// Whether built-in retry is enabled.
28    pub enabled: bool,
29    /// Maximum number of attempts, including the first request.
30    pub max_attempts: u32,
31    /// Optional maximum total retry duration.
32    pub max_duration: Option<Duration>,
33    /// Delay strategy between attempts.
34    pub delay_strategy: Delay,
35    /// Jitter factor passed to the retry delay strategy.
36    pub jitter_factor: f64,
37    /// Method replay policy.
38    pub method_policy: HttpRetryMethodPolicy,
39}
40
41impl Default for HttpRetryOptions {
42    fn default() -> Self {
43        Self {
44            enabled: false,
45            max_attempts: DEFAULT_RETRY_MAX_ATTEMPTS,
46            max_duration: None,
47            delay_strategy: Delay::Exponential {
48                initial: DEFAULT_RETRY_INITIAL_DELAY,
49                max: DEFAULT_RETRY_MAX_DELAY,
50                multiplier: DEFAULT_RETRY_MULTIPLIER,
51            },
52            jitter_factor: DEFAULT_RETRY_JITTER_FACTOR,
53            method_policy: HttpRetryMethodPolicy::default(),
54        }
55    }
56}
57
58struct HttpRetryConfigInput {
59    enabled: Option<bool>,
60    max_attempts: Option<u32>,
61    max_duration: Option<Duration>,
62    delay_strategy: Option<String>,
63    fixed_delay: Option<Duration>,
64    random_min_delay: Option<Duration>,
65    random_max_delay: Option<Duration>,
66    backoff_initial_delay: Option<Duration>,
67    backoff_max_delay: Option<Duration>,
68    backoff_multiplier: Option<f64>,
69    jitter_factor: Option<f64>,
70    method_policy: Option<String>,
71}
72
73impl HttpRetryOptions {
74    fn read_config<R>(config: &R) -> ConfigResult<HttpRetryConfigInput>
75    where
76        R: ConfigReader + ?Sized,
77    {
78        Ok(HttpRetryConfigInput {
79            enabled: config.get_optional("enabled")?,
80            max_attempts: config.get_optional("max_attempts")?,
81            max_duration: config.get_optional("max_duration")?,
82            delay_strategy: config.get_optional_string("delay_strategy")?,
83            fixed_delay: config.get_optional("fixed_delay")?,
84            random_min_delay: config.get_optional("random_min_delay")?,
85            random_max_delay: config.get_optional("random_max_delay")?,
86            backoff_initial_delay: config.get_optional("backoff_initial_delay")?,
87            backoff_max_delay: config.get_optional("backoff_max_delay")?,
88            backoff_multiplier: config.get_optional("backoff_multiplier")?,
89            jitter_factor: config.get_optional("jitter_factor")?,
90            method_policy: config.get_optional_string("method_policy")?,
91        })
92    }
93
94    /// Creates default retry options.
95    ///
96    /// # Returns
97    /// Fresh retry options with built-in retry disabled.
98    pub fn new() -> Self {
99        Self::default()
100    }
101
102    /// Creates [`HttpRetryOptions`] from `config` using relative keys.
103    ///
104    /// # Parameters
105    /// - `config`: Any [`ConfigReader`] scoped to the retry section.
106    ///
107    /// # Returns
108    /// Parsed retry options or [`HttpConfigError`].
109    pub fn from_config<R>(config: &R) -> Result<Self, HttpConfigError>
110    where
111        R: ConfigReader + ?Sized,
112    {
113        let raw = Self::read_config(config).map_err(HttpConfigError::from)?;
114        let mut opts = Self::default();
115
116        if let Some(enabled) = raw.enabled {
117            opts.enabled = enabled;
118        }
119        if let Some(max_attempts) = raw.max_attempts {
120            opts.max_attempts = max_attempts;
121        }
122        opts.max_duration = raw.max_duration;
123        if let Some(jitter_factor) = raw.jitter_factor {
124            opts.jitter_factor = jitter_factor;
125        }
126        if let Some(method_policy) = raw.method_policy.as_ref() {
127            opts.method_policy = HttpRetryMethodPolicy::from_config_value(method_policy)?;
128        }
129
130        if let Some(delay_strategy) = raw.delay_strategy.as_ref() {
131            opts.delay_strategy = parse_retry_delay_strategy(delay_strategy, &raw)?;
132        }
133
134        opts.validate()?;
135        Ok(opts)
136    }
137
138    /// Runs retry option validation.
139    ///
140    /// # Returns
141    /// `Ok(())` when values are usable, otherwise [`HttpConfigError`].
142    pub fn validate(&self) -> Result<(), HttpConfigError> {
143        if self.max_attempts == 0 {
144            return Err(HttpConfigError::invalid_value(
145                "max_attempts",
146                "Retry max_attempts must be greater than 0",
147            ));
148        }
149        if !(0.0..=1.0).contains(&self.jitter_factor) {
150            return Err(HttpConfigError::invalid_value(
151                "jitter_factor",
152                "Retry jitter_factor must be between 0.0 and 1.0",
153            ));
154        }
155        self.delay_strategy
156            .validate()
157            .map_err(|message| HttpConfigError::invalid_value("delay_strategy", message))?;
158        Ok(())
159    }
160
161    /// Returns whether `method` is eligible for built-in retry.
162    ///
163    /// # Parameters
164    /// - `method`: HTTP method to evaluate.
165    ///
166    /// # Returns
167    /// `true` if retry is enabled and the method policy allows replay.
168    pub fn allows_method(&self, method: &http::Method) -> bool {
169        self.enabled && self.method_policy.allows_method(method)
170    }
171}
172
173fn parse_retry_delay_strategy(
174    value: &str,
175    raw: &HttpRetryConfigInput,
176) -> Result<Delay, HttpConfigError> {
177    let normalized = value.trim().to_ascii_uppercase().replace('-', "_");
178    match normalized.as_str() {
179        "NONE" => Ok(Delay::None),
180        "FIXED" => Ok(Delay::Fixed(
181            raw.fixed_delay.unwrap_or(DEFAULT_RETRY_INITIAL_DELAY),
182        )),
183        "RANDOM" => Ok(Delay::Random {
184            min: raw.random_min_delay.unwrap_or(DEFAULT_RETRY_INITIAL_DELAY),
185            max: raw.random_max_delay.unwrap_or(DEFAULT_RETRY_MAX_DELAY),
186        }),
187        "EXPONENTIAL_BACKOFF" | "EXPONENTIAL" => Ok(Delay::Exponential {
188            initial: raw
189                .backoff_initial_delay
190                .unwrap_or(DEFAULT_RETRY_INITIAL_DELAY),
191            max: raw.backoff_max_delay.unwrap_or(DEFAULT_RETRY_MAX_DELAY),
192            multiplier: raw.backoff_multiplier.unwrap_or(DEFAULT_RETRY_MULTIPLIER),
193        }),
194        _ => Err(HttpConfigError::invalid_value(
195            "delay_strategy",
196            format!("Unsupported retry delay strategy: {value}"),
197        )),
198    }
199}