use std::sync::atomic::{AtomicU64, Ordering};
pub type MicroDollar = u64;
pub struct BudgetTracker {
limit: MicroDollar,
used: AtomicU64,
}
impl BudgetTracker {
pub fn new(limit_usd: f64) -> Self {
Self {
limit: usd_to_micro(limit_usd),
used: AtomicU64::new(0),
}
}
pub fn try_reserve(&self, estimated: MicroDollar) -> bool {
loop {
let cur = self.used.load(Ordering::Acquire);
if cur + estimated > self.limit {
return false;
}
match self.used.compare_exchange_weak(
cur,
cur + estimated,
Ordering::AcqRel,
Ordering::Relaxed,
) {
Ok(_) => return true,
Err(_) => std::hint::spin_loop(),
}
}
}
pub fn settle(&self, estimated: MicroDollar, actual: MicroDollar) {
if actual > estimated {
self.used.fetch_add(actual - estimated, Ordering::Relaxed);
} else {
self.used.fetch_sub(estimated - actual, Ordering::Relaxed);
}
}
pub fn used_usd(&self) -> f64 {
micro_to_usd(self.used.load(Ordering::Relaxed))
}
pub fn limit_usd(&self) -> f64 {
micro_to_usd(self.limit)
}
pub fn remaining_usd(&self) -> f64 {
let used = self.used.load(Ordering::Relaxed);
micro_to_usd(self.limit.saturating_sub(used))
}
}
#[inline]
pub fn usd_to_micro(usd: f64) -> MicroDollar {
(usd * 1_000_000.0) as u64
}
#[inline]
pub fn micro_to_usd(micro: MicroDollar) -> f64 {
micro as f64 / 1_000_000.0
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reserve_and_settle() {
let tracker = BudgetTracker::new(10.0);
let est = usd_to_micro(3.0);
assert!(tracker.try_reserve(est));
assert!((tracker.used_usd() - 3.0).abs() < 0.001);
let actual = usd_to_micro(1.5);
tracker.settle(est, actual);
assert!((tracker.used_usd() - 1.5).abs() < 0.001);
}
#[test]
fn budget_exceeded() {
let tracker = BudgetTracker::new(1.0);
let est = usd_to_micro(2.0);
assert!(!tracker.try_reserve(est));
assert!((tracker.used_usd() - 0.0).abs() < 0.001);
}
#[test]
fn full_refund_on_error() {
let tracker = BudgetTracker::new(10.0);
let est = usd_to_micro(5.0);
assert!(tracker.try_reserve(est));
tracker.settle(est, 0); assert!((tracker.used_usd() - 0.0).abs() < 0.001);
}
}