use std::sync::{Arc, Mutex, MutexGuard};
use rust_decimal::Decimal;
#[derive(Debug, Clone)]
pub(crate) struct SharedCost(Arc<Mutex<CostLedger>>);
#[derive(Debug)]
struct CostLedger {
spent: Decimal,
reserved: Decimal,
}
impl SharedCost {
pub(crate) fn new(spent: Decimal) -> Self {
Self(Arc::new(Mutex::new(CostLedger {
spent,
reserved: Decimal::ZERO,
})))
}
pub(crate) fn charge(&self, cost: Decimal) {
self.lock().spent += cost;
}
pub(crate) fn spent(&self) -> Decimal {
self.lock().spent
}
pub(crate) fn reserve(
&self,
budget: Decimal,
limit: Decimal,
) -> Result<BudgetReservation, Decimal> {
let mut ledger = self.lock();
let committed = ledger.spent + ledger.reserved;
if committed + budget > limit {
return Err(committed);
}
ledger.reserved += budget;
Ok(BudgetReservation {
ledger: Some(self.clone()),
amount: budget,
})
}
fn lock(&self) -> MutexGuard<'_, CostLedger> {
self.0
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
}
#[must_use = "the budget is released as soon as the reservation is dropped"]
pub(crate) struct BudgetReservation {
ledger: Option<SharedCost>,
amount: Decimal,
}
impl BudgetReservation {
pub(crate) fn none() -> Self {
Self {
ledger: None,
amount: Decimal::ZERO,
}
}
}
impl Drop for BudgetReservation {
fn drop(&mut self) {
if let Some(ledger) = &self.ledger {
ledger.lock().reserved -= self.amount;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_reservation_holds_its_budget_until_dropped() {
let cost = SharedCost::new(Decimal::new(5, 2));
let limit = Decimal::new(20, 2);
let first = cost
.reserve(Decimal::new(10, 2), limit)
.expect("0.05 + 0.10 fits under 0.20");
assert_eq!(
cost.reserve(Decimal::new(10, 2), limit).err(),
Some(Decimal::new(15, 2)),
"the second step sees the first one's budget"
);
drop(first);
let _second = cost
.reserve(Decimal::new(10, 2), limit)
.expect("the budget is free again");
}
#[test]
fn charged_cost_counts_against_the_next_reservation() {
let cost = SharedCost::new(Decimal::ZERO);
cost.charge(Decimal::new(15, 2));
assert_eq!(cost.spent(), Decimal::new(15, 2));
assert!(
cost.reserve(Decimal::new(10, 2), Decimal::new(20, 2))
.is_err()
);
let _exact = cost
.reserve(Decimal::new(5, 2), Decimal::new(20, 2))
.expect("a budget that reaches the cap exactly fits");
}
}