use std::sync::Arc;
use std::time::Instant;
use core::time::Duration;
use tokio::sync::RwLock;
#[ derive( Debug, Clone, Copy ) ]
pub struct RateLimiterConfig
{
pub requests_per_second : Option< u32 >,
pub requests_per_minute : Option< u32 >,
pub requests_per_hour : Option< u32 >,
}
impl Default for RateLimiterConfig
{
#[ inline ]
fn default() -> Self
{
Self {
requests_per_second : Some( 10 ),
requests_per_minute : Some( 500 ),
requests_per_hour : Some( 10000 ),
}
}
}
#[ derive( Debug ) ]
struct TokenBucket
{
capacity : u32,
tokens : f64,
last_refill : Instant,
refill_rate : f64,
}
impl TokenBucket
{
#[ inline ]
fn new( capacity : u32, refill_duration : Duration ) -> Self
{
let refill_rate = f64::from( capacity ) / refill_duration.as_secs_f64( );
Self {
capacity,
tokens : f64::from( capacity ),
last_refill : Instant::now( ),
refill_rate,
}
}
#[ inline ]
fn refill( &mut self )
{
let now = Instant::now( );
let elapsed = now.duration_since( self.last_refill ).as_secs_f64( );
self.tokens = ( self.tokens + elapsed * self.refill_rate ).min( f64::from( self.capacity ));
self.last_refill = now;
}
#[ inline ]
fn try_consume( &mut self ) -> bool
{
self.refill( );
if self.tokens >= 1.0
{
self.tokens -= 1.0;
true
} else {
false
}
}
#[ inline ]
fn available_tokens( &mut self ) -> u32
{
self.refill( );
#[ allow( clippy::cast_possible_truncation, clippy::cast_sign_loss ) ]
{ self.tokens.floor( ) as u32 }
}
#[ inline ]
fn time_until_token( &mut self ) -> Option< Duration >
{
self.refill( );
if self.tokens >= 1.0
{
None
} else {
let tokens_needed = 1.0 - self.tokens;
let seconds = tokens_needed / self.refill_rate;
if seconds.is_finite( )
{
Some( Duration::from_secs_f64( seconds ))
} else {
Some( Duration::MAX )
}
}
}
}
#[ derive( Debug, Clone ) ]
#[ allow( clippy::struct_field_names ) ] pub struct RateLimiter
{
second_bucket : Option< Arc< RwLock< TokenBucket > > >,
minute_bucket : Option< Arc< RwLock< TokenBucket > > >,
hour_bucket : Option< Arc< RwLock< TokenBucket > > >,
}
impl RateLimiter
{
#[ inline ]
#[ must_use ]
pub fn new( config : RateLimiterConfig ) -> Self
{
Self {
second_bucket : config.requests_per_second.map( |capacity| {
Arc::new( RwLock::new( TokenBucket::new( capacity, Duration::from_secs( 1 )) ))
} ),
minute_bucket : config.requests_per_minute.map( |capacity| {
Arc::new( RwLock::new( TokenBucket::new( capacity, Duration::from_secs( 60 )) ))
} ),
hour_bucket : config.requests_per_hour.map( |capacity| {
Arc::new( RwLock::new( TokenBucket::new( capacity, Duration::from_secs( 3600 )) ))
} ),
}
}
#[ inline ]
pub async fn acquire( &self ) -> Result< ( ), RateLimitError >
{
loop
{
let mut max_wait : Option< Duration > = None;
if let Some( ref bucket ) = self.second_bucket
{
let mut b = bucket.write( ).await;
if !b.try_consume( )
{
if let Some( wait ) = b.time_until_token( )
{
max_wait = Some( max_wait.map_or( wait, |m : Duration| m.max( wait )) );
}
}
}
if let Some( ref bucket ) = self.minute_bucket
{
let mut b = bucket.write( ).await;
if !b.try_consume( )
{
if let Some( wait ) = b.time_until_token( )
{
max_wait = Some( max_wait.map_or( wait, |m : Duration| m.max( wait )) );
}
}
}
if let Some( ref bucket ) = self.hour_bucket
{
let mut b = bucket.write( ).await;
if !b.try_consume( )
{
if let Some( wait ) = b.time_until_token( )
{
max_wait = Some( max_wait.map_or( wait, |m : Duration| m.max( wait )) );
}
}
}
if max_wait.is_none( )
{
return Ok( ( ));
}
if let Some( wait_duration ) = max_wait
{
tokio::time::sleep( wait_duration ).await;
}
}
}
#[ inline ]
pub async fn try_acquire( &self ) -> Result< ( ), RateLimitError >
{
if let Some( ref bucket ) = self.second_bucket
{
let mut b = bucket.write( ).await;
if !b.try_consume( )
{
return Err( RateLimitError::RateLimitExceeded {
window : "per_second".to_string( ),
retry_after : b.time_until_token( ),
} );
}
}
if let Some( ref bucket ) = self.minute_bucket
{
let mut b = bucket.write( ).await;
if !b.try_consume( )
{
return Err( RateLimitError::RateLimitExceeded {
window : "per_minute".to_string( ),
retry_after : b.time_until_token( ),
} );
}
}
if let Some( ref bucket ) = self.hour_bucket
{
let mut b = bucket.write( ).await;
if !b.try_consume( )
{
return Err( RateLimitError::RateLimitExceeded {
window : "per_hour".to_string( ),
retry_after : b.time_until_token( ),
} );
}
}
Ok( ( ))
}
#[ inline ]
pub async fn available_tokens( &self ) -> AvailableTokens
{
AvailableTokens {
per_second : if let Some( ref bucket ) = self.second_bucket
{
Some( bucket.write( ).await.available_tokens( ))
} else {
None
},
per_minute : if let Some( ref bucket ) = self.minute_bucket
{
Some( bucket.write( ).await.available_tokens( ))
} else {
None
},
per_hour : if let Some( ref bucket ) = self.hour_bucket
{
Some( bucket.write( ).await.available_tokens( ))
} else {
None
},
}
}
#[ inline ]
pub async fn reset( &self )
{
if let Some( ref bucket ) = self.second_bucket
{
let mut b = bucket.write( ).await;
b.tokens = f64::from( b.capacity );
b.last_refill = Instant::now( );
}
if let Some( ref bucket ) = self.minute_bucket
{
let mut b = bucket.write( ).await;
b.tokens = f64::from( b.capacity );
b.last_refill = Instant::now( );
}
if let Some( ref bucket ) = self.hour_bucket
{
let mut b = bucket.write( ).await;
b.tokens = f64::from( b.capacity );
b.last_refill = Instant::now( );
}
}
}
#[ derive( Debug, Clone, PartialEq, Eq ) ]
pub struct AvailableTokens
{
pub per_second : Option< u32 >,
pub per_minute : Option< u32 >,
pub per_hour : Option< u32 >,
}
#[ derive( Debug ) ]
pub enum RateLimitError
{
RateLimitExceeded {
window : String,
retry_after : Option< Duration >,
},
}
impl core::fmt::Display for RateLimitError
{
#[ inline ]
fn fmt( &self, f : &mut core::fmt::Formatter< '_ > ) -> core::fmt::Result
{
match self
{
Self::RateLimitExceeded { window, retry_after } => {
if let Some( duration ) = retry_after
{
write!( f, "Rate limit exceeded for {window}, retry after {duration:?}" )
} else {
write!( f, "Rate limit exceeded for {window}" )
}
}
}
}
}
impl std::error::Error for RateLimitError {}