taskvisor/policies/
backoff.rs

1//! # Backoff policy for retrying tasks.
2//!
3//! [`BackoffPolicy`] controls how retry delays grow after repeated failures.
4//! It is parameterized by:
5//! - [`BackoffPolicy::factor`] the multiplicative growth factor;
6//! - [`BackoffPolicy::first`] the initial delay;
7//! - [`BackoffPolicy::max`] the maximum delay cap.
8//!
9//! # Example
10//! ```rust
11//! use std::time::Duration;
12//! use taskvisor::{BackoffPolicy, JitterPolicy};
13//!
14//! let backoff = BackoffPolicy {
15//!     first: Duration::from_millis(100),
16//!     max: Duration::from_secs(10),
17//!     factor: 2.0,
18//!     jitter: JitterPolicy::None,
19//! };
20//!
21//! // First attempt - uses 'first' (clamped to max)
22//! assert_eq!(backoff.next(None), Duration::from_millis(100));
23//!
24//! // Second attempt - multiplied by factor (100ms * 2.0 = 200ms)
25//! assert_eq!(backoff.next(Some(Duration::from_millis(100))), Duration::from_millis(200));
26//!
27//! // When previous delay exceeds max, result is capped at max
28//! // (20s * 2.0 = 40s, but capped at max=10s)
29//! assert_eq!(backoff.next(Some(Duration::from_secs(20))), Duration::from_secs(10));
30//! ```
31
32use std::time::Duration;
33
34use crate::policies::jitter::JitterPolicy;
35
36/// Retry backoff policy.
37///
38/// Encapsulates parameters that determine how retry delays grow:
39/// - [`BackoffPolicy::factor`] — multiplicative growth factor;
40/// - [`BackoffPolicy::first`] — the initial delay;
41/// - [`BackoffPolicy::max`] — the maximum delay cap.
42#[derive(Clone, Copy, Debug)]
43pub struct BackoffPolicy {
44    /// Initial delay before the first retry.
45    pub first: Duration,
46    /// Maximum delay cap for retries.
47    pub max: Duration,
48    /// Multiplicative growth factor (`>= 1.0` recommended).
49    pub factor: f64,
50    /// Jitter policy to prevent thundering herd.
51    pub jitter: JitterPolicy,
52}
53
54impl Default for BackoffPolicy {
55    /// Returns a strategy with:
56    /// - `factor = 1.0` (constant delay);
57    /// - `first = 100ms`;
58    /// - `max = 30s`.
59    fn default() -> Self {
60        Self {
61            first: Duration::from_millis(100),
62            max: Duration::from_secs(30),
63            jitter: JitterPolicy::None,
64            factor: 1.0,
65        }
66    }
67}
68
69impl BackoffPolicy {
70    /// Computes the next delay based on the previous one.
71    ///
72    /// - If `prev` is `None`, returns `first` **clamped to `max`**.
73    /// - Otherwise multiplies the previous delay by [`BackoffPolicy::factor`], and caps it at [`BackoffPolicy::max`].
74    ///
75    /// # Notes
76    /// - If `factor` is less than 1.0, delays decrease over time (not typical).
77    /// - If `factor` equals 1.0, delay remains constant at `first` (up to `max`).
78    /// - If `factor` is greater than 1.0, delays grow exponentially.
79    pub fn next(&self, prev: Option<Duration>) -> Duration {
80        let unclamped = match prev {
81            None => self.first,
82            Some(d) => {
83                let mul = d.as_secs_f64() * self.factor;
84                if !mul.is_finite() {
85                    self.max
86                } else {
87                    d.mul_f64(self.factor)
88                }
89            }
90        };
91
92        let base = if unclamped > self.max {
93            self.max
94        } else {
95            unclamped
96        };
97        match self.jitter {
98            JitterPolicy::Decorrelated => {
99                self.jitter
100                    .apply_decorrelated(self.first.min(self.max), base, self.max)
101            }
102            _ => self.jitter.apply(base),
103        }
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use std::time::Duration;
111
112    #[test]
113    fn test_first_delay_no_jitter() {
114        let policy = BackoffPolicy {
115            first: Duration::from_millis(100),
116            max: Duration::from_secs(30),
117            factor: 2.0,
118            jitter: JitterPolicy::None,
119        };
120        assert_eq!(policy.next(None), Duration::from_millis(100));
121    }
122
123    #[test]
124    fn test_exponential_growth_no_jitter() {
125        let policy = BackoffPolicy {
126            first: Duration::from_millis(100),
127            max: Duration::from_secs(30),
128            factor: 2.0,
129            jitter: JitterPolicy::None,
130        };
131
132        let d1 = policy.next(None);
133        assert_eq!(d1, Duration::from_millis(100));
134
135        let d2 = policy.next(Some(d1));
136        assert_eq!(d2, Duration::from_millis(200));
137
138        let d3 = policy.next(Some(d2));
139        assert_eq!(d3, Duration::from_millis(400));
140
141        let d4 = policy.next(Some(d3));
142        assert_eq!(d4, Duration::from_millis(800));
143    }
144
145    #[test]
146    fn test_first_exceeds_max() {
147        let policy = BackoffPolicy {
148            first: Duration::from_secs(10),
149            max: Duration::from_secs(5),
150            factor: 2.0,
151            jitter: JitterPolicy::None,
152        };
153        let d1 = policy.next(None);
154        assert_eq!(d1, Duration::from_secs(5));
155    }
156
157    #[test]
158    fn test_monotonic_growth_with_equal_jitter() {
159        let policy = BackoffPolicy {
160            first: Duration::from_millis(100),
161            max: Duration::from_secs(30),
162            factor: 2.0,
163            jitter: JitterPolicy::Equal,
164        };
165
166        let mut prev = None;
167        let mut prev_delay = Duration::ZERO;
168
169        for i in 0..20 {
170            let delay = policy.next(prev);
171            if i > 5 {
172                assert!(
173                    delay >= Duration::from_millis(10),
174                    "iteration {}: delay {:?} is suspiciously low (prev: {:?})",
175                    i,
176                    delay,
177                    prev_delay
178                );
179            }
180            prev_delay = delay;
181            prev = Some(delay);
182        }
183    }
184
185    #[test]
186    fn test_decorrelated_jitter_no_negative_feedback() {
187        let policy = BackoffPolicy {
188            first: Duration::from_millis(100),
189            max: Duration::from_secs(30),
190            factor: 2.0,
191            jitter: JitterPolicy::Decorrelated,
192        };
193
194        let mut prev = None;
195        let mut min_seen = Duration::from_secs(999);
196        let mut max_seen = Duration::ZERO;
197        for i in 0..100 {
198            let delay = policy.next(prev);
199
200            min_seen = min_seen.min(delay);
201            max_seen = max_seen.max(delay);
202
203            assert!(
204                delay >= Duration::from_millis(50), // с запасом на jitter
205                "iteration {}: delay {:?} too low (min_seen: {:?})",
206                i,
207                delay,
208                min_seen
209            );
210            if i > 10 {
211                assert!(
212                    delay >= Duration::from_millis(200),
213                    "iteration {}: delay {:?} suspiciously low after warmup",
214                    i,
215                    delay
216                );
217            }
218            prev = Some(delay);
219        }
220        println!("Decorrelated stats: min={:?}, max={:?}", min_seen, max_seen);
221        assert!(
222            max_seen > min_seen * 3,
223            "Range too narrow for decorrelated jitter"
224        );
225    }
226
227    #[test]
228    fn test_full_jitter_bounds() {
229        let policy = BackoffPolicy {
230            first: Duration::from_millis(1000),
231            max: Duration::from_secs(30),
232            factor: 1.0,
233            jitter: JitterPolicy::Full,
234        };
235        for _ in 0..50 {
236            let delay = policy.next(Some(Duration::from_millis(1000)));
237            assert!(delay <= Duration::from_millis(1000));
238        }
239    }
240
241    #[test]
242    fn test_equal_jitter_bounds() {
243        let policy = BackoffPolicy {
244            first: Duration::from_millis(1000),
245            max: Duration::from_secs(30),
246            factor: 1.0, // constant base
247            jitter: JitterPolicy::Equal,
248        };
249        for _ in 0..50 {
250            let delay = policy.next(Some(Duration::from_millis(1000)));
251            assert!(delay >= Duration::from_millis(500));
252            assert!(delay <= Duration::from_millis(1000));
253        }
254    }
255}