use std::cell::RefCell;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Debug)]
pub struct GasMeter {
remaining: AtomicU64,
}
impl GasMeter {
pub fn new(credits: u64) -> Arc<Self> {
Arc::new(Self {
remaining: AtomicU64::new(credits),
})
}
pub fn remaining(&self) -> u64 {
self.remaining.load(Ordering::Relaxed)
}
pub fn charge(&self, cost: u64) -> bool {
self.remaining
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |remaining| {
remaining.checked_sub(cost)
})
.is_ok()
}
}
thread_local! {
static ACTIVE: RefCell<Vec<Arc<GasMeter>>> = const { RefCell::new(Vec::new()) };
static EXHAUSTED: RefCell<Vec<bool>> = const { RefCell::new(Vec::new()) };
}
#[must_use = "dropping GasGuard immediately uninstalls the active gas meter"]
pub struct GasGuard;
impl GasGuard {
pub fn install(meter: Arc<GasMeter>) -> Self {
ACTIVE.with(|active| active.borrow_mut().push(meter));
EXHAUSTED.with(|exhausted| exhausted.borrow_mut().push(false));
Self
}
}
impl Drop for GasGuard {
fn drop(&mut self) {
ACTIVE.with(|active| {
let mut active = active.borrow_mut();
active.pop();
});
EXHAUSTED.with(|exhausted| {
exhausted.borrow_mut().pop();
});
}
}
pub fn charge(cost: u64) -> bool {
if is_exhausted() {
return false;
}
let charged = ACTIVE.with(|active| {
let active = active.borrow();
if active.is_empty() {
return true;
}
if active.iter().any(|meter| meter.remaining() < cost) {
return false;
}
active.iter().all(|meter| meter.charge(cost))
});
if !charged {
ACTIVE.with(|active| {
let active = active.borrow();
EXHAUSTED.with(|exhausted| {
for (index, meter) in active.iter().enumerate() {
if meter.remaining() < cost {
exhausted.borrow_mut()[index] = true;
}
}
});
});
}
charged
}
pub fn is_exhausted() -> bool {
EXHAUSTED.with(|exhausted| exhausted.borrow().iter().any(|value| *value))
}
pub fn active_meters() -> Vec<Arc<GasMeter>> {
ACTIVE.with(|active| active.borrow().clone())
}
pub fn install_meters(meters: &[Arc<GasMeter>]) -> Vec<GasGuard> {
meters.iter().cloned().map(GasGuard::install).collect()
}
pub fn take_exhausted() -> bool {
EXHAUSTED.with(|exhausted| {
let mut exhausted = exhausted.borrow_mut();
let was_exhausted = exhausted.iter().any(|value| *value);
exhausted.fill(false);
was_exhausted
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scoped_meter_charges_without_partial_consumption() {
let meter = GasMeter::new(3);
let _guard = GasGuard::install(meter.clone());
assert!(charge(2));
assert!(!charge(2));
assert_eq!(meter.remaining(), 1);
}
#[test]
fn nested_meters_charge_outer_budget() {
let outer = GasMeter::new(3);
let _outer_guard = GasGuard::install(outer.clone());
let inner = GasMeter::new(2);
let _inner_guard = GasGuard::install(inner.clone());
assert!(charge(2));
assert_eq!(outer.remaining(), 1);
assert_eq!(inner.remaining(), 0);
assert!(!charge(1));
}
#[test]
fn inner_exhaustion_does_not_poison_healthy_outer_scope() {
let outer = GasMeter::new(10);
let _outer_guard = GasGuard::install(outer.clone());
{
let inner = GasMeter::new(0);
let _inner_guard = GasGuard::install(inner);
assert!(!charge(1));
assert!(is_exhausted());
}
assert!(!is_exhausted());
assert!(charge(1));
assert_eq!(outer.remaining(), 9);
}
}