use std::sync::Mutex;
use std::time::{Duration, Instant};
use tracing::Instrument;
use crate::error::MytheclipseError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RateLimitError {
Limited,
}
impl std::fmt::Display for RateLimitError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Limited => write!(f, "rate limit exceeded"),
}
}
}
impl std::error::Error for RateLimitError {}
struct Inner {
capacity: u64,
refill_per_sec: f64,
tokens: Mutex<f64>,
last_refill: Mutex<Instant>,
}
#[derive(Clone)]
pub struct RateLimiter {
inner: std::sync::Arc<Inner>,
}
impl RateLimiter {
pub fn new(rate_per_sec: f64, burst_capacity: u64) -> Self {
assert!(
rate_per_sec.is_finite() && rate_per_sec > 0.0,
"rate must be positive"
);
assert!(burst_capacity > 0, "burst capacity must be > 0");
Self {
inner: std::sync::Arc::new(Inner {
capacity: burst_capacity,
refill_per_sec: rate_per_sec,
tokens: Mutex::new(burst_capacity as f64),
last_refill: Mutex::new(Instant::now()),
}),
}
}
pub async fn acquire(&self) -> Result<(), RateLimitError> {
loop {
if self.try_acquire().is_ok() {
return Ok(());
}
let span = tracing::info_span!("mytheclipse_ratelimit_task");
let sleep = Duration::from_secs_f64(1.0 / self.inner.refill_per_sec);
tokio::time::sleep(sleep).instrument(span).await;
}
}
pub fn try_acquire(&self) -> Result<(), RateLimitError> {
let mut tokens = self.inner.tokens.lock().unwrap();
let mut last = self.inner.last_refill.lock().unwrap();
self.refill(&mut tokens, *last);
*last = Instant::now();
if *tokens >= 1.0 {
*tokens -= 1.0;
Ok(())
} else {
Err(RateLimitError::Limited)
}
}
pub fn available_tokens(&self) -> u64 {
let mut tokens = self.inner.tokens.lock().unwrap();
let last = self.inner.last_refill.lock().unwrap();
self.refill(&mut tokens, *last);
tokens.floor() as u64
}
fn refill(&self, tokens: &mut f64, last: Instant) {
let elapsed_secs = last.elapsed().as_secs_f64();
let added = elapsed_secs * self.inner.refill_per_sec;
*tokens = (*tokens + added).min(self.inner.capacity as f64);
}
pub async fn acquire_err(&self) -> Result<(), MytheclipseError> {
self.acquire()
.await
.map_err(|_| MytheclipseError::RateLimited)
}
pub fn try_acquire_err(&self) -> Result<(), MytheclipseError> {
self.try_acquire()
.map_err(|_| MytheclipseError::RateLimited)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn burst_capacity_consumed_immediately() {
let limiter = RateLimiter::new(1000.0, 3);
assert!(limiter.try_acquire().is_ok());
assert!(limiter.try_acquire().is_ok());
assert!(limiter.try_acquire().is_ok());
assert_eq!(limiter.try_acquire(), Err(RateLimitError::Limited));
}
#[test]
fn available_tokens_bounded_by_capacity() {
let limiter = RateLimiter::new(1000.0, 5);
assert_eq!(limiter.available_tokens(), 5);
let _ = limiter.try_acquire();
assert_eq!(limiter.available_tokens(), 4);
}
#[tokio::test]
async fn acquire_waits_for_refill() {
let limiter = RateLimiter::new(1000.0, 1);
assert!(limiter.try_acquire().is_ok());
let start = Instant::now();
limiter
.acquire()
.await
.expect("acquire should eventually succeed");
assert!(start.elapsed() >= Duration::from_millis(1));
}
#[tokio::test]
async fn try_acquire_err_maps_to_shared_error() {
let limiter = RateLimiter::new(1000.0, 1);
let _ = limiter.try_acquire();
assert_eq!(
limiter.try_acquire_err(),
Err(MytheclipseError::RateLimited)
);
}
#[test]
#[should_panic]
fn zero_burst_panics() {
let _ = RateLimiter::new(1.0, 0);
}
}