1use tokio::time;
5use crate::{_prelude::*, registry::RetryPolicy};
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum AttemptBudget {
11 Granted {
13 timeout: Duration,
15 },
16 Exhausted,
18}
19
20#[derive(Debug)]
22pub struct RetryExecutor<'a> {
23 policy: &'a RetryPolicy,
24 deadline: Instant,
25 retries_used: u32,
26}
27impl<'a> RetryExecutor<'a> {
28 pub fn new(policy: &'a RetryPolicy) -> Self {
30 let deadline = Instant::now() + policy.deadline;
31
32 Self { policy, deadline, retries_used: 0 }
33 }
34
35 pub fn attempt_budget(&self) -> AttemptBudget {
37 let remaining = self.remaining_budget();
38
39 if remaining.is_zero() {
40 AttemptBudget::Exhausted
41 } else {
42 let timeout = remaining.min(self.policy.attempt_timeout);
43
44 if timeout.is_zero() {
45 AttemptBudget::Exhausted
46 } else {
47 AttemptBudget::Granted { timeout }
48 }
49 }
50 }
51
52 pub fn can_retry(&self) -> bool {
54 self.retries_used < self.policy.max_retries
55 }
56
57 pub fn remaining_budget(&self) -> Duration {
59 self.deadline.saturating_duration_since(Instant::now())
60 }
61
62 pub fn attempts_used(&self) -> u32 {
64 self.retries_used
65 }
66
67 pub fn next_backoff(&mut self) -> Option<Duration> {
69 if !self.can_retry() {
70 tracing::debug!(attempt = self.retries_used, "retry budget exhausted");
71
72 return None;
73 }
74
75 let attempt = self.retries_used;
76
77 self.retries_used = self.retries_used.saturating_add(1);
78
79 let mut delay = self.policy.compute_backoff(attempt);
80 let remaining = self.remaining_budget();
81
82 if !remaining.is_zero() {
83 delay = delay.min(remaining);
84 } else {
85 delay = Duration::ZERO;
86 }
87
88 tracing::debug!(attempt = attempt + 1, ?delay, remaining = ?remaining, "retry backoff computed");
89
90 Some(delay)
91 }
92
93 pub async fn sleep_backoff(&mut self) {
95 if let Some(delay) = self.next_backoff()
96 && !delay.is_zero()
97 {
98 time::sleep(delay).await;
99 }
100 }
101}