use parking_lot::Mutex;
use std::time::{Duration, Instant};
const MAX_SLEEP_SECS: f64 = 60.0;
pub struct RateLimiter {
inner: Mutex<RateLimiterInner>,
}
struct RateLimiterInner {
rate_bytes_per_sec: u64,
available: f64,
last_refill: Instant,
}
struct ChunkPlan {
new_available: f64,
wait_secs: f64,
}
fn next_chunk(available: f64, remaining: f64, rate_bytes_per_sec: u64) -> (f64, ChunkPlan) {
let rate = rate_bytes_per_sec as f64;
let max_chunk = rate * MAX_SLEEP_SECS;
let chunk = remaining.min(max_chunk);
if available >= chunk {
return (
chunk,
ChunkPlan {
new_available: available - chunk,
wait_secs: 0.0,
},
);
}
let deficit = chunk - available;
let wait_secs = (deficit / rate).min(MAX_SLEEP_SECS);
(
chunk,
ChunkPlan {
new_available: available - chunk,
wait_secs,
},
)
}
impl RateLimiter {
pub fn new(rate_bytes_per_sec: u64) -> Self {
Self {
inner: Mutex::new(RateLimiterInner {
rate_bytes_per_sec,
available: rate_bytes_per_sec as f64,
last_refill: Instant::now(),
}),
}
}
pub fn request(&self, bytes: usize) {
let mut remaining = bytes as f64;
while remaining > 0.0 {
let mut inner = self.inner.lock();
if inner.rate_bytes_per_sec == 0 {
return;
}
let now = Instant::now();
let elapsed = now.duration_since(inner.last_refill).as_secs_f64();
inner.available += elapsed * inner.rate_bytes_per_sec as f64;
inner.available = inner.available.min(inner.rate_bytes_per_sec as f64 * 2.0); inner.last_refill = now;
let (chunk, plan) = next_chunk(inner.available, remaining, inner.rate_bytes_per_sec);
inner.available = plan.new_available;
remaining -= chunk;
drop(inner);
if plan.wait_secs > 0.0 {
std::thread::sleep(Duration::from_secs_f64(plan.wait_secs));
}
}
}
}
impl RateLimiter {
#[cfg(test)]
pub(crate) fn is_enabled(&self) -> bool {
self.inner.lock().rate_bytes_per_sec > 0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rate_limiter_disabled() {
let rl = RateLimiter::new(0);
assert!(!rl.is_enabled());
rl.request(1_000_000);
}
#[test]
fn test_rate_limiter_basic() {
let rl = RateLimiter::new(1_000_000); assert!(rl.is_enabled());
rl.request(100);
}
#[test]
fn test_rate_limiter_oversized_requests_converge_to_configured_rate() {
let rate: u64 = 1000; let one_request_bytes = 1_000_000.0; let num_requests = 5;
let mut available = rate as f64; let mut total_wait_secs = 0.0;
for _ in 0..num_requests {
let mut remaining = one_request_bytes;
while remaining > 0.0 {
let (chunk, plan) = next_chunk(available, remaining, rate);
assert!(
plan.wait_secs <= MAX_SLEEP_SECS,
"a single chunk must never require more than MAX_SLEEP_SECS"
);
available = plan.new_available;
remaining -= chunk;
total_wait_secs += plan.wait_secs;
available += plan.wait_secs * rate as f64;
available = available.min(rate as f64 * 2.0);
}
}
let total_bytes = one_request_bytes * num_requests as f64;
let expected_secs = total_bytes / rate as f64;
assert!(
total_wait_secs >= expected_secs * 0.9 && total_wait_secs <= expected_secs * 1.1,
"expected total wait ~{expected_secs}s (configured rate), got {total_wait_secs}s"
);
}
#[test]
fn test_rate_limiter_single_oversized_request_needs_multiple_chunks() {
let rate: u64 = 1000;
let bytes = 1_000_000.0; let mut available = rate as f64;
let mut remaining = bytes;
let mut chunks = 0;
while remaining > 0.0 {
let (chunk, plan) = next_chunk(available, remaining, rate);
available = plan.new_available;
remaining -= chunk;
chunks += 1;
assert!(
chunks < 1000,
"should converge in a bounded number of chunks"
);
}
assert!(
chunks > 1,
"an oversized request must be split into multiple chunks, not resolved in one capped sleep"
);
}
}