#![no_std]
#![warn(missing_docs)]
use core::cmp::min;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RateLimiter {
capacity: u64,
refill_amount: u64,
refill_interval: u64,
tokens: u64,
last_refill: u64,
}
impl RateLimiter {
pub const fn new(capacity: u64, refill_amount: u64, refill_interval: u64) -> Self {
let capacity = if capacity == 0 { 1 } else { capacity };
Self {
capacity,
refill_amount: if refill_amount == 0 { 1 } else { refill_amount },
refill_interval: if refill_interval == 0 {
1
} else {
refill_interval
},
tokens: capacity,
last_refill: 0,
}
}
pub const fn capacity(&self) -> u64 {
self.capacity
}
pub const fn refill_amount(&self) -> u64 {
self.refill_amount
}
pub const fn refill_interval(&self) -> u64 {
self.refill_interval
}
fn refill(&mut self, now: u64) {
let elapsed = now.saturating_sub(self.last_refill);
let batches = elapsed / self.refill_interval;
if batches == 0 {
return;
}
let added = batches.saturating_mul(self.refill_amount);
self.tokens = min(self.capacity, self.tokens.saturating_add(added));
self.last_refill = self
.last_refill
.saturating_add(batches.saturating_mul(self.refill_interval));
}
pub fn available(&mut self, now: u64) -> u64 {
self.refill(now);
self.tokens
}
pub fn try_acquire(&mut self, now: u64, tokens: u64) -> bool {
self.refill(now);
if self.tokens >= tokens {
self.tokens -= tokens;
true
} else {
false
}
}
pub fn try_acquire_one(&mut self, now: u64) -> bool {
self.try_acquire(now, 1)
}
pub fn retry_after(&mut self, now: u64, tokens: u64) -> Option<u64> {
if tokens > self.capacity {
return None;
}
self.refill(now);
if self.tokens >= tokens {
return Some(0);
}
let deficit = tokens - self.tokens;
let batches = deficit.div_ceil(self.refill_amount);
let into_interval = now.saturating_sub(self.last_refill);
let time_to_first = self.refill_interval.saturating_sub(into_interval);
Some(
time_to_first.saturating_add(
batches
.saturating_sub(1)
.saturating_mul(self.refill_interval),
),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn starts_full() {
let mut rl = RateLimiter::new(10, 1, 100);
assert_eq!(rl.available(0), 10);
assert_eq!(rl.capacity(), 10);
}
#[test]
fn acquire_consumes_tokens() {
let mut rl = RateLimiter::new(10, 1, 100);
assert!(rl.try_acquire(0, 4));
assert_eq!(rl.available(0), 6);
}
#[test]
fn burst_then_denied() {
let mut rl = RateLimiter::new(10, 1, 100);
for _ in 0..10 {
assert!(rl.try_acquire_one(0));
}
assert!(!rl.try_acquire_one(0));
}
#[test]
fn refills_over_time_capped_at_capacity() {
let mut rl = RateLimiter::new(10, 1, 100);
assert!(rl.try_acquire(0, 10));
assert_eq!(rl.available(0), 0);
assert_eq!(rl.available(250), 2); assert_eq!(rl.available(10_000), 10); }
#[test]
fn partial_interval_does_not_refill_and_keeps_remainder() {
let mut rl = RateLimiter::new(10, 1, 100);
assert!(rl.try_acquire(0, 10));
assert_eq!(rl.available(50), 0); assert_eq!(rl.available(100), 1); }
#[test]
fn refill_amount_greater_than_one() {
let mut rl = RateLimiter::new(100, 5, 10);
assert!(rl.try_acquire(0, 100));
assert_eq!(rl.available(30), 15); }
#[test]
fn acquire_more_than_capacity_always_fails() {
let mut rl = RateLimiter::new(5, 1, 100);
assert!(!rl.try_acquire(0, 6));
assert_eq!(rl.available(0), 5); }
#[test]
fn retry_after_zero_when_available() {
let mut rl = RateLimiter::new(10, 1, 100);
assert_eq!(rl.retry_after(0, 5), Some(0));
}
#[test]
fn retry_after_none_when_above_capacity() {
let mut rl = RateLimiter::new(10, 1, 100);
assert_eq!(rl.retry_after(0, 11), None);
}
#[test]
fn retry_after_full_interval_when_empty() {
let mut rl = RateLimiter::new(10, 1, 100);
assert!(rl.try_acquire(0, 10));
assert_eq!(rl.retry_after(0, 1), Some(100));
assert_eq!(rl.retry_after(0, 3), Some(300));
}
#[test]
fn retry_after_accounts_for_elapsed_partial_interval() {
let mut rl = RateLimiter::new(10, 1, 100);
assert!(rl.try_acquire(0, 10));
assert_eq!(rl.retry_after(60, 1), Some(40));
assert_eq!(rl.retry_after(60, 2), Some(140));
}
#[test]
fn backwards_clock_does_not_panic_or_refill() {
let mut rl = RateLimiter::new(10, 1, 100);
assert!(rl.try_acquire(10_000, 10));
assert!(!rl.try_acquire(5_000, 1)); assert_eq!(rl.available(5_000), 0);
}
#[test]
fn zero_config_is_clamped() {
let rl = RateLimiter::new(0, 0, 0);
assert_eq!(rl.capacity(), 1);
assert_eq!(rl.refill_amount(), 1);
assert_eq!(rl.refill_interval(), 1);
}
}