rate-guard 0.1.0

Thread-safe rate limiting library with multiple algorithms and Duration-based configuration
Documentation
// rate-guard\tests\test_std_time_source_async.rs
//! Async tests for StdTimeSource integration, demonstrating usage in async contexts.

#[cfg(all(feature = "std-time", feature = "tokio-time"))]
mod std_time_async_tests {
    use rate_guard::{Nanos, Millis, StdTimeSource, RateLimit};
    use rate_guard::limits::{
        TokenBucketBuilder, FixedWindowCounterBuilder,
        SlidingWindowCounterBuilder, ApproximateSlidingWindowBuilder
    };
    use std::time::Duration;
    use tokio::time::sleep;

    #[tokio::test]
    async fn test_token_bucket_with_std_time_async() {
        let bucket = TokenBucketBuilder::builder()
            .capacity(100)
            .refill_amount(10)
            .refill_every(Duration::from_millis(100))
            .with_time(StdTimeSource::new())
            .with_precision::<Millis>()
            .build()
            .unwrap();

        // Use some tokens
        assert!(bucket.try_acquire(50).is_ok());
        assert_eq!(bucket.capacity_remaining().unwrap(), 50);

        // Wait for refill (async)
        sleep(Duration::from_millis(150)).await;

        // Should have refilled
        let remaining = bucket.capacity_remaining().unwrap();
        assert!(remaining > 50, "Expected refill, got {} tokens", remaining);
        assert!(remaining <= 60, "Expected at most 60 tokens, got {}", remaining);
    }

    #[tokio::test]
    async fn test_async_retry_pattern() {
        let bucket = TokenBucketBuilder::builder()
            .capacity(10)
            .refill_amount(5)
            .refill_every(Duration::from_millis(100))
            .with_time(StdTimeSource::new())
            .with_precision::<Millis>()
            .build()
            .unwrap();

        // Exhaust the bucket
        assert!(bucket.try_acquire(10).is_ok());
        assert_eq!(bucket.capacity_remaining().unwrap(), 0);

        // Should be rate limited
        assert!(bucket.try_acquire(1).is_err());

        // Async retry pattern
        let result = async_acquire_with_retry(&bucket, 3, 3).await;
        assert!(result.is_ok(), "Should eventually succeed after retry");
    }

    #[tokio::test]
    async fn test_concurrent_async_tasks() {
        use std::sync::Arc;

        let bucket = Arc::new(
            TokenBucketBuilder::builder()
                .capacity(100)
                .refill_amount(20)
                .refill_every(Duration::from_millis(50))
                .with_time(StdTimeSource::new())
                .with_precision::<Millis>()
                .build()
                .unwrap()
        );

        let mut tasks = vec![];

        for _ in 0..5 {
            let bucket_clone = Arc::clone(&bucket);
            let task = tokio::spawn(async move {
                let mut acquired = 0;
                for _ in 0..10 {
                    if bucket_clone.try_acquire(2).is_ok() {
                        acquired += 1;
                    }
                    tokio::time::sleep(Duration::from_millis(25)).await;
                }
                acquired
            });
            tasks.push(task);
        }

        let mut total = 0;
        for task in tasks {
            total += task.await.unwrap();
        }

        println!("Total acquisitions across all tasks: {}", total);
        assert!(total > 0, "Should have acquired some tokens");
    }

    #[tokio::test]
    async fn test_all_limiters_with_async() {
        let time_source = StdTimeSource::new();

        // Token Bucket
        let token_bucket = TokenBucketBuilder::builder()
            .capacity(50)
            .refill_amount(10)
            .refill_every(Duration::from_millis(100))
            .with_time(time_source.clone())
            .with_precision::<Millis>()
            .build()
            .unwrap();

        // Fixed Window Counter
        let fixed_window = FixedWindowCounterBuilder::builder()
            .capacity(50)
            .window_duration(Duration::from_millis(200))
            .with_time(time_source.clone())
            .with_precision::<Millis>()
            .build()
            .unwrap();

        // Sliding Window Counter
        let sliding_window = SlidingWindowCounterBuilder::builder()
            .capacity(50)
            .bucket_duration(Duration::from_millis(50))
            .bucket_count(4)
            .with_time(time_source.clone())
            .with_precision::<Millis>()
            .build()
            .unwrap();

        // Approximate Sliding Window
        let approx_window = ApproximateSlidingWindowBuilder::builder()
            .capacity(50)
            .window_duration(Duration::from_millis(200))
            .with_time(time_source)
            .with_precision::<Millis>()
            .build()
            .unwrap();

        // Test all limiters
        assert!(token_bucket.try_acquire(20).is_ok());
        assert!(fixed_window.try_acquire(20).is_ok());
        assert!(sliding_window.try_acquire(20).is_ok());
        assert!(approx_window.try_acquire(20).is_ok());

        // Wait and test refill/reset behavior
        sleep(Duration::from_millis(250)).await;

        // Token bucket should have refilled
        assert!(token_bucket.capacity_remaining().unwrap() > 30);
        
        // Fixed window should have reset
        assert_eq!(fixed_window.capacity_remaining().unwrap(), 50);
        
        // Sliding window should have some recovery
        assert!(sliding_window.capacity_remaining().unwrap() >= 30);
        
        // Approximate window should show some recovery
        assert!(approx_window.capacity_remaining().unwrap() > 30);
    }

    #[tokio::test]
    async fn test_high_frequency_async_usage() {
        let bucket = TokenBucketBuilder::builder()
            .capacity(1000)
            .refill_amount(100)
            .refill_every(Duration::from_millis(10))
            .with_time(StdTimeSource::new())
            .with_precision::<Nanos>()
            .build()
            .unwrap();

        let mut successful_acquisitions = 0;
        let total_attempts = 100;

        for _ in 0..total_attempts {
            if bucket.try_acquire(5).is_ok() {
                successful_acquisitions += 1;
            }
            // Very short async delay
            sleep(Duration::from_millis(1)).await;
        }

        println!("Successful acquisitions: {}/{}", successful_acquisitions, total_attempts);
        assert!(successful_acquisitions > 0, "Should have some successful acquisitions");
    }

    // Helper function for async retry pattern
    async fn async_acquire_with_retry<R: RateLimit>(
        limiter: &R,
        tokens: rate_guard::types::Uint,
        max_retries: usize,
    ) -> Result<(), rate_guard::error::RateLimitError> {
        for attempt in 0..max_retries {
            match limiter.try_acquire_verbose(tokens) {
                Ok(()) => return Ok(()),
                Err(rate_guard::error::RateLimitError::InsufficientCapacity { retry_after, .. }) => {
                    if attempt < max_retries - 1 {
                        sleep(retry_after).await;
                    }
                }
                Err(e) => return Err(e),
            }
        }
        
        // Final attempt
        limiter.try_acquire_verbose(tokens)
    }
}

#[cfg(not(all(feature = "std-time", feature = "tokio-time")))]
mod async_tests_disabled {
    #[tokio::test]
    async fn test_async_features_disabled() {
        println!("Async StdTimeSource tests skipped - required features not enabled");
        println!("Enable both 'std-time' and 'tokio-time' features to run async tests");
    }
}