qubit_retry/delay.rs
1/*******************************************************************************
2 *
3 * Copyright (c) 2025 - 2026.
4 * Haixing Hu, Qubit Co. Ltd.
5 *
6 * All rights reserved.
7 *
8 ******************************************************************************/
9//! Delay strategies for retry attempts.
10//!
11//! A [`Delay`] produces the base sleep duration after a failed attempt. The
12//! base duration is calculated before [`crate::Jitter`] is applied by a retry
13//! executor.
14
15use std::time::Duration;
16
17use rand::RngExt;
18
19/// Base delay strategy before jitter is applied.
20///
21/// Delay strategies are value types that can be reused across executors. Random
22/// and exponential strategies are validated separately by [`Delay::validate`],
23/// which is called when building [`crate::RetryOptions`].
24#[derive(Debug, Clone, PartialEq)]
25pub enum Delay {
26 /// Retry immediately.
27 None,
28
29 /// Wait for a constant delay after every failed attempt.
30 Fixed(Duration),
31
32 /// Pick a delay uniformly from the inclusive range.
33 Random {
34 /// Lower bound for the delay.
35 min: Duration,
36 /// Upper bound for the delay.
37 max: Duration,
38 },
39
40 /// Exponential backoff capped by `max`.
41 Exponential {
42 /// Delay used for the first retry.
43 initial: Duration,
44 /// Maximum delay.
45 max: Duration,
46 /// Multiplicative factor applied per failed attempt.
47 multiplier: f64,
48 },
49}
50
51impl Delay {
52 /// Creates a no-delay strategy.
53 ///
54 /// # Parameters
55 /// This function has no parameters.
56 ///
57 /// # Returns
58 /// A [`Delay::None`] strategy.
59 ///
60 /// # Errors
61 /// This function does not return errors.
62 #[inline]
63 pub fn none() -> Self {
64 Self::None
65 }
66
67 /// Creates a fixed-delay strategy.
68 ///
69 /// # Parameters
70 /// - `delay`: Duration slept after each failed attempt.
71 ///
72 /// # Returns
73 /// A [`Delay::Fixed`] strategy.
74 ///
75 /// # Errors
76 /// This constructor does not validate `delay`; use [`Delay::validate`] to
77 /// reject a zero duration.
78 #[inline]
79 pub fn fixed(delay: Duration) -> Self {
80 Self::Fixed(delay)
81 }
82
83 /// Creates a random-delay strategy.
84 ///
85 /// # Parameters
86 /// - `min`: Inclusive lower bound for generated delays.
87 /// - `max`: Inclusive upper bound for generated delays.
88 ///
89 /// # Returns
90 /// A [`Delay::Random`] strategy.
91 ///
92 /// # Errors
93 /// This constructor does not validate the range; use [`Delay::validate`] to
94 /// reject a zero minimum or a minimum greater than the maximum.
95 #[inline]
96 pub fn random(min: Duration, max: Duration) -> Self {
97 Self::Random { min, max }
98 }
99
100 /// Creates an exponential-backoff strategy.
101 ///
102 /// # Parameters
103 /// - `initial`: Delay used for the first retry.
104 /// - `max`: Upper bound applied to every calculated delay.
105 /// - `multiplier`: Factor applied for each subsequent failed attempt.
106 ///
107 /// # Returns
108 /// A [`Delay::Exponential`] strategy.
109 ///
110 /// # Errors
111 /// This constructor does not validate the parameters; use
112 /// [`Delay::validate`] to reject a zero initial delay, `max < initial`, or
113 /// a multiplier that is non-finite or less than or equal to `1.0`.
114 #[inline]
115 pub fn exponential(initial: Duration, max: Duration, multiplier: f64) -> Self {
116 Self::Exponential {
117 initial,
118 max,
119 multiplier,
120 }
121 }
122
123 /// Calculates the base delay for an attempt number starting at 1.
124 ///
125 /// Attempt `1` means the first failed attempt, so exponential backoff
126 /// returns `initial` for attempts `0` and `1`. Random delays use a fresh
127 /// random value for every call.
128 ///
129 /// # Parameters
130 /// - `attempt`: Failed attempt number. Values `0` and `1` are treated as
131 /// the first exponential-backoff step.
132 ///
133 /// # Returns
134 /// The base delay before jitter is applied.
135 ///
136 /// # Errors
137 /// This function does not return errors. Invalid strategies should be
138 /// rejected with [`Delay::validate`] before they are used in an executor.
139 pub fn base_delay(&self, attempt: u32) -> Duration {
140 match self {
141 Self::None => Duration::ZERO,
142 Self::Fixed(delay) => *delay,
143 Self::Random { min, max } => {
144 if min >= max {
145 return *min;
146 }
147 let mut rng = rand::rng();
148 let min_nanos = Self::duration_to_nanos_u64(*min);
149 let max_nanos = Self::duration_to_nanos_u64(*max);
150 Duration::from_nanos(rng.random_range(min_nanos..=max_nanos))
151 }
152 Self::Exponential {
153 initial,
154 max,
155 multiplier,
156 } => Self::exponential_delay(*initial, *max, *multiplier, attempt),
157 }
158 }
159
160 /// Converts a [`Duration`] to whole nanoseconds as `u64`.
161 ///
162 /// Values larger than [`u64::MAX`] nanoseconds are saturated to
163 /// [`u64::MAX`] so the result fits in `u64` for uniform random delay sampling
164 /// in [`Delay::base_delay`].
165 ///
166 /// # Parameters
167 /// - `duration`: Duration to convert.
168 ///
169 /// # Returns
170 /// The duration in nanoseconds, capped at [`u64::MAX`].
171 ///
172 /// # Errors
173 /// This function does not return errors.
174 fn duration_to_nanos_u64(duration: Duration) -> u64 {
175 duration.as_nanos().min(u64::MAX as u128) as u64
176 }
177
178 /// Computes the exponential backoff delay for a given failed-attempt index.
179 ///
180 /// The effective exponent is `attempt.saturating_sub(1)`, so attempts `0`
181 /// and `1` both yield the initial delay (matching [`Delay::base_delay`]).
182 /// Each further attempt multiplies the base nanosecond count by
183 /// `multiplier` that many times, then the result is capped at `max`.
184 ///
185 /// # Parameters
186 /// - `initial`: Delay for the first retry step (attempts `0` and `1`).
187 /// - `max`: Upper bound on the returned delay.
188 /// - `multiplier`: Factor applied per additional attempt beyond the first.
189 /// - `attempt`: Failed attempt number (see [`Delay::base_delay`]).
190 ///
191 /// # Returns
192 /// The computed delay, or `max` when the scaled value is not finite or is
193 /// not less than `max` in nanoseconds.
194 ///
195 /// # Errors
196 /// This function does not return errors. Callers must ensure parameters
197 /// satisfy [`Delay::validate`] when constructing a public executor.
198 fn exponential_delay(
199 initial: Duration,
200 max: Duration,
201 multiplier: f64,
202 attempt: u32,
203 ) -> Duration {
204 let power = attempt.saturating_sub(1);
205 let base_nanos = initial.as_nanos() as f64;
206 let max_nanos = max.as_nanos() as f64;
207 let nanos = base_nanos * multiplier.powi(power.min(i32::MAX as u32) as i32);
208 if !nanos.is_finite() || nanos >= max_nanos {
209 return max;
210 }
211 Duration::from_nanos(nanos.max(0.0) as u64)
212 }
213
214 /// Validates strategy parameters.
215 ///
216 /// Returns a human-readable message describing the invalid field when the
217 /// strategy cannot be used safely by an executor.
218 ///
219 /// # Returns
220 /// `Ok(())` when all parameters are usable; otherwise an error message that
221 /// can be wrapped by [`crate::RetryConfigError`].
222 ///
223 /// # Parameters
224 /// This method has no parameters.
225 ///
226 /// # Errors
227 /// Returns an error when a fixed delay is zero, a random range is invalid,
228 /// or exponential backoff parameters are zero, inverted, non-finite, or too
229 /// small.
230 pub fn validate(&self) -> Result<(), String> {
231 match self {
232 Self::None => Ok(()),
233 Self::Fixed(delay) => {
234 if delay.is_zero() {
235 Err("fixed delay cannot be zero".to_string())
236 } else {
237 Ok(())
238 }
239 }
240 Self::Random { min, max } => {
241 if min.is_zero() {
242 Err("random delay minimum cannot be zero".to_string())
243 } else if min > max {
244 Err("random delay minimum cannot be greater than maximum".to_string())
245 } else {
246 Ok(())
247 }
248 }
249 Self::Exponential {
250 initial,
251 max,
252 multiplier,
253 } => {
254 if initial.is_zero() {
255 Err("exponential delay initial value cannot be zero".to_string())
256 } else if max < initial {
257 Err("exponential delay maximum cannot be smaller than initial".to_string())
258 } else if !multiplier.is_finite() || *multiplier <= 1.0 {
259 Err(
260 "exponential delay multiplier must be finite and greater than 1.0"
261 .to_string(),
262 )
263 } else {
264 Ok(())
265 }
266 }
267 }
268 }
269}
270
271impl Default for Delay {
272 /// Creates the default exponential-backoff strategy.
273 ///
274 /// # Returns
275 /// `Delay::Exponential` with one second initial delay, sixty second cap,
276 /// and multiplier `2.0`.
277 ///
278 /// # Parameters
279 /// This function has no parameters.
280 ///
281 /// # Errors
282 /// This function does not return errors.
283 #[inline]
284 fn default() -> Self {
285 Self::Exponential {
286 initial: Duration::from_secs(1),
287 max: Duration::from_secs(60),
288 multiplier: 2.0,
289 }
290 }
291}