use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::time::Duration;
use serde::{Deserialize, Serialize};
use crate::rate_limit::{create_rate_limiter, RateLimitConfig, RateLimiter};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TenantRateLimit {
pub config: RateLimitConfig,
pub quota: Option<u64>,
}
impl TenantRateLimit {
#[must_use]
pub fn new(config: RateLimitConfig) -> Self {
Self {
config,
quota: None,
}
}
#[must_use]
pub fn with_quota(mut self, quota: u64) -> Self {
self.quota = Some(quota);
self
}
}
impl From<RateLimitConfig> for TenantRateLimit {
fn from(config: RateLimitConfig) -> Self {
Self::new(config)
}
}
impl Default for TenantRateLimit {
fn default() -> Self {
Self::new(RateLimitConfig::default())
}
}
struct TenantState {
limiter: Box<dyn RateLimiter>,
quota: Option<u64>,
granted: u64,
denied: u64,
}
impl TenantState {
fn from_policy(policy: &TenantRateLimit) -> Self {
Self {
limiter: create_rate_limiter(policy.config.clone()),
quota: policy.quota,
granted: 0,
denied: 0,
}
}
fn quota_exhausted(&self) -> bool {
matches!(self.quota, Some(limit) if self.granted >= limit)
}
fn try_acquire(&mut self) -> bool {
if self.quota_exhausted() {
self.denied += 1;
return false;
}
if self.limiter.try_acquire() {
self.granted += 1;
true
} else {
self.denied += 1;
false
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct TenantUsage {
pub granted: u64,
pub denied: u64,
pub quota: Option<u64>,
pub quota_remaining: Option<u64>,
}
struct TenantRegistry {
default_policy: Option<TenantRateLimit>,
overrides: HashMap<String, TenantRateLimit>,
tenants: HashMap<String, TenantState>,
}
impl TenantRegistry {
fn new(default_policy: Option<TenantRateLimit>) -> Self {
Self {
default_policy,
overrides: HashMap::new(),
tenants: HashMap::new(),
}
}
fn policy_for(&self, tenant_id: &str) -> Option<TenantRateLimit> {
self.overrides
.get(tenant_id)
.cloned()
.or_else(|| self.default_policy.clone())
}
fn try_acquire(&mut self, tenant_id: &str) -> bool {
if let Some(state) = self.tenants.get_mut(tenant_id) {
return state.try_acquire();
}
let Some(policy) = self.policy_for(tenant_id) else {
return true;
};
let mut state = TenantState::from_policy(&policy);
let granted = state.try_acquire();
self.tenants.insert(tenant_id.to_string(), state);
granted
}
}
#[derive(Clone)]
pub struct TenantRateLimiter {
inner: Arc<RwLock<TenantRegistry>>,
}
impl std::fmt::Debug for TenantRateLimiter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut dbg = f.debug_struct("TenantRateLimiter");
if let Ok(guard) = self.inner.read() {
dbg.field("tracked_tenants", &guard.tenants.len())
.field("overrides", &guard.overrides.len())
.field("has_default", &guard.default_policy.is_some());
}
dbg.finish()
}
}
impl TenantRateLimiter {
#[must_use]
pub fn new() -> Self {
Self {
inner: Arc::new(RwLock::new(TenantRegistry::new(None))),
}
}
#[must_use]
pub fn with_default(config: RateLimitConfig) -> Self {
Self {
inner: Arc::new(RwLock::new(TenantRegistry::new(Some(
TenantRateLimit::new(config),
)))),
}
}
#[must_use]
pub fn with_default_policy(policy: TenantRateLimit) -> Self {
Self {
inner: Arc::new(RwLock::new(TenantRegistry::new(Some(policy)))),
}
}
pub fn set_tenant_config(&self, tenant_id: impl Into<String>, config: RateLimitConfig) {
self.set_tenant_policy(tenant_id, TenantRateLimit::new(config));
}
pub fn set_tenant_policy(&self, tenant_id: impl Into<String>, policy: TenantRateLimit) {
let id = tenant_id.into();
if let Ok(mut guard) = self.inner.write() {
guard.tenants.remove(&id);
guard.overrides.insert(id, policy);
}
}
pub fn set_tenant_quota(&self, tenant_id: impl Into<String>, quota: Option<u64>) {
let id = tenant_id.into();
if let Ok(mut guard) = self.inner.write() {
let base = guard
.policy_for(&id)
.unwrap_or_else(|| TenantRateLimit::new(RateLimitConfig::default()));
let policy = TenantRateLimit {
config: base.config,
quota,
};
guard.tenants.remove(&id);
guard.overrides.insert(id, policy);
}
}
pub fn remove_tenant(&self, tenant_id: &str) {
if let Ok(mut guard) = self.inner.write() {
guard.overrides.remove(tenant_id);
guard.tenants.remove(tenant_id);
}
}
#[must_use]
pub fn try_acquire(&self, tenant_id: &str) -> bool {
if let Ok(mut guard) = self.inner.write() {
guard.try_acquire(tenant_id)
} else {
true
}
}
#[must_use]
pub fn has_capacity(&self, tenant_id: &str) -> bool {
if let Ok(guard) = self.inner.read() {
if let Some(state) = guard.tenants.get(tenant_id) {
return !state.quota_exhausted() && state.limiter.available_permits() > 0;
}
return match guard.policy_for(tenant_id) {
Some(policy) => policy.config.effective_burst() > 0,
None => true,
};
}
true
}
#[must_use]
pub fn time_until_available(&self, tenant_id: &str) -> Duration {
if let Ok(guard) = self.inner.read() {
if let Some(state) = guard.tenants.get(tenant_id) {
if state.quota_exhausted() {
return Duration::MAX;
}
return state.limiter.time_until_available();
}
}
Duration::ZERO
}
#[must_use]
pub fn has_rate_limit(&self, tenant_id: &str) -> bool {
if let Ok(guard) = self.inner.read() {
guard.overrides.contains_key(tenant_id) || guard.default_policy.is_some()
} else {
false
}
}
#[must_use]
pub fn policy_for(&self, tenant_id: &str) -> Option<TenantRateLimit> {
self.inner.read().ok().and_then(|g| g.policy_for(tenant_id))
}
#[must_use]
pub fn usage(&self, tenant_id: &str) -> Option<TenantUsage> {
let guard = self.inner.read().ok()?;
let state = guard.tenants.get(tenant_id)?;
let quota_remaining = state.quota.map(|q| q.saturating_sub(state.granted));
Some(TenantUsage {
granted: state.granted,
denied: state.denied,
quota: state.quota,
quota_remaining,
})
}
pub fn reset_tenant(&self, tenant_id: &str) {
if let Ok(mut guard) = self.inner.write() {
if let Some(state) = guard.tenants.get_mut(tenant_id) {
state.limiter.reset();
state.granted = 0;
state.denied = 0;
}
}
}
pub fn reset_all(&self) {
if let Ok(mut guard) = self.inner.write() {
for state in guard.tenants.values_mut() {
state.limiter.reset();
state.granted = 0;
state.denied = 0;
}
}
}
#[must_use]
pub fn tracked_count(&self) -> usize {
self.inner.read().map(|g| g.tenants.len()).unwrap_or(0)
}
}
impl Default for TenantRateLimiter {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_default_allows_unconfigured_tenants() {
let limiter = TenantRateLimiter::new();
for _ in 0..1000 {
assert!(limiter.try_acquire("anyone"));
}
assert!(!limiter.has_rate_limit("anyone"));
}
#[test]
fn per_tenant_isolation() {
let limiter = TenantRateLimiter::new();
limiter.set_tenant_config("alice", RateLimitConfig::new(1000.0).with_burst(2));
limiter.set_tenant_config("bob", RateLimitConfig::new(1000.0).with_burst(2));
assert!(limiter.try_acquire("alice"));
assert!(limiter.try_acquire("alice"));
assert!(!limiter.try_acquire("alice"));
assert!(limiter.try_acquire("bob"));
assert!(limiter.try_acquire("bob"));
assert!(!limiter.try_acquire("bob"));
}
#[test]
fn default_vs_override() {
let limiter = TenantRateLimiter::with_default(RateLimitConfig::new(1000.0).with_burst(100));
limiter.set_tenant_config("trial", RateLimitConfig::new(1000.0).with_burst(1));
assert!(limiter.try_acquire("trial"));
assert!(!limiter.try_acquire("trial"));
for _ in 0..100 {
assert!(limiter.try_acquire("default_tenant"));
}
assert!(!limiter.try_acquire("default_tenant"));
}
#[test]
fn default_applies_to_all_unknown_tenants() {
let limiter = TenantRateLimiter::with_default(RateLimitConfig::new(1000.0).with_burst(2));
assert!(limiter.has_rate_limit("whoever"));
assert!(limiter.try_acquire("t1"));
assert!(limiter.try_acquire("t1"));
assert!(!limiter.try_acquire("t1"));
assert!(limiter.try_acquire("t2"));
}
#[test]
fn quota_caps_total_permits() {
let limiter = TenantRateLimiter::new();
limiter.set_tenant_policy(
"metered",
TenantRateLimit::new(RateLimitConfig::new(1_000_000.0).with_burst(1000)).with_quota(3),
);
assert!(limiter.try_acquire("metered"));
assert!(limiter.try_acquire("metered"));
assert!(limiter.try_acquire("metered"));
assert!(!limiter.try_acquire("metered"));
assert!(!limiter.try_acquire("metered"));
let usage = limiter.usage("metered").expect("tenant materialized");
assert_eq!(usage.granted, 3);
assert_eq!(usage.denied, 2);
assert_eq!(usage.quota, Some(3));
assert_eq!(usage.quota_remaining, Some(0));
}
#[test]
fn quota_exhaustion_does_not_affect_other_tenant() {
let limiter = TenantRateLimiter::new();
limiter.set_tenant_policy(
"capped",
TenantRateLimit::new(RateLimitConfig::new(1_000_000.0).with_burst(1000)).with_quota(1),
);
limiter.set_tenant_config("uncapped", RateLimitConfig::new(1000.0).with_burst(5));
assert!(limiter.try_acquire("capped"));
assert!(!limiter.try_acquire("capped"));
for _ in 0..5 {
assert!(limiter.try_acquire("uncapped"));
}
}
#[test]
fn set_tenant_quota_preserves_rate() {
let limiter =
TenantRateLimiter::with_default(RateLimitConfig::new(1_000_000.0).with_burst(10));
limiter.set_tenant_quota("vip", Some(2));
let policy = limiter.policy_for("vip").expect("override created");
assert_eq!(policy.quota, Some(2));
assert_eq!(policy.config.effective_burst(), 10);
assert!(limiter.try_acquire("vip"));
assert!(limiter.try_acquire("vip"));
assert!(!limiter.try_acquire("vip"));
limiter.set_tenant_quota("vip", None);
assert!(limiter.policy_for("vip").expect("override").quota.is_none());
for _ in 0..10 {
assert!(limiter.try_acquire("vip"));
}
}
#[test]
fn reset_tenant_restores_capacity() {
let limiter = TenantRateLimiter::new();
limiter.set_tenant_config("svc", RateLimitConfig::new(0.0001).with_burst(2));
assert!(limiter.try_acquire("svc"));
assert!(limiter.try_acquire("svc"));
assert!(!limiter.try_acquire("svc"));
limiter.reset_tenant("svc");
assert!(limiter.try_acquire("svc"));
let usage = limiter.usage("svc").expect("materialized");
assert_eq!(usage.granted, 1);
}
#[test]
fn reset_all_restores_all_tenants() {
let limiter = TenantRateLimiter::new();
limiter.set_tenant_config("a", RateLimitConfig::new(0.0001).with_burst(1));
limiter.set_tenant_config("b", RateLimitConfig::new(0.0001).with_burst(1));
assert!(limiter.try_acquire("a"));
assert!(limiter.try_acquire("b"));
assert!(!limiter.try_acquire("a"));
assert!(!limiter.try_acquire("b"));
limiter.reset_all();
assert!(limiter.try_acquire("a"));
assert!(limiter.try_acquire("b"));
}
#[test]
fn remove_tenant_falls_back_to_default() {
let limiter = TenantRateLimiter::with_default(RateLimitConfig::new(1000.0).with_burst(5));
limiter.set_tenant_config("temp", RateLimitConfig::new(1000.0).with_burst(1));
assert!(limiter.try_acquire("temp"));
assert!(!limiter.try_acquire("temp"));
limiter.remove_tenant("temp");
let policy = limiter.policy_for("temp").expect("default applies");
assert_eq!(policy.config.effective_burst(), 5);
for _ in 0..5 {
assert!(limiter.try_acquire("temp"));
}
}
#[test]
fn sliding_window_tenant_is_supported() {
let limiter = TenantRateLimiter::new();
limiter.set_tenant_config("sw", RateLimitConfig::new(3.0).with_sliding_window(1));
assert!(limiter.try_acquire("sw"));
assert!(limiter.try_acquire("sw"));
assert!(limiter.try_acquire("sw"));
assert!(!limiter.try_acquire("sw"));
}
#[test]
fn usage_absent_for_unmaterialized_tenant() {
let limiter = TenantRateLimiter::with_default(RateLimitConfig::new(10.0));
assert!(limiter.usage("never_seen").is_none());
assert_eq!(limiter.tracked_count(), 0);
let _ = limiter.try_acquire("now_seen");
assert_eq!(limiter.tracked_count(), 1);
assert!(limiter.usage("now_seen").is_some());
}
#[test]
fn time_until_available_for_exhausted_quota_is_max() {
let limiter = TenantRateLimiter::new();
limiter.set_tenant_policy(
"q",
TenantRateLimit::new(RateLimitConfig::new(1_000_000.0).with_burst(100)).with_quota(1),
);
assert!(limiter.try_acquire("q"));
assert!(!limiter.try_acquire("q"));
assert_eq!(limiter.time_until_available("q"), Duration::MAX);
}
#[test]
fn has_capacity_reflects_state() {
let limiter = TenantRateLimiter::new();
limiter.set_tenant_config("c", RateLimitConfig::new(0.0001).with_burst(1));
assert!(limiter.has_capacity("c"));
assert!(limiter.try_acquire("c"));
assert!(!limiter.has_capacity("c"));
}
#[test]
fn policy_from_config_conversion() {
let policy: TenantRateLimit = RateLimitConfig::new(50.0).into();
assert!(policy.quota.is_none());
assert!((policy.config.rate - 50.0).abs() < f64::EPSILON);
}
#[test]
fn tenant_rate_limit_serde_round_trip() {
let policy =
TenantRateLimit::new(RateLimitConfig::new(25.0).with_burst(50)).with_quota(500);
let json = serde_json::to_string(&policy).expect("serialize");
let parsed: TenantRateLimit = serde_json::from_str(&json).expect("deserialize");
assert_eq!(parsed.quota, Some(500));
assert_eq!(parsed.config.burst, Some(50));
}
#[test]
fn concurrent_tenants_respect_caps() {
use std::thread;
let limiter = TenantRateLimiter::new();
limiter.set_tenant_config("shared", RateLimitConfig::new(0.001).with_burst(10));
let handles: Vec<_> = (0..4)
.map(|_| {
let l = limiter.clone();
thread::spawn(move || {
let mut count = 0;
for _ in 0..10 {
if l.try_acquire("shared") {
count += 1;
}
}
count
})
})
.collect();
let total: usize = handles
.into_iter()
.map(|h| h.join().expect("thread joins"))
.sum();
assert!(total <= 10, "granted {total} permits, expected <= 10");
}
}