Skip to main content

crispy_stream_checker/
backoff.rs

1//! Backoff strategies for stream check retries.
2//!
3//! Translated from IPTVChecker-Python `get_retry_delay()` inside
4//! `check_channel_status()`:
5//!
6//! ```python
7//! def get_retry_delay(attempt_index):
8//!     if backoff_mode == 'none':
9//!         return 0
10//!     if backoff_mode == 'exponential':
11//!         return min(2 ** attempt_index, 30)
12//!     return min(attempt_index + 1, 10)  # linear
13//! ```
14
15use std::time::Duration;
16
17use serde::{Deserialize, Serialize};
18
19/// Backoff strategy for retrying failed stream checks.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
21#[serde(rename_all = "lowercase")]
22pub enum BackoffStrategy {
23    /// No delay between retries.
24    None,
25    /// Linear backoff: delay = min(attempt + 1, 10) seconds.
26    #[default]
27    Linear,
28    /// Exponential backoff: delay = min(2^attempt, 30) seconds.
29    Exponential,
30}
31
32impl BackoffStrategy {
33    /// Compute the delay for a given zero-based attempt index.
34    ///
35    /// Faithful translation of IPTVChecker-Python `get_retry_delay()`:
36    /// - None: always 0
37    /// - Linear: `min(attempt_index + 1, 10)` seconds
38    /// - Exponential: `min(2^attempt_index, 30)` seconds
39    pub fn delay(&self, attempt_index: u32) -> Duration {
40        match self {
41            BackoffStrategy::None => Duration::ZERO,
42            BackoffStrategy::Linear => {
43                let secs = (attempt_index + 1).min(10);
44                Duration::from_secs(u64::from(secs))
45            }
46            BackoffStrategy::Exponential => {
47                let secs = 2u64.saturating_pow(attempt_index).min(30);
48                Duration::from_secs(secs)
49            }
50        }
51    }
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    #[test]
59    fn no_backoff_always_zero() {
60        for attempt in 0..10 {
61            assert_eq!(BackoffStrategy::None.delay(attempt), Duration::ZERO);
62        }
63    }
64
65    #[test]
66    fn linear_backoff_increments_by_one() {
67        assert_eq!(BackoffStrategy::Linear.delay(0), Duration::from_secs(1));
68        assert_eq!(BackoffStrategy::Linear.delay(1), Duration::from_secs(2));
69        assert_eq!(BackoffStrategy::Linear.delay(2), Duration::from_secs(3));
70    }
71
72    #[test]
73    fn linear_backoff_caps_at_10() {
74        assert_eq!(BackoffStrategy::Linear.delay(9), Duration::from_secs(10));
75        assert_eq!(BackoffStrategy::Linear.delay(15), Duration::from_secs(10));
76        assert_eq!(BackoffStrategy::Linear.delay(100), Duration::from_secs(10));
77    }
78
79    #[test]
80    fn exponential_backoff_doubles() {
81        assert_eq!(
82            BackoffStrategy::Exponential.delay(0),
83            Duration::from_secs(1)
84        );
85        assert_eq!(
86            BackoffStrategy::Exponential.delay(1),
87            Duration::from_secs(2)
88        );
89        assert_eq!(
90            BackoffStrategy::Exponential.delay(2),
91            Duration::from_secs(4)
92        );
93        assert_eq!(
94            BackoffStrategy::Exponential.delay(3),
95            Duration::from_secs(8)
96        );
97    }
98
99    #[test]
100    fn exponential_backoff_caps_at_30() {
101        // 2^5 = 32, capped to 30
102        assert_eq!(
103            BackoffStrategy::Exponential.delay(5),
104            Duration::from_secs(30)
105        );
106        assert_eq!(
107            BackoffStrategy::Exponential.delay(10),
108            Duration::from_secs(30)
109        );
110    }
111}