use core::fmt;
#[cfg(not(all(test, loom)))]
use std::sync::{
Arc,
atomic::{AtomicU64, Ordering},
};
#[cfg(all(test, loom))]
use loom::sync::{
Arc,
atomic::{AtomicU64, Ordering},
};
pub trait SpillBudget: Send + Sync + 'static {
fn reserve(&self, bytes: u64) -> bool;
fn release(&self, bytes: u64);
}
struct Inner {
limit: AtomicU64,
in_use: AtomicU64,
}
#[derive(Clone)]
pub struct SpillQuota {
inner: Arc<Inner>,
}
impl fmt::Debug for SpillQuota {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SpillQuota")
.field("in_use", &self.in_use())
.field("limit", &self.limit())
.finish()
}
}
impl SpillQuota {
#[must_use]
pub fn new(max_bytes: u64) -> SpillQuota {
SpillQuota {
inner: Arc::new(Inner {
limit: AtomicU64::new(max_bytes),
in_use: AtomicU64::new(0),
}),
}
}
#[must_use]
pub fn in_use(&self) -> u64 {
self.inner.in_use.load(Ordering::Acquire)
}
#[must_use]
pub fn limit(&self) -> u64 {
self.inner.limit.load(Ordering::Acquire)
}
pub fn set_limit(&self, max_bytes: u64) {
self.inner.limit.store(max_bytes, Ordering::Release);
}
}
impl SpillBudget for SpillQuota {
fn reserve(&self, bytes: u64) -> bool {
let limit = self.inner.limit.load(Ordering::Acquire);
let mut current = self.inner.in_use.load(Ordering::Acquire);
loop {
let Some(next) = current.checked_add(bytes) else {
return false;
};
if next > limit {
return false;
}
match self.inner.in_use.compare_exchange_weak(
current,
next,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => return true,
Err(now) => current = now,
}
}
}
fn release(&self, bytes: u64) {
let mut current = self.inner.in_use.load(Ordering::Acquire);
loop {
let next = current.saturating_sub(bytes);
match self.inner.in_use.compare_exchange_weak(
current,
next,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => return,
Err(now) => current = now,
}
}
}
}
#[cfg(all(test, not(loom)))]
mod tests {
use super::*;
use alloc::vec::Vec;
#[test]
fn test_quota_reserve_release_and_limits() {
let q = SpillQuota::new(10);
assert!(q.reserve(6) && q.reserve(4));
assert!(!q.reserve(1), "full");
q.release(4);
assert!(!q.reserve(5) && q.reserve(4));
q.set_limit(5);
assert_eq!(q.in_use(), 10, "reservations made stay");
assert!(!q.reserve(1));
q.release(100);
assert_eq!(q.in_use(), 0, "never below zero");
assert!(!q.reserve(u64::MAX) || q.limit() == u64::MAX);
}
#[test]
fn test_quota_holds_under_threads() {
let q = SpillQuota::new(1000);
let threads: Vec<_> = (0..8)
.map(|_| {
let q = q.clone();
std::thread::spawn(move || {
let mut got = 0u64;
for _ in 0..10_000 {
if q.reserve(7) {
assert!(q.in_use() <= 1000);
got += 7;
q.release(7);
}
}
got
})
})
.collect();
for t in threads {
let _ = t.join().unwrap();
}
assert_eq!(q.in_use(), 0);
}
}
#[cfg(all(test, loom))]
mod loom_model {
use super::*;
use alloc::vec::Vec;
#[test]
fn loom_quota_three_threads() {
let mut model = loom::model::Builder::new();
model.preemption_bound = Some(3);
model.check(|| {
let q = SpillQuota::new(10);
let handles: Vec<_> = [4u64, 5, 6]
.into_iter()
.map(|n| {
let q = q.clone();
loom::thread::spawn(move || {
if q.reserve(n) {
assert!(q.in_use() <= 10);
q.release(n);
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
assert_eq!(q.in_use(), 0, "a release was lost");
});
}
}