use std::collections::HashMap;
use parking_lot::RwLock;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct TenantLimits {
pub max_vectors: Option<u64>,
pub max_disk_bytes: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct QuotaExceeded;
#[derive(Debug, Default)]
pub struct TenantVectorQuota {
counts: RwLock<HashMap<u128, u64>>,
}
impl TenantVectorQuota {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn try_add(&self, tenant: u128, delta: u64, limit: u64) -> Result<(), QuotaExceeded> {
let mut g = self.counts.write();
let cur = g.get(&tenant).copied().unwrap_or(0);
let new = cur
.checked_add(delta)
.filter(|n| *n <= limit)
.ok_or(QuotaExceeded)?;
g.insert(tenant, new);
Ok(())
}
pub fn sub(&self, tenant: u128, delta: u64) {
let mut g = self.counts.write();
if let Some(c) = g.get_mut(&tenant) {
*c = c.saturating_sub(delta);
if *c == 0 {
g.remove(&tenant);
}
}
}
#[must_use]
pub fn count(&self, tenant: u128) -> u64 {
self.counts.read().get(&tenant).copied().unwrap_or(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_try_add_within_limit() {
let q = TenantVectorQuota::new();
assert!(q.try_add(7, 1, 3).is_ok());
assert!(q.try_add(7, 2, 3).is_ok());
assert_eq!(q.count(7), 3);
}
#[test]
fn test_try_add_rejects_at_limit() {
let q = TenantVectorQuota::new();
q.try_add(7, 3, 3).unwrap();
assert_eq!(
q.try_add(7, 1, 3),
Err(QuotaExceeded),
"exactly at limit rejects"
);
assert_eq!(q.count(7), 3, "rejected add must not change the count");
}
#[test]
fn test_sub_frees_a_slot() {
let q = TenantVectorQuota::new();
q.try_add(7, 3, 3).unwrap();
q.sub(7, 1);
assert_eq!(q.count(7), 2);
assert!(q.try_add(7, 1, 3).is_ok(), "freed slot is reusable");
}
#[test]
fn test_sub_saturates_and_drops_entry() {
let q = TenantVectorQuota::new();
q.try_add(7, 1, 10).unwrap();
q.sub(7, 5); assert_eq!(q.count(7), 0);
q.sub(7, 1); assert_eq!(q.count(7), 0);
}
#[test]
fn test_tenants_isolated() {
let q = TenantVectorQuota::new();
q.try_add(7, 2, 2).unwrap();
assert!(q.try_add(9, 2, 2).is_ok(), "tenant 9 has its own budget");
assert_eq!(q.try_add(7, 1, 2), Err(QuotaExceeded));
assert_eq!(q.count(9), 2);
}
#[test]
fn test_overflow_rejected() {
let q = TenantVectorQuota::new();
q.try_add(7, 5, u64::MAX).unwrap();
assert_eq!(q.try_add(7, u64::MAX, u64::MAX), Err(QuotaExceeded));
assert_eq!(q.count(7), 5);
}
}