Skip to main content

tuwunel_core/utils/time/
exponential_backoff.rs

1//! Retry-backoff calculations.
2//!
3//! The helpers determine whether a retry delay remains active and derive
4//! retry-streak caps from duration bounds. Delay calculations saturate at the
5//! configured maximum where applicable.
6
7use std::time::Duration;
8
9/// Returns false if the exponential backoff has expired based on the inputs
10#[inline]
11#[must_use]
12pub fn continue_exponential_backoff_secs(
13	min: u64,
14	max: u64,
15	elapsed: Duration,
16	tries: u32,
17) -> bool {
18	let min = Duration::from_secs(min);
19	let max = Duration::from_secs(max);
20	continue_exponential_backoff(min, max, elapsed, tries)
21}
22
23/// Returns false if the exponential backoff has expired based on the inputs
24#[inline]
25#[must_use]
26pub fn continue_exponential_backoff(
27	min: Duration,
28	max: Duration,
29	elapsed: Duration,
30	tries: u32,
31) -> bool {
32	let min = min
33		.saturating_mul(tries)
34		.saturating_mul(tries)
35		.min(max);
36
37	elapsed < min
38}
39
40/// Derives a retry-streak cap from the whole-second ratio of `max` to `min`.
41///
42/// Let `r = max.as_secs() / min.as_secs().max(1)` using integer division. The
43/// result is `ceil(sqrt(r))`, clamped to the range `1..=u32::MAX`. Subsecond
44/// components and the division remainder are discarded.
45#[inline]
46#[must_use]
47pub fn exponential_backoff_streak_cap(min: Duration, max: Duration) -> u32 {
48	let min_secs = min.as_secs().max(1);
49	let ratio = max.as_secs().checked_div(min_secs).unwrap_or(0);
50	let floor = ratio.isqrt();
51	let ceil = if floor.saturating_mul(floor) < ratio {
52		floor.saturating_add(1)
53	} else {
54		floor
55	};
56
57	u32::try_from(ceil).unwrap_or(u32::MAX).max(1)
58}