use tokio::time;
use crate::{_prelude::*, registry::RetryPolicy};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AttemptBudget {
Granted {
timeout: Duration,
},
Exhausted,
}
#[derive(Debug)]
pub struct RetryExecutor<'a> {
policy: &'a RetryPolicy,
deadline: Instant,
retries_used: u32,
}
impl<'a> RetryExecutor<'a> {
pub fn new(policy: &'a RetryPolicy) -> Self {
let deadline = Instant::now() + policy.deadline;
Self { policy, deadline, retries_used: 0 }
}
pub fn attempt_budget(&self) -> AttemptBudget {
let remaining = self.remaining_budget();
if remaining.is_zero() {
AttemptBudget::Exhausted
} else {
let timeout = remaining.min(self.policy.attempt_timeout);
if timeout.is_zero() {
AttemptBudget::Exhausted
} else {
AttemptBudget::Granted { timeout }
}
}
}
pub fn can_retry(&self) -> bool {
self.retries_used < self.policy.max_retries
}
pub fn remaining_budget(&self) -> Duration {
self.deadline.saturating_duration_since(Instant::now())
}
pub fn attempts_used(&self) -> u32 {
self.retries_used
}
pub fn next_backoff(&mut self) -> Option<Duration> {
if !self.can_retry() {
tracing::debug!(attempt = self.retries_used, "retry budget exhausted");
return None;
}
let attempt = self.retries_used;
self.retries_used = self.retries_used.saturating_add(1);
let mut delay = self.policy.compute_backoff(attempt);
let remaining = self.remaining_budget();
if !remaining.is_zero() {
delay = delay.min(remaining);
} else {
delay = Duration::ZERO;
}
tracing::debug!(attempt = attempt + 1, ?delay, remaining = ?remaining, "retry backoff computed");
Some(delay)
}
pub async fn sleep_backoff(&mut self) {
if let Some(delay) = self.next_backoff()
&& !delay.is_zero()
{
time::sleep(delay).await;
}
}
}