do-over 0.1.0

Async resilience policies for Rust inspired by Polly
Documentation
//! Retry Policy Example
//!
//! This example demonstrates the RetryPolicy with both fixed and exponential backoff strategies.
//! It shows how retries work with simulated transient failures.

use do_over::{error::DoOverError, policy::Policy, retry::RetryPolicy};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};

#[tokio::main]
async fn main() {
    println!("=== Do-Over Retry Policy Example ===\n");

    // Scenario 1: Fixed backoff - fails twice, succeeds on third attempt
    fixed_backoff_example().await;

    println!("\n{}\n", "".repeat(60));

    // Scenario 2: Exponential backoff - show increasing delays
    exponential_backoff_example().await;

    println!("\n{}\n", "".repeat(60));

    // Scenario 3: Exhausts all retries
    exhausted_retries_example().await;
}

async fn fixed_backoff_example() {
    println!("📌 Scenario 1: Fixed Backoff (fails twice, succeeds on third)");
    println!("   Configuration: max_retries=2, delay=100ms\n");

    let policy = RetryPolicy::fixed(2, Duration::from_millis(100));
    let attempt_count = Arc::new(AtomicUsize::new(0));
    let start = Instant::now();

    let result: Result<String, DoOverError<String>> = policy
        .execute(|| {
            let attempts = Arc::clone(&attempt_count);
            async move {
                let attempt = attempts.fetch_add(1, Ordering::SeqCst) + 1;
                let elapsed = start.elapsed().as_millis();

                if attempt < 3 {
                    println!(
                        "   [+{:>4}ms] Attempt {}: ❌ Simulated failure",
                        elapsed, attempt
                    );
                    Err(DoOverError::Inner(format!("Transient error on attempt {}", attempt)))
                } else {
                    println!(
                        "   [+{:>4}ms] Attempt {}: ✅ Success!",
                        elapsed, attempt
                    );
                    Ok("Operation completed successfully".to_string())
                }
            }
        })
        .await;

    let total_time = start.elapsed().as_millis();
    println!("\n   Result: {:?}", result.unwrap());
    println!("   Total time: {}ms (expected ~200ms for 2 retries)", total_time);
}

async fn exponential_backoff_example() {
    println!("📌 Scenario 2: Exponential Backoff (show increasing delays)");
    println!("   Configuration: max_retries=3, base=100ms, factor=2.0\n");

    let policy = RetryPolicy::exponential(3, Duration::from_millis(100), 2.0);
    let attempt_count = Arc::new(AtomicUsize::new(0));
    let start = Instant::now();

    let result: Result<String, DoOverError<String>> = policy
        .execute(|| {
            let attempts = Arc::clone(&attempt_count);
            async move {
                let attempt = attempts.fetch_add(1, Ordering::SeqCst) + 1;
                let elapsed = start.elapsed().as_millis();

                if attempt < 4 {
                    println!(
                        "   [+{:>4}ms] Attempt {}: ❌ Simulated failure (next delay: {}ms)",
                        elapsed,
                        attempt,
                        100 * 2_i32.pow(attempt as u32)
                    );
                    Err(DoOverError::Inner(format!("Transient error")))
                } else {
                    println!(
                        "   [+{:>4}ms] Attempt {}: ✅ Success!",
                        elapsed, attempt
                    );
                    Ok("Operation completed with exponential backoff".to_string())
                }
            }
        })
        .await;

    let total_time = start.elapsed().as_millis();
    println!("\n   Result: {:?}", result.unwrap());
    println!(
        "   Total time: {}ms (expected ~1400ms: 200+400+800)",
        total_time
    );
    println!("   Delays: 200ms → 400ms → 800ms (exponential growth)");
}

async fn exhausted_retries_example() {
    println!("📌 Scenario 3: Exhausted Retries (all attempts fail)");
    println!("   Configuration: max_retries=2, delay=50ms\n");

    let policy = RetryPolicy::fixed(2, Duration::from_millis(50));
    let attempt_count = Arc::new(AtomicUsize::new(0));
    let start = Instant::now();

    let result: Result<String, DoOverError<String>> = policy
        .execute(|| {
            let attempts = Arc::clone(&attempt_count);
            async move {
                let attempt = attempts.fetch_add(1, Ordering::SeqCst) + 1;
                let elapsed = start.elapsed().as_millis();
                println!(
                    "   [+{:>4}ms] Attempt {}: ❌ Permanent failure",
                    elapsed, attempt
                );
                Err(DoOverError::Inner(format!(
                    "Service unavailable (attempt {})",
                    attempt
                )))
            }
        })
        .await;

    let total_time = start.elapsed().as_millis();
    println!("\n   Result: {:?}", result.unwrap_err());
    println!(
        "   Total time: {}ms (3 attempts, 2 retries exhausted)",
        total_time
    );
}