use crate::portability::{AtomicU64, Ordering};
use std::collections::BTreeSet;
use std::time::Duration;
#[cfg(test)]
use std::time::Instant;
use crate::sync::{Condvar, Mutex};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Priority {
High,
Low,
}
impl Priority {
fn class(self) -> u8 {
match self {
Priority::High => 0,
Priority::Low => 1,
}
}
}
pub trait RateLimiter: Send + Sync + 'static {
fn request(&self, bytes: u64, pri: Priority);
fn set_bytes_per_second(&self, bytes_per_second: u64);
fn get_bytes_per_second(&self) -> u64;
fn get_total_bytes_through(&self, pri: Priority) -> u64;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct WaiterKey {
class: u8,
seq: u64,
}
struct State {
bytes_per_second: u64,
available: i128,
last_refill: u64,
next_seq: u64,
waiters: BTreeSet<WaiterKey>,
shutdown: bool,
}
pub struct TokenBucketRateLimiter {
state: Mutex<State>,
cv: Condvar,
burst_bytes: u64,
refill_period: Duration,
total_high: AtomicU64,
total_low: AtomicU64,
}
impl TokenBucketRateLimiter {
pub fn new(bytes_per_second: u64, refill_period: Duration, burst_bytes: u64) -> Self {
let burst_bytes = burst_bytes.max(1);
Self {
state: Mutex::new(State {
bytes_per_second,
available: burst_bytes as i128,
last_refill: crate::env::platform_nanos().unwrap_or(0),
next_seq: 0,
waiters: BTreeSet::new(),
shutdown: false,
}),
cv: Condvar::new(),
burst_bytes,
refill_period,
total_high: AtomicU64::new(0),
total_low: AtomicU64::new(0),
}
}
pub fn stop(&self) {
let mut state = self.state.lock();
state.shutdown = true;
self.cv.notify_all();
}
fn refill_locked(&self, state: &mut State, now: u64) {
let elapsed = u128::from(now.saturating_sub(state.last_refill));
if elapsed < self.refill_period.as_nanos() {
return;
}
let period_nanos: u128 = self.refill_period.as_nanos().max(1);
let periods = (elapsed / period_nanos) as u64;
if periods == 0 {
return;
}
let rate = state.bytes_per_second as u128;
let tokens = rate
.saturating_mul(period_nanos)
.saturating_mul(periods as u128)
/ 1_000_000_000u128;
state.available = (state.available + tokens as i128).min(self.burst_bytes as i128);
state.last_refill = state
.last_refill
.saturating_add((period_nanos.saturating_mul(periods as u128)) as u64);
}
fn request_chunk(&self, bytes: u64, pri: Priority) -> bool {
if bytes == 0 {
return true;
}
let Some(mut now) = crate::env::platform_nanos() else {
return true;
};
let class = pri.class();
let mut state = self.state.lock();
if state.shutdown {
return false;
}
let my_seq = state.next_seq;
state.next_seq += 1;
let key = WaiterKey { class, seq: my_seq };
state.waiters.insert(key);
let served = loop {
if state.shutdown {
break false;
}
self.refill_locked(&mut state, now);
let Some(front) = state.waiters.iter().next().copied() else {
break false;
};
if front == key && state.available >= bytes as i128 {
state.available -= bytes as i128;
break true;
}
now = crate::env::platform_nanos().unwrap_or(now);
let wait = self
.refill_period
.saturating_sub(Duration::from_nanos(now.saturating_sub(state.last_refill)));
let wait = if wait.is_zero() {
self.refill_period
} else {
wait
};
state = self
.cv
.wait_timeout(state, wait)
.unwrap_or_else(std::sync::PoisonError::into_inner)
.0;
};
state.waiters.remove(&key);
self.cv.notify_all();
if served {
match pri {
Priority::High => {
self.total_high.fetch_add(bytes, Ordering::Relaxed);
}
Priority::Low => {
self.total_low.fetch_add(bytes, Ordering::Relaxed);
}
}
}
served
}
}
impl Drop for TokenBucketRateLimiter {
fn drop(&mut self) {
self.stop();
}
}
impl RateLimiter for TokenBucketRateLimiter {
fn request(&self, bytes: u64, pri: Priority) {
if self.get_bytes_per_second() == 0 {
match pri {
Priority::High => {
self.total_high.fetch_add(bytes, Ordering::Relaxed);
}
Priority::Low => {
self.total_low.fetch_add(bytes, Ordering::Relaxed);
}
}
return;
}
let mut remaining = bytes;
while remaining > 0 {
let chunk = remaining.min(self.burst_bytes);
if !self.request_chunk(chunk, pri) {
return;
}
remaining -= chunk;
}
}
fn set_bytes_per_second(&self, bytes_per_second: u64) {
let mut state = self.state.lock();
if let Some(now) = crate::env::platform_nanos() {
self.refill_locked(&mut state, now);
}
state.bytes_per_second = bytes_per_second;
self.cv.notify_all();
}
fn get_bytes_per_second(&self) -> u64 {
self.state.lock().bytes_per_second
}
fn get_total_bytes_through(&self, pri: Priority) -> u64 {
match pri {
Priority::High => self.total_high.load(Ordering::Relaxed),
Priority::Low => self.total_low.load(Ordering::Relaxed),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
use std::thread;
#[test]
fn single_request_within_burst_is_instant() {
let lim = TokenBucketRateLimiter::new(1_000_000, Duration::from_millis(100), 1_000_000);
let start = Instant::now();
lim.request(500_000, Priority::Low);
assert!(start.elapsed() < Duration::from_millis(50));
assert_eq!(lim.get_total_bytes_through(Priority::Low), 500_000);
}
#[test]
fn ten_mb_through_one_mbps_takes_at_least_nine_seconds() {
let lim = TokenBucketRateLimiter::new(1_000_000, Duration::from_millis(100), 1_000_000);
let start = Instant::now();
lim.request(10_000_000, Priority::Low);
let elapsed = start.elapsed();
assert!(
elapsed >= Duration::from_secs(9),
"10 MB through 1 MB/s took {:?}, expected >= 9s",
elapsed
);
assert_eq!(lim.get_total_bytes_through(Priority::Low), 10_000_000);
}
#[test]
fn high_priority_preempts_low() {
let lim = Arc::new(TokenBucketRateLimiter::new(
100_000,
Duration::from_millis(100),
10_000,
));
lim.request(10_000, Priority::Low);
let order: Arc<Mutex<Vec<&'static str>>> = Arc::new(Mutex::new(Vec::new()));
let low_handles: Vec<_> = (0..2)
.map(|i| {
let lim = lim.clone();
let order = order.clone();
thread::spawn(move || {
lim.request(30_000, Priority::Low);
let label = if i == 0 { "lo1" } else { "lo2" };
order.lock().push(label);
})
})
.collect();
thread::sleep(Duration::from_millis(30));
let high = {
let lim = lim.clone();
let order = order.clone();
thread::spawn(move || {
lim.request(30_000, Priority::High);
order.lock().push("high");
})
};
high.join().unwrap();
for h in low_handles {
h.join().unwrap();
}
let order = order.lock();
let high_idx = order.iter().position(|&s| s == "high").unwrap();
assert!(
high_idx < 2,
"high did not preempt any low: order = {:?}",
*order
);
}
#[test]
fn shutdown_wakes_blocked_waiters() {
let lim = Arc::new(TokenBucketRateLimiter::new(
1_000,
Duration::from_secs(60),
1_000,
));
lim.request(1_000, Priority::Low);
let blocked = {
let lim = lim.clone();
thread::spawn(move || {
let start = Instant::now();
lim.request(1_000, Priority::Low);
start.elapsed()
})
};
thread::sleep(Duration::from_millis(100));
lim.stop();
let waited = blocked.join().unwrap();
assert!(
waited < Duration::from_secs(5),
"blocked waiter did not wake promptly after stop: {:?}",
waited
);
}
#[test]
fn set_bytes_per_second_live_update_is_respected() {
let lim = Arc::new(TokenBucketRateLimiter::new(
100_000,
Duration::from_millis(50),
100_000,
));
lim.request(100_000, Priority::Low); assert_eq!(lim.get_bytes_per_second(), 100_000);
lim.set_bytes_per_second(10_000_000);
assert_eq!(lim.get_bytes_per_second(), 10_000_000);
let start = Instant::now();
lim.request(1_000_000, Priority::Low);
assert!(
start.elapsed() < Duration::from_secs(2),
"request took {:?} after rate bump",
start.elapsed()
);
}
#[test]
fn zero_rate_disables_limiter() {
let lim = TokenBucketRateLimiter::new(0, Duration::from_millis(100), 1);
let start = Instant::now();
lim.request(100_000_000, Priority::Low);
assert!(start.elapsed() < Duration::from_millis(50));
assert_eq!(lim.get_total_bytes_through(Priority::Low), 100_000_000);
}
#[test]
fn get_total_bytes_through_tracks_both_classes() {
let lim = TokenBucketRateLimiter::new(10_000_000, Duration::from_millis(50), 10_000_000);
lim.request(1_000, Priority::High);
lim.request(2_000, Priority::Low);
lim.request(3_000, Priority::High);
assert_eq!(lim.get_total_bytes_through(Priority::High), 4_000);
assert_eq!(lim.get_total_bytes_through(Priority::Low), 2_000);
}
}