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
19#[cfg(not(feature = "std"))]
20fn pown(mut x: f32, mut n: u32) -> f32 {
21    let mut out = 1.0;
22    while 0 < n {
23        if n & 1 != 0 {
24            out *= x;
25        }
26        x *= x;
27        n /= 2;
28    }
29    out
30}
31
32#[cfg(feature = "std")]
33fn pown(x: f32, n: u32) -> f32 {
34    x.powi(n.min(i32::MAX as _) as _)
35}
36
37impl SseRetryConfig {
38    /// Creates a new retry configuration with sensible defaults.
39    ///
40    /// The default configuration applies an exponential backoff multiplier of `2.0`, caps the
41    /// maximum delay at `60,000` milliseconds (1 minute), caps the number of retries to 20, and
42    /// enables jitter to prevent thundering herd scenarios.
43    ///
44    /// # Example
45    /// ```rust
46    /// use sse_core::SseRetryConfig;
47    ///
48    /// let config = SseRetryConfig::new();
49    /// assert_eq!(config.max_backoff_ms, 60_000);
50    /// assert!(config.jitter);
51    /// ```
52    #[inline]
53    #[must_use]
54    pub const fn new() -> Self {
55        Self {
56            max_retries: 20,
57            max_backoff_ms: 60_000,
58            min_sleep_ms: 200,
59            backoff_multiplier: 2.0,
60            jitter: true,
61        }
62    }
63
64    /// Creates a retry configuration that disables all automatic retries.
65    #[inline]
66    #[must_use]
67    pub const fn disabled() -> Self {
68        Self {
69            max_retries: 0,
70            ..Self::new()
71        }
72    }
73
74    /// Calculates the delay duration for the next reconnection attempt.
75    ///
76    /// Returns [`None`] if the `attempt` count exceeds [`Self::max_retries`].
77    #[must_use]
78    pub fn calculate_backoff_with_factor(
79        &self,
80        reconnect_time_ms: u32,
81        attempt: u32,
82        jitter_factor: f32,
83    ) -> Option<Duration> {
84        if self.max_retries <= attempt {
85            return None;
86        }
87
88        assert!(self.min_sleep_ms <= self.max_backoff_ms);
89
90        let reconnect_time_ms = reconnect_time_ms.max(self.min_sleep_ms) as f32;
91        let mut sleep_ms =
92            match self.backoff_multiplier.is_finite() && 1.0 <= self.backoff_multiplier {
93                true => reconnect_time_ms * pown(self.backoff_multiplier, attempt),
94                false => reconnect_time_ms,
95            };
96
97        if !sleep_ms.is_finite() || (self.max_backoff_ms as f32) <= sleep_ms {
98            sleep_ms = self.max_backoff_ms as _;
99        }
100
101        if self.jitter && reconnect_time_ms < sleep_ms {
102            let jitter_factor =
103                match jitter_factor.is_finite() && (0.0..=1.0).contains(&jitter_factor) {
104                    true => jitter_factor,
105                    false => 1.0,
106                };
107            sleep_ms = reconnect_time_ms + jitter_factor * (sleep_ms - reconnect_time_ms)
108        }
109
110        Some(Duration::from_millis(sleep_ms as _))
111    }
112
113    /// Calculates the delay duration for the next reconnection attempt.
114    ///
115    /// Returns [`None`] if the `attempt` count exceeds [`Self::max_retries`].
116    #[must_use]
117    #[cfg(feature = "fastrand")]
118    pub fn calculate_backoff(&self, reconnect_time_ms: u32, attempt: u32) -> Option<Duration> {
119        self.calculate_backoff_with_factor(reconnect_time_ms, attempt, fastrand::f32())
120    }
121}
122
123impl Default for SseRetryConfig {
124    fn default() -> Self {
125        Self::new()
126    }
127}