do-over 0.1.0

Async resilience policies for Rust inspired by Polly
Documentation
//! Hedge Policy Example
//!
//! This example demonstrates the Hedge policy for reducing tail latency.
//! A hedge request is launched after a delay if the primary hasn't completed yet.
//! Whichever request completes first wins.

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

#[tokio::main]
async fn main() {
    println!("=== Do-Over Hedge Policy Example ===\n");
    println!("Configuration: hedge_delay=100ms");
    println!("(A backup request is sent 100ms after the primary)\n");

    // Scenario 1: Fast primary - wins before hedge starts
    fast_primary_example().await;

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

    // Scenario 2: Slow primary - hedge wins
    slow_primary_example().await;

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

    // Scenario 3: Both requests similar speed
    race_example().await;
}

async fn fast_primary_example() {
    println!("📌 Scenario 1: Fast Primary (completes before hedge starts)");
    println!("   Primary latency: 50ms, Hedge delay: 100ms\n");

    let hedge = Hedge::new(Duration::from_millis(100));
    let request_count = Arc::new(AtomicUsize::new(0));
    let start = Instant::now();

    let result: Result<String, DoOverError<String>> = {
        let rc = Arc::clone(&request_count);
        hedge
            .execute(|| {
                let count = Arc::clone(&rc);
                async move {
                    let req_num = count.fetch_add(1, Ordering::SeqCst) + 1;
                    let elapsed = start.elapsed().as_millis();
                    println!(
                        "   [+{:>4}ms] Request {} started ({})",
                        elapsed,
                        req_num,
                        if req_num == 1 { "primary" } else { "hedge" }
                    );

                    // Primary is fast - 50ms
                    tokio::time::sleep(Duration::from_millis(50)).await;

                    let elapsed = start.elapsed().as_millis();
                    println!(
                        "   [+{:>4}ms] Request {} completed",
                        elapsed, req_num
                    );
                    Ok(format!("Response from request {}", req_num))
                }
            })
            .await
    };

    let total_time = start.elapsed().as_millis();
    let total_requests = request_count.load(Ordering::SeqCst);

    println!("\n   Result: {:?}", result.unwrap());
    println!("   Total latency: {}ms", total_time);
    println!("   Requests made: {}", total_requests);
    println!("\n   💡 Primary completed in 50ms, before the 100ms hedge delay");
}

async fn slow_primary_example() {
    println!("📌 Scenario 2: Slow Primary (hedge wins)");
    println!("   Primary latency: 300ms, Hedge delay: 100ms, Hedge latency: 50ms\n");

    let hedge = Hedge::new(Duration::from_millis(100));
    let request_count = Arc::new(AtomicUsize::new(0));
    let start = Instant::now();

    let result: Result<String, DoOverError<String>> = {
        let rc = Arc::clone(&request_count);
        hedge
            .execute(|| {
                let count = Arc::clone(&rc);
                async move {
                    let req_num = count.fetch_add(1, Ordering::SeqCst) + 1;
                    let elapsed = start.elapsed().as_millis();
                    let is_primary = req_num == 1;

                    println!(
                        "   [+{:>4}ms] Request {} started ({})",
                        elapsed,
                        req_num,
                        if is_primary { "primary" } else { "hedge" }
                    );

                    // Primary is slow (300ms), hedge is fast (50ms)
                    let delay = if is_primary { 300 } else { 50 };
                    tokio::time::sleep(Duration::from_millis(delay)).await;

                    let elapsed = start.elapsed().as_millis();
                    println!(
                        "   [+{:>4}ms] Request {} completed",
                        elapsed, req_num
                    );
                    Ok(format!("Response from request {}", req_num))
                }
            })
            .await
    };

    let total_time = start.elapsed().as_millis();
    let total_requests = request_count.load(Ordering::SeqCst);

    println!("\n   Result: {:?}", result.unwrap());
    println!("   Total latency: {}ms (would be 300ms without hedging)", total_time);
    println!("   Requests made: {}", total_requests);
    println!("\n   💡 Hedge started at 100ms, completed at 150ms, beating the slow primary");
}

async fn race_example() {
    println!("📌 Scenario 3: Close Race (both requests similar speed)");
    println!("   Primary latency: 120ms, Hedge delay: 100ms, Hedge latency: 30ms\n");

    let hedge = Hedge::new(Duration::from_millis(100));
    let request_count = Arc::new(AtomicUsize::new(0));
    let start = Instant::now();

    let result: Result<String, DoOverError<String>> = {
        let rc = Arc::clone(&request_count);
        hedge
            .execute(|| {
                let count = Arc::clone(&rc);
                async move {
                    let req_num = count.fetch_add(1, Ordering::SeqCst) + 1;
                    let elapsed = start.elapsed().as_millis();
                    let is_primary = req_num == 1;

                    println!(
                        "   [+{:>4}ms] Request {} started ({})",
                        elapsed,
                        req_num,
                        if is_primary { "primary" } else { "hedge" }
                    );

                    // Primary: 120ms total, Hedge: starts at 100ms + 30ms = 130ms total
                    // Primary should win by ~10ms
                    let delay = if is_primary { 120 } else { 30 };
                    tokio::time::sleep(Duration::from_millis(delay)).await;

                    let elapsed = start.elapsed().as_millis();
                    println!(
                        "   [+{:>4}ms] Request {} completed",
                        elapsed, req_num
                    );
                    Ok(format!("Response from request {} ({})", req_num, if is_primary { "primary" } else { "hedge" }))
                }
            })
            .await
    };

    let total_time = start.elapsed().as_millis();
    let total_requests = request_count.load(Ordering::SeqCst);

    println!("\n   Result: {:?}", result.unwrap());
    println!("   Total latency: {}ms", total_time);
    println!("   Requests made: {}", total_requests);
    println!("\n   💡 Primary wins at ~120ms, hedge (would finish at ~130ms) is cancelled");
}