use super::RetryBudgetSnapshot;
use backoff::{backoff::Backoff, ExponentialBackoff};
use sea_orm::FromJsonQueryResult;
use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, FromJsonQueryResult)]
#[serde(rename_all = "camelCase")]
pub struct RetryPolicy {
#[serde(default = "RetryPolicy::default_max_attempts")]
#[serde(alias = "max_attempts")]
pub max_attempts: Option<u32>,
#[serde(default = "RetryPolicy::default_initial_interval_ms")]
#[serde(alias = "initial_interval_ms")]
pub initial_interval_ms: u64,
#[serde(default = "RetryPolicy::default_max_interval_ms")]
#[serde(alias = "max_interval_ms")]
pub max_interval_ms: u64,
#[serde(default = "RetryPolicy::default_randomization_factor")]
#[serde(alias = "randomization_factor")]
pub randomization_factor: f64,
#[serde(default = "RetryPolicy::default_multiplier")]
pub multiplier: f64,
#[serde(default = "RetryPolicy::default_max_elapsed_time_ms")]
#[serde(alias = "max_elapsed_time_ms")]
pub max_elapsed_time_ms: Option<u64>,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_attempts: Self::default_max_attempts(),
initial_interval_ms: Self::default_initial_interval_ms(),
max_interval_ms: Self::default_max_interval_ms(),
randomization_factor: Self::default_randomization_factor(),
multiplier: Self::default_multiplier(),
max_elapsed_time_ms: Self::default_max_elapsed_time_ms(),
}
}
}
impl RetryPolicy {
fn default_max_attempts() -> Option<u32> {
None }
fn default_initial_interval_ms() -> u64 {
1_000 }
fn default_max_interval_ms() -> u64 {
30_000 }
fn default_randomization_factor() -> f64 {
0.2 }
fn default_multiplier() -> f64 {
2.0 }
fn default_max_elapsed_time_ms() -> Option<u64> {
None }
pub fn no_retry() -> Self {
Self {
max_attempts: Some(0),
..Default::default()
}
}
pub fn unlimited() -> Self {
Self {
max_attempts: None,
max_elapsed_time_ms: None,
..Default::default()
}
}
pub fn with_max_attempts(max_attempts: u32) -> Self {
Self {
max_attempts: Some(max_attempts),
..Default::default()
}
}
}
impl sea_orm::IntoActiveValue<RetryPolicy> for RetryPolicy {
fn into_active_value(self) -> sea_orm::ActiveValue<RetryPolicy> {
sea_orm::ActiveValue::Set(self)
}
}
pub fn build_exponential_backoff(policy: &RetryPolicy) -> ExponentialBackoff {
ExponentialBackoff {
initial_interval: Duration::from_millis(policy.initial_interval_ms.max(1)),
max_interval: Duration::from_millis(policy.max_interval_ms.max(policy.initial_interval_ms)),
randomization_factor: policy.randomization_factor.clamp(0.0, 1.0),
multiplier: policy.multiplier.max(1.0),
max_elapsed_time: policy.max_elapsed_time_ms.map(Duration::from_millis),
..ExponentialBackoff::default()
}
}
#[derive(Debug, Clone)]
pub struct RetryController {
backoff: ExponentialBackoff,
max_attempts: Option<u32>,
retries_used: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RetryDecision {
RetryAfter(Duration),
Exhausted,
}
impl RetryController {
#[inline]
pub fn new(policy: &RetryPolicy) -> Self {
Self {
backoff: build_exponential_backoff(policy),
max_attempts: policy.max_attempts,
retries_used: 0,
}
}
#[inline]
pub fn reset(&mut self) {
self.backoff.reset();
self.retries_used = 0;
}
#[inline]
pub fn on_failure(&mut self) -> RetryDecision {
if let Some(max) = self.max_attempts {
if self.retries_used >= max {
return RetryDecision::Exhausted;
}
}
match self.backoff.next_backoff() {
Some(dur) => {
self.retries_used = self.retries_used.saturating_add(1);
RetryDecision::RetryAfter(dur)
}
None => RetryDecision::Exhausted,
}
}
#[inline]
pub fn retries_used(&self) -> u32 {
self.retries_used
}
#[inline]
pub fn budget_snapshot(&self) -> RetryBudgetSnapshot {
match self.max_attempts {
Some(max) => {
let remaining = max.saturating_sub(self.retries_used);
RetryBudgetSnapshot {
exhausted: remaining == 0,
remaining_hint: Some(remaining),
}
}
None => RetryBudgetSnapshot {
exhausted: false,
remaining_hint: None,
},
}
}
}