use std::collections::HashMap;
use std::collections::hash_map::RandomState;
use std::hash::BuildHasher;
use parking_lot::Mutex;
pub(crate) const MAX_NS_ENTRIES: usize = 100_000;
pub(crate) const MAX_NS_BYTES: usize = 64 << 20;
pub(crate) const MAX_TOTAL_ENTRIES: usize = 5_000_000;
pub(crate) const MAX_TOTAL_BYTES: usize = 1 << 30;
#[derive(Default, Clone, Copy)]
struct NsUsage {
entries: usize,
bytes: usize,
}
struct QuotaInner {
ns: HashMap<String, NsUsage>,
total_entries: usize,
total_bytes: usize,
}
const STRIPE_COUNT: usize = 64;
pub(crate) struct KvQuota {
inner: Mutex<QuotaInner>,
stripes: Box<[Mutex<()>]>,
hasher: RandomState,
}
impl KvQuota {
pub(crate) fn new() -> Self {
Self {
inner: Mutex::new(QuotaInner {
ns: HashMap::new(),
total_entries: 0,
total_bytes: 0,
}),
stripes: (0..STRIPE_COUNT).map(|_| Mutex::new(())).collect(),
hasher: RandomState::new(),
}
}
#[allow(clippy::indexing_slicing)]
fn stripe(&self, key: &[u8]) -> &Mutex<()> {
&self.stripes[(self.hasher.hash_one(key) as usize) & (STRIPE_COUNT - 1)]
}
#[cfg(test)]
pub(crate) fn admit(&self, ns: &str, entries_delta: isize, bytes_delta: isize) -> bool {
self.charge_and_apply(ns, ns.as_bytes(), || (entries_delta, bytes_delta), || ())
.is_some()
}
#[cfg(test)]
pub(crate) fn same_stripe_for_test(&self, a: &[u8], b: &[u8]) -> bool {
std::ptr::eq(self.stripe(a), self.stripe(b))
}
#[cfg(test)]
pub(crate) fn release(&self, ns: &str, entries: usize, bytes: usize) {
self.admit(ns, -(entries as isize), -(bytes as isize));
}
#[cfg(test)]
pub(crate) fn usage_for_test(&self, ns: &str) -> (usize, usize) {
let g = self.inner.lock();
let u = g.ns.get(ns).copied().unwrap_or_default();
(u.entries, u.bytes)
}
pub(crate) fn charge_and_apply<T>(
&self,
ns: &str,
key: &[u8],
read_and_delta: impl FnOnce() -> (isize, isize),
apply: impl FnOnce() -> T,
) -> Option<T> {
let _key_guard = self.stripe(key).lock();
let (entries_delta, bytes_delta) = read_and_delta();
{
let mut g = self.inner.lock();
let cur = g.ns.get(ns).copied().unwrap_or_default();
let new_ns_entries = cur.entries as isize + entries_delta;
let new_ns_bytes = cur.bytes as isize + bytes_delta;
let new_total_entries = g.total_entries as isize + entries_delta;
let new_total_bytes = g.total_bytes as isize + bytes_delta;
if (entries_delta > 0
&& (new_ns_entries as usize > MAX_NS_ENTRIES
|| new_total_entries as usize > MAX_TOTAL_ENTRIES))
|| (bytes_delta > 0
&& (new_ns_bytes as usize > MAX_NS_BYTES
|| new_total_bytes as usize > MAX_TOTAL_BYTES))
{
return None;
}
let usage = match g.ns.get_mut(ns) {
Some(u) => u,
None => g.ns.entry(ns.to_string()).or_default(),
};
usage.entries = new_ns_entries.max(0) as usize;
usage.bytes = new_ns_bytes.max(0) as usize;
g.total_entries = new_total_entries.max(0) as usize;
g.total_bytes = new_total_bytes.max(0) as usize;
}
Some(apply())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn quota_admit_rejects_growth_past_caps_and_allows_shrink() {
let q = KvQuota::new();
assert!(q.admit("ns", 1, 100), "a small entry fits");
assert!(
!q.admit("ns", 1, MAX_NS_BYTES as isize),
"a value that would exceed the per-namespace byte cap is rejected"
);
assert!(
!q.admit("ns2", 1, MAX_TOTAL_BYTES as isize),
"a value that would exceed the host-wide byte cap is rejected"
);
q.release("ns", 1, 100);
assert!(
q.admit("ns", 1, 100),
"freed budget is reusable after a release"
);
}
#[test]
fn a_stalled_apply_on_one_key_does_not_block_other_keys() {
use std::sync::{Arc, Barrier, mpsc};
use std::thread;
use std::time::Duration;
let q = Arc::new(KvQuota::new());
let stalled_key = b"key-a".to_vec();
let other_key = (0..)
.map(|i| format!("key-b{i}").into_bytes())
.find(|k| !q.same_stripe_for_test(&stalled_key, k))
.unwrap();
let entered = Arc::new(Barrier::new(2));
let release = Arc::new(Barrier::new(2));
let stalled = {
let q = q.clone();
let entered = entered.clone();
let release = release.clone();
thread::spawn(move || {
q.charge_and_apply(
"ns-a",
&stalled_key,
|| (1, 100),
|| {
entered.wait();
release.wait();
},
);
})
};
entered.wait();
let (tx, rx) = mpsc::channel();
let other = {
let q = q.clone();
thread::spawn(move || {
q.charge_and_apply("ns-b", &other_key, || (1, 100), || ());
let _ = tx.send(());
})
};
let completed = rx.recv_timeout(Duration::from_secs(2));
release.wait();
stalled.join().unwrap();
other.join().unwrap();
assert!(
completed.is_ok(),
"a stalled backend write on one key must not block an unrelated key's call"
);
}
}