Skip to main content

sse_core/
retry.rs

1use core::time::Duration;
2
3/// Configuration for exponential backoff and jitter during stream reconnections.
4#[derive(Debug, Clone, Copy)]
5#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
6pub struct SseRetryConfig {
7    /// The maximum number of consecutive connection attempts before giving up.
8    pub max_retries: u32,
9    /// The absolute maximum wait time between connection attempts in milliseconds.
10    pub max_backoff_ms: u32,
11    /// The absolute minimum wait time between connection attempts in milliseconds.
12    pub min_sleep_ms: u32,
13    /// The multiplier applied to the delay after each failed attempt.
14    pub backoff_multiplier: f32,
15    /// Whether to apply randomness (jitter) to the reconnect delay.
16    pub jitter: bool,
17}
18
19impl SseRetryConfig {
20    /// Creates a new retry configuration with sensible defaults.
21    ///
22    /// The default configuration applies an exponential backoff multiplier of `2.0`, caps the
23    /// maximum delay at `60,000` milliseconds (1 minute), caps the number of retries to 20, and
24    /// enables jitter to prevent thundering herd scenarios.
25    ///
26    /// # Example
27    /// ```rust
28    /// use sse_core::SseRetryConfig;
29    ///
30    /// let config = SseRetryConfig::new();
31    /// assert_eq!(config.max_backoff_ms, 60_000);
32    /// assert!(config.jitter);
33    /// ```
34    #[inline]
35    #[must_use]
36    pub const fn new() -> Self {
37        Self {
38            max_retries: 20,
39            max_backoff_ms: 60_000,
40            min_sleep_ms: 200,
41            backoff_multiplier: 2.0,
42            jitter: true,
43        }
44    }
45
46    /// Creates a retry configuration that disables all automatic retries.
47    #[inline]
48    #[must_use]
49    pub const fn disabled() -> Self {
50        Self {
51            max_retries: 0,
52            ..Self::new()
53        }
54    }
55
56    /// Calculates the delay duration for the next reconnection attempt.
57    ///
58    /// Returns [`None`] if the `attempt` count exceeds [`Self::max_retries`].
59    #[must_use]
60    pub fn calculate_backoff_with_factor(
61        &self,
62        reconnect_time_ms: u32,
63        attempt: u32,
64        jitter_factor: f32,
65    ) -> Option<Duration> {
66        if self.max_retries <= attempt {
67            return None;
68        }
69
70        assert!(self.min_sleep_ms <= self.max_backoff_ms);
71
72        let reconnect_time_ms = reconnect_time_ms.max(self.min_sleep_ms) as f32;
73        let mut sleep_ms =
74            match self.backoff_multiplier.is_finite() && 1.0 <= self.backoff_multiplier {
75                true => reconnect_time_ms * self.backoff_multiplier.powi(attempt as _),
76                false => reconnect_time_ms,
77            };
78
79        if !sleep_ms.is_finite() || (self.max_backoff_ms as f32) <= sleep_ms {
80            sleep_ms = self.max_backoff_ms as _;
81        }
82
83        if self.jitter && reconnect_time_ms < sleep_ms {
84            let jitter_factor =
85                match jitter_factor.is_finite() && (0.0..=1.0).contains(&jitter_factor) {
86                    true => jitter_factor,
87                    false => 1.0,
88                };
89            sleep_ms = reconnect_time_ms + jitter_factor * (sleep_ms - reconnect_time_ms)
90        }
91
92        Some(Duration::from_millis(sleep_ms as _))
93    }
94
95    /// Calculates the delay duration for the next reconnection attempt.
96    ///
97    /// Returns [`None`] if the `attempt` count exceeds [`Self::max_retries`].
98    #[must_use]
99    #[cfg(feature = "fastrand")]
100    pub fn calculate_backoff(&self, reconnect_time_ms: u32, attempt: u32) -> Option<Duration> {
101        self.calculate_backoff_with_factor(reconnect_time_ms, attempt, fastrand::f32())
102    }
103}
104
105impl Default for SseRetryConfig {
106    fn default() -> Self {
107        Self::new()
108    }
109}