refactor: move rate limiter into http layer
This commit is contained in:
90
src/http/rate_limit.rs
Normal file
90
src/http/rate_limit.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user