use std::cell::Cell;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Condvar, Mutex, MutexGuard, OnceLock, PoisonError, TryLockError};
use std::time::{Duration, Instant};
use crate::config::MaxFootprint;
const BYTES_PER_MB: u64 = 1024 * 1024;
const DEFAULT_POLL_INTERVAL: Duration = Duration::from_millis(100);
const DEFAULT_MAX_WAIT: Duration = Duration::from_secs(5);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AdmitOutcome {
Disabled,
Unavailable,
Clear,
Throttled,
WaitedOut,
Serialized,
}
pub struct AdmissionState {
overshoot: Mutex<()>,
in_flight: AtomicUsize,
}
impl AdmissionState {
const fn new() -> Self {
Self {
overshoot: Mutex::new(()),
in_flight: AtomicUsize::new(0),
}
}
}
static GLOBAL_ADMISSIONS: AdmissionState = AdmissionState::new();
thread_local! {
static HOLDS_OVERSHOOT: Cell<usize> = const { Cell::new(0) };
}
#[must_use = "the admission must be held while the admitted work runs"]
pub struct Admission {
outcome: AdmitOutcome,
state: &'static AdmissionState,
token: Option<MutexGuard<'static, ()>>,
}
impl Admission {
fn new(state: &'static AdmissionState, outcome: AdmitOutcome, token: Option<MutexGuard<'static, ()>>) -> Self {
state.in_flight.fetch_add(1, Ordering::AcqRel);
Self { outcome, state, token }
}
pub fn outcome(&self) -> AdmitOutcome {
self.outcome
}
}
impl Drop for Admission {
fn drop(&mut self) {
self.state.in_flight.fetch_sub(1, Ordering::AcqRel);
if self.token.take().is_some() {
HOLDS_OVERSHOOT.with(|held| held.set(0));
}
}
}
pub struct FootprintGate<S = fn() -> Option<u64>>
where
S: Fn() -> Option<u64>,
{
limit_bytes: u64,
sampler: S,
poll_interval: Duration,
max_wait: Duration,
admissions: &'static AdmissionState,
}
impl FootprintGate {
pub fn new(setting: MaxFootprint) -> Self {
FootprintGate::with_sampler(setting.resolve_mb(), crate::sysres::phys_footprint)
}
}
impl<S> FootprintGate<S>
where
S: Fn() -> Option<u64>,
{
pub fn with_sampler(max_footprint_mb: usize, sampler: S) -> Self {
Self {
limit_bytes: (max_footprint_mb as u64).saturating_mul(BYTES_PER_MB),
sampler,
poll_interval: DEFAULT_POLL_INTERVAL,
max_wait: DEFAULT_MAX_WAIT,
admissions: &GLOBAL_ADMISSIONS,
}
}
#[cfg(test)]
pub fn with_admissions(mut self, state: &'static AdmissionState) -> Self {
self.admissions = state;
self
}
pub fn exceeds_budget(&self, estimated_bytes: u64) -> bool {
self.limit_bytes != 0 && estimated_bytes > self.limit_bytes
}
pub fn limit_mb(&self) -> u64 {
self.limit_bytes / BYTES_PER_MB
}
#[cfg(test)]
pub fn with_timing(mut self, poll_interval: Duration, max_wait: Duration) -> Self {
self.poll_interval = poll_interval;
self.max_wait = max_wait;
self
}
pub fn admit(&self) -> AdmitOutcome {
if self.limit_bytes == 0 {
return AdmitOutcome::Disabled;
}
let start = Instant::now();
let mut parked = false;
loop {
match (self.sampler)() {
None => return AdmitOutcome::Unavailable,
Some(footprint) if footprint <= self.limit_bytes => {
return if parked {
AdmitOutcome::Throttled
} else {
AdmitOutcome::Clear
};
}
Some(_) => {
let elapsed = start.elapsed();
if elapsed >= self.max_wait {
tracing::warn!(
limit_mb = self.limit_bytes / BYTES_PER_MB,
waited_ms = elapsed.as_millis() as u64,
"footprint gate over ceiling for max_wait; admitting to guarantee progress"
);
return AdmitOutcome::WaitedOut;
}
parked = true;
std::thread::sleep(self.poll_interval);
}
}
}
}
}
impl<S> FootprintGate<S>
where
S: Fn() -> Option<u64>,
{
pub fn admit_exclusive(&self) -> Admission {
let state = self.admissions;
if self.limit_bytes == 0 {
return Admission::new(state, AdmitOutcome::Disabled, None);
}
let identity = std::ptr::from_ref(state) as usize;
if HOLDS_OVERSHOOT.with(Cell::get) == identity {
return Admission::new(state, AdmitOutcome::Serialized, None);
}
let start = Instant::now();
let mut parked = false;
let mut token: Option<MutexGuard<'static, ()>> = None;
loop {
match (self.sampler)() {
None => return Admission::new(state, AdmitOutcome::Unavailable, None),
Some(footprint) if footprint <= self.limit_bytes => {
let outcome = if parked {
AdmitOutcome::Throttled
} else {
AdmitOutcome::Clear
};
return Admission::new(state, outcome, None);
}
Some(_) => {
if token.is_none() {
token = match state.overshoot.try_lock() {
Ok(guard) => Some(guard),
Err(TryLockError::Poisoned(poisoned)) => Some(poisoned.into_inner()),
Err(TryLockError::WouldBlock) => None,
};
}
if token.is_some() {
let idle = state.in_flight.load(Ordering::Acquire) == 0;
let elapsed = start.elapsed();
if idle || elapsed >= self.max_wait {
if !idle {
tracing::warn!(
limit_mb = self.limit_bytes / BYTES_PER_MB,
waited_ms = elapsed.as_millis() as u64,
"footprint gate over ceiling for max_wait; admitting one unit of work alone"
);
}
HOLDS_OVERSHOOT.with(|held| held.set(identity));
return Admission::new(state, AdmitOutcome::Serialized, token);
}
}
parked = true;
std::thread::sleep(self.poll_interval);
}
}
}
}
}
struct DocSemaphore {
available: Mutex<usize>,
released: Condvar,
}
fn doc_semaphore(max_concurrent: usize) -> Option<&'static DocSemaphore> {
if max_concurrent == 0 {
return None;
}
static SEMAPHORE: OnceLock<DocSemaphore> = OnceLock::new();
Some(SEMAPHORE.get_or_init(|| DocSemaphore {
available: Mutex::new(max_concurrent),
released: Condvar::new(),
}))
}
pub struct DocSlot {
semaphore: &'static DocSemaphore,
}
impl Drop for DocSlot {
fn drop(&mut self) {
let mut available = self.semaphore.available.lock().unwrap_or_else(PoisonError::into_inner);
*available += 1;
self.semaphore.released.notify_one();
}
}
pub fn acquire_doc_slot(max_concurrent: usize) -> Option<DocSlot> {
let semaphore = doc_semaphore(max_concurrent)?;
let mut available = semaphore.available.lock().unwrap_or_else(PoisonError::into_inner);
while *available == 0 {
available = semaphore
.released
.wait(available)
.unwrap_or_else(PoisonError::into_inner);
}
*available -= 1;
drop(available);
Some(DocSlot { semaphore })
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicUsize, Ordering};
use super::*;
const MB: u64 = 1024 * 1024;
#[test]
fn disabled_gate_admits_without_sampling() {
let polled = AtomicUsize::new(0);
let gate = FootprintGate::with_sampler(0, || {
polled.fetch_add(1, Ordering::SeqCst);
Some(u64::MAX)
});
assert_eq!(gate.admit(), AdmitOutcome::Disabled);
assert_eq!(polled.load(Ordering::SeqCst), 0, "disabled gate must not sample");
}
#[test]
fn unavailable_sample_admits_without_throttling() {
let gate = FootprintGate::with_sampler(100, || None);
assert_eq!(gate.admit(), AdmitOutcome::Unavailable);
}
#[test]
fn under_ceiling_admits_without_waiting() {
let gate = FootprintGate::with_sampler(100, || Some(10 * MB));
assert_eq!(gate.admit(), AdmitOutcome::Clear);
}
#[test]
fn over_then_under_parks_until_clear() {
let calls = AtomicUsize::new(0);
let gate = FootprintGate::with_sampler(200, || {
let n = calls.fetch_add(1, Ordering::SeqCst);
if n < 2 { Some(500 * MB) } else { Some(50 * MB) }
})
.with_timing(Duration::from_millis(1), Duration::from_secs(5));
assert_eq!(gate.admit(), AdmitOutcome::Throttled);
assert!(
calls.load(Ordering::SeqCst) >= 3,
"gate must re-sample until the footprint falls under the ceiling"
);
}
#[test]
fn the_document_semaphore_bounds_concurrent_extractions() {
const LIMIT: usize = 2;
let live = AtomicUsize::new(0);
let peak = AtomicUsize::new(0);
std::thread::scope(|scope| {
for _ in 0..8 {
scope.spawn(|| {
let _slot = acquire_doc_slot(LIMIT).expect("a positive limit must yield a real slot");
peak.fetch_max(live.fetch_add(1, Ordering::SeqCst) + 1, Ordering::SeqCst);
std::thread::sleep(Duration::from_millis(5));
live.fetch_sub(1, Ordering::SeqCst);
});
}
});
let peak = peak.load(Ordering::SeqCst);
assert!(peak >= 1, "every worker must eventually get a slot");
assert!(peak <= LIMIT, "at most {LIMIT} extractions may overlap, saw {peak}");
assert_eq!(live.load(Ordering::SeqCst), 0, "every slot must be released on drop");
}
#[test]
fn unbounded_callers_do_not_latch_the_semaphore() {
assert!(
acquire_doc_slot(0).is_none(),
"an auto limit yields no slot and must cost nothing"
);
assert!(
acquire_doc_slot(0).is_none(),
"repeating it must not latch a decision either"
);
assert!(
acquire_doc_slot(1).is_some(),
"a later bounded caller must still get a real semaphore"
);
}
fn isolated_state() -> &'static AdmissionState {
Box::leak(Box::new(AdmissionState::new()))
}
fn over_gate(state: &'static AdmissionState, max_wait: Duration) -> FootprintGate<impl Fn() -> Option<u64>> {
FootprintGate::with_sampler(100, || Some(500 * MB))
.with_timing(Duration::from_millis(1), max_wait)
.with_admissions(state)
}
#[test]
fn over_the_ceiling_the_gate_serialises_instead_of_opening_the_floodgates() {
let state = isolated_state();
let live = AtomicUsize::new(0);
let peak = AtomicUsize::new(0);
let admitted = AtomicUsize::new(0);
std::thread::scope(|scope| {
for _ in 0..8 {
scope.spawn(|| {
let gate = over_gate(state, Duration::from_millis(5));
let admission = gate.admit_exclusive();
assert_eq!(admission.outcome(), AdmitOutcome::Serialized);
peak.fetch_max(live.fetch_add(1, Ordering::SeqCst) + 1, Ordering::SeqCst);
std::thread::sleep(Duration::from_millis(15));
live.fetch_sub(1, Ordering::SeqCst);
admitted.fetch_add(1, Ordering::SeqCst);
drop(admission);
});
}
});
assert_eq!(admitted.load(Ordering::SeqCst), 8, "serialising must not starve anyone");
assert_eq!(
peak.load(Ordering::SeqCst),
1,
"more than one worker was inside while the process was over the ceiling"
);
assert_eq!(
state.in_flight.load(Ordering::SeqCst),
0,
"every admission must release"
);
}
#[test]
fn the_advisory_admit_still_admits_every_waiter_which_is_why_documents_do_not_use_it() {
let peak = AtomicUsize::new(0);
let live = AtomicUsize::new(0);
std::thread::scope(|scope| {
for _ in 0..4 {
scope.spawn(|| {
let gate = FootprintGate::with_sampler(100, || Some(500 * MB))
.with_timing(Duration::from_millis(1), Duration::from_millis(5));
assert_eq!(gate.admit(), AdmitOutcome::WaitedOut);
peak.fetch_max(live.fetch_add(1, Ordering::SeqCst) + 1, Ordering::SeqCst);
std::thread::sleep(Duration::from_millis(30));
live.fetch_sub(1, Ordering::SeqCst);
});
}
});
assert!(peak.load(Ordering::SeqCst) > 1, "advisory admission overlaps by design");
}
#[test]
fn an_idle_process_over_the_ceiling_is_admitted_without_waiting() {
let gate = over_gate(isolated_state(), Duration::from_secs(30));
let start = Instant::now();
let admission = gate.admit_exclusive();
assert_eq!(admission.outcome(), AdmitOutcome::Serialized);
assert!(
start.elapsed() < Duration::from_secs(5),
"nothing is running, so there is nothing to wait for (took {:?})",
start.elapsed()
);
}
#[test]
fn with_other_work_in_flight_the_gate_waits_max_wait_then_proceeds_alone() {
let state = isolated_state();
let under = FootprintGate::with_sampler(100, || Some(10 * MB)).with_admissions(state);
let running = under.admit_exclusive();
assert_eq!(running.outcome(), AdmitOutcome::Clear);
let gate = over_gate(state, Duration::from_millis(40));
let start = Instant::now();
let admission = gate.admit_exclusive();
assert_eq!(admission.outcome(), AdmitOutcome::Serialized);
assert!(
start.elapsed() >= Duration::from_millis(40),
"must give in-flight work its max_wait"
);
drop((admission, running));
assert_eq!(state.in_flight.load(Ordering::SeqCst), 0);
}
#[test]
fn waiters_resume_in_parallel_once_the_footprint_drops() {
let state = isolated_state();
let over = std::sync::atomic::AtomicBool::new(true);
let gate = || {
FootprintGate::with_sampler(100, || {
Some(if over.load(Ordering::SeqCst) { 500 * MB } else { 10 * MB })
})
.with_timing(Duration::from_millis(1), Duration::from_secs(30))
.with_admissions(state)
};
let holder = gate().admit_exclusive();
assert_eq!(holder.outcome(), AdmitOutcome::Serialized);
std::thread::scope(|scope| {
let waiter = scope.spawn(|| gate().admit_exclusive().outcome());
std::thread::sleep(Duration::from_millis(20));
assert!(
!waiter.is_finished(),
"a second worker must wait while over the ceiling"
);
over.store(false, Ordering::SeqCst);
let outcome = waiter.join().expect("waiter");
assert_eq!(outcome, AdmitOutcome::Throttled, "admitted without needing the token");
});
drop(holder);
}
#[test]
fn a_nested_admission_on_the_token_holding_thread_does_not_deadlock() {
let state = isolated_state();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let outer = over_gate(state, Duration::from_millis(5)).admit_exclusive();
let inner = over_gate(state, Duration::from_millis(5)).admit_exclusive();
tx.send((outer.outcome(), inner.outcome())).ok();
});
let outcomes = rx
.recv_timeout(Duration::from_secs(10))
.expect("nested admission deadlocked on its own token");
assert_eq!(outcomes, (AdmitOutcome::Serialized, AdmitOutcome::Serialized));
}
#[test]
fn a_unit_of_work_larger_than_the_whole_ceiling_is_over_budget() {
let gate = FootprintGate::with_sampler(100, || Some(0));
assert!(!gate.exceeds_budget(100 * MB), "exactly the ceiling still fits");
assert!(gate.exceeds_budget(100 * MB + 1));
let disabled = FootprintGate::with_sampler(0, || Some(0));
assert!(!disabled.exceeds_budget(u64::MAX), "no ceiling, no verdict");
}
#[test]
fn persistent_over_waits_out_then_admits() {
let gate = FootprintGate::with_sampler(100, || Some(u64::MAX))
.with_timing(Duration::from_millis(1), Duration::from_millis(20));
let start = Instant::now();
assert_eq!(gate.admit(), AdmitOutcome::WaitedOut);
assert!(
start.elapsed() >= Duration::from_millis(20),
"gate must park the full max_wait before giving up"
);
}
}