Skip to main content

graceful_worker/
backoff.rs

1//! How long a failing operation waits before trying again — and how a
2//! stop cuts the wait short.
3//!
4//! # What this adds over the other backoff crates
5//!
6//! `backoff`, `backon` and `exponential-backoff` compute a schedule and
7//! sleep it. None of them takes a shutdown signal, so a worker that must
8//! stop promptly has to wrap every wait in its own `select!` against
9//! whatever it uses for cancellation, and get that right at each call site.
10//!
11//! [`Backoff::wait`] takes a [`Watcher`] and returns `false` when a stop
12//! arrived mid-wait, so the loop reads:
13//!
14//! ```
15//! # #[tokio::main(flavor = "current_thread")]
16//! # async fn main() {
17//! use graceful_worker::{Backoff, Shutdown};
18//!
19//! let shutdown = Shutdown::new();
20//! let watcher = shutdown.watcher();
21//! let mut backoff = Backoff::new();
22//!
23//! shutdown.stop();
24//!
25//! // A fifteen-minute delay, abandoned at once rather than in fifteen
26//! // minutes.
27//! for _ in 0..20 {
28//!     backoff.failed();
29//! }
30//! assert!(!backoff.wait(&watcher).await);
31//! # }
32//! ```
33//!
34//! # On slicing, honestly
35//!
36//! [`Backoff::wait`] sleeps the delay in bounded slices ([`with_slice`]).
37//! **That is not what makes the wait interruptible.** Each slice is a
38//! [`Watcher::sleep`], which is a `select!` against the cancellation token
39//! and so returns the moment a stop arrives — a single unsliced sleep would
40//! be exactly as responsive.
41//!
42//! Slicing therefore buys nothing for shutdown latency here, and an earlier
43//! version of this documentation claimed otherwise. It is kept because it
44//! bounds the granularity of the wait for anyone who wants that — a
45//! progress tick, a slice-boundary check of some other condition — and
46//! because [`Duration::ZERO`] disables it. If you have no such need, leave
47//! it alone: the default costs one extra timer per minute of waiting.
48//!
49//! The integration with [`Watcher`] is the reason to use this crate. The
50//! slicing is a knob.
51//!
52//! [`with_slice`]: Backoff::with_slice
53//! [`Watcher::sleep`]: crate::Watcher::sleep
54//!
55//! # Why any of this is configurable
56//!
57//! The defaults are a working set, not a recommendation: five seconds,
58//! doubling to a fifteen-minute ceiling, sliced at a minute. What matters
59//! for a given system is the ceiling and the platform's grace period, and
60//! only the caller knows either.
61
62use std::time::Duration;
63
64use crate::shutdown::Watcher;
65
66/// The wait before the first retry, by default.
67pub const DEFAULT_INITIAL_DELAY: Duration = Duration::from_secs(5);
68
69/// The longest wait between attempts, by default.
70pub const DEFAULT_MAX_DELAY: Duration = Duration::from_secs(900);
71
72/// The longest single sleep, by default.
73///
74/// Also the worst case for how long a stop goes unnoticed during a wait.
75pub const DEFAULT_SLICE: Duration = Duration::from_secs(60);
76
77/// What the delay is multiplied by after each failure, by default.
78pub const DEFAULT_FACTOR: u32 = 2;
79
80/// The retry state of one loop.
81///
82/// A loop holds one of these, tells it when an attempt succeeded or failed,
83/// and asks it to wait. Nothing else in the loop needs to know about
84/// multiplying, ceilings or slicing.
85///
86/// # Examples
87///
88/// ```
89/// use std::time::Duration;
90/// use graceful_worker::Backoff;
91///
92/// let mut backoff = Backoff::new();
93/// assert_eq!(backoff.delay(), Duration::from_secs(5));
94///
95/// backoff.failed();
96/// assert_eq!(backoff.delay(), Duration::from_secs(10));
97/// backoff.failed();
98/// assert_eq!(backoff.delay(), Duration::from_secs(20));
99///
100/// // Any success puts it straight back to the start.
101/// backoff.succeeded();
102/// assert_eq!(backoff.delay(), Duration::from_secs(5));
103/// assert_eq!(backoff.attempt(), 0);
104/// ```
105///
106/// A schedule of your own:
107///
108/// ```
109/// use std::time::Duration;
110/// use graceful_worker::Backoff;
111///
112/// let backoff = Backoff::new()
113///     .with_initial_delay(Duration::from_millis(100))
114///     .with_max_delay(Duration::from_secs(30))
115///     .with_slice(Duration::from_secs(5))
116///     .with_factor(3);
117///
118/// assert_eq!(backoff.delay(), Duration::from_millis(100));
119/// ```
120#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct Backoff {
122    /// What the next wait will be.
123    delay: Duration,
124    /// How many consecutive failures there have been.
125    attempt: u32,
126    /// Where a reset returns to.
127    initial: Duration,
128    /// The ceiling.
129    max: Duration,
130    /// The longest single sleep. Zero means do not slice.
131    slice: Duration,
132    /// The multiplier applied after each failure.
133    factor: u32,
134}
135
136impl Backoff {
137    /// A fresh backoff on the default schedule.
138    #[must_use]
139    pub const fn new() -> Self {
140        Self {
141            delay: DEFAULT_INITIAL_DELAY,
142            attempt: 0,
143            initial: DEFAULT_INITIAL_DELAY,
144            max: DEFAULT_MAX_DELAY,
145            slice: DEFAULT_SLICE,
146            factor: DEFAULT_FACTOR,
147        }
148    }
149
150    /// Set the first delay, and reset to it.
151    ///
152    /// Clamped to the ceiling, so an initial delay longer than the maximum
153    /// is the maximum rather than a schedule that counts downwards.
154    #[must_use]
155    pub const fn with_initial_delay(mut self, initial: Duration) -> Self {
156        self.initial = initial;
157        self.delay = initial;
158        self.clamp()
159    }
160
161    /// Set the ceiling.
162    #[must_use]
163    pub const fn with_max_delay(mut self, max: Duration) -> Self {
164        self.max = max;
165        self.clamp()
166    }
167
168    /// Set the longest single sleep [`Backoff::wait`] performs.
169    ///
170    /// This is the worst case for how long a stop goes unnoticed during a
171    /// wait, so it should be comfortably shorter than the platform's grace
172    /// period. [`Duration::ZERO`] disables slicing, which restores the
173    /// behaviour of every other backoff crate — including its drawback.
174    #[must_use]
175    pub const fn with_slice(mut self, slice: Duration) -> Self {
176        self.slice = slice;
177        self
178    }
179
180    /// Set the multiplier applied after each failure.
181    ///
182    /// A factor of 1 is a constant delay. Zero is treated as 1, because a
183    /// backoff that multiplies by zero would retry instantly forever.
184    #[must_use]
185    pub const fn with_factor(mut self, factor: u32) -> Self {
186        self.factor = if factor == 0 { 1 } else { factor };
187        self
188    }
189
190    /// Keep the current and initial delays within the ceiling.
191    const fn clamp(mut self) -> Self {
192        if self.initial.as_nanos() > self.max.as_nanos() {
193            self.initial = self.max;
194        }
195        if self.delay.as_nanos() > self.max.as_nanos() {
196            self.delay = self.max;
197        }
198        self
199    }
200
201    /// How long to wait before the next attempt.
202    #[must_use]
203    pub const fn delay(&self) -> Duration {
204        self.delay
205    }
206
207    /// How many consecutive failures there have been.
208    ///
209    /// Useful as a dimension on a retry metric.
210    #[must_use]
211    pub const fn attempt(&self) -> u32 {
212        self.attempt
213    }
214
215    /// Whether anything has failed since the last success.
216    ///
217    /// A loop uses this to decide whether an empty queue is unremarkable or
218    /// worth a log line.
219    #[must_use]
220    pub const fn is_retrying(&self) -> bool {
221        self.attempt > 0
222    }
223
224    /// The ceiling this backoff will not exceed.
225    #[must_use]
226    pub const fn max_delay(&self) -> Duration {
227        self.max
228    }
229
230    /// Record a failure: multiply the delay, up to the ceiling.
231    ///
232    /// Returns the delay that was in force *for this failure*, which is
233    /// what a metric should record — not the multiplied value the next one
234    /// will use.
235    pub const fn failed(&mut self) -> Duration {
236        let current = self.delay;
237
238        // Saturating rather than wrapping: at the ceiling this is a no-op,
239        // and no arithmetic here can overflow into a tiny delay.
240        let raised = self.delay.saturating_mul(self.factor);
241        self.delay = if raised.as_nanos() > self.max.as_nanos() {
242            self.max
243        } else {
244            raised
245        };
246        self.attempt = self.attempt.saturating_add(1);
247
248        current
249    }
250
251    /// Record a success: back to the initial delay, attempt count zero.
252    ///
253    /// Any success resets it, not just a run of them.
254    pub const fn succeeded(&mut self) {
255        self.delay = self.initial;
256        self.attempt = 0;
257    }
258
259    /// The slice to sleep for, given how much of a delay is left.
260    ///
261    /// Never zero while `remaining` is non-zero, which is what stops
262    /// [`Backoff::wait`] spinning.
263    ///
264    /// # Examples
265    ///
266    /// ```
267    /// use std::time::Duration;
268    /// use graceful_worker::Backoff;
269    ///
270    /// let backoff = Backoff::new();
271    ///
272    /// // A short remainder is slept in one go.
273    /// assert_eq!(backoff.sleep_slice(Duration::from_secs(3)), Duration::from_secs(3));
274    /// // A long one is sliced, so shutdown stays responsive.
275    /// assert_eq!(backoff.sleep_slice(Duration::from_secs(900)), Duration::from_secs(60));
276    /// ```
277    #[must_use]
278    pub const fn sleep_slice(&self, remaining: Duration) -> Duration {
279        if self.slice.is_zero() || remaining.as_nanos() <= self.slice.as_nanos() {
280            remaining
281        } else {
282            self.slice
283        }
284    }
285
286    /// Wait out the current delay, in slices, unless a stop arrives first.
287    ///
288    /// Returns `true` if the whole delay elapsed, `false` if it was cut
289    /// short — in which case the loop should stop rather than retry.
290    ///
291    /// This is the method the crate exists for. See the module
292    /// documentation.
293    ///
294    /// # Examples
295    ///
296    /// The usual loop shape. Marked `no_run` because running it would do
297    /// exactly what it says: wait five real seconds.
298    ///
299    /// ```no_run
300    /// # #[tokio::main(flavor = "current_thread")]
301    /// # async fn main() {
302    /// use graceful_worker::{Backoff, Shutdown};
303    ///
304    /// let shutdown = Shutdown::new();
305    /// let watcher = shutdown.watcher();
306    /// let mut backoff = Backoff::new();
307    ///
308    /// while watcher.is_running() {
309    ///     let worked = false; // ... do a unit of work ...
310    ///     if worked {
311    ///         backoff.succeeded();
312    ///     } else {
313    ///         backoff.failed();
314    ///         if !backoff.wait(&watcher).await {
315    ///             break; // asked to stop mid-wait
316    ///         }
317    ///     }
318    ///     # break;
319    /// }
320    /// # }
321    /// ```
322    pub async fn wait(&self, watcher: &Watcher) -> bool {
323        let mut remaining = self.delay;
324
325        while !remaining.is_zero() {
326            let slice = self.sleep_slice(remaining);
327            if !watcher.sleep(slice).await {
328                return false;
329            }
330            remaining = remaining.saturating_sub(slice);
331        }
332
333        true
334    }
335}
336
337impl Default for Backoff {
338    fn default() -> Self {
339        Self::new()
340    }
341}
342
343#[cfg(test)]
344mod tests {
345    use crate::shutdown::Shutdown;
346
347    use super::*;
348
349    #[test]
350    fn the_default_schedule_starts_at_five_seconds() {
351        assert_eq!(Backoff::new().delay(), DEFAULT_INITIAL_DELAY);
352        assert_eq!(Backoff::new().max_delay(), DEFAULT_MAX_DELAY);
353    }
354
355    #[test]
356    fn the_delay_multiplies_on_each_failure() {
357        let mut backoff = Backoff::new();
358        let seen: Vec<u64> = (0..6).map(|_| backoff.failed().as_secs()).collect();
359        assert_eq!(seen, [5, 10, 20, 40, 80, 160]);
360    }
361
362    #[test]
363    fn the_delay_stops_at_the_ceiling() {
364        let mut backoff = Backoff::new();
365        for _ in 0..50 {
366            backoff.failed();
367        }
368        assert_eq!(backoff.delay(), DEFAULT_MAX_DELAY);
369    }
370
371    #[test]
372    fn the_ceiling_is_not_overshot_on_the_way_up() {
373        // 10, 20, 40, 80, 160, 320, 640, and then a doubling that would
374        // reach 1280.
375        let mut backoff = Backoff::new();
376        for _ in 0..7 {
377            backoff.failed();
378        }
379        assert_eq!(backoff.delay(), Duration::from_secs(640));
380        backoff.failed();
381        assert_eq!(backoff.delay(), DEFAULT_MAX_DELAY, "clamped rather than 1280");
382    }
383
384    #[test]
385    fn any_success_resets_it_completely() {
386        let mut backoff = Backoff::new();
387        for _ in 0..10 {
388            backoff.failed();
389        }
390        assert!(backoff.is_retrying());
391
392        backoff.succeeded();
393
394        assert_eq!(backoff.delay(), DEFAULT_INITIAL_DELAY);
395        assert_eq!(backoff.attempt(), 0);
396        assert!(!backoff.is_retrying());
397    }
398
399    #[test]
400    fn failed_reports_the_delay_that_applied_not_the_next_one() {
401        // A metric should chart the actual schedule.
402        let mut backoff = Backoff::new();
403        assert_eq!(backoff.failed(), Duration::from_secs(5));
404        assert_eq!(backoff.delay(), Duration::from_secs(10));
405    }
406
407    #[test]
408    fn the_schedule_is_configurable() {
409        let mut backoff = Backoff::new()
410            .with_initial_delay(Duration::from_millis(100))
411            .with_max_delay(Duration::from_secs(1))
412            .with_factor(3);
413
414        assert_eq!(backoff.delay(), Duration::from_millis(100));
415        assert_eq!(backoff.failed(), Duration::from_millis(100));
416        assert_eq!(backoff.delay(), Duration::from_millis(300));
417        backoff.failed();
418        assert_eq!(backoff.delay(), Duration::from_millis(900));
419        backoff.failed();
420        assert_eq!(backoff.delay(), Duration::from_secs(1), "clamped");
421
422        backoff.succeeded();
423        assert_eq!(backoff.delay(), Duration::from_millis(100));
424    }
425
426    #[test]
427    fn an_initial_longer_than_the_maximum_is_clamped() {
428        // Otherwise the schedule would count downwards.
429        let backoff = Backoff::new()
430            .with_max_delay(Duration::from_secs(2))
431            .with_initial_delay(Duration::from_secs(30));
432
433        assert_eq!(backoff.delay(), Duration::from_secs(2));
434    }
435
436    #[test]
437    fn a_factor_of_one_is_a_constant_delay() {
438        let mut backoff = Backoff::new().with_factor(1);
439        for _ in 0..5 {
440            assert_eq!(backoff.failed(), DEFAULT_INITIAL_DELAY);
441        }
442    }
443
444    #[test]
445    fn a_factor_of_zero_is_treated_as_one() {
446        // A backoff that multiplied by zero would retry instantly forever.
447        let mut backoff = Backoff::new().with_factor(0);
448        backoff.failed();
449        assert_eq!(backoff.delay(), DEFAULT_INITIAL_DELAY);
450    }
451
452    #[test]
453    fn a_long_wait_is_slept_in_slices() {
454        let backoff = Backoff::new();
455        assert_eq!(backoff.sleep_slice(DEFAULT_MAX_DELAY), DEFAULT_SLICE);
456        assert_eq!(backoff.sleep_slice(Duration::from_secs(61)), DEFAULT_SLICE);
457        assert_eq!(backoff.sleep_slice(Duration::from_secs(60)), DEFAULT_SLICE);
458        assert_eq!(
459            backoff.sleep_slice(Duration::from_secs(1)),
460            Duration::from_secs(1)
461        );
462        assert_eq!(backoff.sleep_slice(Duration::ZERO), Duration::ZERO);
463    }
464
465    #[test]
466    fn a_zero_slice_disables_slicing() {
467        let backoff = Backoff::new().with_slice(Duration::ZERO);
468        assert_eq!(backoff.sleep_slice(DEFAULT_MAX_DELAY), DEFAULT_MAX_DELAY);
469    }
470
471    #[test]
472    fn a_slice_is_never_zero_while_time_remains() {
473        // What stops `wait` spinning.
474        for slice in [Duration::ZERO, Duration::from_secs(1), Duration::from_secs(90)] {
475            let backoff = Backoff::new().with_slice(slice);
476            assert!(!backoff.sleep_slice(Duration::from_secs(30)).is_zero());
477        }
478    }
479
480    #[tokio::test(start_paused = true)]
481    async fn waiting_serves_the_whole_delay_when_nothing_stops_it() {
482        let shutdown = Shutdown::new();
483        let watcher = shutdown.watcher();
484        let mut backoff = Backoff::new();
485        for _ in 0..20 {
486            backoff.failed();
487        }
488        assert_eq!(backoff.delay(), DEFAULT_MAX_DELAY);
489
490        assert!(backoff.wait(&watcher).await, "fifteen minutes, in slices");
491    }
492
493    #[tokio::test(start_paused = true)]
494    async fn waiting_is_abandoned_when_a_stop_arrives() {
495        // The reason this crate exists: a fifteen-minute backoff must not
496        // outlast a thirty-second grace period.
497        let shutdown = Shutdown::new();
498        let watcher = shutdown.watcher();
499        let mut backoff = Backoff::new();
500        for _ in 0..20 {
501            backoff.failed();
502        }
503
504        let waiting = tokio::spawn(async move { backoff.wait(&watcher).await });
505        tokio::task::yield_now().await;
506        shutdown.stop();
507
508        assert!(!waiting.await.expect("the waiting task"));
509    }
510
511    #[tokio::test(start_paused = true)]
512    async fn a_stop_is_noticed_within_one_slice() {
513        // The property, stated as an elapsed-time assertion rather than as
514        // a description of the loop.
515        let shutdown = Shutdown::new();
516        let watcher = shutdown.watcher();
517        let backoff = Backoff::new().with_slice(Duration::from_secs(10));
518        let mut backoff = backoff;
519        for _ in 0..20 {
520            backoff.failed();
521        }
522
523        let started = tokio::time::Instant::now();
524        let waiting = tokio::spawn(async move { backoff.wait(&watcher).await });
525
526        // Let one slice pass, then ask to stop partway through the next.
527        tokio::time::sleep(Duration::from_secs(15)).await;
528        shutdown.stop();
529        assert!(!waiting.await.expect("the waiting task"));
530
531        let elapsed = started.elapsed();
532        assert!(
533            elapsed < Duration::from_secs(30),
534            "a stop must be noticed within a slice, not after the whole \
535             delay; took {elapsed:?}"
536        );
537    }
538
539    #[tokio::test(start_paused = true)]
540    async fn slicing_is_not_what_makes_the_wait_interruptible() {
541        // The corrected claim, as a test. With slicing switched off
542        // entirely the wait is a single 15-minute sleep, and a stop still
543        // ends it at once -- because `Watcher::sleep` is a `select!`
544        // against the token, not because the sleep was chopped up.
545        let shutdown = Shutdown::new();
546        let watcher = shutdown.watcher();
547        let mut backoff = Backoff::new().with_slice(Duration::ZERO);
548        for _ in 0..20 {
549            backoff.failed();
550        }
551        assert_eq!(backoff.delay(), DEFAULT_MAX_DELAY);
552        assert_eq!(
553            backoff.sleep_slice(DEFAULT_MAX_DELAY),
554            DEFAULT_MAX_DELAY,
555            "one unsliced sleep"
556        );
557
558        let started = tokio::time::Instant::now();
559        let waiting = tokio::spawn(async move { backoff.wait(&watcher).await });
560        tokio::time::sleep(Duration::from_secs(5)).await;
561        shutdown.stop();
562
563        assert!(!waiting.await.expect("the waiting task"));
564        assert!(
565            started.elapsed() < Duration::from_secs(30),
566            "an unsliced wait is abandoned just as promptly; took {:?}",
567            started.elapsed()
568        );
569    }
570
571    #[tokio::test(start_paused = true)]
572    async fn a_zero_delay_waits_for_nothing() {
573        let shutdown = Shutdown::new();
574        let watcher = shutdown.watcher();
575        let backoff = Backoff::new().with_initial_delay(Duration::ZERO);
576        assert!(backoff.wait(&watcher).await);
577    }
578
579    #[test]
580    fn a_default_backoff_is_a_new_one() {
581        assert_eq!(Backoff::default(), Backoff::new());
582    }
583}