reliakit_retry/policy.rs
1//! The retry policy: how many attempts, and the backoff schedule between them.
2
3use core::time::Duration;
4
5use reliakit_backoff::Backoff;
6
7/// How many times to attempt an operation and how long to wait between tries.
8///
9/// `max_attempts` counts the *total* number of attempts, including the first
10/// one, so `max_attempts = 1` means "try once, never retry" and
11/// `max_attempts = 3` means "the first try plus up to two retries". A value of
12/// `0` is rejected by [`new`](Self::new).
13///
14/// The [`Backoff`] supplies the delay *before each retry*. It is consulted only
15/// for delay values; the attempt count is governed solely by `max_attempts`, so
16/// the two limits never fight. If the backoff yields no delay for a given retry
17/// index, [`Duration::ZERO`] is used.
18#[derive(Debug, Clone, Copy)]
19pub struct RetryPolicy {
20 max_attempts: u32,
21 backoff: Backoff,
22}
23
24impl RetryPolicy {
25 /// Creates a policy that makes at most `max_attempts` attempts, waiting
26 /// according to `backoff` between them.
27 ///
28 /// Returns `None` if `max_attempts` is `0`, which would never run the
29 /// operation at all.
30 pub const fn new(max_attempts: u32, backoff: Backoff) -> Option<Self> {
31 if max_attempts == 0 {
32 return None;
33 }
34 Some(Self {
35 max_attempts,
36 backoff,
37 })
38 }
39
40 /// A policy that tries exactly once and never retries.
41 ///
42 /// Equivalent to `RetryPolicy::new(1, _).unwrap()`; the backoff is never
43 /// consulted because there is no retry.
44 pub const fn single(backoff: Backoff) -> Self {
45 Self {
46 max_attempts: 1,
47 backoff,
48 }
49 }
50
51 /// The maximum number of attempts (always `>= 1`).
52 pub const fn max_attempts(&self) -> u32 {
53 self.max_attempts
54 }
55
56 /// The backoff schedule used between attempts.
57 pub const fn backoff(&self) -> &Backoff {
58 &self.backoff
59 }
60
61 /// The delay to wait before the next retry, given how many attempts have
62 /// already completed.
63 ///
64 /// `completed_attempts` is the 1-based number of attempts already made, so
65 /// the delay before the first retry is `delay_before_retry(1)`. The backoff
66 /// is indexed zero-based (retry `0` is the first retry); if it yields no
67 /// delay, [`Duration::ZERO`] is returned.
68 pub fn delay_before_retry(&self, completed_attempts: u32) -> Duration {
69 let retry_index = completed_attempts.saturating_sub(1);
70 self.backoff.delay(retry_index).unwrap_or(Duration::ZERO)
71 }
72}