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