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}