#![ allow( clippy::doc_markdown ) ]
mod inc;
use api_huggingface::reliability::{ RateLimiter, RateLimiterConfig };
use core::time::Duration;
use std::time::Instant;
#[ cfg( feature = "integration" ) ]
use api_huggingface::{
Client,
environment::HuggingFaceEnvironmentImpl,
providers::ChatMessage,
Secret,
};
#[ cfg( feature = "integration" ) ]
fn create_integration_client() -> Client< HuggingFaceEnvironmentImpl >
{
let api_key = crate::inc::get_api_key_for_integration();
let secret = Secret::new( api_key );
let env = HuggingFaceEnvironmentImpl::build( secret, None )
.expect( "Failed to build environment" );
Client::build( env ).expect( "Failed to create client" )
}
#[ tokio::test ]
async fn test_rate_limiter_per_second_limit()
{
let config = RateLimiterConfig {
requests_per_second : Some( 2 ),
requests_per_minute : None,
requests_per_hour : None,
};
let limiter = RateLimiter::new( config );
assert!( limiter.try_acquire( ).await.is_ok( ));
assert!( limiter.try_acquire( ).await.is_ok( ));
assert!( limiter.try_acquire( ).await.is_err( ));
}
#[ tokio::test ]
async fn test_rate_limiter_per_minute_limit()
{
let config = RateLimiterConfig {
requests_per_second : None,
requests_per_minute : Some( 3 ),
requests_per_hour : None,
};
let limiter = RateLimiter::new( config );
for _ in 0..3
{
assert!( limiter.try_acquire( ).await.is_ok( ));
}
assert!( limiter.try_acquire( ).await.is_err( ));
}
#[ tokio::test ]
async fn test_rate_limiter_per_hour_limit()
{
let config = RateLimiterConfig {
requests_per_second : None,
requests_per_minute : None,
requests_per_hour : Some( 5 ),
};
let limiter = RateLimiter::new( config );
for _ in 0..5
{
assert!( limiter.try_acquire( ).await.is_ok( ));
}
assert!( limiter.try_acquire( ).await.is_err( ));
}
#[ tokio::test ]
async fn test_rate_limiter_token_refill()
{
let config = RateLimiterConfig {
requests_per_second : Some( 10 ),
requests_per_minute : None,
requests_per_hour : None,
};
let limiter = RateLimiter::new( config );
for _ in 0..10
{
assert!( limiter.try_acquire( ).await.is_ok( ));
}
assert!( limiter.try_acquire( ).await.is_err( ));
tokio::time::sleep( Duration::from_millis( 150 )).await;
assert!( limiter.try_acquire( ).await.is_ok( ));
}
#[ tokio::test ]
async fn test_rate_limiter_gradual_refill()
{
let config = RateLimiterConfig {
requests_per_second : Some( 10 ),
requests_per_minute : None,
requests_per_hour : None,
};
let limiter = RateLimiter::new( config );
for _ in 0..10
{
limiter.try_acquire( ).await.unwrap( );
}
tokio::time::sleep( Duration::from_millis( 500 )).await;
let mut count = 0;
for _ in 0..10
{
if limiter.try_acquire( ).await.is_ok( )
{
count += 1;
} else {
break;
}
}
assert!( count >= 4, "Expected at least 4 tokens, got {count}" );
}
#[ tokio::test ]
async fn test_rate_limiter_multiple_windows()
{
let config = RateLimiterConfig {
requests_per_second : Some( 5 ),
requests_per_minute : Some( 10 ),
requests_per_hour : None,
};
let limiter = RateLimiter::new( config );
for _ in 0..5
{
assert!( limiter.try_acquire( ).await.is_ok( ));
}
let result = limiter.try_acquire( ).await;
assert!( result.is_err( ));
}
#[ tokio::test ]
async fn test_rate_limiter_minute_limit_after_second_refill()
{
let config = RateLimiterConfig {
requests_per_second : Some( 3 ),
requests_per_minute : Some( 5 ),
requests_per_hour : None,
};
let limiter = RateLimiter::new( config );
for _ in 0..3
{
limiter.try_acquire( ).await.unwrap( );
}
tokio::time::sleep( Duration::from_secs( 1 )).await;
limiter.try_acquire( ).await.unwrap( );
limiter.try_acquire( ).await.unwrap( );
tokio::time::sleep( Duration::from_secs( 1 )).await;
assert!( limiter.try_acquire( ).await.is_err( ));
}
#[ tokio::test ]
async fn test_rate_limiter_acquire_blocks_and_succeeds()
{
let config = RateLimiterConfig {
requests_per_second : Some( 2 ),
requests_per_minute : None,
requests_per_hour : None,
};
let limiter = RateLimiter::new( config );
limiter.try_acquire( ).await.unwrap( );
limiter.try_acquire( ).await.unwrap( );
let start = Instant::now( );
limiter.acquire( ).await.unwrap( );
let elapsed = start.elapsed( );
assert!( elapsed >= Duration::from_millis( 400 ), "Expected wait >=400ms, got {elapsed:?}" );
}
#[ tokio::test ]
async fn test_rate_limiter_acquire_with_available_tokens()
{
let config = RateLimiterConfig {
requests_per_second : Some( 5 ),
requests_per_minute : None,
requests_per_hour : None,
};
let limiter = RateLimiter::new( config );
let start = Instant::now( );
limiter.acquire( ).await.unwrap( );
let elapsed = start.elapsed( );
assert!( elapsed < Duration::from_millis( 50 ), "Expected instant acquire, got {elapsed:?}" );
}
#[ tokio::test ]
async fn test_rate_limiter_available_tokens_initial()
{
let config = RateLimiterConfig {
requests_per_second : Some( 10 ),
requests_per_minute : Some( 100 ),
requests_per_hour : Some( 1000 ),
};
let limiter = RateLimiter::new( config );
let tokens = limiter.available_tokens( ).await;
assert_eq!( tokens.per_second, Some( 10 ));
assert_eq!( tokens.per_minute, Some( 100 ));
assert_eq!( tokens.per_hour, Some( 1000 ));
}
#[ tokio::test ]
async fn test_rate_limiter_available_tokens_after_consumption()
{
let config = RateLimiterConfig {
requests_per_second : Some( 5 ),
requests_per_minute : Some( 10 ),
requests_per_hour : None,
};
let limiter = RateLimiter::new( config );
limiter.try_acquire( ).await.unwrap( );
limiter.try_acquire( ).await.unwrap( );
let tokens = limiter.available_tokens( ).await;
assert_eq!( tokens.per_second, Some( 3 ));
assert_eq!( tokens.per_minute, Some( 8 ));
assert_eq!( tokens.per_hour, None );
}
#[ tokio::test ]
async fn test_rate_limiter_available_tokens_tracks_refill()
{
let config = RateLimiterConfig {
requests_per_second : Some( 10 ),
requests_per_minute : None,
requests_per_hour : None,
};
let limiter = RateLimiter::new( config );
for _ in 0..10
{
limiter.try_acquire( ).await.unwrap( );
}
let tokens_before = limiter.available_tokens( ).await;
assert_eq!( tokens_before.per_second, Some( 0 ));
tokio::time::sleep( Duration::from_millis( 200 )).await;
let tokens_after = limiter.available_tokens( ).await;
assert!( tokens_after.per_second.unwrap( ) >= 1 );
}
#[ tokio::test ]
async fn test_rate_limiter_reset()
{
let config = RateLimiterConfig {
requests_per_second : Some( 3 ),
requests_per_minute : Some( 5 ),
requests_per_hour : None,
};
let limiter = RateLimiter::new( config );
for _ in 0..3
{
limiter.try_acquire( ).await.unwrap( );
}
assert!( limiter.try_acquire( ).await.is_err( ));
limiter.reset( ).await;
let tokens = limiter.available_tokens( ).await;
assert_eq!( tokens.per_second, Some( 3 ));
assert_eq!( tokens.per_minute, Some( 5 ));
assert!( limiter.try_acquire( ).await.is_ok( ));
}
#[ cfg( feature = "integration" ) ]
#[ tokio::test ]
async fn test_rate_limiter_with_real_api_calls()
{
let client = create_integration_client();
let config = RateLimiterConfig {
requests_per_second : None,
requests_per_minute : Some( 2 ),
requests_per_hour : None,
};
let limiter = RateLimiter::new( config );
for i in 0..2
{
limiter.acquire( ).await.unwrap( );
let result = client.providers( ).chat_completion(
"meta-llama/Llama-3.3-70B-Instruct",
vec![ChatMessage {
role : "user".to_string( ),
content : format!( "Say the number {i}" ),
tool_calls : None,
tool_call_id : None,
} ],
Some( 10 ),
None,
None,
).await;
assert!( result.is_ok( ), "API call {i} should succeed" );
}
let rate_limited = limiter.try_acquire( ).await;
assert!( rate_limited.is_err( ), "Rate limiter must block after per-minute quota exhausted" );
}
#[ cfg( feature = "integration" ) ]
#[ tokio::test ]
async fn test_rate_limiter_prevents_api_overload()
{
let client = create_integration_client();
let config = RateLimiterConfig {
requests_per_second : Some( 3 ),
requests_per_minute : Some( 5 ),
requests_per_hour : None,
};
let limiter = RateLimiter::new( config );
let mut successful_calls = 0;
let mut rate_limited_calls = 0;
for i in 0..10
{
if limiter.try_acquire( ).await.is_ok( )
{
let result = client.providers( ).chat_completion(
"meta-llama/Llama-3.3-70B-Instruct",
vec![ChatMessage {
role : "user".to_string( ),
content : format!( "Count {i}" ),
tool_calls : None,
tool_call_id : None,
} ],
Some( 5 ),
None,
None,
).await;
if result.is_ok( )
{
successful_calls += 1;
}
} else {
rate_limited_calls += 1;
}
}
assert!( rate_limited_calls > 0, "Expected some calls to be rate limited" );
assert!( successful_calls <= 5, "Should not exceed minute limit" );
}
#[ tokio::test ]
async fn test_rate_limiter_concurrent_acquire()
{
let config = RateLimiterConfig {
requests_per_second : Some( 10 ),
requests_per_minute : None,
requests_per_hour : None,
};
let limiter = RateLimiter::new( config );
let mut handles = vec![ ];
for _ in 0..15
{
let l = limiter.clone( );
let handle = tokio::spawn( async move {
l.acquire( ).await
} );
handles.push( handle );
}
for handle in handles
{
let result = handle.await.expect( "Task should complete" );
assert!( result.is_ok( ));
}
}
#[ tokio::test ]
async fn test_rate_limiter_concurrent_try_acquire()
{
let config = RateLimiterConfig {
requests_per_second : Some( 5 ),
requests_per_minute : None,
requests_per_hour : None,
};
let limiter = RateLimiter::new( config );
let mut handles = vec![ ];
for _ in 0..10
{
let l = limiter.clone( );
let handle = tokio::spawn( async move {
l.try_acquire( ).await
} );
handles.push( handle );
}
let mut succeeded = 0;
let mut failed = 0;
for handle in handles
{
let result = handle.await.expect( "Task should complete" );
if result.is_ok( )
{
succeeded += 1;
} else {
failed += 1;
}
}
assert!( succeeded <= 5, "Should not exceed limit" );
assert!( failed >= 5, "Some should be rate limited" );
}
#[ tokio::test ]
async fn test_rate_limiter_zero_capacity_try_acquire_no_panic()
{
let rl = RateLimiter::new( RateLimiterConfig
{
requests_per_second : Some( 0 ),
requests_per_minute : None,
requests_per_hour : None,
} );
let result = rl.try_acquire( ).await;
assert!( result.is_err( ), "zero-capacity limiter must reject request without panicking" );
}
#[ tokio::test ]
async fn test_rate_limiter_no_limits()
{
let config = RateLimiterConfig {
requests_per_second : None,
requests_per_minute : None,
requests_per_hour : None,
};
let limiter = RateLimiter::new( config );
for _ in 0..100
{
assert!( limiter.try_acquire( ).await.is_ok( ));
}
}
#[ tokio::test ]
async fn test_rate_limiter_default_config()
{
let config = RateLimiterConfig::default( );
assert_eq!( config.requests_per_second, Some( 10 ));
assert_eq!( config.requests_per_minute, Some( 500 ));
assert_eq!( config.requests_per_hour, Some( 10000 ));
let limiter = RateLimiter::new( config );
for _ in 0..10
{
assert!( limiter.try_acquire( ).await.is_ok( ));
}
assert!( limiter.try_acquire( ).await.is_err( ));
}
#[ tokio::test ]
async fn test_rate_limiter_very_low_limit()
{
let config = RateLimiterConfig {
requests_per_second : Some( 1 ),
requests_per_minute : None,
requests_per_hour : None,
};
let limiter = RateLimiter::new( config );
assert!( limiter.try_acquire( ).await.is_ok( ));
assert!( limiter.try_acquire( ).await.is_err( ));
tokio::time::sleep( Duration::from_secs( 1 )).await;
assert!( limiter.try_acquire( ).await.is_ok( ));
}