use std::sync::Mutex;
use std::time::Duration;
use tokio::time::Instant;
#[derive(Debug)]
pub struct RateLimiter {
bytes_per_sec: u64,
next_free: Mutex<Option<Instant>>,
}
impl RateLimiter {
pub fn new(bytes_per_sec: u64) -> Self {
Self {
bytes_per_sec: bytes_per_sec.max(1),
next_free: Mutex::new(None),
}
}
pub fn bytes_per_sec(&self) -> u64 {
self.bytes_per_sec
}
pub fn cost(&self, n: u64) -> Duration {
Duration::from_secs_f64(n as f64 / self.bytes_per_sec as f64)
}
pub async fn acquire(&self, n: u64) {
let until = {
let mut next = self.next_free.lock().unwrap_or_else(|p| p.into_inner());
let start = next.map_or_else(Instant::now, |t| t.max(Instant::now()));
let end = start + self.cost(n);
*next = Some(end);
end
};
tokio::time::sleep_until(until).await;
}
}
#[derive(Debug, Default)]
pub struct Bandwidth {
pub up: Option<RateLimiter>,
pub down: Option<RateLimiter>,
}
impl Bandwidth {
pub fn new(up: Option<u64>, down: Option<u64>) -> Self {
Self {
up: up.map(RateLimiter::new),
down: down.map(RateLimiter::new),
}
}
pub async fn upload(&self, n: usize) {
if let Some(l) = &self.up {
l.acquire(n as u64).await;
}
}
pub async fn download(&self, n: usize) {
if let Some(l) = &self.down {
l.acquire(n as u64).await;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test(start_paused = true)]
async fn transfers_never_beat_the_rate_and_concurrent_callers_share_it() {
let l = RateLimiter::new(1_000);
let t0 = Instant::now();
l.acquire(2_000).await;
assert_eq!(t0.elapsed(), Duration::from_secs(2));
let t1 = Instant::now();
tokio::join!(l.acquire(500), l.acquire(500), l.acquire(1_000));
assert_eq!(t1.elapsed(), Duration::from_secs(2));
}
#[tokio::test(start_paused = true)]
async fn unlimited_does_not_wait_and_idle_time_is_not_banked() {
let b = Bandwidth::new(None, Some(100));
let t0 = Instant::now();
b.upload(1 << 30).await;
assert_eq!(t0.elapsed(), Duration::ZERO);
tokio::time::sleep(Duration::from_secs(60)).await;
let t1 = Instant::now();
b.download(100).await;
assert_eq!(t1.elapsed(), Duration::from_secs(1), "no burst credit from idling");
}
}