Skip to main content

pgsync_core/
retry.rs

1//! Exponential backoff for transient failures.
2
3use crate::config::RetryBackoff;
4use std::time::Duration;
5
6#[derive(Debug, Clone)]
7pub struct Backoff {
8    policy: RetryBackoff,
9    attempt: u32,
10    next: Duration,
11}
12
13impl Backoff {
14    pub fn new(policy: &RetryBackoff) -> Self {
15        Backoff {
16            policy: policy.clone(),
17            attempt: 0,
18            next: policy.initial,
19        }
20    }
21
22    /// Delay before the next attempt, or `None` when attempts are exhausted.
23    pub fn next_delay(&mut self) -> Option<Duration> {
24        if self.policy.max_attempts != 0 && self.attempt >= self.policy.max_attempts {
25            return None;
26        }
27        self.attempt += 1;
28        let d = self.next.min(self.policy.max);
29        let scaled = self.next.as_secs_f64() * self.policy.multiplier;
30        self.next = Duration::from_secs_f64(scaled.min(self.policy.max.as_secs_f64()));
31        Some(d)
32    }
33
34    pub fn attempt(&self) -> u32 {
35        self.attempt
36    }
37
38    /// Call after a successful operation so the next failure starts from `initial`.
39    pub fn reset(&mut self) {
40        self.attempt = 0;
41        self.next = self.policy.initial;
42    }
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48
49    #[test]
50    fn grows_and_caps() {
51        let p = RetryBackoff {
52            initial: Duration::from_secs(1),
53            max: Duration::from_secs(5),
54            multiplier: 2.0,
55            max_attempts: 0,
56        };
57        let mut b = Backoff::new(&p);
58        let seq: Vec<u64> = (0..5).map(|_| b.next_delay().unwrap().as_secs()).collect();
59        assert_eq!(seq, vec![1, 2, 4, 5, 5]);
60        b.reset();
61        assert_eq!(b.next_delay().unwrap().as_secs(), 1);
62    }
63
64    #[test]
65    fn max_attempts_exhausts() {
66        let p = RetryBackoff {
67            initial: Duration::from_millis(10),
68            max: Duration::from_millis(10),
69            multiplier: 1.0,
70            max_attempts: 2,
71        };
72        let mut b = Backoff::new(&p);
73        assert!(b.next_delay().is_some());
74        assert!(b.next_delay().is_some());
75        assert!(b.next_delay().is_none());
76        assert_eq!(b.attempt(), 2);
77    }
78}