use governor::{
clock::DefaultClock,
state::{InMemoryState, NotKeyed},
Quota, RateLimiter as GovernorLimiter,
};
use std::num::NonZeroU32;
pub struct RateLimiter {
inner: GovernorLimiter<NotKeyed, InMemoryState, DefaultClock>,
}
impl RateLimiter {
pub fn new(per_minute: u32) -> Self {
let fallback = NonZeroU32::new(40).expect("40 != 0");
let n = match NonZeroU32::new(per_minute) {
Some(n) => n,
None => {
tracing::warn!(
"rate_limit_per_minute is 0, falling back to {}/min",
fallback
);
fallback
}
};
Self {
inner: GovernorLimiter::direct(Quota::per_minute(n)),
}
}
pub async fn acquire(&self) {
self.inner.until_ready().await;
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[tokio::test]
async fn first_acquire_does_not_block_with_room_in_bucket() {
let lim = RateLimiter::new(60);
tokio::time::timeout(Duration::from_millis(500), lim.acquire())
.await
.expect("first acquire should be immediate (full bucket)");
}
#[tokio::test]
async fn zero_per_minute_falls_back_to_default_and_does_not_deadlock() {
let lim = RateLimiter::new(0);
tokio::time::timeout(Duration::from_millis(500), lim.acquire())
.await
.expect("zero rpm should fall back, not block forever");
}
}