1// -------------------------------------------------------------------------------------------------
2// Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3// https://nautechsystems.io
4//
5// Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6// You may not use this file except in compliance with the License.
7// You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14// -------------------------------------------------------------------------------------------------
1516//! Provides an implementation of an exponential backoff mechanism with jitter support.
17//! It is used for managing reconnection delays in the socket clients.
18//!
19//! The backoff mechanism allows the delay to grow exponentially up to a configurable
20//! maximum, optionally applying random jitter to avoid synchronized reconnection storms.
21//! An "immediate first" flag is available so that the very first reconnect attempt
22//! can occur without any delay.
2324use std::time::Duration;
2526use rand::Rng;
2728#[derive(Clone, Debug)]
29pub struct ExponentialBackoff {
30/// The initial backoff delay.
31delay_initial: Duration,
32/// The maximum delay to cap the backoff.
33delay_max: Duration,
34/// The current backoff delay.
35delay_current: Duration,
36/// The factor to multiply the delay on each iteration.
37factor: f64,
38/// The maximum random jitter to add (in milliseconds).
39jitter_ms: u64,
40/// If true, the first call to `next()` returns zero delay (immediate reconnect).
41immediate_first: bool,
42}
4344/// An exponential backoff mechanism with optional jitter and immediate-first behavior.
45///
46/// This struct computes successive delays for reconnect attempts.
47/// It starts from an initial delay and multiplies it by a factor on each iteration,
48/// capping the delay at a maximum value. Random jitter is added (up to a configured
49/// maximum) to the delay. When `immediate_first` is true, the first call to `next_duration`
50/// returns zero delay, triggering an immediate reconnect, after which the immediate flag is disabled.
51impl ExponentialBackoff {
52/// Creates a new [`ExponentialBackoff]` instance.
53#[must_use]
54pub const fn new(
55 delay_initial: Duration,
56 delay_max: Duration,
57 factor: f64,
58 jitter_ms: u64,
59 immediate_first: bool,
60 ) -> Self {
61Self {
62 delay_initial,
63 delay_max,
64 delay_current: delay_initial,
65 factor,
66 jitter_ms,
67 immediate_first,
68 }
69 }
7071/// Return the next backoff delay with jitter and update the internal state.
72 ///
73 /// If the `immediate_first` flag is set and this is the first call (i.e. the current
74 /// delay equals the initial delay), it returns `Duration::ZERO` to trigger an immediate
75 /// reconnect and disables the immediate behavior for subsequent calls.
76pub fn next_duration(&mut self) -> Duration {
77if self.immediate_first && self.delay_current == self.delay_initial {
78self.immediate_first = false;
79return Duration::ZERO;
80 }
8182// Generate random jitter
83let jitter = rand::rng().random_range(0..=self.jitter_ms);
84let delay_with_jitter = self.delay_current + Duration::from_millis(jitter);
8586// Prepare the next delay
87let current_nanos = self.delay_current.as_nanos();
88let max_nanos = self.delay_max.as_nanos() as u64;
89let next_nanos = (current_nanos as f64 * self.factor) as u64;
90self.delay_current = Duration::from_nanos(std::cmp::min(next_nanos, max_nanos));
9192 delay_with_jitter
93 }
9495/// Reset the backoff to its initial state.
96pub const fn reset(&mut self) {
97self.delay_current = self.delay_initial;
98 }
99100/// Returns the current base delay without jitter.
101 /// This represents the delay that would be used as the base for the next call to `next()`,
102 /// before any jitter is applied.
103#[must_use]
104pub const fn current_delay(&self) -> Duration {
105self.delay_current
106 }
107}
108109////////////////////////////////////////////////////////////////////////////////
110// Tests
111////////////////////////////////////////////////////////////////////////////////
112#[cfg(test)]
113mod tests {
114use std::time::Duration;
115116use rstest::rstest;
117118use super::*;
119120#[rstest]
121fn test_no_jitter_exponential_growth() {
122let initial = Duration::from_millis(100);
123let max = Duration::from_millis(1600);
124let factor = 2.0;
125let jitter = 0;
126let mut backoff = ExponentialBackoff::new(initial, max, factor, jitter, false);
127128// 1st call returns the initial delay
129let d1 = backoff.next_duration();
130assert_eq!(d1, Duration::from_millis(100));
131132// 2nd call: current becomes 200ms
133let d2 = backoff.next_duration();
134assert_eq!(d2, Duration::from_millis(200));
135136// 3rd call: current becomes 400ms
137let d3 = backoff.next_duration();
138assert_eq!(d3, Duration::from_millis(400));
139140// 4th call: current becomes 800ms
141let d4 = backoff.next_duration();
142assert_eq!(d4, Duration::from_millis(800));
143144// 5th call: current would be 1600ms (800 * 2) which is within the cap
145let d5 = backoff.next_duration();
146assert_eq!(d5, Duration::from_millis(1600));
147148// 6th call: should still be capped at 1600ms
149let d6 = backoff.next_duration();
150assert_eq!(d6, Duration::from_millis(1600));
151 }
152153#[rstest]
154fn test_reset() {
155let initial = Duration::from_millis(100);
156let max = Duration::from_millis(1600);
157let factor = 2.0;
158let jitter = 0;
159let mut backoff = ExponentialBackoff::new(initial, max, factor, jitter, false);
160161// Call next() once so that the internal state updates
162let _ = backoff.next_duration(); // current_delay becomes 200ms
163backoff.reset();
164let d = backoff.next_duration();
165// After reset, the next delay should be the initial delay (100ms)
166assert_eq!(d, Duration::from_millis(100));
167 }
168169#[rstest]
170fn test_jitter_within_bounds() {
171let initial = Duration::from_millis(100);
172let max = Duration::from_millis(1000);
173let factor = 2.0;
174let jitter = 50;
175// Run several iterations to ensure that jitter stays within bounds
176for _ in 0..10 {
177let mut backoff = ExponentialBackoff::new(initial, max, factor, jitter, false);
178// Capture the expected base delay before jitter is applied
179let base = backoff.delay_current;
180let delay = backoff.next_duration();
181// The returned delay must be at least the base delay and at most base + jitter
182let min_expected = base;
183let max_expected = base + Duration::from_millis(jitter);
184assert!(
185 delay >= min_expected,
186"Delay {delay:?} is less than expected minimum {min_expected:?}"
187);
188assert!(
189 delay <= max_expected,
190"Delay {delay:?} exceeds expected maximum {max_expected:?}"
191);
192 }
193 }
194195#[rstest]
196fn test_factor_less_than_two() {
197let initial = Duration::from_millis(100);
198let max = Duration::from_millis(200);
199let factor = 1.5;
200let jitter = 0;
201let mut backoff = ExponentialBackoff::new(initial, max, factor, jitter, false);
202203// First call returns 100ms
204let d1 = backoff.next_duration();
205assert_eq!(d1, Duration::from_millis(100));
206207// Second call: current_delay becomes 100 * 1.5 = 150ms
208let d2 = backoff.next_duration();
209assert_eq!(d2, Duration::from_millis(150));
210211// Third call: current_delay becomes 150 * 1.5 = 225ms, but capped to 200ms
212let d3 = backoff.next_duration();
213assert_eq!(d3, Duration::from_millis(200));
214215// Fourth call: remains at the max of 200ms
216let d4 = backoff.next_duration();
217assert_eq!(d4, Duration::from_millis(200));
218 }
219220#[rstest]
221fn test_max_delay_is_respected() {
222let initial = Duration::from_millis(500);
223let max = Duration::from_millis(1000);
224let factor = 3.0;
225let jitter = 0;
226let mut backoff = ExponentialBackoff::new(initial, max, factor, jitter, false);
227228// 1st call returns 500ms
229let d1 = backoff.next_duration();
230assert_eq!(d1, Duration::from_millis(500));
231232// 2nd call: would be 500 * 3 = 1500ms but is capped to 1000ms
233let d2 = backoff.next_duration();
234assert_eq!(d2, Duration::from_millis(1000));
235236// Subsequent calls should continue to return the max delay
237let d3 = backoff.next_duration();
238assert_eq!(d3, Duration::from_millis(1000));
239 }
240241#[rstest]
242fn test_current_delay_getter() {
243let initial = Duration::from_millis(100);
244let max = Duration::from_millis(1600);
245let factor = 2.0;
246let jitter = 0;
247let mut backoff = ExponentialBackoff::new(initial, max, factor, jitter, false);
248249assert_eq!(backoff.current_delay(), initial);
250251let _ = backoff.next_duration();
252assert_eq!(backoff.current_delay(), Duration::from_millis(200));
253254let _ = backoff.next_duration();
255assert_eq!(backoff.current_delay(), Duration::from_millis(400));
256257 backoff.reset();
258assert_eq!(backoff.current_delay(), initial);
259 }
260261#[rstest]
262fn test_immediate_first() {
263let initial = Duration::from_millis(100);
264let max = Duration::from_millis(1600);
265let factor = 2.0;
266let jitter = 0;
267let mut backoff = ExponentialBackoff::new(initial, max, factor, jitter, true);
268269// The first call should yield an immediate (zero) delay
270let d1 = backoff.next_duration();
271assert_eq!(
272 d1,
273 Duration::ZERO,
274"Expected immediate reconnect (zero delay) on first call"
275);
276277// The next call should return the current delay (i.e. the base initial delay)
278let d2 = backoff.next_duration();
279assert_eq!(
280 d2, initial,
281"Expected the delay to be the initial delay after immediate reconnect"
282);
283284// Subsequent calls should continue with the exponential growth
285let d3 = backoff.next_duration();
286let expected = initial * 2; // 100ms * 2 = 200ms
287assert_eq!(
288 d3, expected,
289"Expected exponential growth from the initial delay"
290);
291 }
292}