use crate::backoff::BackoffStrategy;
use crate::sleep::Sleeper;
use core::fmt;
use rand::rngs::StdRng;
type DefaultRetryBuilder<F, B, T, E> = RetryBuilder<F, B, T, E, fn(&E) -> bool>;
type NotifyCallback<E> = Box<dyn FnMut(&RetryContext<E>)>;
type FailureCallback<E> = Box<dyn FnMut(&RetryError<E>)>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RetryErrorKind {
Exhausted,
PredicateRejected,
}
#[derive(Debug)]
pub struct RetryContext<'a, E> {
pub attempt: u8,
pub next_delay_ms: Option<u64>,
pub cumulative_delay_ms: u64,
pub error: Option<&'a E>,
}
#[derive(Debug, Clone)]
pub struct RetryError<E> {
kind: RetryErrorKind,
attempts: u8,
max_attempts: u8,
cumulative_delay_ms: u64,
cause: Option<E>,
}
impl<E> RetryError<E> {
fn new(
kind: RetryErrorKind,
attempts: u8,
max_attempts: u8,
cumulative_delay_ms: u64,
cause: Option<E>,
) -> Self {
Self {
kind,
attempts,
max_attempts,
cumulative_delay_ms,
cause,
}
}
pub fn cause(&self) -> Option<&E> {
self.cause.as_ref()
}
pub fn into_cause(self) -> Option<E> {
self.cause
}
pub fn attempts(&self) -> u8 {
self.attempts
}
pub fn max_attempts(&self) -> u8 {
self.max_attempts
}
pub fn cumulative_delay_ms(&self) -> u64 {
self.cumulative_delay_ms
}
pub fn kind(&self) -> RetryErrorKind {
self.kind
}
}
impl<E> fmt::Display for RetryError<E>
where
E: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.kind {
RetryErrorKind::Exhausted => {
write!(
f,
"retry exhausted after {} of {} attempts",
self.attempts, self.max_attempts
)?;
}
RetryErrorKind::PredicateRejected => {
write!(f, "retry aborted by predicate on attempt {}", self.attempts)?;
}
}
write!(f, " (cumulative delay {}ms)", self.cumulative_delay_ms)?;
if let Some(cause) = self.cause.as_ref() {
write!(f, ": {}", cause)?;
}
Ok(())
}
}
#[cfg(feature = "std")]
impl<E> std::error::Error for RetryError<E> where E: std::error::Error {}
#[derive(Debug)]
pub struct RetryOutcome<T> {
value: T,
attempts: u8,
cumulative_delay_ms: u64,
}
impl<T> RetryOutcome<T> {
fn new(value: T, attempts: u8, cumulative_delay_ms: u64) -> Self {
Self {
value,
attempts,
cumulative_delay_ms,
}
}
pub fn attempts(&self) -> u8 {
self.attempts
}
pub fn cumulative_delay_ms(&self) -> u64 {
self.cumulative_delay_ms
}
pub fn value(&self) -> &T {
&self.value
}
pub fn into_inner(self) -> T {
self.value
}
}
pub trait Retryable<T, E> {
fn retry<B: BackoffStrategy>(self, backoff: B) -> DefaultRetryBuilder<Self, B, T, E>
where
Self: Sized;
}
impl<F, T, E> Retryable<T, E> for F
where
F: FnMut() -> Result<T, E>,
{
fn retry<B: BackoffStrategy>(self, backoff: B) -> RetryBuilder<Self, B, T, E, fn(&E) -> bool> {
RetryBuilder {
operation: self,
backoff,
when: None,
notify: None,
on_success: None,
on_failure: None,
_phantom_t: core::marker::PhantomData,
_phantom_e: core::marker::PhantomData,
}
}
}
pub trait RetryableExt<T, E>: Retryable<T, E> {
fn with_exponential(self) -> DefaultRetryBuilder<Self, crate::backoff::ExponentialBackoff, T, E>
where
Self: Sized,
{
self.retry(crate::backoff::ExponentialBackoff::default())
}
fn with_constant(self, delay_ms: u64) -> DefaultRetryBuilder<Self, crate::backoff::ConstantBackoff, T, E>
where
Self: Sized,
{
self.retry(crate::backoff::ConstantBackoff::new().delay_ms(delay_ms))
}
fn with_fibonacci(self) -> DefaultRetryBuilder<Self, crate::backoff::FibonacciBackoff, T, E>
where
Self: Sized,
{
self.retry(crate::backoff::FibonacciBackoff::default())
}
}
impl<F, T, E> RetryableExt<T, E> for F where F: Retryable<T, E> {}
pub struct RetryBuilder<F, B, T, E, W> {
operation: F,
backoff: B,
when: Option<W>,
notify: Option<NotifyCallback<E>>,
on_success: Option<NotifyCallback<E>>,
on_failure: Option<FailureCallback<E>>,
_phantom_t: core::marker::PhantomData<T>,
_phantom_e: core::marker::PhantomData<E>,
}
impl<F, B, T, E, W> RetryBuilder<F, B, T, E, W>
where
F: FnMut() -> Result<T, E>,
B: BackoffStrategy,
W: Fn(&E) -> bool,
{
pub fn when<P>(self, predicate: P) -> RetryBuilder<F, B, T, E, P>
where
P: Fn(&E) -> bool,
{
RetryBuilder {
operation: self.operation,
backoff: self.backoff,
when: Some(predicate),
notify: self.notify,
on_success: self.on_success,
on_failure: self.on_failure,
_phantom_t: core::marker::PhantomData,
_phantom_e: core::marker::PhantomData,
}
}
pub fn notify<C>(mut self, callback: C) -> Self
where
C: FnMut(&RetryContext<E>) + 'static,
{
self.notify = Some(Box::new(callback));
self
}
pub fn on_success<C>(mut self, callback: C) -> Self
where
C: FnMut(&RetryContext<E>) + 'static,
{
self.on_success = Some(Box::new(callback));
self
}
pub fn on_failure<C>(mut self, callback: C) -> Self
where
C: FnMut(&RetryError<E>) + 'static,
{
self.on_failure = Some(Box::new(callback));
self
}
#[cfg(feature = "std")]
pub fn call(self) -> Result<RetryOutcome<T>, RetryError<E>> {
use crate::sleep::StdSleeper;
self.call_with_sleeper(StdSleeper)
}
pub fn call_with_sleeper<S: Sleeper>(
mut self,
sleeper: S,
) -> Result<RetryOutcome<T>, RetryError<E>> {
let mut rng: StdRng = rand::make_rng();
let mut attempt = 1u8;
let max_attempts = self.backoff.max_attempts();
let mut cumulative_delay_ms: u64 = 0;
loop {
match (self.operation)() {
Ok(_value) => {
if let Some(ref mut callback) = self.on_success {
let ctx = RetryContext {
attempt,
next_delay_ms: None,
cumulative_delay_ms,
error: None,
};
callback(&ctx);
}
return Ok(RetryOutcome::new(_value, attempt, cumulative_delay_ms));
}
Err(error) => {
if let Some(ref predicate) = self.when
&& !predicate(&error) {
let retry_error = RetryError::new(
RetryErrorKind::PredicateRejected,
attempt,
max_attempts,
cumulative_delay_ms,
Some(error),
);
if let Some(ref mut callback) = self.on_failure {
callback(&retry_error);
}
return Err(retry_error);
}
if !self.backoff.should_retry(attempt) {
let retry_error = RetryError::new(
RetryErrorKind::Exhausted,
attempt,
max_attempts,
cumulative_delay_ms,
Some(error),
);
if let Some(ref mut callback) = self.on_failure {
callback(&retry_error);
}
return Err(retry_error);
}
match self.backoff.delay(attempt, &mut rng) {
Some(delay_ms) => {
if let Some(ref mut notify) = self.notify {
let ctx = RetryContext {
attempt,
next_delay_ms: Some(delay_ms),
cumulative_delay_ms,
error: Some(&error),
};
notify(&ctx);
}
sleeper.sleep_ms(delay_ms);
cumulative_delay_ms = cumulative_delay_ms.saturating_add(delay_ms);
attempt = attempt.saturating_add(1);
}
None => {
let retry_error = RetryError::new(
RetryErrorKind::Exhausted,
attempt,
max_attempts,
cumulative_delay_ms,
Some(error),
);
if let Some(ref mut callback) = self.on_failure {
callback(&retry_error);
}
return Err(retry_error);
}
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backoff::{ConstantBackoff, ExponentialBackoff};
use crate::sleep::FnSleeper;
#[derive(Debug, PartialEq)]
enum TestError {
Retryable,
Fatal,
}
#[test]
fn test_retry_success_on_first_attempt() {
fn always_succeeds() -> Result<i32, TestError> {
Ok(42)
}
let result = always_succeeds
.retry(ExponentialBackoff::default())
.call_with_sleeper(FnSleeper(|_| {}));
let outcome = result.expect("retry should succeed");
assert_eq!(outcome.attempts(), 1);
assert_eq!(outcome.into_inner(), 42);
}
#[test]
fn test_retry_success_after_failures() {
use core::cell::Cell;
let attempts = Cell::new(0);
let operation = || {
let current = attempts.get();
attempts.set(current + 1);
if current < 2 {
Err(TestError::Retryable)
} else {
Ok(42)
}
};
let result = operation
.retry(ExponentialBackoff::default().max_attempts(3))
.call_with_sleeper(FnSleeper(|_| {}));
let outcome = result.expect("retry should succeed");
assert_eq!(outcome.attempts(), 3);
assert_eq!(outcome.into_inner(), 42);
assert_eq!(attempts.get(), 3);
}
#[test]
fn test_retry_exhausted() {
fn always_fails() -> Result<i32, TestError> {
Err(TestError::Retryable)
}
let result = always_fails
.retry(ExponentialBackoff::default().max_attempts(3))
.call_with_sleeper(FnSleeper(|_| {}));
let err = result.expect_err("retry should exhaust");
assert_eq!(err.kind(), RetryErrorKind::Exhausted);
assert_eq!(err.attempts(), 3);
assert_eq!(err.max_attempts(), 3);
assert!(err.cumulative_delay_ms() > 0);
if let Some(cause) = err.cause() {
assert_eq!(cause, &TestError::Retryable);
} else {
panic!("expected underlying cause");
}
}
#[test]
fn test_retry_when_predicate() {
fn fails_with_fatal() -> Result<i32, TestError> {
Err(TestError::Fatal)
}
let result = fails_with_fatal
.retry(ExponentialBackoff::default())
.when(|e| matches!(e, TestError::Retryable))
.call_with_sleeper(FnSleeper(|_| {}));
let err = result.expect_err("retry should stop due to predicate");
assert_eq!(err.kind(), RetryErrorKind::PredicateRejected);
if let Some(cause) = err.cause() {
assert_eq!(cause, &TestError::Fatal);
} else {
panic!("expected underlying cause");
}
}
#[test]
fn test_retry_notify_callback() {
use core::cell::{Cell, RefCell};
#[cfg(feature = "std")]
use std::rc::Rc;
#[cfg(not(feature = "std"))]
use alloc::rc::Rc;
let attempts = Cell::new(0);
let notify_calls = Rc::new(RefCell::new(Vec::new()));
let notify_calls_clone = Rc::clone(¬ify_calls);
let operation = || {
let current = attempts.get();
attempts.set(current + 1);
if current < 2 {
Err(TestError::Retryable)
} else {
Ok(42)
}
};
let result = operation
.retry(ExponentialBackoff::default().max_attempts(3))
.notify(move |ctx| {
notify_calls_clone.borrow_mut().push((
ctx.attempt,
ctx.next_delay_ms,
ctx.cumulative_delay_ms,
ctx.error.is_some(),
));
assert!(ctx.attempt >= 1);
assert!(ctx.next_delay_ms.is_some());
assert!(ctx.error.is_some());
})
.call_with_sleeper(FnSleeper(|_| {}));
let outcome = result.expect("retry should succeed");
assert_eq!(outcome.attempts(), 3);
let calls = notify_calls.borrow();
assert_eq!(calls.len(), 2);
assert_eq!(calls[0].0, 1); assert_eq!(calls[1].0, 2); }
#[test]
fn test_on_success_callback_invoked() {
use core::cell::Cell;
use core::sync::atomic::{AtomicUsize, Ordering};
static SUCCESS_ATTEMPT: AtomicUsize = AtomicUsize::new(0);
static SUCCESS_CUMULATIVE_DELAY: AtomicUsize = AtomicUsize::new(0);
let attempts = Cell::new(0);
let operation = || {
let current = attempts.get();
attempts.set(current + 1);
if current < 1 {
Err(TestError::Retryable)
} else {
Ok(7)
}
};
SUCCESS_ATTEMPT.store(0, Ordering::SeqCst);
SUCCESS_CUMULATIVE_DELAY.store(0, Ordering::SeqCst);
let outcome = operation
.retry(ExponentialBackoff::default().max_attempts(3))
.on_success(|ctx| {
SUCCESS_ATTEMPT.store(ctx.attempt as usize, Ordering::SeqCst);
SUCCESS_CUMULATIVE_DELAY.store(ctx.cumulative_delay_ms as usize, Ordering::SeqCst);
assert!(ctx.error.is_none());
assert!(ctx.next_delay_ms.is_none());
})
.call_with_sleeper(FnSleeper(|_| {}))
.expect("retry should succeed");
assert_eq!(outcome.into_inner(), 7);
assert_eq!(SUCCESS_ATTEMPT.load(Ordering::SeqCst), 2);
assert!(SUCCESS_CUMULATIVE_DELAY.load(Ordering::SeqCst) > 0);
}
#[test]
fn test_on_failure_callback_invoked() {
use core::sync::atomic::{AtomicUsize, Ordering};
static FAILURE_KIND: AtomicUsize = AtomicUsize::new(0);
static FAILURE_CUMULATIVE_DELAY: AtomicUsize = AtomicUsize::new(0);
fn always_fails() -> Result<(), TestError> {
Err(TestError::Retryable)
}
FAILURE_KIND.store(0, Ordering::SeqCst);
FAILURE_CUMULATIVE_DELAY.store(0, Ordering::SeqCst);
let result = always_fails
.retry(ExponentialBackoff::default().max_attempts(2))
.on_failure(|err| {
let marker = match err.kind() {
RetryErrorKind::Exhausted => 1,
RetryErrorKind::PredicateRejected => 2,
};
FAILURE_KIND.store(marker, Ordering::SeqCst);
FAILURE_CUMULATIVE_DELAY.store(err.cumulative_delay_ms() as usize, Ordering::SeqCst);
})
.call_with_sleeper(FnSleeper(|_| {}));
assert!(result.is_err());
assert_eq!(FAILURE_KIND.load(Ordering::SeqCst), 1);
assert!(FAILURE_CUMULATIVE_DELAY.load(Ordering::SeqCst) > 0);
}
#[test]
fn test_constant_backoff_retry() {
use core::cell::Cell;
let attempts = Cell::new(0);
let operation = || {
let current = attempts.get();
attempts.set(current + 1);
if current < 1 {
Err(TestError::Retryable)
} else {
Ok(42)
}
};
let result = operation
.retry(ConstantBackoff::new().delay_ms(10).max_attempts(2))
.call_with_sleeper(FnSleeper(|_| {}));
let outcome = result.expect("retry should succeed");
assert_eq!(outcome.attempts(), 2);
assert_eq!(outcome.into_inner(), 42);
assert_eq!(attempts.get(), 2);
}
#[cfg(feature = "std")]
#[test]
fn test_retry_with_std_sleeper() {
use core::cell::Cell;
let attempts = Cell::new(0);
let operation = || {
let current = attempts.get();
attempts.set(current + 1);
if current < 1 {
Err(TestError::Retryable)
} else {
Ok(42)
}
};
let start = std::time::Instant::now();
let result = operation
.retry(
ConstantBackoff::new()
.delay_ms(10)
.max_attempts(2)
.jitter_factor(0.0),
)
.call();
let elapsed = start.elapsed();
let outcome = result.expect("retry should succeed");
assert_eq!(outcome.attempts(), 2);
assert_eq!(outcome.into_inner(), 42);
assert!(elapsed.as_millis() >= 9); }
#[test]
fn test_retry_context_comprehensive() {
use core::cell::{Cell, RefCell};
#[cfg(feature = "std")]
use std::rc::Rc;
#[cfg(not(feature = "std"))]
use alloc::rc::Rc;
let attempts = Cell::new(0);
let notify_contexts = Rc::new(RefCell::new(Vec::new()));
let notify_contexts_clone = Rc::clone(¬ify_contexts);
let operation = || {
let current = attempts.get();
attempts.set(current + 1);
if current < 3 {
Err(TestError::Retryable)
} else {
Ok(42)
}
};
let result = operation
.retry(ConstantBackoff::new().delay_ms(100).max_attempts(5).jitter_factor(0.0))
.notify(move |ctx| {
notify_contexts_clone.borrow_mut().push((
ctx.attempt,
ctx.next_delay_ms,
ctx.cumulative_delay_ms,
));
assert!(ctx.error.is_some());
if let Some(err) = ctx.error {
assert_eq!(err, &TestError::Retryable);
}
})
.call_with_sleeper(FnSleeper(|_| {}));
let outcome = result.expect("retry should succeed");
assert_eq!(outcome.attempts(), 4);
let contexts = notify_contexts.borrow();
assert_eq!(contexts.len(), 3);
assert_eq!(contexts[0].0, 1);
assert_eq!(contexts[0].1, Some(100));
assert_eq!(contexts[0].2, 0);
assert_eq!(contexts[1].0, 2);
assert_eq!(contexts[1].1, Some(100));
assert_eq!(contexts[1].2, 100);
assert_eq!(contexts[2].0, 3);
assert_eq!(contexts[2].1, Some(100));
assert_eq!(contexts[2].2, 200); }
#[test]
fn test_retry_context_on_success() {
use core::cell::{Cell, RefCell};
#[cfg(feature = "std")]
use std::rc::Rc;
#[cfg(not(feature = "std"))]
use alloc::rc::Rc;
let attempts = Cell::new(0);
let success_context = Rc::new(RefCell::new(None));
let success_context_clone = Rc::clone(&success_context);
let operation = || {
let current = attempts.get();
attempts.set(current + 1);
if current < 2 {
Err(TestError::Retryable)
} else {
Ok(100)
}
};
let result = operation
.retry(ConstantBackoff::new().delay_ms(50).max_attempts(5).jitter_factor(0.0))
.on_success(move |ctx| {
success_context_clone.borrow_mut().replace((
ctx.attempt,
ctx.next_delay_ms,
ctx.cumulative_delay_ms,
ctx.error.is_none(),
));
})
.call_with_sleeper(FnSleeper(|_| {}));
let outcome = result.expect("retry should succeed");
assert_eq!(outcome.attempts(), 3);
assert_eq!(outcome.cumulative_delay_ms(), 100);
let ctx = success_context.borrow();
assert!(ctx.is_some());
let (attempt, next_delay, cumulative, no_error) = ctx.unwrap();
assert_eq!(attempt, 3);
assert_eq!(next_delay, None); assert_eq!(cumulative, 100);
assert!(no_error); }
#[test]
fn test_retry_context_cumulative_accuracy() {
use core::cell::{Cell, RefCell};
#[cfg(feature = "std")]
use std::rc::Rc;
#[cfg(not(feature = "std"))]
use alloc::rc::Rc;
let attempts = Cell::new(0);
let cumulative_progression = Rc::new(RefCell::new(Vec::new()));
let cumulative_progression_clone = Rc::clone(&cumulative_progression);
let operation = || {
let current = attempts.get();
attempts.set(current + 1);
Err::<(), TestError>(TestError::Retryable)
};
let _result = operation
.retry(ConstantBackoff::new().delay_ms(25).max_attempts(4).jitter_factor(0.0))
.notify(move |ctx| {
cumulative_progression_clone.borrow_mut().push(ctx.cumulative_delay_ms);
})
.call_with_sleeper(FnSleeper(|_| {}));
let progression = cumulative_progression.borrow();
assert_eq!(progression.len(), 3); assert_eq!(progression[0], 0); assert_eq!(progression[1], 25); assert_eq!(progression[2], 50); }
#[test]
fn test_with_exponential_success() {
use super::RetryableExt;
fn always_succeeds() -> Result<i32, TestError> {
Ok(42)
}
let result = always_succeeds
.with_exponential()
.call_with_sleeper(FnSleeper(|_| {}));
let outcome = result.expect("retry should succeed");
assert_eq!(outcome.attempts(), 1);
assert_eq!(outcome.into_inner(), 42);
}
#[test]
fn test_with_exponential_retry_behavior() {
use super::RetryableExt;
use core::cell::Cell;
let attempts = Cell::new(0);
let operation = || {
let current = attempts.get();
attempts.set(current + 1);
if current < 2 {
Err(TestError::Retryable)
} else {
Ok(100)
}
};
let result = operation
.with_exponential()
.call_with_sleeper(FnSleeper(|_| {}));
let outcome = result.expect("retry should succeed");
assert_eq!(outcome.attempts(), 3);
assert_eq!(outcome.into_inner(), 100);
assert_eq!(attempts.get(), 3);
}
#[test]
fn test_with_exponential_exhausted() {
use super::RetryableExt;
fn always_fails() -> Result<i32, TestError> {
Err(TestError::Retryable)
}
let result = always_fails
.with_exponential()
.call_with_sleeper(FnSleeper(|_| {}));
let err = result.expect_err("retry should exhaust");
assert_eq!(err.kind(), RetryErrorKind::Exhausted);
assert_eq!(err.attempts(), 3); assert_eq!(err.max_attempts(), 3);
}
#[test]
fn test_with_exponential_chaining() {
use super::RetryableExt;
use core::cell::Cell;
let attempts = Cell::new(0);
let operation = || {
let current = attempts.get();
attempts.set(current + 1);
if current < 1 {
Err(TestError::Retryable)
} else {
Ok(7)
}
};
let result = operation
.with_exponential()
.when(|e| matches!(e, TestError::Retryable))
.call_with_sleeper(FnSleeper(|_| {}));
let outcome = result.expect("retry should succeed");
assert_eq!(outcome.attempts(), 2);
assert_eq!(outcome.into_inner(), 7);
}
#[test]
fn test_with_constant_success() {
use super::RetryableExt;
fn always_succeeds() -> Result<i32, TestError> {
Ok(99)
}
let result = always_succeeds
.with_constant(250)
.call_with_sleeper(FnSleeper(|_| {}));
let outcome = result.expect("retry should succeed");
assert_eq!(outcome.attempts(), 1);
assert_eq!(outcome.into_inner(), 99);
}
#[test]
fn test_with_constant_uses_correct_delay() {
use super::RetryableExt;
use core::cell::{Cell, RefCell};
#[cfg(feature = "std")]
use std::rc::Rc;
#[cfg(not(feature = "std"))]
use alloc::rc::Rc;
let attempts = Cell::new(0);
let delays = Rc::new(RefCell::new(Vec::new()));
let delays_clone = Rc::clone(&delays);
let operation = || {
let current = attempts.get();
attempts.set(current + 1);
if current < 2 {
Err(TestError::Retryable)
} else {
Ok(42)
}
};
let result = operation
.with_constant(500)
.notify(move |ctx| {
if let Some(delay) = ctx.next_delay_ms {
delays_clone.borrow_mut().push(delay);
}
})
.call_with_sleeper(FnSleeper(|_| {}));
let outcome = result.expect("retry should succeed");
assert_eq!(outcome.attempts(), 3);
let recorded_delays = delays.borrow();
assert_eq!(recorded_delays.len(), 2); assert_eq!(recorded_delays[0], 500);
assert_eq!(recorded_delays[1], 500);
}
#[test]
fn test_with_constant_chaining() {
use super::RetryableExt;
use core::cell::Cell;
let attempts = Cell::new(0);
let operation = || {
let current = attempts.get();
attempts.set(current + 1);
if current < 1 {
Err(TestError::Fatal)
} else {
Ok(123)
}
};
let result = operation
.with_constant(100)
.when(|e| matches!(e, TestError::Retryable))
.call_with_sleeper(FnSleeper(|_| {}));
let err = result.expect_err("retry should fail due to predicate");
assert_eq!(err.kind(), RetryErrorKind::PredicateRejected);
assert_eq!(err.attempts(), 1);
}
#[test]
fn test_with_fibonacci_success() {
use super::RetryableExt;
fn always_succeeds() -> Result<String, TestError> {
Ok("success".to_string())
}
let result = always_succeeds
.with_fibonacci()
.call_with_sleeper(FnSleeper(|_| {}));
let outcome = result.expect("retry should succeed");
assert_eq!(outcome.attempts(), 1);
assert_eq!(outcome.into_inner(), "success");
}
#[test]
fn test_with_fibonacci_retry_behavior() {
use super::RetryableExt;
use core::cell::Cell;
let attempts = Cell::new(0);
let operation = || {
let current = attempts.get();
attempts.set(current + 1);
if current < 3 {
Err(TestError::Retryable)
} else {
Ok(777)
}
};
let result = operation
.with_fibonacci()
.call_with_sleeper(FnSleeper(|_| {}));
let outcome = result.expect("retry should succeed");
assert_eq!(outcome.attempts(), 4);
assert_eq!(outcome.into_inner(), 777);
}
#[test]
fn test_with_fibonacci_exhausted() {
use super::RetryableExt;
fn always_fails() -> Result<i32, TestError> {
Err(TestError::Retryable)
}
let result = always_fails
.with_fibonacci()
.call_with_sleeper(FnSleeper(|_| {}));
let err = result.expect_err("retry should exhaust");
assert_eq!(err.kind(), RetryErrorKind::Exhausted);
assert_eq!(err.attempts(), 8); assert_eq!(err.max_attempts(), 8);
}
#[test]
fn test_with_fibonacci_chaining() {
use super::RetryableExt;
use core::cell::Cell;
use core::sync::atomic::{AtomicUsize, Ordering};
static SUCCESS_COUNT: AtomicUsize = AtomicUsize::new(0);
let attempts = Cell::new(0);
let operation = || {
let current = attempts.get();
attempts.set(current + 1);
if current < 2 {
Err(TestError::Retryable)
} else {
Ok(555)
}
};
SUCCESS_COUNT.store(0, Ordering::SeqCst);
let result = operation
.with_fibonacci()
.on_success(|_ctx| {
SUCCESS_COUNT.fetch_add(1, Ordering::SeqCst);
})
.call_with_sleeper(FnSleeper(|_| {}));
let outcome = result.expect("retry should succeed");
assert_eq!(outcome.attempts(), 3);
assert_eq!(outcome.into_inner(), 555);
assert_eq!(SUCCESS_COUNT.load(Ordering::SeqCst), 1);
}
#[test]
fn test_all_extension_methods_produce_working_retries() {
use super::RetryableExt;
use core::cell::Cell;
let attempts_exp = Cell::new(0);
let op_exp = || {
let current = attempts_exp.get();
attempts_exp.set(current + 1);
if current < 1 {
Err(TestError::Retryable)
} else {
Ok(1)
}
};
let attempts_const = Cell::new(0);
let op_const = || {
let current = attempts_const.get();
attempts_const.set(current + 1);
if current < 1 {
Err(TestError::Retryable)
} else {
Ok(2)
}
};
let attempts_fib = Cell::new(0);
let op_fib = || {
let current = attempts_fib.get();
attempts_fib.set(current + 1);
if current < 1 {
Err(TestError::Retryable)
} else {
Ok(3)
}
};
let r1 = op_exp
.with_exponential()
.call_with_sleeper(FnSleeper(|_| {}))
.expect("exponential retry works");
let r2 = op_const
.with_constant(100)
.call_with_sleeper(FnSleeper(|_| {}))
.expect("constant retry works");
let r3 = op_fib
.with_fibonacci()
.call_with_sleeper(FnSleeper(|_| {}))
.expect("fibonacci retry works");
assert_eq!(r1.into_inner(), 1);
assert_eq!(r2.into_inner(), 2);
assert_eq!(r3.into_inner(), 3);
}
#[cfg(feature = "std")]
#[test]
fn test_with_exponential_std_sleeper() {
use super::RetryableExt;
use core::cell::Cell;
let attempts = Cell::new(0);
let operation = || {
let current = attempts.get();
attempts.set(current + 1);
if current < 1 {
Err(TestError::Retryable)
} else {
Ok(999)
}
};
let result = operation.with_exponential().call();
let outcome = result.expect("retry should succeed");
assert_eq!(outcome.attempts(), 2);
assert_eq!(outcome.into_inner(), 999);
}
#[cfg(feature = "std")]
#[test]
fn test_with_constant_std_sleeper() {
use super::RetryableExt;
fn always_succeeds() -> Result<i32, TestError> {
Ok(888)
}
let result = always_succeeds.with_constant(50).call();
let outcome = result.expect("retry should succeed");
assert_eq!(outcome.into_inner(), 888);
}
#[cfg(feature = "std")]
#[test]
fn test_with_fibonacci_std_sleeper() {
use super::RetryableExt;
fn always_succeeds() -> Result<i32, TestError> {
Ok(444)
}
let result = always_succeeds.with_fibonacci().call();
let outcome = result.expect("retry should succeed");
assert_eq!(outcome.into_inner(), 444);
}
}