Skip to main content

tuwunel_core/utils/time/
exponential_backoff.rs

1use std::time::Duration;
2
3/// Returns false if the exponential backoff has expired based on the inputs
4#[inline]
5#[must_use]
6pub fn continue_exponential_backoff_secs(
7	min: u64,
8	max: u64,
9	elapsed: Duration,
10	tries: u32,
11) -> bool {
12	let min = Duration::from_secs(min);
13	let max = Duration::from_secs(max);
14	continue_exponential_backoff(min, max, elapsed, tries)
15}
16
17/// Returns false if the exponential backoff has expired based on the inputs
18#[inline]
19#[must_use]
20pub fn continue_exponential_backoff(
21	min: Duration,
22	max: Duration,
23	elapsed: Duration,
24	tries: u32,
25) -> bool {
26	let min = min
27		.saturating_mul(tries)
28		.saturating_mul(tries)
29		.min(max);
30
31	elapsed < min
32}
33
34/// Smallest streak length `n` at which the quadratic curve `min * n²`
35/// saturates `max`. Bounds walk-back / retry-streak loops that consult
36/// [`continue_exponential_backoff`]: further iterations cannot change the
37/// verdict once `n` is reached. Operates at second precision; sub-second
38/// components of `min` and `max` are discarded, and `min < 1s` is clamped to
39/// 1s. Returns at least 1.
40#[inline]
41#[must_use]
42pub fn exponential_backoff_streak_cap(min: Duration, max: Duration) -> u32 {
43	let min_secs = min.as_secs().max(1);
44	let ratio = max.as_secs().checked_div(min_secs).unwrap_or(0);
45	let floor = ratio.isqrt();
46	let ceil = if floor.saturating_mul(floor) < ratio {
47		floor.saturating_add(1)
48	} else {
49		floor
50	};
51
52	u32::try_from(ceil).unwrap_or(u32::MAX).max(1)
53}