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        let reconnect_time_ms = reconnect_time_ms.max(self.min_sleep_ms) as f32;
71        let mut sleep_ms =
72            match self.backoff_multiplier.is_finite() && 1.0 <= self.backoff_multiplier {
73                true => reconnect_time_ms * self.backoff_multiplier.powi(attempt as _),
74                false => reconnect_time_ms,
75            };
76
77        if !sleep_ms.is_finite() || (self.max_backoff_ms as f32) <= sleep_ms {
78            sleep_ms = self.max_backoff_ms as _;
79        }
80
81        if self.jitter && reconnect_time_ms < sleep_ms {
82            let jitter_factor =
83                match jitter_factor.is_finite() && (0.0..=1.0).contains(&jitter_factor) {
84                    true => jitter_factor,
85                    false => 1.0,
86                };
87            sleep_ms = reconnect_time_ms + jitter_factor * (sleep_ms - reconnect_time_ms)
88        }
89
90        Some(Duration::from_millis(sleep_ms as _))
91    }
92
93    /// Calculates the delay duration for the next reconnection attempt.
94    ///
95    /// Returns [`None`] if the `attempt` count exceeds [`Self::max_retries`].
96    #[must_use]
97    #[cfg(feature = "fastrand")]
98    pub fn calculate_backoff(&self, reconnect_time_ms: u32, attempt: u32) -> Option<Duration> {
99        self.calculate_backoff_with_factor(reconnect_time_ms, attempt, fastrand::f32())
100    }
101}
102
103impl Default for SseRetryConfig {
104    fn default() -> Self {
105        Self::new()
106    }
107}