use std::num::NonZeroU32;
use std::sync::atomic::{AtomicU32, Ordering};
use arc_swap::ArcSwapOption;
use governor::DefaultDirectRateLimiter as RateLimiter;
use governor::Quota;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::Notify;
#[derive(Default, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
pub struct BandwidthConfig {
pub download_bps: Option<NonZeroU32>,
}
struct Bucket {
limiter: RateLimiter,
burst: NonZeroU32,
}
struct Limit {
limiter: ArcSwapOption<Bucket>,
current_bps: AtomicU32,
changed: Notify,
}
impl Limit {
fn new_inner(bps: Option<NonZeroU32>) -> Option<Arc<Bucket>> {
let bps = bps?;
Some(Arc::new(Bucket {
limiter: RateLimiter::direct(Quota::per_second(bps)),
burst: bps,
}))
}
fn new(bps: Option<NonZeroU32>) -> Self {
Self {
limiter: ArcSwapOption::new(Self::new_inner(bps)),
current_bps: AtomicU32::new(bps.map(|v| v.get()).unwrap_or(0)),
changed: Notify::new(),
}
}
async fn acquire(&self, size: NonZeroU32) -> anyhow::Result<()> {
let mut remaining = size.get();
while remaining > 0 {
let changed = self.changed.notified();
tokio::pin!(changed);
changed.as_mut().enable();
let lim = self.limiter.load_full();
let Some(bucket) = lim else {
return Ok(());
};
let chunk = remaining.min(bucket.burst.get());
let n = NonZeroU32::new(chunk).expect("chunk > 0");
tokio::select! {
res = bucket.limiter.until_n_ready(n) => {
res?;
remaining -= chunk;
}
() = &mut changed => {}
}
}
Ok(())
}
fn set(&self, limit: Option<NonZeroU32>) {
let new = Self::new_inner(limit);
self.limiter.swap(new);
self.current_bps
.store(limit.map(|v| v.get()).unwrap_or(0), Ordering::Relaxed);
self.changed.notify_waiters();
}
fn get(&self) -> Option<NonZeroU32> {
NonZeroU32::new(self.current_bps.load(Ordering::Relaxed))
}
}
pub struct BandwidthLimiter {
download: Limit,
}
impl BandwidthLimiter {
pub fn new(config: BandwidthConfig) -> Self {
Self {
download: Limit::new(config.download_bps),
}
}
pub async fn acquire_download(&self, len: NonZeroU32) -> anyhow::Result<()> {
self.download.acquire(len).await
}
pub fn set_download_bps(&self, bps: Option<NonZeroU32>) {
self.download.set(bps);
}
pub fn get_download_bps(&self) -> Option<NonZeroU32> {
self.download.get()
}
pub fn get_config(&self) -> BandwidthConfig {
BandwidthConfig {
download_bps: self.get_download_bps(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn request_larger_than_burst_is_throttled_not_rejected() {
let limiter = BandwidthLimiter::new(BandwidthConfig {
download_bps: NonZeroU32::new(200_000),
});
let start = std::time::Instant::now();
limiter
.acquire_download(NonZeroU32::new(500_000).unwrap())
.await
.expect("oversized request must be throttled, not rejected");
assert!(
start.elapsed() >= std::time::Duration::from_millis(1_000),
"500 KB at 200 KB/s returned after {:?}",
start.elapsed()
);
}
async fn acquire_then_reconfigure(
initial: u32,
changed: Option<NonZeroU32>,
) -> std::time::Duration {
let limiter = Arc::new(BandwidthLimiter::new(BandwidthConfig {
download_bps: NonZeroU32::new(initial),
}));
let waiter = {
let limiter = Arc::clone(&limiter);
tokio::spawn(async move {
limiter
.acquire_download(NonZeroU32::new(750_000).unwrap())
.await
})
};
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
assert!(!waiter.is_finished(), "acquire should be throttled");
let changed_at = std::time::Instant::now();
limiter.set_download_bps(changed);
tokio::time::timeout(std::time::Duration::from_secs(5), waiter)
.await
.expect("parked acquire never observed the limit change")
.unwrap()
.unwrap();
changed_at.elapsed()
}
#[tokio::test]
async fn switching_to_unlimited_releases_parked_acquire() {
let took = acquire_then_reconfigure(20, None).await;
assert!(
took < std::time::Duration::from_secs(2),
"unlimited took {took:?} to apply"
);
}
#[tokio::test]
async fn raising_limit_releases_parked_acquire() {
let took = acquire_then_reconfigure(20, NonZeroU32::new(10_000_000)).await;
assert!(
took < std::time::Duration::from_secs(2),
"raised limit took {took:?} to apply"
);
}
#[tokio::test]
async fn unlimited_acquire_returns_immediately() {
let limiter = BandwidthLimiter::new(BandwidthConfig::default());
tokio::time::timeout(
std::time::Duration::from_millis(100),
limiter.acquire_download(NonZeroU32::new(u32::MAX).unwrap()),
)
.await
.expect("unlimited acquire must not wait")
.unwrap();
}
#[tokio::test]
async fn limiter_can_be_reconfigured_without_recreation() {
let limiter = BandwidthLimiter::new(BandwidthConfig::default());
assert_eq!(limiter.get_download_bps(), None);
limiter
.acquire_download(NonZeroU32::new(1).unwrap())
.await
.unwrap();
limiter.set_download_bps(NonZeroU32::new(1_000));
assert_eq!(limiter.get_config().download_bps.unwrap().get(), 1_000);
limiter.set_download_bps(None);
assert_eq!(limiter.get_download_bps(), None);
}
}