exocore_core/utils/
backoff.rs

1use crate::time::{Clock, Duration, Instant};
2
3#[derive(Clone, Copy)]
4pub struct BackoffConfig {
5    pub normal_constant: Duration,
6    pub failure_constant: Duration,
7    pub failure_exp_base: f32,
8    pub failure_exp_multiplier: Duration,
9    pub failure_maximum: Duration,
10}
11
12pub struct BackoffCalculator {
13    clock: Clock,
14    config: BackoffConfig,
15    consecutive_failures_count: u32,
16    next_execution_allow: Option<Instant>,
17}
18
19impl BackoffCalculator {
20    pub fn new(clock: Clock, config: BackoffConfig) -> BackoffCalculator {
21        BackoffCalculator {
22            clock,
23            config,
24            consecutive_failures_count: 0,
25            next_execution_allow: None,
26        }
27    }
28
29    pub fn can_execute_now(&self) -> bool {
30        if self.consecutive_failures_count == 0 {
31            return true;
32        }
33
34        self.next_execution_time() <= self.clock.instant()
35    }
36
37    pub fn next_execution_time(&self) -> Instant {
38        self.next_execution_allow
39            .unwrap_or_else(|| self.clock.instant())
40    }
41
42    pub fn increment_failure(&mut self) {
43        self.consecutive_failures_count += 1;
44        self.next_execution_allow = Some(self.clock.instant() + self.backoff_duration());
45    }
46
47    pub fn consecutive_failures_count(&self) -> u32 {
48        self.consecutive_failures_count
49    }
50
51    pub fn reset(&mut self) {
52        self.consecutive_failures_count = 0;
53        self.next_execution_allow = None;
54    }
55
56    pub fn backoff_duration(&self) -> Duration {
57        if self.consecutive_failures_count == 0 {
58            self.config.normal_constant
59        } else {
60            let multiplier =
61                self.config
62                    .failure_exp_base
63                    .powi((self.consecutive_failures_count - 1) as i32) as u32;
64
65            let duration =
66                self.config.failure_constant + self.config.failure_exp_multiplier * multiplier;
67
68            duration.min(self.config.failure_maximum)
69        }
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn basic_case() {
79        let clock = Clock::new();
80        let mut exp = BackoffCalculator::new(
81            clock,
82            BackoffConfig {
83                normal_constant: Duration::new(0, 0),
84                failure_constant: Duration::from_millis(5),
85                failure_exp_base: 2.0,
86                failure_exp_multiplier: Duration::from_millis(10),
87                failure_maximum: Duration::from_millis(50),
88            },
89        );
90
91        assert!(exp.can_execute_now());
92
93        exp.increment_failure();
94        assert_eq!(exp.backoff_duration().as_millis(), 15); // 5 + 2**0 * 10.0
95
96        exp.increment_failure();
97        assert_eq!(exp.backoff_duration().as_millis(), 25);
98
99        exp.increment_failure();
100        assert_eq!(exp.backoff_duration().as_millis(), 45);
101
102        exp.increment_failure();
103        assert_eq!(exp.backoff_duration().as_millis(), 50); // max
104
105        assert!(!exp.can_execute_now());
106
107        exp.reset();
108
109        assert!(exp.can_execute_now());
110    }
111}