use std::time::Duration;
use tokio::sync::Mutex;
use tokio::time::Instant;
pub(crate) fn reserve(
next_allowed: Instant,
now: Instant,
count: u32,
interval: Duration,
) -> (Instant, Instant) {
let start = if next_allowed > now {
next_allowed
} else {
now
};
(start, start + interval * count)
}
pub(crate) fn interval_for(messages_per_minute: usize) -> Duration {
Duration::from_secs(60) / messages_per_minute.max(1) as u32
}
pub(crate) struct Pacer {
next_allowed: Mutex<Instant>,
interval: Duration,
}
impl Pacer {
pub(crate) fn new(messages_per_minute: usize) -> Self {
Self {
next_allowed: Mutex::new(Instant::now()),
interval: interval_for(messages_per_minute),
}
}
pub(crate) async fn acquire(&self, count: usize) {
let wait_until = {
let mut next_allowed = self.next_allowed.lock().await;
let (wait_until, updated) = reserve(
*next_allowed,
Instant::now(),
count.max(1) as u32,
self.interval,
);
*next_allowed = updated;
wait_until
};
tokio::time::sleep_until(wait_until).await;
}
}
#[cfg(test)]
mod tests {
use super::*;
const INTERVAL: Duration = Duration::from_millis(500);
#[test]
fn interval_for_divides_a_minute_by_the_rate() {
assert_eq!(interval_for(120), Duration::from_millis(500));
assert_eq!(interval_for(60), Duration::from_secs(1));
}
#[test]
fn interval_for_clamps_a_zero_rate_to_one_per_minute() {
assert_eq!(interval_for(0), Duration::from_secs(60));
}
#[test]
fn reserve_spaces_consecutive_single_reservations_by_one_interval() {
let start = Instant::now();
let (first_wait, after_first) = reserve(start, start, 1, INTERVAL);
let (second_wait, after_second) = reserve(after_first, start, 1, INTERVAL);
assert_eq!(first_wait, start);
assert_eq!(second_wait, start + INTERVAL);
assert_eq!(after_second, start + INTERVAL * 2);
}
#[test]
fn reserve_charges_one_interval_per_message_in_a_batch() {
let start = Instant::now();
let (wait, next) = reserve(start, start, 20, INTERVAL);
assert_eq!(wait, start, "the batch itself starts immediately");
assert_eq!(next, start + INTERVAL * 20);
}
#[test]
fn reserve_does_not_pay_back_idle_time() {
let reserved_at = Instant::now();
let (_, next_allowed) = reserve(reserved_at, reserved_at, 1, INTERVAL);
let much_later = reserved_at + Duration::from_secs(3600);
let (wait, next) = reserve(next_allowed, much_later, 1, INTERVAL);
assert_eq!(wait, much_later, "no credit is banked for the idle hour");
assert_eq!(next, much_later + INTERVAL);
}
#[test]
fn reserve_staggers_two_concurrent_batches() {
let start = Instant::now();
let (first_wait, after_first) = reserve(start, start, 5, INTERVAL);
let (second_wait, _) = reserve(after_first, start, 5, INTERVAL);
assert_eq!(first_wait, start);
assert_eq!(second_wait, start + INTERVAL * 5);
}
#[tokio::test(start_paused = true)]
async fn pacer_acquire_advances_the_clock_by_the_reserved_time() {
let pacer = Pacer::new(120);
let start = Instant::now();
pacer.acquire(4).await;
assert_eq!(Instant::now(), start);
pacer.acquire(1).await;
assert_eq!(Instant::now(), start + INTERVAL * 4);
}
#[tokio::test(start_paused = true)]
async fn pacer_acquire_charges_at_least_one_slot() {
let pacer = Pacer::new(120);
let start = Instant::now();
pacer.acquire(0).await;
pacer.acquire(0).await;
assert_eq!(Instant::now(), start + INTERVAL);
}
}