refactor: move rate limiter into http layer

This commit is contained in:
2026-06-13 06:39:42 +02:00
parent 0f195de9e5
commit 6fc5cc1bc8
4 changed files with 6 additions and 5 deletions

90
src/http/rate_limit.rs Normal file
View File

@@ -0,0 +1,90 @@
use std::{sync::Mutex, time::Instant};
#[derive(Debug)]
pub struct RateLimiter {
bucket: Mutex<TokenBucket>,
}
impl RateLimiter {
pub fn new(refill_per_second: f64, burst: u32) -> Result<Self, RateLimitError> {
if !refill_per_second.is_sign_positive() || burst == 0 {
return Err(RateLimitError::InvalidConfiguration);
}
Ok(Self {
bucket: Mutex::new(TokenBucket {
capacity: f64::from(burst),
tokens: f64::from(burst),
refill_per_second,
last_refill: Instant::now(),
}),
})
}
#[must_use]
pub fn try_acquire(&self) -> bool {
self.bucket
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.try_acquire()
}
}
#[derive(Debug, thiserror::Error)]
pub enum RateLimitError {
#[error("rate limit configuration must be positive")]
InvalidConfiguration,
}
#[derive(Debug)]
struct TokenBucket {
capacity: f64,
tokens: f64,
refill_per_second: f64,
last_refill: Instant,
}
impl TokenBucket {
fn try_acquire(&mut self) -> bool {
self.refill();
if self.tokens >= 1.0 {
self.tokens -= 1.0;
true
} else {
false
}
}
fn refill(&mut self) {
let elapsed = self.last_refill.elapsed().as_secs_f64();
self.tokens = self
.capacity
.min(self.tokens + elapsed.mul_add(self.refill_per_second, 0.0));
self.last_refill = Instant::now();
}
}
#[cfg(test)]
mod tests {
use super::RateLimiter;
use std::time::Duration;
#[test]
fn allows_requests_up_to_the_burst_capacity() {
let limiter = RateLimiter::new(100.0, 2).expect("rate limiter should build");
assert!(limiter.try_acquire());
assert!(limiter.try_acquire());
assert!(!limiter.try_acquire());
}
#[test]
fn refills_tokens_over_time() {
let limiter = RateLimiter::new(100.0, 1).expect("rate limiter should build");
assert!(limiter.try_acquire());
assert!(!limiter.try_acquire());
std::thread::sleep(Duration::from_millis(20));
assert!(limiter.try_acquire());
}
}