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}