Skip to main content

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}