qubit_http/options/
http_retry_options.rs1use 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#[derive(Debug, Clone, PartialEq)]
26pub struct HttpRetryOptions {
27 pub enabled: bool,
29 pub max_attempts: u32,
31 pub max_duration: Option<Duration>,
33 pub delay_strategy: Delay,
35 pub jitter_factor: f64,
37 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 pub fn new() -> Self {
99 Self::default()
100 }
101
102 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 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 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}