use std::hash::{BuildHasher, Hasher};
use std::time::Duration;
use reliar_core::FailureKind;
use crate::error::ConfigError;
use crate::store::{DeadReason, FailureOutcome};
pub trait RetryPolicy: Send + Sync {
fn next(&self, attempts: u32, kind: FailureKind) -> FailureOutcome;
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(default, deny_unknown_fields))]
#[non_exhaustive]
pub struct ExponentialBackoff {
#[cfg_attr(
feature = "serde",
serde(rename = "base_ms", with = "reliar_core::serde_millis::millis")
)]
pub base: Duration,
#[cfg_attr(
feature = "serde",
serde(rename = "max_delay_ms", with = "reliar_core::serde_millis::millis")
)]
pub max_delay: Duration,
pub max_attempts: u32,
pub jitter: f64,
}
impl Default for ExponentialBackoff {
fn default() -> Self {
Self {
base: Duration::from_secs(1),
max_delay: Duration::from_secs(5 * 60),
max_attempts: 10,
jitter: 0.2,
}
}
}
impl ExponentialBackoff {
#[must_use]
pub const fn base(mut self, base: Duration) -> Self {
self.base = base;
self
}
#[must_use]
pub const fn max_delay(mut self, max_delay: Duration) -> Self {
self.max_delay = max_delay;
self
}
#[must_use]
pub const fn max_attempts(mut self, max_attempts: u32) -> Self {
self.max_attempts = max_attempts;
self
}
#[must_use]
pub const fn jitter(mut self, jitter: f64) -> Self {
self.jitter = jitter;
self
}
pub fn validate(&self) -> Result<(), ConfigError> {
if !(0.0..1.0).contains(&self.jitter) {
return Err(ConfigError::InvalidJitter { value: self.jitter });
}
if self.max_attempts == 0 {
return Err(ConfigError::ZeroMaxAttempts);
}
if self.base.is_zero() {
return Err(ConfigError::ZeroRetryBase);
}
Ok(())
}
fn jitter_factor(jitter: f64) -> f64 {
let jitter = if jitter.is_finite() {
jitter.clamp(0.0, 1.0)
} else {
0.0
};
if jitter <= 0.0 {
return 1.0;
}
let entropy = std::collections::hash_map::RandomState::new()
.build_hasher()
.finish();
#[allow(
clippy::cast_precision_loss,
reason = "jitter is a thundering-herd nicety, not a correctness input; losing a few \
low bits of a 64-bit value when producing a [0.0, 1.0] fraction is fine"
)]
let unit = entropy as f64 / u64::MAX as f64;
(1.0 + jitter * (2.0 * unit - 1.0)).max(0.0)
}
}
impl RetryPolicy for ExponentialBackoff {
fn next(&self, attempts: u32, kind: FailureKind) -> FailureOutcome {
if kind == FailureKind::Permanent {
return FailureOutcome::Dead {
reason: DeadReason::PermanentError,
};
}
if attempts.saturating_add(1) >= self.max_attempts {
return FailureOutcome::Dead {
reason: DeadReason::AttemptsExhausted,
};
}
let exponent = 2u32.saturating_pow(attempts);
let uncapped = self.base.saturating_mul(exponent);
let capped = uncapped.min(self.max_delay);
let delay = capped.mul_f64(Self::jitter_factor(self.jitter));
FailureOutcome::Retry { delay }
}
}