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