resilient-rs 0.2.1

A Rust utility library for fault tolerance, including retry strategies, backoff mechanisms, and failure handling.
Documentation
use crate::config::RetryConfig;
use log::{info, warn};
use tokio::time::sleep;

/// Retries an asynchronous operation using exponential backoff.
///
/// This function repeatedly attempts to execute the provided asynchronous operation
/// until it either succeeds or reaches the maximum number of retry attempts.
///
/// # Parameters
/// - `operation`: A function that returns a `Future` resolving to a `Result<T, E>`.
/// - `retry_config`: A reference to a `RetryConfig` struct specifying the delay and maximum attempts.
///
/// # Returns
/// - `Ok(T)`: If the operation succeeds within the allowed retry attempts.
/// - `Err(E)`: If the operation continues to fail after the maximum retry attempts.
///
/// # Behavior
/// - Starts with an initial delay specified in `retry_config.delay`.
/// - On each failure, logs a warning and doubles the delay before retrying.
/// - Stops retrying once `retry_config.max_attempts` is reached.
///
/// # Example
/// ```rust
/// use std::time::Duration;
/// use tokio::time::sleep;
/// use resilient_rs::asynchronous::retry_with_backoff::retry_with_exponential_backoff;
/// use resilient_rs::config::RetryConfig;
///
/// async fn my_operation() -> Result<(), &'static str> {
///     Err("Some error")
/// }
///
/// #[tokio::main]
/// async fn main() {
/// let config = RetryConfig::default();
///
///     let result = retry_with_exponential_backoff(my_operation, &config).await;
///     match result {
///         Ok(_) => println!("Success!"),
///         Err(e) => println!("Failed: {}", e),
///     }
/// }
/// ```
///
/// # Notes
/// - The delay is multiplied by 2 after each failed attempt.
/// - The function logs warnings for failed attempts and final failure.
pub async fn retry_with_exponential_backoff<F, Fut, T, E>(
    mut operation: F,
    retry_config: &RetryConfig,
) -> Result<T, E>
where
    F: FnMut() -> Fut,
    Fut: Future<Output = Result<T, E>>,
{
    let mut attempts = 0;
    let mut delay = retry_config.delay;

    loop {
        match operation().await {
            Ok(output) => {
                info!("Operation succeeded after {} attempts", attempts + 1);
                return Ok(output);
            }
            Err(_) if attempts + 1 < retry_config.max_attempts => {
                warn!(
                    "Operation failed (attempt {}/{}), retrying after {:?}...",
                    attempts + 1,
                    retry_config.max_attempts,
                    delay
                );
                sleep(delay).await;
                delay *= 2;
            }
            Err(err) => {
                warn!(
                    "Operation failed after {} attempts, giving up.",
                    attempts + 1
                );
                return Err(err);
            }
        }

        attempts += 1;
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::{Arc, Mutex};
    use std::time::Duration;

    #[derive(Debug, PartialEq, Eq)]
    struct DummyError(&'static str);

    #[tokio::test]
    async fn test_retry_success_first_try() {
        let config = RetryConfig::default();

        let attempts = Arc::new(Mutex::new(0));

        let op_attempts = attempts.clone();
        let operation = move || {
            let op_attempts = op_attempts.clone();
            async move {
                let mut count = op_attempts.lock().unwrap();
                *count += 1;
                Ok::<_, DummyError>("successful")
            }
        };

        let result = retry_with_exponential_backoff(operation, &config).await;
        assert_eq!(result, Ok("successful"));
        assert_eq!(*attempts.lock().unwrap(), 1);
    }

    #[tokio::test]
    async fn test_retry_success_after_failures() {
        let config = RetryConfig {
            max_attempts: 5,
            delay: Duration::from_millis(10),
        };

        let attempts = Arc::new(Mutex::new(0));

        let op_attempts = attempts.clone();
        let operation = move || {
            let op_attempts = op_attempts.clone();
            async move {
                let mut count = op_attempts.lock().unwrap();
                *count += 1;
                if *count < 4 {
                    Err(DummyError("temporary fail"))
                } else {
                    Ok("eventual success")
                }
            }
        };

        let result = retry_with_exponential_backoff(operation, &config).await;
        assert_eq!(result, Ok("eventual success"));
        assert_eq!(*attempts.lock().unwrap(), 4);
    }

    #[tokio::test]
    async fn test_retry_failure_all_attempts() {
        let config = RetryConfig {
            max_attempts: 3,
            delay: Duration::from_millis(10),
        };

        let attempts = Arc::new(Mutex::new(0));

        let op_attempts = attempts.clone();
        let operation = move || {
            let op_attempts = op_attempts.clone();
            async move {
                let mut count = op_attempts.lock().unwrap();
                *count += 1;
                Err(DummyError("always fail"))
            }
        };

        let result: Result<(), DummyError> =
            retry_with_exponential_backoff(operation, &config).await;
        assert_eq!(result, Err(DummyError("always fail")));
        assert_eq!(*attempts.lock().unwrap(), config.max_attempts);
    }
}