1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
//! Internal support for retrying operations.
///
/// This is intended for use by this crate and special-purpose crates like `taskcluster-download`,
/// not for general use.
use backoff::backoff::Backoff as BackoffTrait;
use backoff::ExponentialBackoff;
use std::time::Duration;

/// Configuration for a client's automatic retrying.  The field names here match those
/// of the JS client.
#[derive(Debug, Clone)]
pub struct Retry {
    /// Number of retries (not counting the first try) for transient errors. Zero
    /// to disable retries entirely. (default 5)
    pub retries: u32,

    /// Maximum interval between retries (default 30s)
    pub max_delay: Duration,

    /// Factor for delay: 2 ^ retry * delay_factor.  100ms (default) is a good value for servers,
    /// and 500ms a good value for background processes. (default 100ms)
    pub delay_factor: Duration,

    /// Randomization factor added as.
    /// delay = delay * random([1 - randomizationFactor; 1 + randomizationFactor]) (default 0.25)
    pub randomization_factor: f64,
}

impl Default for Retry {
    fn default() -> Self {
        Self {
            retries: 5,
            max_delay: Duration::from_secs(30),
            delay_factor: Duration::from_millis(100),
            randomization_factor: 0.25,
        }
    }
}

/// Backoff tracker for a single, possibly-retried operation.  This is a thin wrapper around
/// [backoff::ExponentialBackoff].
#[derive(Debug)]
pub struct Backoff<'a> {
    retry: &'a Retry,
    tries: u32,
    backoff: ExponentialBackoff,
}

impl<'a> Backoff<'a> {
    pub fn new(retry: &Retry) -> Backoff {
        let mut backoff = ExponentialBackoff {
            max_elapsed_time: None, // we count retries instead
            max_interval: retry.max_delay,
            initial_interval: retry.delay_factor,
            multiplier: 2.0, // hard-coded value in JS client
            #[cfg(not(test))]
            randomization_factor: retry.randomization_factor,
            #[cfg(test)]
            randomization_factor: 0.0,
            ..Default::default()
        };
        backoff.reset();
        Backoff {
            retry,
            tries: 0,
            backoff,
        }
    }

    /// Return the next backoff interval or, if the operation should not be retried,
    /// None.
    pub fn next_backoff(&mut self) -> Option<Duration> {
        self.tries += 1;
        if self.tries > self.retry.retries {
            None
        } else {
            self.backoff.next_backoff()
        }
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[tokio::test]
    async fn backoff_three_retries() {
        let retry = Retry {
            retries: 3,
            ..Default::default()
        };
        let mut backoff = Backoff::new(&retry);
        // ..try, fail
        assert_eq!(backoff.next_backoff(), Some(Duration::from_millis(100)));
        // ..retry 1, fail
        assert_eq!(backoff.next_backoff(), Some(Duration::from_millis(200)));
        // ..retry 2, fail
        assert_eq!(backoff.next_backoff(), Some(Duration::from_millis(400)));
        // ..retry 3, fail
        assert_eq!(backoff.next_backoff(), None); // out of retries
    }
}