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}