use std::cell::{Cell, RefCell};
use super::{ResourceDimension, ResourceLimits};
#[derive(Debug)]
struct Target {
dimension: ResourceDimension,
operation: String,
skip: usize,
peak: u64,
}
#[derive(Debug)]
struct Recording {
dimension: ResourceDimension,
operation: String,
charged: u64,
}
thread_local! {
static TARGET: RefCell<Option<Target>> = const { RefCell::new(None) };
static RECORDING: RefCell<Option<Recording>> = const { RefCell::new(None) };
static WORK: Cell<Option<u64>> = const { Cell::new(None) };
}
#[derive(Debug)]
struct RestoreTarget(Option<Target>);
impl Drop for RestoreTarget {
fn drop(&mut self) {
TARGET.with(|target| *target.borrow_mut() = self.0.take());
}
}
pub fn at_charge<T>(
dimension: ResourceDimension,
operation: &str,
skip: usize,
run: impl FnOnce() -> T,
) -> T {
with_target(
Some(Target {
dimension,
operation: operation.to_owned(),
skip,
peak: 0,
}),
run,
)
}
pub fn charged_at<T>(
dimension: ResourceDimension,
operation: &str,
run: impl FnOnce() -> T,
) -> (T, u64) {
let previous = RECORDING.with(|recording| {
recording.replace(Some(Recording {
dimension,
operation: operation.to_owned(),
charged: 0,
}))
});
let guard = RestoreRecording(previous);
let result = run();
let charged = RECORDING.with(|recording| {
recording
.borrow()
.as_ref()
.map_or(0, |recording| recording.charged)
});
drop(guard);
(result, charged)
}
#[derive(Debug)]
struct RestoreRecording(Option<Recording>);
impl Drop for RestoreRecording {
fn drop(&mut self) {
RECORDING.with(|recording| *recording.borrow_mut() = self.0.take());
}
}
pub(super) fn record_charge(dimension: ResourceDimension, operation: &str, amount: u64) {
RECORDING.with(|recording| {
if let Some(recording) = recording.borrow_mut().as_mut() {
if recording.dimension == dimension && recording.operation == operation {
if let Some(total) = recording.charged.checked_add(amount) {
recording.charged = total;
}
}
}
});
}
pub fn work_charged<T>(run: impl FnOnce() -> T) -> (T, u64) {
let outer = WORK.with(|work| work.replace(Some(0)));
let guard = RestoreWork(outer);
let result = run();
let charged = WORK.with(|work| work.get().unwrap_or(0));
drop(guard);
(result, charged)
}
#[derive(Debug)]
struct RestoreWork(Option<u64>);
impl Drop for RestoreWork {
fn drop(&mut self) {
let inner = WORK.with(|work| work.get().unwrap_or(0));
let outer = self.0.map(|outer| outer.saturating_add(inner));
WORK.with(|work| work.set(outer));
}
}
pub(super) fn record_work(work: usize) {
WORK.with(|recorded| {
if let Some(total) = recorded.get() {
let work = u64::try_from(work).unwrap_or(u64::MAX);
recorded.set(Some(total.saturating_add(work)));
}
});
}
pub fn without_probe<T>(run: impl FnOnce() -> T) -> T {
with_target(None, run)
}
fn with_target<T>(replacement: Option<Target>, run: impl FnOnce() -> T) -> T {
let previous = TARGET.with(|target| target.replace(replacement));
let guard = RestoreTarget(previous);
let result = run();
drop(guard);
result
}
pub(super) fn charge_limit(
limits: &ResourceLimits,
dimension: ResourceDimension,
operation: &str,
used: u64,
additional: u64,
) -> Option<u64> {
if additional == 0 {
return None;
}
TARGET.with(|target| {
let mut target = target.borrow_mut();
let target = target.as_mut()?;
if target.dimension != dimension {
return None;
}
let ceiling = match dimension {
ResourceDimension::InputBytes => limits.max_input_bytes,
ResourceDimension::DecompressedBytes => limits.max_decompressed_bytes_total,
ResourceDimension::RetainedBytes => limits.max_retained_bytes,
ResourceDimension::Entities => limits.max_entities,
ResourceDimension::RecursionDepth => limits.max_recursion_depth,
};
if ceiling != u64::MAX {
return None;
}
let need = used.checked_add(additional)?;
if need <= target.peak {
return None;
}
target.peak = need;
if target.operation != operation {
return None;
}
if target.skip > 0 {
target.skip -= 1;
return None;
}
need.checked_sub(1)
})
}
#[cfg(test)]
mod tests {
use super::{at_charge, charged_at, without_probe, work_charged};
use crate::decode::budget::DecodeBudget;
use crate::decode::{DecodePolicy, ResourceDimension, ResourceLimit, WorkBudget};
fn entity_probe_policy() -> DecodePolicy {
let mut policy = DecodePolicy::desktop();
policy.limits.max_entities = u64::MAX;
policy
}
fn run(policy: DecodePolicy) -> Result<(), ResourceLimit> {
let limit = |error| match error {
crate::CodecError::ResourceLimit(limit) => limit,
other => panic!("expected a resource refusal: {other:?}"),
};
let budget = DecodeBudget::new(policy, 0);
budget.charge_entities(2, "before").map_err(limit)?;
budget.charge_entities(0, "target").map_err(limit)?;
budget.charge_entities(2, "target").map_err(limit)
}
#[test]
fn probe_matches_normal_policy_refusal() {
let found = at_charge(ResourceDimension::Entities, "target", 0, || {
run(entity_probe_policy())
})
.expect_err("target is probed");
assert_eq!(
(found.operation, found.used, found.additional),
("target", 2, 2)
);
let mut policy = DecodePolicy::desktop();
policy.limits.max_entities = found.used + found.additional - 1;
assert_eq!(run(policy), Err(found));
assert!(run(entity_probe_policy()).is_ok());
}
#[test]
fn probes_are_suspended_and_restored() {
at_charge(ResourceDimension::Entities, "target", 0, || {
assert!(without_probe(|| run(entity_probe_policy())).is_ok());
assert!(run(entity_probe_policy()).is_err());
});
assert!(run(entity_probe_policy()).is_ok());
}
#[test]
fn work_recording_counts_each_charge_once() {
let root = WorkBudget::new(10);
let ((), outer) = work_charged(|| {
let child = root.charging_slice(5);
let ((), inner) = work_charged(|| assert!(child.charge_by(2)));
assert_eq!(inner, 2);
let detached = root.child_slice(3);
assert!(detached.charge_by(3));
assert!(root.consume_child(&detached).is_ok());
assert!(!child.charge_by(4));
});
assert_eq!(outer, 5);
assert_eq!(root.consumed(), 5);
}
#[test]
fn recording_sums_matching_charges() {
let (result, charged) = charged_at(ResourceDimension::Entities, "target", || {
run(entity_probe_policy())
});
assert_eq!(result, Ok(()));
assert_eq!(charged, 2);
}
}