use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
const UNIT: u64 = 4096;
struct Counters {
held: AtomicU64,
high_water: AtomicU64,
}
pub struct ByteGate {
sem: Arc<Semaphore>,
ctr: Arc<Counters>,
max_units: u32,
max_bytes: u64,
}
pub struct BytePermit {
_permit: OwnedSemaphorePermit,
bytes: u64,
ctr: Arc<Counters>,
}
impl std::fmt::Debug for ByteGate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ByteGate")
.field("max_bytes", &self.max_bytes)
.field("held", &self.held())
.field("high_water", &self.high_water())
.finish()
}
}
impl ByteGate {
pub fn new(max_bytes: u64) -> ByteGate {
let max_units = max_bytes.div_ceil(UNIT).clamp(1, u32::MAX as u64) as u32;
ByteGate {
sem: Arc::new(Semaphore::new(max_units as usize)),
ctr: Arc::new(Counters { held: AtomicU64::new(0), high_water: AtomicU64::new(0) }),
max_units,
max_bytes: max_units as u64 * UNIT,
}
}
pub fn max_bytes(&self) -> u64 {
self.max_bytes
}
pub async fn acquire(&self, bytes: u64) -> BytePermit {
let want = bytes.div_ceil(UNIT).clamp(1, self.max_units as u64) as u32;
let permit = self
.sem
.clone()
.acquire_many_owned(want)
.await
.expect("the upload byte gate's semaphore is never closed");
let now = self.ctr.held.fetch_add(bytes, Ordering::SeqCst) + bytes;
self.ctr.high_water.fetch_max(now, Ordering::SeqCst);
BytePermit { _permit: permit, bytes, ctr: self.ctr.clone() }
}
pub fn held(&self) -> u64 {
self.ctr.held.load(Ordering::SeqCst)
}
pub fn high_water(&self) -> u64 {
self.ctr.high_water.load(Ordering::SeqCst)
}
}
impl Drop for BytePermit {
fn drop(&mut self) {
self.ctr.held.fetch_sub(self.bytes, Ordering::SeqCst);
}
}
#[cfg(test)]
mod tests {
use super::*;
const MIB: u64 = 1 << 20;
#[tokio::test]
async fn a_request_above_the_budget_clamps_and_proceeds() {
let g = ByteGate::new(2 * MIB);
assert_eq!(g.max_bytes(), 2 * MIB);
let big = tokio::time::timeout(std::time::Duration::from_secs(5), g.acquire(10 * MIB))
.await
.expect("a request above the budget deadlocked");
assert_eq!(g.held(), 10 * MIB, "the counter carries the TRUE bytes");
let waiter = tokio::time::timeout(std::time::Duration::from_millis(100), g.acquire(1)).await;
assert!(waiter.is_err(), "a clamped holder must own the whole budget");
drop(big);
assert_eq!(g.held(), 0);
let _one = tokio::time::timeout(std::time::Duration::from_secs(5), g.acquire(1))
.await
.expect("released bytes were not granted on");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn high_water_never_exceeds_the_budget_under_contention() {
async fn run(gate: Arc<ByteGate>) -> u64 {
let mut set = tokio::task::JoinSet::new();
for i in 0..64u64 {
let g = gate.clone();
set.spawn(async move {
let _p = g.acquire(MIB / 2 + (i % 3) * 100_000).await;
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
});
}
while set.join_next().await.is_some() {}
assert_eq!(gate.held(), 0, "every permit releases");
gate.high_water()
}
let bounded = Arc::new(ByteGate::new(3 * MIB));
let hw = run(bounded.clone()).await;
assert!(hw <= 3 * MIB, "high-water {hw} exceeded the 3 MiB budget");
assert!(hw > 0);
let control = Arc::new(ByteGate::new(1 << 30));
let chw = run(control.clone()).await;
assert!(
chw > 3 * MIB,
"the control's high-water {chw} did not exceed the budget: the tree is too small \
to prove the bound load-bearing"
);
}
#[tokio::test]
async fn permits_release_on_drop_including_cancellation() {
let g = Arc::new(ByteGate::new(MIB));
let p = g.acquire(MIB).await;
assert_eq!(g.held(), MIB);
let g2 = g.clone();
let parked = tokio::spawn(async move {
let _p = g2.acquire(MIB).await;
std::future::pending::<()>().await;
});
tokio::task::yield_now().await;
parked.abort();
let _ = parked.await;
assert_eq!(g.held(), MIB, "a cancelled waiter charged nothing");
drop(p);
assert_eq!(g.held(), 0);
assert_eq!(g.high_water(), MIB);
let g3 = g.clone();
let holder = tokio::spawn(async move {
let _p = g3.acquire(MIB).await;
std::future::pending::<()>().await;
});
tokio::task::yield_now().await;
assert_eq!(g.held(), MIB);
holder.abort();
let _ = holder.await;
assert_eq!(g.held(), 0, "an aborted holder released its bytes");
let _again = tokio::time::timeout(std::time::Duration::from_secs(5), g.acquire(MIB))
.await
.expect("the budget was not returned");
}
}