use std::collections::HashMap;
use std::sync::atomic::{AtomicI64, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
#[derive(Clone, Debug, PartialEq)]
pub enum BudgetReservation {
Reserved {
remaining_microcents: i64,
},
Insufficient {
remaining_microcents: i64,
required_microcents: i64,
},
MissingTenant,
MissingReservation,
}
#[derive(Clone, Debug, PartialEq)]
pub enum BudgetSettlement {
Committed {
remaining_microcents: i64,
actual_microcents: i64,
},
Released {
remaining_microcents: i64,
returned_microcents: i64,
},
Overrun {
remaining_microcents: i64,
},
InvalidAmount,
MissingReservation,
MissingTenant,
}
#[derive(Debug)]
struct ReservationRecord {
tenant_id: Arc<str>,
reserved_microcents: i64,
}
#[inline]
fn debit_if_available(budget: &AtomicI64, amount: i64) -> Result<i64, i64> {
let mut current = budget.load(Ordering::Acquire);
loop {
if current < amount {
return Err(current);
}
match budget.compare_exchange_weak(
current,
current - amount,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(_) => return Ok(current - amount),
Err(actual) => current = actual,
}
}
}
pub struct BudgetEngine {
tenant_budgets: Mutex<HashMap<Arc<str>, Arc<AtomicI64>>>,
reservations: Mutex<HashMap<u64, ReservationRecord>>,
next_id: AtomicU64,
}
impl BudgetEngine {
pub fn new() -> Self {
Self {
tenant_budgets: Mutex::new(HashMap::new()),
reservations: Mutex::new(HashMap::new()),
next_id: AtomicU64::new(1),
}
}
pub fn ensure_tenant(&self, tenant_id: &str, budget_microcents: i64) {
debug_assert!(
budget_microcents >= 0,
"initial budget must be non-negative"
);
let mut budgets = self.tenant_budgets.lock().unwrap();
let key: Arc<str> = Arc::from(tenant_id);
budgets
.entry(key)
.or_insert_with(|| Arc::new(AtomicI64::new(budget_microcents)));
}
#[must_use]
pub fn remaining_microcents(&self, tenant_id: &str) -> Option<i64> {
let budgets = self.tenant_budgets.lock().unwrap();
let key: Arc<str> = Arc::from(tenant_id);
budgets.get(&key).map(|b| b.load(Ordering::Acquire))
}
pub fn try_reserve(
&self,
tenant_id: &str,
cost_microcents: i64,
) -> (BudgetReservation, Option<u64>) {
if cost_microcents <= 0 {
return (
BudgetReservation::Insufficient {
remaining_microcents: 0,
required_microcents: cost_microcents,
},
None,
);
}
let key: Arc<str> = Arc::from(tenant_id);
let budget = {
let budgets = self.tenant_budgets.lock().unwrap();
match budgets.get(&key) {
Some(b) => Arc::clone(b),
None => return (BudgetReservation::MissingTenant, None),
}
};
match debit_if_available(&budget, cost_microcents) {
Err(current) => (
BudgetReservation::Insufficient {
remaining_microcents: current,
required_microcents: cost_microcents,
},
None,
),
Ok(remaining) => {
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
let mut reservations = self.reservations.lock().unwrap();
reservations.insert(
id,
ReservationRecord {
tenant_id: Arc::clone(&key),
reserved_microcents: cost_microcents,
},
);
(
BudgetReservation::Reserved {
remaining_microcents: remaining,
},
Some(id),
)
}
}
}
pub fn commit(&self, reservation_id: u64, actual_microcents: i64) -> BudgetSettlement {
if actual_microcents < 0 {
return BudgetSettlement::InvalidAmount;
}
let mut reservations = self.reservations.lock().unwrap();
let Some(reservation) = reservations.remove(&reservation_id) else {
return BudgetSettlement::MissingReservation;
};
let budget = {
let budgets = self.tenant_budgets.lock().unwrap();
match budgets.get(&reservation.tenant_id) {
Some(b) => Arc::clone(b),
None => {
reservations.insert(reservation_id, reservation);
return BudgetSettlement::MissingTenant;
}
}
};
drop(reservations);
let delta: i64 = actual_microcents - reservation.reserved_microcents;
if delta > 0 {
if let Err(remaining) = debit_if_available(&budget, delta) {
let mut reservations = self.reservations.lock().unwrap();
reservations.insert(reservation_id, reservation);
return BudgetSettlement::Overrun {
remaining_microcents: remaining,
};
}
} else if delta < 0 {
budget.fetch_add(-delta, Ordering::AcqRel);
}
let remaining = budget.load(Ordering::Acquire);
BudgetSettlement::Committed {
remaining_microcents: remaining,
actual_microcents,
}
}
pub fn release(&self, reservation_id: u64) -> BudgetSettlement {
let mut reservations = self.reservations.lock().unwrap();
let Some((_, reservation)) = reservations.remove_entry(&reservation_id) else {
return BudgetSettlement::MissingReservation;
};
let budget = {
let budgets = self.tenant_budgets.lock().unwrap();
match budgets.get(&reservation.tenant_id) {
Some(b) => Arc::clone(b),
None => {
reservations.insert(reservation_id, reservation);
return BudgetSettlement::MissingTenant;
}
}
};
drop(reservations);
let returned = reservation.reserved_microcents;
let remaining = budget.fetch_add(returned, Ordering::AcqRel) + returned;
BudgetSettlement::Released {
remaining_microcents: remaining,
returned_microcents: returned,
}
}
}
impl Default for BudgetEngine {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reserve_and_commit() {
let engine = BudgetEngine::new();
engine.ensure_tenant("t1", 100_000_000);
let (_, id) = engine.try_reserve("t1", 25_000_000);
let settlement = engine.commit(id.unwrap(), 20_000_000);
assert!(matches!(settlement, BudgetSettlement::Committed { .. }));
assert_eq!(engine.remaining_microcents("t1"), Some(80_000_000));
}
#[test]
fn reserve_insufficient() {
let engine = BudgetEngine::new();
engine.ensure_tenant("t1", 10_000_000);
let (res, id) = engine.try_reserve("t1", 50_000_000);
assert!(matches!(res, BudgetReservation::Insufficient { .. }));
assert!(id.is_none());
}
#[test]
fn release_returns_full_amount() {
let engine = BudgetEngine::new();
engine.ensure_tenant("t1", 100_000_000);
let (_, id) = engine.try_reserve("t1", 30_000_000);
engine.release(id.unwrap());
assert_eq!(engine.remaining_microcents("t1"), Some(100_000_000));
}
#[test]
fn missing_tenant_rejected() {
let engine = BudgetEngine::new();
let (res, _) = engine.try_reserve("nonexistent", 1000);
assert!(matches!(res, BudgetReservation::MissingTenant));
}
#[test]
fn missing_reservation_rejected() {
let engine = BudgetEngine::new();
assert!(matches!(
engine.commit(999, 1000),
BudgetSettlement::MissingReservation
));
assert!(matches!(
engine.release(999),
BudgetSettlement::MissingReservation
));
}
#[test]
fn conservation_invariant() {
let engine = BudgetEngine::new();
engine.ensure_tenant("t1", 100_000_000);
let (_, id1) = engine.try_reserve("t1", 30_000_000);
let (_, id2) = engine.try_reserve("t1", 20_000_000);
engine.commit(id1.unwrap(), 25_000_000);
engine.release(id2.unwrap());
assert_eq!(engine.remaining_microcents("t1"), Some(75_000_000));
}
#[test]
fn concurrent_reserve_never_overspends() {
let engine = Arc::new(BudgetEngine::new());
engine.ensure_tenant("t1", 100_000_000);
let handles: Vec<_> = (0..100)
.map(|_| {
let e = Arc::clone(&engine);
std::thread::spawn(move || {
let (res, _) = e.try_reserve("t1", 1_000_000);
matches!(res, BudgetReservation::Reserved { .. })
})
})
.collect();
let success: usize = handles
.into_iter()
.map(|h| if h.join().unwrap() { 1 } else { 0 })
.sum();
assert_eq!(success, 100);
assert_eq!(engine.remaining_microcents("t1"), Some(0));
}
#[test]
fn zero_cost_rejected() {
let engine = BudgetEngine::new();
engine.ensure_tenant("t1", 100_000_000);
let (res, _) = engine.try_reserve("t1", 0);
assert!(matches!(res, BudgetReservation::Insufficient { .. }));
}
#[test]
fn negative_cost_rejected() {
let engine = BudgetEngine::new();
engine.ensure_tenant("t1", 100_000_000);
let (res, _) = engine.try_reserve("t1", -500);
assert!(matches!(res, BudgetReservation::Insufficient { .. }));
}
#[test]
fn negative_commit_rejected() {
let engine = BudgetEngine::new();
engine.ensure_tenant("t1", 100_000_000);
let (_, id) = engine.try_reserve("t1", 10_000_000);
let result = engine.commit(id.unwrap(), -5_000_000);
assert!(matches!(result, BudgetSettlement::InvalidAmount));
}
#[test]
fn failed_overrun_does_not_create_budget() {
let engine = BudgetEngine::new();
engine.ensure_tenant("t1", 10_000_000);
let (_, id) = engine.try_reserve("t1", 7_000_000);
let id = id.unwrap();
let result = engine.commit(id, 11_000_000);
assert!(matches!(result, BudgetSettlement::Overrun { .. }));
assert_eq!(engine.remaining_microcents("t1"), Some(3_000_000));
engine.release(id);
assert_eq!(engine.remaining_microcents("t1"), Some(10_000_000));
}
#[test]
fn no_deadlock_under_contention() {
let engine = Arc::new(BudgetEngine::new());
engine.ensure_tenant("t1", 1_000_000_000);
let handles: Vec<_> = (0..50)
.map(|i| {
let e = Arc::clone(&engine);
std::thread::spawn(move || {
let (_, id) = e.try_reserve("t1", 1_000_000);
if let Some(id) = id {
if i % 3 == 0 {
e.release(id);
} else {
e.commit(id, 800_000);
}
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
assert!(engine.remaining_microcents("t1").unwrap() > 0);
}
#[test]
fn concurrent_overrun_never_goes_negative() {
let engine = Arc::new(BudgetEngine::new());
engine.ensure_tenant("t1", 50_000_000);
let handles: Vec<_> = (0..20)
.map(|_| {
let e = Arc::clone(&engine);
std::thread::spawn(move || {
let (_, id) = e.try_reserve("t1", 1_000_000);
if let Some(id) = id {
let _ = e.commit(id, 3_000_000);
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
let remaining = engine.remaining_microcents("t1").unwrap();
assert!(
remaining >= 0,
"budget must never go negative, got {remaining}"
);
}
}