#![allow(clippy::cast_possible_truncation)]
use std::{
collections::VecDeque,
future::Future,
pin::Pin,
task::{Context, Poll, ready},
};
use tokio::time::{Duration, Instant, Sleep};
const PERIOD: Duration = Duration::from_secs(60);
const PERMITS: u8 = 120;
#[derive(Debug)]
pub struct CommandRatelimiter {
delay: Pin<Box<Sleep>>,
queue: VecDeque<u16>,
}
impl CommandRatelimiter {
pub(crate) fn new(heartbeat_interval: Duration) -> Self {
let capacity = usize::from(nonreserved_commands_per_reset(heartbeat_interval)) - 1;
let mut queue = VecDeque::with_capacity(capacity);
if queue.capacity() != capacity {
queue.resize(capacity, 0);
let vec = Vec::from(queue).into_boxed_slice().into_vec();
queue = VecDeque::from(vec);
queue.clear();
}
Self {
delay: Box::pin(tokio::time::sleep_until(Instant::now())),
queue,
}
}
pub fn available(&self) -> u8 {
let now = Instant::now();
let acquired = if now >= self.delay.deadline() {
self.next_acquired_position(now)
.map_or(0, |released_count| self.queue.len() - released_count)
} else {
self.queue.len() + 1
};
self.max() - acquired as u8
}
pub fn max(&self) -> u8 {
self.queue.capacity() as u8 + 1
}
pub fn next_available(&self) -> Duration {
self.delay
.deadline()
.saturating_duration_since(Instant::now())
}
pub(crate) fn poll_acquire(&mut self, cx: &mut Context<'_>) -> Poll<()> {
ready!(self.poll_available(cx));
let now = Instant::now();
if now >= self.delay.deadline() {
if let Some(new_deadline_idx) = self.next_acquired_position(now) {
self.rebase(new_deadline_idx);
} else {
self.queue.clear();
self.delay.as_mut().reset(now + PERIOD);
return Poll::Ready(());
}
}
let releases = (now + PERIOD) - self.delay.deadline();
debug_assert_ne!(self.queue.capacity(), self.queue.len());
self.queue.push_back(releases.as_millis() as u16);
if self.queue.len() == self.queue.capacity() {
tracing::debug!(duration = ?(self.delay.deadline() - now), "ratelimited");
}
Poll::Ready(())
}
pub(crate) fn poll_available(&mut self, cx: &mut Context<'_>) -> Poll<()> {
if self.queue.len() < self.queue.capacity() {
return Poll::Ready(());
}
self.delay.as_mut().poll(cx)
}
fn next_acquired_position(&self, now: Instant) -> Option<usize> {
self.queue
.iter()
.map(|&m| self.delay.deadline() + Duration::from_millis(m.into()))
.position(|deadline| deadline > now)
}
fn rebase(&mut self, new_deadline_idx: usize) {
let duration = Duration::from_millis(self.queue[new_deadline_idx].into());
let new_deadline = self.delay.deadline() + duration;
self.queue.drain(..=new_deadline_idx);
for timestamp in &mut self.queue {
let deadline = self.delay.deadline() + Duration::from_millis((*timestamp).into());
*timestamp = (deadline - new_deadline).as_millis() as u16;
}
self.delay.as_mut().reset(new_deadline);
}
}
fn nonreserved_commands_per_reset(heartbeat_interval: Duration) -> u8 {
const MAX_NONRESERVED_COMMANDS_PER_PERIOD: u8 = PERMITS - 10;
let heartbeats_per_reset = PERIOD.as_secs_f32() / heartbeat_interval.as_secs_f32();
#[allow(clippy::cast_sign_loss)]
let heartbeats_per_reset = heartbeats_per_reset.ceil() as u8;
let heartbeats_per_reset = heartbeats_per_reset.saturating_add(1);
let nonreserved_commands_per_reset = PERMITS.saturating_sub(heartbeats_per_reset);
nonreserved_commands_per_reset.max(MAX_NONRESERVED_COMMANDS_PER_PERIOD)
}
#[cfg(test)]
mod tests {
#![allow(clippy::unchecked_time_subtraction)]
use super::{CommandRatelimiter, PERIOD, nonreserved_commands_per_reset};
use static_assertions::assert_impl_all;
use std::{fmt::Debug, future::poll_fn, time::Duration};
use tokio::time;
assert_impl_all!(CommandRatelimiter: Debug, Send, Sync);
#[test]
fn nonreserved_commands() {
assert_eq!(
118,
nonreserved_commands_per_reset(Duration::from_secs(u64::MAX))
);
assert_eq!(118, nonreserved_commands_per_reset(Duration::from_secs(60)));
assert_eq!(
117,
nonreserved_commands_per_reset(Duration::from_millis(42_500))
);
assert_eq!(117, nonreserved_commands_per_reset(Duration::from_secs(30)));
assert_eq!(
116,
nonreserved_commands_per_reset(Duration::from_millis(29_999))
);
assert_eq!(110, nonreserved_commands_per_reset(Duration::ZERO));
}
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(60);
#[tokio::test(start_paused = true)]
async fn full_reset() {
let mut ratelimiter = CommandRatelimiter::new(HEARTBEAT_INTERVAL);
assert_eq!(ratelimiter.available(), ratelimiter.max());
for _ in 0..ratelimiter.max() {
poll_fn(|cx| ratelimiter.poll_acquire(cx)).await;
}
assert_eq!(ratelimiter.available(), 0);
time::advance(PERIOD - Duration::from_millis(100)).await;
assert_eq!(ratelimiter.available(), 0);
time::advance(Duration::from_millis(100)).await;
assert_eq!(ratelimiter.available(), ratelimiter.max());
}
#[tokio::test(start_paused = true)]
async fn half_reset() {
let mut ratelimiter = CommandRatelimiter::new(HEARTBEAT_INTERVAL);
assert_eq!(ratelimiter.available(), ratelimiter.max());
for _ in 0..ratelimiter.max() / 2 {
poll_fn(|cx| ratelimiter.poll_acquire(cx)).await;
}
assert_eq!(ratelimiter.available(), ratelimiter.max() / 2);
time::advance(PERIOD / 2).await;
assert_eq!(ratelimiter.available(), ratelimiter.max() / 2);
for _ in 0..ratelimiter.max() / 2 {
poll_fn(|cx| ratelimiter.poll_acquire(cx)).await;
}
assert_eq!(ratelimiter.available(), 0);
time::advance(PERIOD / 2).await;
assert_eq!(ratelimiter.available(), ratelimiter.max() / 2);
time::advance(PERIOD / 2).await;
assert_eq!(ratelimiter.available(), ratelimiter.max());
}
#[tokio::test(start_paused = true)]
async fn constant_capacity() {
let mut ratelimiter = CommandRatelimiter::new(HEARTBEAT_INTERVAL);
let max = ratelimiter.max();
for _ in 0..max {
poll_fn(|cx| ratelimiter.poll_acquire(cx)).await;
}
assert_eq!(ratelimiter.available(), 0);
poll_fn(|cx| ratelimiter.poll_acquire(cx)).await;
assert_eq!(max, ratelimiter.max());
}
#[tokio::test(start_paused = true)]
async fn rebase() {
let mut ratelimiter = CommandRatelimiter::new(HEARTBEAT_INTERVAL);
for _ in 0..5 {
time::advance(Duration::from_millis(20)).await;
poll_fn(|cx| ratelimiter.poll_acquire(cx)).await;
}
assert_eq!(ratelimiter.available(), ratelimiter.max() - 5);
time::advance(PERIOD - Duration::from_millis(80)).await;
assert_eq!(ratelimiter.available(), ratelimiter.max() - 4);
for _ in 0..4 {
poll_fn(|cx| ratelimiter.poll_acquire(cx)).await;
time::advance(Duration::from_millis(20)).await;
assert_eq!(ratelimiter.available(), ratelimiter.max() - 4);
}
}
}