use governor::clock::DefaultClock;
use governor::state::keyed::DefaultKeyedStateStore;
use governor::{Quota, RateLimiter};
use std::num::NonZeroU32;
use std::sync::Arc;
use std::time::Duration;
pub type Limiter = RateLimiter<String, DefaultKeyedStateStore<String>, DefaultClock>;
pub fn build_limiter(rps: u32) -> Option<Arc<Limiter>> {
let quota = Quota::per_second(NonZeroU32::new(rps)?);
Some(Arc::new(RateLimiter::keyed(quota)))
}
pub fn check(limiter: &Limiter, tenant: &str) -> bool {
limiter.check_key(&tenant.to_string()).is_ok()
}
pub fn spawn_gc(limiter: Arc<Limiter>, every: Duration) {
tokio::spawn(async move {
let mut ticker = tokio::time::interval(every);
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
loop {
ticker.tick().await;
limiter.retain_recent();
limiter.shrink_to_fit();
}
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zero_rps_disables_limiting() {
assert!(build_limiter(0).is_none());
}
#[test]
fn quota_allows_burst_then_rejects() {
let limiter = build_limiter(2).unwrap();
assert!(check(&limiter, "tenant-a"));
assert!(check(&limiter, "tenant-a"));
assert!(!check(&limiter, "tenant-a"));
}
#[test]
fn separate_tenants_are_independent() {
let limiter = build_limiter(1).unwrap();
assert!(check(&limiter, "tenant-a"));
assert!(!check(&limiter, "tenant-a"));
assert!(check(&limiter, "tenant-b"));
}
#[tokio::test]
async fn gc_does_not_panic_on_empty_limiter() {
let limiter = build_limiter(10).unwrap();
check(&limiter, "tenant-a");
spawn_gc(limiter.clone(), Duration::from_millis(10));
tokio::time::sleep(Duration::from_millis(50)).await;
}
}