use std::num::NonZeroU32;
use std::time::Duration;
use qubit_config::{ConfigReader, ConfigResult};
use crate::{RetryConfigError, RetryDelay, RetryJitter};
#[derive(Debug, Clone, PartialEq)]
pub struct RetryOptions {
pub max_attempts: NonZeroU32,
pub max_elapsed: Option<Duration>,
pub delay: RetryDelay,
pub jitter: RetryJitter,
}
impl RetryOptions {
pub const KEY_MAX_ATTEMPTS: &'static str = "max_attempts";
pub const KEY_MAX_ELAPSED_MILLIS: &'static str = "max_elapsed_millis";
pub const KEY_DELAY: &'static str = "delay";
pub const KEY_DELAY_STRATEGY: &'static str = "delay_strategy";
pub const KEY_FIXED_DELAY_MILLIS: &'static str = "fixed_delay_millis";
pub const KEY_RANDOM_MIN_DELAY_MILLIS: &'static str = "random_min_delay_millis";
pub const KEY_RANDOM_MAX_DELAY_MILLIS: &'static str = "random_max_delay_millis";
pub const KEY_EXPONENTIAL_INITIAL_DELAY_MILLIS: &'static str =
"exponential_initial_delay_millis";
pub const KEY_EXPONENTIAL_MAX_DELAY_MILLIS: &'static str = "exponential_max_delay_millis";
pub const KEY_EXPONENTIAL_MULTIPLIER: &'static str = "exponential_multiplier";
pub const KEY_JITTER_FACTOR: &'static str = "jitter_factor";
pub fn new(
max_attempts: u32,
max_elapsed: Option<Duration>,
delay: RetryDelay,
jitter: RetryJitter,
) -> Result<Self, RetryConfigError> {
let max_attempts = NonZeroU32::new(max_attempts).ok_or_else(|| {
RetryConfigError::invalid_value(
Self::KEY_MAX_ATTEMPTS,
"max_attempts must be greater than zero",
)
})?;
let options = Self {
max_attempts,
max_elapsed,
delay,
jitter,
};
options.validate()?;
Ok(options)
}
pub fn from_config<R>(config: &R) -> Result<Self, RetryConfigError>
where
R: ConfigReader + ?Sized,
{
let default = Self::default();
let values = RetryConfigValues::read_from(config).map_err(RetryConfigError::from)?;
values.to_options(&default)
}
pub fn validate(&self) -> Result<(), RetryConfigError> {
self.delay
.validate()
.map_err(|message| RetryConfigError::invalid_value(Self::KEY_DELAY, message))?;
self.jitter
.validate()
.map_err(|message| RetryConfigError::invalid_value(Self::KEY_JITTER_FACTOR, message))?;
Ok(())
}
}
impl Default for RetryOptions {
#[inline]
fn default() -> Self {
Self {
max_attempts: NonZeroU32::new(3).expect("default retry attempts must be non-zero"),
max_elapsed: None,
delay: RetryDelay::default(),
jitter: RetryJitter::None,
}
}
}
#[derive(Debug, Clone, PartialEq)]
struct RetryConfigValues {
max_attempts: Option<u32>,
max_elapsed_millis: Option<u64>,
delay: Option<String>,
delay_strategy: Option<String>,
fixed_delay_millis: Option<u64>,
random_min_delay_millis: Option<u64>,
random_max_delay_millis: Option<u64>,
exponential_initial_delay_millis: Option<u64>,
exponential_max_delay_millis: Option<u64>,
exponential_multiplier: Option<f64>,
jitter_factor: Option<f64>,
}
impl RetryConfigValues {
fn read_from<R>(config: &R) -> ConfigResult<Self>
where
R: ConfigReader + ?Sized,
{
Ok(Self {
max_attempts: config.get_optional(RetryOptions::KEY_MAX_ATTEMPTS)?,
max_elapsed_millis: config.get_optional(RetryOptions::KEY_MAX_ELAPSED_MILLIS)?,
delay: config.get_optional_string(RetryOptions::KEY_DELAY)?,
delay_strategy: config.get_optional_string(RetryOptions::KEY_DELAY_STRATEGY)?,
fixed_delay_millis: config.get_optional(RetryOptions::KEY_FIXED_DELAY_MILLIS)?,
random_min_delay_millis: config
.get_optional(RetryOptions::KEY_RANDOM_MIN_DELAY_MILLIS)?,
random_max_delay_millis: config
.get_optional(RetryOptions::KEY_RANDOM_MAX_DELAY_MILLIS)?,
exponential_initial_delay_millis: config
.get_optional(RetryOptions::KEY_EXPONENTIAL_INITIAL_DELAY_MILLIS)?,
exponential_max_delay_millis: config
.get_optional(RetryOptions::KEY_EXPONENTIAL_MAX_DELAY_MILLIS)?,
exponential_multiplier: config
.get_optional(RetryOptions::KEY_EXPONENTIAL_MULTIPLIER)?,
jitter_factor: config.get_optional(RetryOptions::KEY_JITTER_FACTOR)?,
})
}
fn to_options(&self, default: &RetryOptions) -> Result<RetryOptions, RetryConfigError> {
let max_attempts = self.max_attempts.unwrap_or(default.max_attempts.get());
let max_elapsed = self.max_elapsed();
let delay = self.delay(default)?;
let jitter = self.jitter(default);
RetryOptions::new(max_attempts, max_elapsed, delay, jitter)
}
fn max_elapsed(&self) -> Option<Duration> {
match self.max_elapsed_millis {
Some(0) | None => None,
Some(millis) => Some(Duration::from_millis(millis)),
}
}
fn delay(&self, default: &RetryOptions) -> Result<RetryDelay, RetryConfigError> {
let strategy = self
.delay
.as_deref()
.or(self.delay_strategy.as_deref())
.map(str::trim)
.map(|value| value.to_ascii_lowercase());
match strategy.as_deref() {
None => Ok(self
.implicit_delay()
.unwrap_or_else(|| default.delay.clone())),
Some("none") => Ok(RetryDelay::None),
Some("fixed") => Ok(RetryDelay::fixed(Duration::from_millis(
self.fixed_delay_millis.unwrap_or(1000),
))),
Some("random") => Ok(RetryDelay::random(
Duration::from_millis(self.random_min_delay_millis.unwrap_or(1000)),
Duration::from_millis(self.random_max_delay_millis.unwrap_or(10000)),
)),
Some("exponential") | Some("exponential_backoff") => Ok(RetryDelay::exponential(
Duration::from_millis(self.exponential_initial_delay_millis.unwrap_or(1000)),
Duration::from_millis(self.exponential_max_delay_millis.unwrap_or(60000)),
self.exponential_multiplier.unwrap_or(2.0),
)),
Some(other) => Err(RetryConfigError::invalid_value(
RetryOptions::KEY_DELAY,
format!("unsupported delay strategy '{other}'"),
)),
}
}
fn implicit_delay(&self) -> Option<RetryDelay> {
if let Some(millis) = self.fixed_delay_millis {
return Some(RetryDelay::fixed(Duration::from_millis(millis)));
}
if self.random_min_delay_millis.is_some() || self.random_max_delay_millis.is_some() {
return Some(RetryDelay::random(
Duration::from_millis(self.random_min_delay_millis.unwrap_or(1000)),
Duration::from_millis(self.random_max_delay_millis.unwrap_or(10000)),
));
}
if self.exponential_initial_delay_millis.is_some()
|| self.exponential_max_delay_millis.is_some()
|| self.exponential_multiplier.is_some()
{
return Some(RetryDelay::exponential(
Duration::from_millis(self.exponential_initial_delay_millis.unwrap_or(1000)),
Duration::from_millis(self.exponential_max_delay_millis.unwrap_or(60000)),
self.exponential_multiplier.unwrap_or(2.0),
));
}
None
}
fn jitter(&self, default: &RetryOptions) -> RetryJitter {
match self.jitter_factor {
Some(0.0) | None => default.jitter,
Some(factor) => RetryJitter::Factor(factor),
}
}
}