use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::Mutex;
use tokio::time::sleep;
use tracing::{debug, warn};
#[derive(Debug, Clone)]
pub struct RateLimitConfig {
pub max_requests: u32,
pub time_window: Duration,
pub burst_size: u32,
}
impl Default for RateLimitConfig {
fn default() -> Self {
Self {
max_requests: 100,
time_window: Duration::from_secs(60), burst_size: 10,
}
}
}
impl RateLimitConfig {
pub fn graphql() -> Self {
Self {
max_requests: 60,
time_window: Duration::from_secs(60), burst_size: 5,
}
}
pub fn rest_api() -> Self {
Self {
max_requests: 120,
time_window: Duration::from_secs(60), burst_size: 10,
}
}
pub fn blockchain() -> Self {
Self {
max_requests: 200,
time_window: Duration::from_secs(60), burst_size: 20,
}
}
pub fn conservative() -> Self {
Self {
max_requests: 30,
time_window: Duration::from_secs(60), burst_size: 3,
}
}
}
#[derive(Debug)]
pub struct RateLimiter {
config: RateLimitConfig,
state: Arc<Mutex<RateLimiterState>>,
}
#[derive(Debug)]
struct RateLimiterState {
tokens: f64,
last_refill: Instant,
}
impl RateLimiter {
pub fn new(config: RateLimitConfig) -> Self {
let burst_size = config.burst_size;
Self {
state: Arc::new(Mutex::new(RateLimiterState {
tokens: burst_size as f64,
last_refill: Instant::now(),
})),
config,
}
}
pub async fn acquire(&self) -> Result<(), RateLimitError> {
loop {
{
let mut state = self.state.lock().await;
self.refill_tokens(&mut state);
if state.tokens >= 1.0 {
state.tokens -= 1.0;
debug!(
"Rate limit token acquired, {} tokens remaining",
state.tokens
);
return Ok(());
}
}
let wait_time = self.calculate_wait_time().await;
debug!(
"Rate limit exceeded, waiting {:?} for next token",
wait_time
);
if wait_time > Duration::from_secs(30) {
warn!("Rate limit wait time exceeds 30 seconds, rejecting request");
return Err(RateLimitError::ExcessiveWait(wait_time));
}
sleep(wait_time).await;
}
}
pub async fn try_acquire(&self) -> bool {
let mut state = self.state.lock().await;
self.refill_tokens(&mut state);
if state.tokens >= 1.0 {
state.tokens -= 1.0;
debug!(
"Rate limit token acquired, {} tokens remaining",
state.tokens
);
true
} else {
debug!("Rate limit exceeded, no tokens available");
false
}
}
pub async fn available_tokens(&self) -> f64 {
let mut state = self.state.lock().await;
self.refill_tokens(&mut state);
state.tokens
}
async fn calculate_wait_time(&self) -> Duration {
let state = self.state.lock().await;
let tokens_per_second =
self.config.max_requests as f64 / self.config.time_window.as_secs_f64();
let time_per_token = Duration::from_secs_f64(1.0 / tokens_per_second);
if state.tokens <= 0.0 {
time_per_token
} else {
Duration::from_millis(100)
}
}
fn refill_tokens(&self, state: &mut RateLimiterState) {
let now = Instant::now();
let elapsed = now.duration_since(state.last_refill);
let tokens_per_second =
self.config.max_requests as f64 / self.config.time_window.as_secs_f64();
let tokens_to_add = elapsed.as_secs_f64() * tokens_per_second;
state.tokens = (state.tokens + tokens_to_add).min(self.config.burst_size as f64);
state.last_refill = now;
}
}
#[derive(Debug, thiserror::Error)]
pub enum RateLimitError {
#[error("Rate limit wait time would be excessive: {0:?}")]
ExcessiveWait(Duration),
}
#[derive(Debug)]
pub struct RateLimiterManager {
graphql_limiter: Option<RateLimiter>,
rest_limiter: Option<RateLimiter>,
blockchain_limiter: Option<RateLimiter>,
}
impl Default for RateLimiterManager {
fn default() -> Self {
Self::new()
}
}
impl RateLimiterManager {
pub fn new() -> Self {
Self {
graphql_limiter: None,
rest_limiter: None,
blockchain_limiter: None,
}
}
pub fn with_graphql_rate_limit(mut self, config: RateLimitConfig) -> Self {
self.graphql_limiter = Some(RateLimiter::new(config));
self
}
pub fn with_rest_rate_limit(mut self, config: RateLimitConfig) -> Self {
self.rest_limiter = Some(RateLimiter::new(config));
self
}
pub fn with_blockchain_rate_limit(mut self, config: RateLimitConfig) -> Self {
self.blockchain_limiter = Some(RateLimiter::new(config));
self
}
pub fn with_default_limits(mut self) -> Self {
self.graphql_limiter = Some(RateLimiter::new(RateLimitConfig::graphql()));
self.rest_limiter = Some(RateLimiter::new(RateLimitConfig::rest_api()));
self.blockchain_limiter = Some(RateLimiter::new(RateLimitConfig::blockchain()));
self
}
pub async fn acquire_graphql(&self) -> Result<(), RateLimitError> {
if let Some(limiter) = &self.graphql_limiter {
limiter.acquire().await
} else {
Ok(())
}
}
pub async fn acquire_rest(&self) -> Result<(), RateLimitError> {
if let Some(limiter) = &self.rest_limiter {
limiter.acquire().await
} else {
Ok(())
}
}
pub async fn acquire_blockchain(&self) -> Result<(), RateLimitError> {
if let Some(limiter) = &self.blockchain_limiter {
limiter.acquire().await
} else {
Ok(())
}
}
pub async fn try_acquire_graphql(&self) -> bool {
if let Some(limiter) = &self.graphql_limiter {
limiter.try_acquire().await
} else {
true
}
}
pub async fn try_acquire_rest(&self) -> bool {
if let Some(limiter) = &self.rest_limiter {
limiter.try_acquire().await
} else {
true
}
}
pub async fn try_acquire_blockchain(&self) -> bool {
if let Some(limiter) = &self.blockchain_limiter {
limiter.try_acquire().await
} else {
true
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::time::{sleep, Duration};
#[tokio::test]
async fn test_rate_limiter_basic() {
let config = RateLimitConfig {
max_requests: 5,
time_window: Duration::from_secs(1),
burst_size: 2,
};
let limiter = RateLimiter::new(config);
assert!(limiter.try_acquire().await);
assert!(limiter.try_acquire().await);
assert!(!limiter.try_acquire().await);
}
#[tokio::test]
async fn test_rate_limiter_refill() {
let config = RateLimitConfig {
max_requests: 10,
time_window: Duration::from_secs(1),
burst_size: 1,
};
let limiter = RateLimiter::new(config);
assert!(limiter.try_acquire().await);
assert!(!limiter.try_acquire().await);
sleep(Duration::from_millis(150)).await;
assert!(limiter.try_acquire().await);
}
#[tokio::test]
async fn test_rate_limiter_manager() {
let manager = RateLimiterManager::new().with_graphql_rate_limit(RateLimitConfig {
max_requests: 2,
time_window: Duration::from_secs(1),
burst_size: 1,
});
assert!(manager.acquire_graphql().await.is_ok());
assert!(manager.try_acquire_rest().await);
assert!(manager.try_acquire_rest().await);
}
}