macro_rules! retry_once {
($expr:expr $(, $key:ident = $val:expr)* $(,)?) => {{
match $expr {
Ok(val) => Ok(val),
Err(first_error) => {
tracing::warn!($($key = %$val,)* error = ?first_error, "Backend error, retrying once");
let jitter_ms = {
use rand::RngExt as _;
let mut rng = rand::rng();
rng.random_range(1..10)
};
tokio::time::sleep(std::time::Duration::from_millis(jitter_ms)).await;
match $expr {
Ok(val) => Ok(val),
Err(_retry_error) => {
tracing::warn!($($key = %$val,)* retry_error = ?_retry_error, "Retry also failed");
Err(first_error)
}
}
}
}
}};
}
pub(crate) use retry_once;
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicU32, Ordering};
fn numbered_fail(c: &AtomicU32) -> Result<(), String> {
let n = c.fetch_add(1, Ordering::SeqCst);
Err(format!("error {n}"))
}
fn fallible_op(counter: &AtomicU32, fail_first_n: u32) -> Result<&'static str, &'static str> {
let n = counter.fetch_add(1, Ordering::SeqCst);
if n < fail_first_n {
Err("stale")
} else {
Ok("success")
}
}
#[tokio::test]
async fn test_succeeds_on_first_try() {
let counter = AtomicU32::new(0);
let result = retry_once!(fallible_op(&counter, 0));
assert_eq!(result, Ok("success"));
assert_eq!(counter.load(Ordering::SeqCst), 1);
}
#[tokio::test(start_paused = true)]
async fn test_succeeds_on_retry() {
let counter = AtomicU32::new(0);
let result = retry_once!(fallible_op(&counter, 1));
assert_eq!(result, Ok("success"));
assert_eq!(counter.load(Ordering::SeqCst), 2);
}
#[tokio::test(start_paused = true)]
async fn test_returns_first_error_on_double_failure() {
let counter = AtomicU32::new(0);
let result = retry_once!(numbered_fail(&counter));
assert_eq!(result, Err("error 0".to_string()));
assert_eq!(counter.load(Ordering::SeqCst), 2);
}
#[test]
fn test_jitter_range() {
for _ in 0..100 {
let jitter_ms = {
use rand::RngExt as _;
let mut rng = rand::rng();
rng.random_range(1..10)
};
assert!(
(1..10).contains(&jitter_ms),
"Jitter {jitter_ms} out of range [1, 10)",
);
}
}
}