1use std::thread;
5
6use reifydb_runtime::context::rng::Rng;
7use reifydb_value::{error::Error, value::duration::Duration};
8use tracing::{debug, warn};
9
10use crate::execution::ExecutionResult;
11
12pub enum Backoff {
13 None,
14
15 Fixed(Duration),
16
17 Exponential {
18 base: Duration,
19 max: Duration,
20 },
21 ExponentialJitter {
22 base: Duration,
23 max: Duration,
24 },
25}
26
27pub struct RetryStrategy {
28 pub max_attempts: u32,
29 pub backoff: Backoff,
30}
31
32impl Default for RetryStrategy {
33 fn default() -> Self {
34 Self {
35 max_attempts: 10,
36 backoff: Backoff::ExponentialJitter {
37 base: Duration::from_milliseconds(5).unwrap(),
38 max: Duration::from_milliseconds(200).unwrap(),
39 },
40 }
41 }
42}
43
44impl RetryStrategy {
45 pub fn no_retry() -> Self {
46 Self {
47 max_attempts: 1,
48 backoff: Backoff::None,
49 }
50 }
51
52 pub fn default_conflict_retry() -> Self {
53 Self::default()
54 }
55
56 pub fn with_fixed_backoff(max_attempts: u32, delay: Duration) -> Self {
57 Self {
58 max_attempts,
59 backoff: Backoff::Fixed(delay),
60 }
61 }
62
63 pub fn with_exponential_backoff(max_attempts: u32, base: Duration, max: Duration) -> Self {
64 Self {
65 max_attempts,
66 backoff: Backoff::Exponential {
67 base,
68 max,
69 },
70 }
71 }
72
73 pub fn with_jittered_backoff(max_attempts: u32, base: Duration, max: Duration) -> Self {
74 Self {
75 max_attempts,
76 backoff: Backoff::ExponentialJitter {
77 base,
78 max,
79 },
80 }
81 }
82
83 pub fn execute<F>(&self, rng: &Rng, rql: &str, f: F) -> ExecutionResult
84 where
85 F: FnMut() -> ExecutionResult,
86 {
87 self.run(rng, rql, f, |result| result.error.as_ref().is_some_and(is_conflict))
88 }
89
90 pub fn retry<T, F>(&self, rng: &Rng, operation: &str, f: F) -> Result<T, Error>
91 where
92 F: FnMut() -> Result<T, Error>,
93 {
94 self.run(rng, operation, f, |result| result.as_ref().is_err_and(is_conflict))
95 }
96
97 fn run<R>(&self, rng: &Rng, rql: &str, mut f: impl FnMut() -> R, conflicted: impl Fn(&R) -> bool) -> R {
98 let mut last_result = None;
99 for attempt in 0..self.max_attempts {
100 let result = f();
101 if !conflicted(&result) {
102 return result;
103 }
104 last_result = Some(result);
105 let is_last_attempt = attempt + 1 >= self.max_attempts;
106 if is_last_attempt {
107 warn!(
108 attempt = attempt + 1,
109 max_attempts = self.max_attempts,
110 rql = %rql,
111 "Transaction conflict retries exhausted"
112 );
113 } else {
114 let delay = compute_backoff(&self.backoff, attempt, rng);
115 debug!(
116 attempt = attempt + 1,
117 max_attempts = self.max_attempts,
118 delay_us = delay.microseconds().unwrap_or(0) as u64,
119 rql = %rql,
120 "Transaction conflict detected, retrying after backoff"
121 );
122 if !delay.is_zero() {
123 thread::sleep(delay.to_std());
124 }
125 }
126 }
127 last_result.unwrap()
128 }
129}
130
131pub fn is_conflict(err: &Error) -> bool {
132 err.code == "TXN_001"
133}
134
135fn compute_backoff(backoff: &Backoff, attempt: u32, rng: &Rng) -> Duration {
136 match backoff {
137 Backoff::None => Duration::zero(),
138 Backoff::Fixed(d) => *d,
139 Backoff::Exponential {
140 base,
141 max,
142 } => exponential_cap(*base, *max, attempt),
143 Backoff::ExponentialJitter {
144 base,
145 max,
146 } => {
147 let cap = exponential_cap(*base, *max, attempt);
148 let cap_nanos = cap.as_nanos().unwrap_or(0).max(0) as u64;
149 if cap_nanos == 0 {
150 return Duration::zero();
151 }
152 let sampled = rng.infra_u64_inclusive(cap_nanos);
153 Duration::from_nanoseconds(sampled as i64).unwrap()
154 }
155 }
156}
157
158fn exponential_cap(base: Duration, max: Duration, attempt: u32) -> Duration {
159 let shift = attempt.min(30);
160 let multiplier = 1i64 << shift;
161 base.saturating_mul(multiplier).min(max)
162}
163
164#[cfg(test)]
165mod retry_tests {
166 use std::cell::Cell;
167
168 use reifydb_runtime::context::rng::Rng;
169 use reifydb_value::{
170 error::{Diagnostic, Error},
171 fragment::Fragment,
172 value::duration::Duration,
173 };
174
175 use super::{Backoff, RetryStrategy, compute_backoff, exponential_cap};
176 use crate::{execution::ExecutionResult, metrics::execution::ExecutionMetrics};
177
178 fn ok() -> ExecutionResult {
179 ExecutionResult {
180 frames: vec![],
181 named_system_columns: vec![],
182 error: None,
183 metrics: ExecutionMetrics::default(),
184 }
185 }
186
187 fn err(code: &str) -> ExecutionResult {
188 ExecutionResult {
189 frames: vec![],
190 named_system_columns: vec![],
191 error: Some(Error(Box::new(Diagnostic {
192 code: code.to_string(),
193 rql: None,
194 message: format!("{} test", code),
195 column: None,
196 fragment: Fragment::None,
197 label: None,
198 help: None,
199 notes: vec![],
200 cause: None,
201 operator_chain: None,
202 }))),
203 metrics: ExecutionMetrics::default(),
204 }
205 }
206
207 fn no_sleep_strategy(max_attempts: u32) -> RetryStrategy {
208 RetryStrategy {
209 max_attempts,
210 backoff: Backoff::None,
211 }
212 }
213
214 #[test]
215 fn success_first_try_runs_closure_once() {
216 let strategy = no_sleep_strategy(5);
217 let rng = Rng::default();
218 let calls = Cell::new(0u32);
219 let result = strategy.execute(&rng, "", || {
220 calls.set(calls.get() + 1);
221 ok()
222 });
223 assert!(result.is_ok());
224 assert_eq!(calls.get(), 1);
225 }
226
227 #[test]
228 fn non_conflict_error_is_not_retried() {
229 let strategy = no_sleep_strategy(5);
230 let rng = Rng::default();
231 let calls = Cell::new(0u32);
232 let result = strategy.execute(&rng, "", || {
233 calls.set(calls.get() + 1);
234 err("TXN_002")
235 });
236 assert!(result.is_err());
237 assert_eq!(calls.get(), 1);
238 }
239
240 #[test]
241 fn conflict_retries_then_succeeds() {
242 let strategy = no_sleep_strategy(5);
243 let rng = Rng::default();
244 let calls = Cell::new(0u32);
245 let result = strategy.execute(&rng, "", || {
246 let n = calls.get();
247 calls.set(n + 1);
248 if n < 2 {
249 err("TXN_001")
250 } else {
251 ok()
252 }
253 });
254 assert!(result.is_ok());
255 assert_eq!(calls.get(), 3);
256 }
257
258 #[test]
259 fn conflict_exhausts_attempts_returns_last_error() {
260 let strategy = no_sleep_strategy(4);
261 let rng = Rng::default();
262 let calls = Cell::new(0u32);
263 let result = strategy.execute(&rng, "", || {
264 calls.set(calls.get() + 1);
265 err("TXN_001")
266 });
267 assert!(result.is_err());
268 assert_eq!(result.error.as_ref().unwrap().code, "TXN_001");
269 assert_eq!(calls.get(), 4);
270 }
271
272 #[test]
273 fn jittered_backoff_stays_within_cap() {
274 let base = Duration::from_milliseconds(10).unwrap();
275 let max = Duration::from_milliseconds(100).unwrap();
276 let backoff = Backoff::ExponentialJitter {
277 base,
278 max,
279 };
280 let rng = Rng::default();
281 for attempt in 0..8 {
282 let cap = exponential_cap(base, max, attempt);
283 for _ in 0..50 {
284 let d = compute_backoff(&backoff, attempt, &rng);
285 assert!(d <= cap, "attempt {}: {:?} exceeds cap {:?}", attempt, d, cap);
286 }
287 }
288 }
289
290 #[test]
291 fn seeded_rng_produces_deterministic_jitter() {
292 let base = Duration::from_milliseconds(5).unwrap();
293 let max = Duration::from_milliseconds(200).unwrap();
294 let backoff = Backoff::ExponentialJitter {
295 base,
296 max,
297 };
298 let sample = |seed: u64| -> Vec<Duration> {
299 let rng = Rng::seeded(seed);
300 (0..8).map(|attempt| compute_backoff(&backoff, attempt, &rng)).collect()
301 };
302 assert_eq!(sample(42), sample(42));
303 assert_ne!(sample(42), sample(43));
304 }
305
306 #[test]
307 fn seeded_rng_produces_exact_pinned_jitter_values() {
308 let base = Duration::from_milliseconds(5).unwrap();
309 let max = Duration::from_milliseconds(200).unwrap();
310 let backoff = Backoff::ExponentialJitter {
311 base,
312 max,
313 };
314 let nanos = |seed: u64| -> Vec<u64> {
315 let rng = Rng::seeded(seed);
316 (0..8).map(|attempt| compute_backoff(&backoff, attempt, &rng).as_nanos().unwrap() as u64)
317 .collect()
318 };
319
320 let expected_42: Vec<u64> = vec![
321 3_848_394,
322 113_809,
323 2_934_288,
324 23_292_485,
325 77_680_508,
326 31_066_617,
327 36_519_179,
328 190_866_841,
329 ];
330 let expected_43: Vec<u64> = vec![
331 3_974_671, 4_842_103, 12_057_439, 29_830_325, 72_334_216, 22_229_100, 36_417_439, 81_417_246,
332 ];
333
334 assert_eq!(nanos(42), expected_42);
335 assert_eq!(nanos(43), expected_43);
336
337 assert_eq!(nanos(42), expected_42);
338 assert_eq!(nanos(43), expected_43);
339 }
340
341 #[test]
342 fn exponential_cap_saturates_at_max() {
343 let base = Duration::from_milliseconds(5).unwrap();
344 let max = Duration::from_milliseconds(200).unwrap();
345 assert_eq!(exponential_cap(base, max, 0), Duration::from_milliseconds(5).unwrap());
346 assert_eq!(exponential_cap(base, max, 1), Duration::from_milliseconds(10).unwrap());
347 assert_eq!(exponential_cap(base, max, 5), Duration::from_milliseconds(160).unwrap());
348 assert_eq!(exponential_cap(base, max, 6), max);
349 assert_eq!(exponential_cap(base, max, 100), max);
350 }
351
352 #[test]
353 fn default_uses_jittered_backoff() {
354 let s = RetryStrategy::default();
355 assert_eq!(s.max_attempts, 10);
356 match s.backoff {
357 Backoff::ExponentialJitter {
358 base,
359 max,
360 } => {
361 assert_eq!(base, Duration::from_milliseconds(5).unwrap());
362 assert_eq!(max, Duration::from_milliseconds(200).unwrap());
363 }
364 _ => panic!("expected ExponentialJitter default"),
365 }
366 }
367}