use std::sync::{Mutex, OnceLock};
use std::thread;
use std::time::{Duration, Instant};
#[derive(Debug)]
pub struct RateLimiter {
interval: Duration,
next: Mutex<Instant>,
}
impl RateLimiter {
pub fn per_second(pps: u32) -> Option<RateLimiter> {
if pps == 0 {
return None;
}
let interval = Duration::from_secs_f64(1.0 / f64::from(pps));
Some(RateLimiter {
interval,
next: Mutex::new(Instant::now()),
})
}
pub fn interval(&self) -> Duration {
self.interval
}
pub fn acquire(&self) {
let scheduled = {
let mut next = self.next.lock().expect("rate limiter mutex poisoned");
let (scheduled, new_next) = reserve(Instant::now(), *next, self.interval);
*next = new_next;
scheduled
};
let wait = scheduled.saturating_duration_since(Instant::now());
if !wait.is_zero() {
thread::sleep(wait);
}
}
}
fn reserve(now: Instant, next: Instant, interval: Duration) -> (Instant, Instant) {
let scheduled = next.max(now);
(scheduled, scheduled + interval)
}
static LIMITER: OnceLock<RateLimiter> = OnceLock::new();
pub fn install(pps: u32) {
if let Some(limiter) = RateLimiter::per_second(pps) {
let _ = LIMITER.set(limiter);
}
}
pub fn gate() {
if let Some(limiter) = LIMITER.get() {
limiter.acquire();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_limiter_for_zero_pps() {
assert!(RateLimiter::per_second(0).is_none());
}
#[test]
fn interval_is_the_reciprocal_of_the_rate() {
let limiter = RateLimiter::per_second(1000).unwrap();
assert_eq!(limiter.interval(), Duration::from_millis(1));
let limiter = RateLimiter::per_second(4).unwrap();
assert_eq!(limiter.interval(), Duration::from_millis(250));
}
#[test]
fn reserve_runs_immediately_when_the_slot_is_in_the_past() {
let now = Instant::now();
let past = now - Duration::from_secs(1);
let interval = Duration::from_millis(10);
let (scheduled, new_next) = reserve(now, past, interval);
assert_eq!(scheduled, now, "a stale slot runs now");
assert_eq!(new_next, now + interval);
}
#[test]
fn reserve_queues_behind_a_future_slot() {
let now = Instant::now();
let future = now + Duration::from_millis(50);
let interval = Duration::from_millis(10);
let (scheduled, new_next) = reserve(now, future, interval);
assert_eq!(scheduled, future, "must wait for the reserved slot");
assert_eq!(new_next, future + interval);
}
#[test]
fn consecutive_reservations_are_spaced_by_the_interval() {
let start = Instant::now();
let interval = Duration::from_millis(5);
let (s1, next) = reserve(start, start, interval);
let (s2, _) = reserve(start, next, interval);
assert_eq!(s2 - s1, interval);
}
}