use std::num::NonZeroU32;
use std::sync::Arc;
use governor::{DefaultDirectRateLimiter, Quota, RateLimiter};
pub struct RpsLimiter {
inner: DefaultDirectRateLimiter,
}
impl RpsLimiter {
pub fn new(rps: usize) -> Option<Arc<Self>> {
let rps_u32: u32 = u32::try_from(rps).ok()?;
let nz = NonZeroU32::new(rps_u32)?;
Some(Arc::new(Self {
inner: RateLimiter::direct(Quota::per_second(nz)),
}))
}
pub async fn acquire(&self) {
self.inner.until_ready().await;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rps_limiter_zero_returns_none() {
assert!(RpsLimiter::new(0).is_none());
}
#[test]
fn rps_limiter_valid_rps_returns_some() {
assert!(RpsLimiter::new(100).is_some());
}
#[tokio::test(flavor = "current_thread")]
async fn first_acquire_is_immediate() {
let limiter = RpsLimiter::new(10).expect("rps=10 is valid");
let start = std::time::Instant::now();
limiter.acquire().await;
assert!(
start.elapsed().as_millis() < 50,
"first acquire should be near-instant (bucket starts full)"
);
}
}