do-over 0.1.0

Async resilience policies for Rust inspired by Polly
Documentation
//! Hedge policy for reducing tail latency.
//!
//! The hedge policy starts a backup request if the primary request is slow,
//! returning whichever completes first. This reduces tail latency at the cost
//! of potentially sending more requests.
//!
//! # How It Works
//!
//! 1. Start the primary request
//! 2. After a configured delay, start a hedged (backup) request
//! 3. Return whichever completes first
//! 4. Cancel the slower request
//!
//! # Important
//!
//! Only use hedging with **idempotent** operations (safe to execute multiple times).
//!
//! # Examples
//!
//! ```rust
//! use do_over::{policy::Policy, hedge::Hedge, error::DoOverError};
//! use std::time::Duration;
//!
//! # async fn example() -> Result<(), DoOverError<std::io::Error>> {
//! // Start backup request after 100ms if primary hasn't completed
//! let hedge = Hedge::new(Duration::from_millis(100));
//!
//! let result = hedge.execute(|| async {
//!     Ok::<_, DoOverError<std::io::Error>>("completed")
//! }).await?;
//! # Ok(())
//! # }
//! ```

use std::time::Duration;
use tokio::time::sleep;
use crate::{policy::Policy, error::DoOverError};

/// A policy that sends backup requests to reduce tail latency.
///
/// After a configured delay, if the primary request hasn't completed,
/// a hedge (backup) request is started. The first response wins.
///
/// # Warning
///
/// Only use with idempotent operations. Using hedging with non-idempotent
/// operations (like payment processing) can cause duplicate effects.
///
/// # Examples
///
/// ```rust
/// use do_over::{policy::Policy, hedge::Hedge, error::DoOverError};
/// use std::time::Duration;
///
/// # async fn example() {
/// // Good use case: read operations
/// let hedge = Hedge::new(Duration::from_millis(100));
///
/// // The hedge request starts if primary takes > 100ms
/// let result: Result<String, DoOverError<String>> = hedge.execute(|| async {
///     Ok("data".to_string())
/// }).await;
/// # }
/// ```
#[derive(Clone)]
pub struct Hedge {
    delay: Duration,
}

impl Hedge {
    /// Create a new hedge policy.
    ///
    /// # Arguments
    ///
    /// * `delay` - How long to wait before starting the hedge request
    ///
    /// # Examples
    ///
    /// ```rust
    /// use do_over::hedge::Hedge;
    /// use std::time::Duration;
    ///
    /// // Start hedge after 100ms
    /// let hedge = Hedge::new(Duration::from_millis(100));
    /// ```
    pub fn new(delay: Duration) -> Self {
        Self { delay }
    }
}

#[async_trait::async_trait]
impl<E> Policy<DoOverError<E>> for Hedge
where
    E: Send + Sync,
{
    async fn execute<F, Fut, T>(&self, f: F) -> Result<T, DoOverError<E>>
    where
        F: Fn() -> Fut + Send + Sync,
        Fut: std::future::Future<Output = Result<T, DoOverError<E>>> + Send,
        T: Send,
    {
        // Use tokio::select! with futures instead of spawning tasks
        tokio::select! {
            r = f() => r,
            r = async {
                sleep(self.delay).await;
                f().await
            } => r,
        }
    }
}