Skip to main content

jwks_cache/http/
retry.rs

1//! Retry utilities for HTTP requests.
2
3// crates.io
4use tokio::time;
5// self
6use crate::{_prelude::*, registry::RetryPolicy};
7
8/// Result of budgeting a retry attempt.
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum AttemptBudget {
11	/// Additional attempt is permitted with the provided per-attempt timeout.
12	Granted {
13		/// Timeout window allocated for the upcoming attempt.
14		timeout: Duration,
15	},
16	/// Retry window exhausted; no further attempts allowed.
17	Exhausted,
18}
19
20/// Controls retry backoff progression and attempt budgeting.
21#[derive(Debug)]
22pub struct RetryExecutor<'a> {
23	policy: &'a RetryPolicy,
24	deadline: Instant,
25	retries_used: u32,
26}
27impl<'a> RetryExecutor<'a> {
28	/// Create a new executor respecting the supplied retry policy.
29	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	/// Budget the next attempt, returning either the permitted timeout or exhaustion.
36	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	/// Whether another retry is permitted under the policy.
53	pub fn can_retry(&self) -> bool {
54		self.retries_used < self.policy.max_retries
55	}
56
57	/// Remaining wall-clock budget for the overall retry window.
58	pub fn remaining_budget(&self) -> Duration {
59		self.deadline.saturating_duration_since(Instant::now())
60	}
61
62	/// Number of retries that have already been consumed.
63	pub fn attempts_used(&self) -> u32 {
64		self.retries_used
65	}
66
67	/// Advance retry state and compute the backoff delay for the next attempt.
68	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	/// Sleep for the computed backoff window if retrying is permitted.
94	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}