use crate::ctx::Ctx;
use crate::middleware::{Flow, Middleware, Next};
use async_trait::async_trait;
use nagisa_types::id::Peer;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RateLimitScope {
PerPeer,
Global,
}
#[derive(Clone)]
pub struct RateLimit {
scope: RateLimitScope,
max: f64,
per: Duration,
buckets: Arc<Mutex<HashMap<Option<Peer>, Bucket>>>,
}
#[derive(Clone, Copy)]
struct Bucket {
tokens: f64,
last: Instant,
}
impl RateLimit {
pub fn per_peer(max: u32, per: Duration) -> Self {
RateLimit {
scope: RateLimitScope::PerPeer,
max: max as f64,
per,
buckets: Arc::new(Mutex::new(HashMap::new())),
}
}
pub fn global(max: u32, per: Duration) -> Self {
RateLimit { scope: RateLimitScope::Global, max: max as f64, per, buckets: Arc::new(Mutex::new(HashMap::new())) }
}
pub fn check(&self, peer: Option<Peer>) -> bool {
let key = match self.scope {
RateLimitScope::PerPeer => match peer {
Some(p) => Some(p),
None => return true, },
RateLimitScope::Global => None,
};
let refill_per_sec = self.max / self.per.as_secs_f64();
let now = Instant::now();
let mut map = self.buckets.lock().expect("ratelimit buckets poisoned");
let bucket = map.entry(key).or_insert(Bucket { tokens: self.max, last: now });
let elapsed = now.saturating_duration_since(bucket.last).as_secs_f64();
bucket.tokens = (bucket.tokens + elapsed * refill_per_sec).min(self.max);
bucket.last = now;
if bucket.tokens >= 1.0 {
bucket.tokens -= 1.0;
true
} else {
false
}
}
}
#[async_trait]
impl Middleware for RateLimit {
async fn handle(&self, ctx: Arc<Ctx>, next: Next<'_>) -> Flow {
let peer = ctx.event().peer();
if self.check(peer) {
next.run(ctx).await
} else {
tracing::debug!(?peer, "rate limited; dropping event");
Flow::Stop
}
}
}