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    /// `jitter_factor` selects a point between the base delay and the computed
77    /// backoff; values outside `0.0..=1.0` (and non-finite ones) are treated as
78    /// `1.0`. It is ignored unless [`Self::jitter`] is set *and* the backoff has
79    /// actually grown past the base delay, since otherwise there is nothing to
80    /// pick between.
81    ///
82    /// Returns [`None`] if the `attempt` count exceeds [`Self::max_retries`].
83    ///
84    /// # Panics
85    ///
86    /// Panics if [`Self::min_sleep_ms`] is greater than [`Self::max_backoff_ms`],
87    /// which would make the delay range empty. Both fields are public, so this is
88    /// reachable by mutating a config after construction; the constructors
89    /// ([`new()`](Self::new), [`disabled()`](Self::disabled)) never produce such a
90    /// config. An exhausted `attempt` returns [`None`] before the check, so such a
91    /// config only panics while it still has retries left.
92    #[must_use]
93    pub fn calculate_backoff_with_factor(
94        &self,
95        reconnect_time_ms: u32,
96        attempt: u32,
97        jitter_factor: f32,
98    ) -> Option<Duration> {
99        if self.max_retries <= attempt {
100            return None;
101        }
102
103        assert!(self.min_sleep_ms <= self.max_backoff_ms);
104
105        let reconnect_time_ms = reconnect_time_ms.max(self.min_sleep_ms) as f32;
106        let mut sleep_ms =
107            match self.backoff_multiplier.is_finite() && 1.0 <= self.backoff_multiplier {
108                true => reconnect_time_ms * pown(self.backoff_multiplier, attempt),
109                false => reconnect_time_ms,
110            };
111
112        if !sleep_ms.is_finite() || (self.max_backoff_ms as f32) <= sleep_ms {
113            sleep_ms = self.max_backoff_ms as _;
114        }
115
116        if self.jitter && reconnect_time_ms < sleep_ms {
117            let jitter_factor =
118                match jitter_factor.is_finite() && (0.0..=1.0).contains(&jitter_factor) {
119                    true => jitter_factor,
120                    false => 1.0,
121                };
122            sleep_ms = reconnect_time_ms + jitter_factor * (sleep_ms - reconnect_time_ms)
123        }
124
125        Some(Duration::from_millis(sleep_ms as _))
126    }
127
128    /// Calculates the delay duration for the next reconnection attempt, drawing
129    /// the jitter factor from [`fastrand`].
130    ///
131    /// Returns [`None`] if the `attempt` count exceeds [`Self::max_retries`].
132    ///
133    /// # Panics
134    ///
135    /// Panics under the same conditions as
136    /// [`calculate_backoff_with_factor()`](Self::calculate_backoff_with_factor).
137    #[must_use]
138    #[cfg(feature = "fastrand")]
139    pub fn calculate_backoff(&self, reconnect_time_ms: u32, attempt: u32) -> Option<Duration> {
140        self.calculate_backoff_with_factor(reconnect_time_ms, attempt, fastrand::f32())
141    }
142}
143
144impl Default for SseRetryConfig {
145    fn default() -> Self {
146        Self::new()
147    }
148}