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 [`handle_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 [`handle_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    #[inline]
67    pub fn decay(&mut self) {
68        match self {
69            Self::Linear { current, .. } => *current = current.saturating_sub(1),
70            Self::Exponential { current, .. } => *current = current.saturating_div(2),
71        }
72    }
73
74    /// Performs the backoff by spinning the CPU.
75    ///
76    /// This method:
77    /// 1. Generates a random number jitter from 0 to ${limit}.
78    /// 2. Executes `std::hint::spin_loop()` jitter times.
79    /// 3. Increments the internal limit according to the chosen strategy.
80    pub fn wait(&mut self) {
81        let limit = self.current_limit();
82
83        // Full Jitter: randomize within the [0, limit] range to desynchronize threads.
84        let jitter = fastrand::usize(..=limit);
85
86        for _ in 0..jitter {
87            spin_loop();
88        }
89
90        self.step();
91    }
92}
93
94/// Configuration for creating [`Backoff`] instances.
95///
96/// This struct acts as a factory, allowing you to store a preferred backoff
97/// strategy (e.g., in a cache or connection pool) and generate fresh,
98/// thread-local [`Backoff`] states as needed.
99#[derive(Debug, Clone, Copy)]
100pub struct BackoffConfig {
101    /// The mathematical strategy used to increase delay.
102    pub policy: BackoffPolicy,
103    /// The maximum number of spins allowed in a single backoff iteration.
104    pub limit: usize,
105}
106
107impl BackoffConfig {
108    #[inline(always)]
109    pub fn linear(limit: usize) -> BackoffConfig {
110        Self {
111            policy: BackoffPolicy::Linear,
112            limit,
113        }
114    }
115
116    #[inline(always)]
117    pub fn exponential(limit: usize) -> BackoffConfig {
118        Self {
119            policy: BackoffPolicy::Exponential,
120            limit,
121        }
122    }
123}
124
125/// Defines how the backoff duration scales after each failed attempt.
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub enum BackoffPolicy {
128    /// Increases the maximum spin count linearly (e.g., 1, 2, 3, 4...).
129    Linear,
130    /// Increases the maximum spin count exponentially (e.g., 1, 2, 4, 8...).
131    /// This is generally preferred for high-contention scenarios.
132    Exponential,
133}
134
135impl BackoffConfig {
136    /// Creates a fresh, stateful [`Backoff`] instance based on this configuration.
137    ///
138    /// This is typically called at the start of a retry loop. Because the
139    /// returned `Backoff` is owned and stateful, it should be kept local
140    /// to the thread performing the retries.
141    #[must_use]
142    pub fn build(&self) -> Backoff {
143        match self.policy {
144            BackoffPolicy::Linear => Backoff::linear(self.limit),
145            BackoffPolicy::Exponential => Backoff::exponential(self.limit),
146        }
147    }
148}
149
150#[inline(always)]
151pub fn handle_backoff(backoff: Option<&mut Backoff>) {
152    match backoff {
153        None => {
154            spin_loop();
155        }
156        Some(backoff) => {
157            backoff.wait();
158        }
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn test_linear_growth() {
168        let mut backoff = Backoff::linear(10);
169
170        // Initial limit is 1
171        assert_eq!(backoff.current_limit(), 1);
172
173        // After one backoff, limit should be 2
174        backoff.wait();
175        assert_eq!(backoff.current_limit(), 2);
176
177        // After another, limit should be 3
178        backoff.wait();
179        assert_eq!(backoff.current_limit(), 3);
180    }
181
182    #[test]
183    fn test_exponential_growth() {
184        let mut backoff = Backoff::exponential(100);
185
186        // Initial limit is 1
187        assert_eq!(backoff.current_limit(), 1);
188
189        backoff.wait(); // 1 -> 2
190        assert_eq!(backoff.current_limit(), 2);
191
192        backoff.wait(); // 2 -> 4
193        assert_eq!(backoff.current_limit(), 4);
194
195        backoff.wait(); // 4 -> 8
196        assert_eq!(backoff.current_limit(), 8);
197    }
198
199    #[test]
200    fn test_cap_limit() {
201        // Test that linear caps
202        let mut lin = Backoff::linear(2);
203        lin.wait(); // 1 -> 2
204        lin.wait(); // 2 -> 2 (capped)
205        assert_eq!(lin.current_limit(), 2);
206
207        // Test that exponential caps
208        let mut exp = Backoff::exponential(10);
209        exp.wait(); // 1 -> 2
210        exp.wait(); // 2 -> 4
211        exp.wait(); // 4 -> 8
212        exp.wait(); // 8 -> 10 (capped)
213        assert_eq!(exp.current_limit(), 10);
214    }
215
216    #[test]
217    fn test_backoff_does_not_panic() {
218        let mut backoff = Backoff::exponential(10);
219        // Simple smoke test to ensure no internal panics during execution
220        for _ in 0..10 {
221            backoff.wait();
222        }
223    }
224}