do-over 0.1.0

Async resilience policies for Rust inspired by Polly
Documentation
//! Rate Limiter Example
//!
//! This example demonstrates the RateLimiter policy using a token bucket algorithm.
//! It shows how burst capacity works and how tokens are replenished over time.

use do_over::{error::DoOverError, policy::Policy, rate_limit::RateLimiter};
use std::time::{Duration, Instant};

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

    // Scenario 1: Burst of requests
    burst_example().await;

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

    // Scenario 2: Token replenishment
    replenishment_example().await;
}

async fn burst_example() {
    println!("📌 Scenario 1: Burst of Requests");
    println!("   Configuration: capacity=5, interval=1s");
    println!("   Sending 8 requests in rapid succession...\n");

    let limiter = RateLimiter::new(5, Duration::from_secs(1));
    let start = Instant::now();

    let mut allowed = 0;
    let mut rejected = 0;

    for i in 1..=8 {
        let result: Result<String, DoOverError<String>> = limiter
            .execute(|| async move { Ok(format!("Request {} processed", i)) })
            .await;

        let elapsed = start.elapsed().as_millis();
        match result {
            Ok(msg) => {
                println!("   [+{:>4}ms] Request {}: ✅ {}", elapsed, i, msg);
                allowed += 1;
            }
            Err(DoOverError::BulkheadFull) => {
                println!("   [+{:>4}ms] Request {}: 🚫 Rate limited", elapsed, i);
                rejected += 1;
            }
            Err(e) => println!("   [+{:>4}ms] Request {}: Error - {:?}", elapsed, i, e),
        }
    }

    println!("\n   Summary:");
    println!("   - Allowed:  {} (capacity was 5)", allowed);
    println!("   - Rejected: {} (exceeded capacity)", rejected);
    println!("\n   💡 First 5 requests used the burst capacity, remaining 3 were rate limited");
}

async fn replenishment_example() {
    println!("📌 Scenario 2: Token Replenishment");
    println!("   Configuration: capacity=3, interval=500ms");
    println!("   Demonstrating token refill after interval...\n");

    let limiter = RateLimiter::new(3, Duration::from_millis(500));
    let start = Instant::now();

    // Phase 1: Use up all tokens
    println!("   Phase 1: Using up all 3 tokens");
    for i in 1..=4 {
        let result: Result<String, DoOverError<String>> = limiter
            .execute(|| async move { Ok(format!("Request {}", i)) })
            .await;

        let elapsed = start.elapsed().as_millis();
        match result {
            Ok(_) => println!("   [+{:>4}ms] Request {}: ✅ Token consumed", elapsed, i),
            Err(DoOverError::BulkheadFull) => {
                println!("   [+{:>4}ms] Request {}: 🚫 No tokens available", elapsed, i)
            }
            Err(e) => println!("   [+{:>4}ms] Request {}: Error - {:?}", elapsed, i, e),
        }
    }

    // Phase 2: Wait for tokens to replenish
    println!("\n   Phase 2: Waiting 500ms for token replenishment...");
    tokio::time::sleep(Duration::from_millis(500)).await;
    let elapsed = start.elapsed().as_millis();
    println!("   [+{:>4}ms] → Tokens should be replenished now", elapsed);

    // Phase 3: Try again after replenishment
    println!("\n   Phase 3: Sending requests after replenishment");
    for i in 5..=8 {
        let result: Result<String, DoOverError<String>> = limiter
            .execute(|| async move { Ok(format!("Request {}", i)) })
            .await;

        let elapsed = start.elapsed().as_millis();
        match result {
            Ok(_) => println!("   [+{:>4}ms] Request {}: ✅ Token consumed", elapsed, i),
            Err(DoOverError::BulkheadFull) => {
                println!("   [+{:>4}ms] Request {}: 🚫 No tokens available", elapsed, i)
            }
            Err(e) => println!("   [+{:>4}ms] Request {}: Error - {:?}", elapsed, i, e),
        }
    }

    println!(
        "\n   💡 After 500ms, all 3 tokens were replenished and available for use"
    );
}