use std::cell::RefCell;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OverBudget;
static ARMED_GUARDS: AtomicUsize = AtomicUsize::new(0);
thread_local! {
static ARMED: RefCell<Option<Arc<AtomicBool>>> = const { RefCell::new(None) };
}
#[inline]
pub fn check() {
if ARMED_GUARDS.load(Ordering::Relaxed) == 0 {
return;
}
let tripped = ARMED.with(|a| {
a.try_borrow()
.ok()
.and_then(|g| g.as_ref().map(|f| f.load(Ordering::Relaxed)))
.unwrap_or(false)
});
if tripped {
std::panic::resume_unwind(Box::new(OverBudget));
}
}
pub fn current_rss_bytes() -> Option<u64> {
let status = std::fs::read_to_string("/proc/self/status").ok()?;
let line = status.lines().find(|l| l.starts_with("VmRSS:"))?;
let kb: u64 = line.split_whitespace().nth(1)?.parse().ok()?;
Some(kb * 1024)
}
#[cfg(not(target_arch = "wasm32"))]
const WATCH_INTERVAL: std::time::Duration = std::time::Duration::from_millis(5);
#[cfg(not(target_arch = "wasm32"))]
pub struct QueryGuard {
tripped: Arc<AtomicBool>,
stop: Arc<AtomicBool>,
thread: Option<std::thread::JoinHandle<()>>,
}
#[cfg(not(target_arch = "wasm32"))]
impl QueryGuard {
pub fn start(ceiling_bytes: u64, read: fn() -> Option<u64>) -> Self {
let tripped = Arc::new(AtomicBool::new(false));
let stop = Arc::new(AtomicBool::new(false));
let thread = read().is_some().then(|| {
let (tripped, stop) = (tripped.clone(), stop.clone());
std::thread::spawn(move || {
while !stop.load(Ordering::Relaxed) {
if read().is_some_and(|rss| rss > ceiling_bytes) {
tripped.store(true, Ordering::Relaxed);
return;
}
std::thread::sleep(WATCH_INTERVAL);
}
})
});
ARMED.with(|a| *a.borrow_mut() = Some(tripped.clone()));
ARMED_GUARDS.fetch_add(1, Ordering::Relaxed);
QueryGuard {
tripped,
stop,
thread,
}
}
pub fn tripped(&self) -> bool {
self.tripped.load(Ordering::Relaxed)
}
pub fn trip(&self) {
self.tripped.store(true, Ordering::Relaxed);
}
}
#[cfg(not(target_arch = "wasm32"))]
impl Drop for QueryGuard {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
ARMED_GUARDS.fetch_sub(1, Ordering::Relaxed);
ARMED.with(|a| {
if let Ok(mut g) = a.try_borrow_mut() {
*g = None;
}
});
if let Some(t) = self.thread.take() {
let _ = t.join();
}
}
}
pub fn run_guarded<T>(f: impl FnOnce() -> T) -> Result<T, OverBudget> {
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
Ok(v) => Ok(v),
Err(payload) => match payload.downcast::<OverBudget>() {
Ok(_) => Err(OverBudget),
Err(other) => std::panic::resume_unwind(other),
},
}
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use super::*;
fn always_big() -> Option<u64> {
Some(u64::MAX)
}
fn always_small() -> Option<u64> {
Some(1)
}
#[test]
fn an_unarmed_thread_never_unwinds() {
check();
}
#[test]
fn a_crossed_ceiling_stops_the_next_check() {
let g = QueryGuard::start(1 << 20, always_big);
let r = run_guarded(|| loop {
check();
std::thread::sleep(std::time::Duration::from_millis(1));
});
assert_eq!(r, Err(OverBudget));
assert!(g.tripped());
}
#[test]
fn a_ceiling_never_crossed_lets_the_run_finish() {
let g = QueryGuard::start(1 << 20, always_small);
let r = run_guarded(|| {
for _ in 0..100 {
check();
}
7
});
assert_eq!(r, Ok(7));
assert!(!g.tripped());
}
#[test]
fn dropping_the_guard_disarms_the_thread() {
{
let g = QueryGuard::start(1, always_big);
g.trip();
}
check();
}
#[test]
fn other_panics_pass_through() {
let r = std::panic::catch_unwind(|| {
let _ = run_guarded(|| std::panic::resume_unwind(Box::new("boom")));
});
assert!(r.is_err());
}
}