Skip to main content

omega_cache/core/
backoff.rs

1use std::hint::spin_loop;
2
3/// A stateful backoff strategy used to reduce CPU contention in busy-wait loops.
4///
5/// This enum encapsulates both the configuration and the current state of the backoff.
6/// It uses "Full Jitter" to prevent "thundering herd" problems by picking a random
7/// number of spins between `0` and the current limit.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum Backoff {
10    /// Linear growth: increases the maximum spin count by 1 each step until it hits the limit.
11    Linear {
12        /// The current maximum number of spins for the next jitter calculation.
13        current: usize,
14        /// The maximum allowed value for `current`.
15        limit: usize,
16    },
17    /// Exponential growth: doubles the maximum spin count each step until it hits the limit.
18    Exponential {
19        /// The current maximum number of spins for the next jitter calculation.
20        current: usize,
21        /// The maximum allowed value for `current`.
22        limit: usize,
23    },
24}
25
26impl Backoff {
27    /// Creates a new linear backoff starting with a maximum spin of 1.
28    ///
29    /// At each call to [`backoff`], the internal limit will increase by 1 until `limit` is reached.
30    #[inline]
31    pub fn linear(limit: usize) -> Self {
32        Self::Linear { current: 1, limit }
33    }
34
35    /// Creates a new exponential backoff starting with a maximum spin of 1.
36    ///
37    /// At each call to [`backoff`], the internal limit will double (multiplicative growth)
38    /// until `limit` is reached.
39    #[inline]
40    pub fn exponential(limit: usize) -> Self {
41        Self::Exponential { current: 1, limit }
42    }
43
44    /// Returns the current upper bound for the spin jitter.
45    #[inline]
46    fn current_limit(&self) -> usize {
47        match *self {
48            Self::Linear { current, .. } => current,
49            Self::Exponential { current, .. } => current,
50        }
51    }
52
53    /// Updates the internal state for the next backoff attempt.
54    #[inline]
55    fn step(&mut self) {
56        match self {
57            Self::Linear { current, limit } => {
58                *current = current.saturating_add(1).min(*limit);
59            }
60            Self::Exponential { current, limit } => {
61                *current = current.saturating_mul(2).min(*limit);
62            }
63        }
64    }
65
66    /// Performs the backoff by spinning the CPU.
67    ///
68    /// This method:
69    /// 1. Generates a random number jitter from 0 to ${limit}.
70    /// 2. Executes `std::hint::spin_loop()` jitter times.
71    /// 3. Increments the internal limit according to the chosen strategy.
72    pub fn backoff(&mut self) {
73        let limit = self.current_limit();
74
75        // Full Jitter: randomize within the [0, limit] range to desynchronize threads.
76        let jitter = fastrand::usize(..=limit);
77
78        for _ in 0..jitter {
79            spin_loop();
80        }
81
82        self.step();
83    }
84}
85
86/// Configuration for creating [`Backoff`] instances.
87///
88/// This struct acts as a factory, allowing you to store a preferred backoff
89/// strategy (e.g., in a cache or connection pool) and generate fresh,
90/// thread-local [`Backoff`] states as needed.
91#[derive(Debug, Clone, Copy)]
92pub struct BackoffConfig {
93    /// The mathematical strategy used to increase delay.
94    pub policy: BackoffPolicy,
95    /// The maximum number of spins allowed in a single backoff iteration.
96    pub limit: usize,
97}
98
99impl BackoffConfig {
100    #[inline(always)]
101    pub fn linear(limit: usize) -> BackoffConfig {
102        Self {
103            policy: BackoffPolicy::Linear,
104            limit,
105        }
106    }
107
108    #[inline(always)]
109    pub fn exponential(limit: usize) -> BackoffConfig {
110        Self {
111            policy: BackoffPolicy::Exponential,
112            limit,
113        }
114    }
115}
116
117/// Defines how the backoff duration scales after each failed attempt.
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub enum BackoffPolicy {
120    /// Increases the maximum spin count linearly (e.g., 1, 2, 3, 4...).
121    Linear,
122    /// Increases the maximum spin count exponentially (e.g., 1, 2, 4, 8...).
123    /// This is generally preferred for high-contention scenarios.
124    Exponential,
125}
126
127impl BackoffConfig {
128    /// Creates a fresh, stateful [`Backoff`] instance based on this configuration.
129    ///
130    /// This is typically called at the start of a retry loop. Because the
131    /// returned `Backoff` is owned and stateful, it should be kept local
132    /// to the thread performing the retries.
133    #[must_use]
134    pub fn build(&self) -> Backoff {
135        match self.policy {
136            BackoffPolicy::Linear => Backoff::linear(self.limit),
137            BackoffPolicy::Exponential => Backoff::exponential(self.limit),
138        }
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn test_linear_growth() {
148        let mut backoff = Backoff::linear(10);
149
150        // Initial limit is 1
151        assert_eq!(backoff.current_limit(), 1);
152
153        // After one backoff, limit should be 2
154        backoff.backoff();
155        assert_eq!(backoff.current_limit(), 2);
156
157        // After another, limit should be 3
158        backoff.backoff();
159        assert_eq!(backoff.current_limit(), 3);
160    }
161
162    #[test]
163    fn test_exponential_growth() {
164        let mut backoff = Backoff::exponential(100);
165
166        // Initial limit is 1
167        assert_eq!(backoff.current_limit(), 1);
168
169        backoff.backoff(); // 1 -> 2
170        assert_eq!(backoff.current_limit(), 2);
171
172        backoff.backoff(); // 2 -> 4
173        assert_eq!(backoff.current_limit(), 4);
174
175        backoff.backoff(); // 4 -> 8
176        assert_eq!(backoff.current_limit(), 8);
177    }
178
179    #[test]
180    fn test_cap_limit() {
181        // Test that linear caps
182        let mut lin = Backoff::linear(2);
183        lin.backoff(); // 1 -> 2
184        lin.backoff(); // 2 -> 2 (capped)
185        assert_eq!(lin.current_limit(), 2);
186
187        // Test that exponential caps
188        let mut exp = Backoff::exponential(10);
189        exp.backoff(); // 1 -> 2
190        exp.backoff(); // 2 -> 4
191        exp.backoff(); // 4 -> 8
192        exp.backoff(); // 8 -> 10 (capped)
193        assert_eq!(exp.current_limit(), 10);
194    }
195
196    #[test]
197    fn test_backoff_does_not_panic() {
198        let mut backoff = Backoff::exponential(10);
199        // Simple smoke test to ensure no internal panics during execution
200        for _ in 0..10 {
201            backoff.backoff();
202        }
203    }
204}