use std::cell::Cell;
thread_local! {
static FUEL: Cell<u64> = const { Cell::new(u64::MAX) };
static EXHAUSTED: Cell<bool> = const { Cell::new(false) };
static USED: Cell<u64> = const { Cell::new(0) };
static REDUCTION_LIMIT_HIT: Cell<bool> = const { Cell::new(false) };
static REDUCTION_CAP: Cell<bool> = const { Cell::new(true) };
static TERM_DEPTH_LIMIT: Cell<u32> = const { Cell::new(u32::MAX) };
static TERM_DEPTH_HIT: Cell<bool> = const { Cell::new(false) };
static NESTING: Nesting = const {
Nesting {
limit: Cell::new(u32::MAX),
current: Cell::new(0),
}
};
}
struct Nesting {
limit: Cell<u32>,
current: Cell<u32>,
}
pub fn set_budget(budget: Option<u64>) {
FUEL.with(|f| f.set(budget.unwrap_or(u64::MAX)));
EXHAUSTED.with(|e| e.set(false));
USED.with(|u| u.set(0));
REDUCTION_LIMIT_HIT.with(|l| l.set(false));
TERM_DEPTH_HIT.with(|h| h.set(false));
REDUCTION_CAP.with(|c| c.set(true));
}
#[inline]
#[must_use]
pub fn is_metered() -> bool {
FUEL.with(|f| f.get() != u64::MAX) || crate::deadline::is_armed()
}
#[inline]
pub fn set_reduction_cap(enabled: bool) {
REDUCTION_CAP.with(|c| c.set(enabled));
}
#[inline]
#[must_use]
pub fn reduction_cap_applies() -> bool {
REDUCTION_CAP.with(|c| c.get()) || !is_metered()
}
#[inline]
pub fn set_reduction_nesting_limit(limit: Option<u32>) {
NESTING.with(|n| n.limit.set(limit.unwrap_or(u32::MAX)));
}
#[inline]
#[must_use]
pub fn reduction_nesting_limit() -> Option<u32> {
let limit = NESTING.with(|n| n.limit.get());
(limit != u32::MAX).then_some(limit)
}
pub(crate) struct NestedReduction(());
impl NestedReduction {
#[inline]
#[must_use]
pub(crate) fn enter() -> Option<Self> {
NESTING.with(|n| {
let current = n.current.get();
if current >= n.limit.get() {
return None;
}
n.current.set(current + 1);
Some(Self(()))
})
}
}
impl Drop for NestedReduction {
#[inline]
fn drop(&mut self) {
NESTING.with(|n| n.current.set(n.current.get().saturating_sub(1)));
}
}
pub const TERM_DEPTH_TRACKED: bool = cfg!(feature = "term-depth");
#[inline]
pub fn set_term_depth_limit(limit: Option<u32>) {
TERM_DEPTH_LIMIT.with(|l| l.set(limit.unwrap_or(u32::MAX)));
}
#[inline]
#[must_use]
pub fn term_depth_limit() -> Option<u32> {
let limit = TERM_DEPTH_LIMIT.with(|l| l.get());
(limit != u32::MAX).then_some(limit)
}
#[inline]
#[must_use]
pub fn term_depth_limit_hit() -> bool {
TERM_DEPTH_HIT.with(|h| h.get())
}
#[cfg(feature = "term-depth")]
#[inline]
pub(crate) fn note_term_depth(depth: u32) {
if depth > TERM_DEPTH_LIMIT.with(|l| l.get()) {
TERM_DEPTH_HIT.with(|h| h.set(true));
EXHAUSTED.with(|e| e.set(true));
}
}
#[inline]
pub fn note_reduction_limit() {
REDUCTION_LIMIT_HIT.with(|l| l.set(true));
}
#[inline]
pub fn clear_reduction_limit() {
REDUCTION_LIMIT_HIT.with(|l| l.set(false));
}
#[inline]
#[must_use]
pub fn reduction_limit_hit() -> bool {
REDUCTION_LIMIT_HIT.with(|l| l.get())
}
#[inline]
pub fn charge(n: u64) {
USED.with(|u| u.set(u.get().saturating_add(n)));
FUEL.with(|f| {
let cur = f.get();
if cur == u64::MAX {
return; }
if cur >= n {
f.set(cur - n);
if cur == n {
EXHAUSTED.with(|e| e.set(true));
}
} else {
f.set(0);
EXHAUSTED.with(|e| e.set(true));
}
});
}
#[inline]
#[must_use]
pub fn is_exhausted() -> bool {
EXHAUSTED.with(|e| e.get()) || crate::deadline::is_expired()
}
#[must_use]
pub fn used() -> u64 {
USED.with(|u| u.get())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unlimited_never_exhausts_but_counts() {
set_budget(None);
charge(1_000_000);
assert!(!is_exhausted());
assert_eq!(used(), 1_000_000);
set_budget(None);
assert_eq!(used(), 0);
}
#[test]
fn budget_latches_on_exhaustion() {
set_budget(Some(10));
charge(9);
assert!(!is_exhausted());
charge(1);
assert!(is_exhausted(), "hitting exactly zero must latch");
charge(0);
assert!(is_exhausted(), "latch must persist");
set_budget(Some(10));
assert!(!is_exhausted(), "set_budget clears the latch");
}
#[test]
fn reduction_limit_latch_is_independent_of_exhaustion() {
set_budget(None);
assert!(!reduction_limit_hit());
note_reduction_limit();
assert!(reduction_limit_hit(), "the latch must persist");
assert!(
!is_exhausted(),
"a reduction-limit hit must not degrade the kernel"
);
set_budget(Some(10));
assert!(!reduction_limit_hit(), "set_budget clears the latch");
note_reduction_limit();
clear_reduction_limit();
assert!(!reduction_limit_hit(), "clear_reduction_limit clears it");
assert!(is_metered(), "clearing the latch leaves the budget armed");
set_budget(None);
}
#[test]
fn reduction_cap_is_lifted_only_under_a_budget_and_restored_by_set_budget() {
set_budget(None);
crate::deadline::set_deadline(None);
assert!(reduction_cap_applies(), "on by default");
set_reduction_cap(false);
assert!(
reduction_cap_applies(),
"with no budget armed the cap cannot be lifted"
);
set_budget(Some(100));
assert!(reduction_cap_applies(), "set_budget puts the cap back on");
set_reduction_cap(false);
assert!(!reduction_cap_applies(), "lifted under a budget");
set_reduction_cap(true);
assert!(reduction_cap_applies());
set_reduction_cap(false);
set_budget(None);
assert!(reduction_cap_applies());
}
#[test]
fn metered_means_a_finite_budget_or_an_armed_deadline() {
set_budget(None);
crate::deadline::set_deadline(None);
assert!(!is_metered());
set_budget(Some(1));
assert!(is_metered());
charge(5);
assert!(is_metered(), "a spent budget is still a budget");
set_budget(None);
assert!(!is_metered());
crate::deadline::set_deadline(Some(core::time::Duration::from_secs(3600)));
assert!(is_metered());
crate::deadline::set_deadline(None);
assert!(!is_metered());
}
#[test]
fn overshoot_saturates() {
set_budget(Some(5));
charge(100);
assert!(is_exhausted());
assert_eq!(used(), 100);
set_budget(None);
}
#[test]
fn nested_reductions_are_counted_against_the_limit_and_released_when_left() {
assert_eq!(reduction_nesting_limit(), None, "no limit by default");
set_reduction_nesting_limit(Some(2));
assert_eq!(reduction_nesting_limit(), Some(2));
let first = NestedReduction::enter();
let second = NestedReduction::enter();
assert!(first.is_some() && second.is_some());
assert!(
NestedReduction::enter().is_none(),
"a third nested reduction is over a limit of two"
);
assert!(NestedReduction::enter().is_none());
drop(second);
let again = NestedReduction::enter();
assert!(again.is_some(), "leaving one makes room for one");
drop(again);
drop(first);
set_reduction_nesting_limit(Some(0));
assert!(NestedReduction::enter().is_none());
set_reduction_nesting_limit(None);
let many: Vec<Option<NestedReduction>> =
(0..10_000).map(|_| NestedReduction::enter()).collect();
assert!(many.iter().all(Option::is_some));
drop(many);
set_reduction_nesting_limit(Some(1));
let only = NestedReduction::enter();
assert!(only.is_some() && NestedReduction::enter().is_none());
drop(only);
set_reduction_nesting_limit(None);
}
#[test]
fn a_nested_reduction_left_by_unwinding_is_not_counted_any_more() {
set_reduction_nesting_limit(Some(1));
let unwound = std::panic::catch_unwind(|| {
let _nested = NestedReduction::enter();
assert!(NestedReduction::enter().is_none());
std::panic::resume_unwind(Box::new("unwinding out of a reduction"));
});
assert!(unwound.is_err());
let nested = NestedReduction::enter();
assert!(nested.is_some(), "the unwound reduction no longer counts");
drop(nested);
set_reduction_nesting_limit(None);
}
#[test]
fn the_limits_are_the_threads_and_a_new_budget_leaves_them() {
set_reduction_nesting_limit(Some(7));
set_term_depth_limit(Some(9));
set_budget(Some(100));
set_budget(None);
assert_eq!(reduction_nesting_limit(), Some(7));
assert_eq!(term_depth_limit(), Some(9));
let other = std::thread::spawn(|| (reduction_nesting_limit(), term_depth_limit()))
.join()
.unwrap_or_else(|_| panic!("the other thread panicked"));
assert_eq!(other, (None, None));
set_reduction_nesting_limit(None);
set_term_depth_limit(None);
assert_eq!(reduction_nesting_limit(), None);
assert_eq!(term_depth_limit(), None);
}
#[cfg(feature = "term-depth")]
#[test]
fn a_term_over_the_depth_limit_spends_the_budget_and_is_named() {
set_budget(Some(1_000));
note_term_depth(u32::MAX);
assert!(!term_depth_limit_hit() && !is_exhausted());
set_term_depth_limit(Some(4));
note_term_depth(4);
assert!(!term_depth_limit_hit() && !is_exhausted(), "at the limit");
note_term_depth(5);
assert!(term_depth_limit_hit(), "over the limit");
assert!(is_exhausted(), "which spends the budget");
set_budget(Some(1_000));
assert!(!term_depth_limit_hit() && !is_exhausted());
assert_eq!(term_depth_limit(), Some(4));
charge(2_000);
assert!(is_exhausted() && !term_depth_limit_hit());
set_budget(None);
set_term_depth_limit(None);
}
}