use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenPool {
pub name: String,
pub capacity: u64,
pub used: u64,
pub reserved: u64,
}
impl TokenPool {
pub fn new(name: impl Into<String>, capacity: u64) -> Self {
Self {
name: name.into(),
capacity,
used: 0,
reserved: 0,
}
}
pub fn available(&self) -> u64 {
self.capacity
.saturating_sub(self.used.saturating_add(self.reserved))
}
pub fn can_reserve(&self, tokens: u64) -> bool {
self.available() >= tokens
}
pub fn reserve(&mut self, tokens: u64) -> bool {
if !self.can_reserve(tokens) {
return false;
}
self.reserved += tokens;
true
}
pub fn commit(&mut self, reserved: u64, actual: u64) {
self.reserved = self.reserved.saturating_sub(reserved);
self.used = self.used.saturating_add(actual);
}
pub fn release(&mut self, tokens: u64) {
self.reserved = self.reserved.saturating_sub(tokens);
}
pub fn utilization(&self) -> f64 {
if self.capacity == 0 {
return 0.0;
}
self.used as f64 / self.capacity as f64
}
}
pub struct TokenBudget {
pools: HashMap<String, TokenPool>,
}
impl TokenBudget {
pub fn new() -> Self {
Self {
pools: HashMap::new(),
}
}
pub fn add_pool(&mut self, pool: TokenPool) {
self.pools.insert(pool.name.clone(), pool);
}
pub fn get_pool(&self, name: &str) -> Option<&TokenPool> {
self.pools.get(name)
}
pub fn get_pool_mut(&mut self, name: &str) -> Option<&mut TokenPool> {
self.pools.get_mut(name)
}
pub fn pools(&self) -> &HashMap<String, TokenPool> {
&self.pools
}
pub fn check(&self, pool_name: &str, tokens: u64) -> bool {
self.pools
.get(pool_name)
.map(|p| p.can_reserve(tokens))
.unwrap_or(false)
}
pub fn reserve(&mut self, pool_name: &str, tokens: u64) -> bool {
self.pools
.get_mut(pool_name)
.map(|p| p.reserve(tokens))
.unwrap_or(false)
}
pub fn report(&mut self, pool_name: &str, reserved: u64, actual: u64) {
if let Some(pool) = self.pools.get_mut(pool_name) {
pool.commit(reserved, actual);
}
}
}
impl Default for TokenBudget {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pool_basic() {
let mut pool = TokenPool::new("test", 1000);
assert_eq!(pool.available(), 1000);
assert!(pool.reserve(400));
assert_eq!(pool.available(), 600);
pool.commit(400, 350);
assert_eq!(pool.used, 350);
assert_eq!(pool.reserved, 0);
assert_eq!(pool.available(), 650);
}
#[test]
fn pool_over_budget() {
let pool = TokenPool::new("test", 100);
assert!(!pool.can_reserve(200));
}
#[test]
fn pool_release() {
let mut pool = TokenPool::new("test", 1000);
pool.reserve(500);
pool.release(500);
assert_eq!(pool.reserved, 0);
assert_eq!(pool.available(), 1000);
}
#[test]
fn pool_utilization() {
let mut pool = TokenPool::new("test", 1000);
pool.used = 750;
assert!((pool.utilization() - 0.75).abs() < f64::EPSILON);
}
#[test]
fn budget_multi_pool() {
let mut budget = TokenBudget::new();
budget.add_pool(TokenPool::new("default", 10000));
budget.add_pool(TokenPool::new("agent-1", 5000));
assert!(budget.check("default", 8000));
assert!(!budget.check("agent-1", 8000));
assert!(!budget.check("nonexistent", 1));
}
#[test]
fn budget_reserve_report() {
let mut budget = TokenBudget::new();
budget.add_pool(TokenPool::new("pool", 1000));
assert!(budget.reserve("pool", 500));
budget.report("pool", 500, 420);
let pool = budget.get_pool("pool").unwrap();
assert_eq!(pool.used, 420);
assert_eq!(pool.reserved, 0);
}
}