use crate::limiters::{ConcurrencyLimiter, TokenBucketLimiter};
use dashmap::DashMap;
use std::collections::HashSet;
use std::sync::Arc;
use std::sync::LazyLock;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
#[cfg(feature = "quota-control")]
use crate::limiters::QuotaLimiter;
#[cfg(feature = "quota-control")]
use crate::quota::{AlertConfig, QuotaConfig, QuotaType};
const MAX_LIMITER_ENTRIES: usize = 100_000;
const CLEANUP_THRESHOLD: usize = 110_000;
const CLEANUP_RATIO: f64 = 0.1;
fn now_nanos() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0)
}
fn redact_key(key: &str) -> String {
if key.len() > 16 {
let prefix: String = key.chars().take(8).collect();
format!("{}...({} chars)", prefix, key.len())
} else {
format!("<{} chars>", key.len())
}
}
fn cleanup_lru<L>(
limiters: &DashMap<String, Arc<L>>,
access_times: &DashMap<String, AtomicU64>,
max_entries: usize,
) {
let target_count = (max_entries as f64 * (1.0 - CLEANUP_RATIO)) as usize;
let mut entries: Vec<(String, u64)> = access_times
.iter()
.map(|e| (e.key().clone(), e.value().load(Ordering::Relaxed)))
.collect();
if entries.len() <= target_count {
return;
}
let to_remove = entries.len() - target_count;
entries.select_nth_unstable_by_key(to_remove - 1, |e| e.1);
let to_remove_keys: HashSet<String> = entries
.into_iter()
.take(to_remove)
.map(|(k, _)| k)
.collect();
limiters.retain(|k, _| !to_remove_keys.contains(k));
access_times.retain(|k, _| !to_remove_keys.contains(k));
}
pub struct LimiterManager {
rate_limiters: DashMap<String, Arc<TokenBucketLimiter>>,
rate_access_times: DashMap<String, AtomicU64>,
#[cfg(feature = "quota-control")]
quota_limiters: DashMap<String, Arc<QuotaLimiter>>,
#[cfg(feature = "quota-control")]
quota_access_times: DashMap<String, AtomicU64>,
concurrency_limiters: DashMap<String, Arc<ConcurrencyLimiter>>,
concurrency_access_times: DashMap<String, AtomicU64>,
rate_cleanup_in_progress: AtomicBool,
#[cfg(feature = "quota-control")]
quota_cleanup_in_progress: AtomicBool,
concurrency_cleanup_in_progress: AtomicBool,
}
impl LimiterManager {
pub fn new() -> Self {
Self {
rate_limiters: DashMap::new(),
rate_access_times: DashMap::new(),
#[cfg(feature = "quota-control")]
quota_limiters: DashMap::new(),
#[cfg(feature = "quota-control")]
quota_access_times: DashMap::new(),
concurrency_limiters: DashMap::new(),
concurrency_access_times: DashMap::new(),
rate_cleanup_in_progress: AtomicBool::new(false),
#[cfg(feature = "quota-control")]
quota_cleanup_in_progress: AtomicBool::new(false),
concurrency_cleanup_in_progress: AtomicBool::new(false),
}
}
pub fn get_rate_limiter(
&self,
key: &str,
amount: u64,
unit_secs: u64,
) -> Arc<TokenBucketLimiter> {
let key = key.to_string(); let refill_rate = if unit_secs > 0 {
(amount / unit_secs).max(1)
} else {
amount
};
if let Some(existing) = self.rate_limiters.get(&key) {
let existing_limiter = existing.value();
assert!(
existing_limiter.capacity() == amount
&& existing_limiter.refill_rate() == refill_rate,
"LimiterManager: rate limiter key '{}' already exists with different params (existing: capacity={}, refill_rate={}; new: capacity={}, refill_rate={})",
redact_key(&key),
existing_limiter.capacity(),
existing_limiter.refill_rate(),
amount,
refill_rate
);
if let Some(t) = self.rate_access_times.get(&key) {
t.store(now_nanos(), Ordering::Relaxed);
} else {
self.rate_access_times
.entry(key.clone())
.or_insert_with(|| AtomicU64::new(now_nanos()));
}
return existing_limiter.clone();
}
let limiter = self
.rate_limiters
.entry(key.clone())
.or_insert_with(|| Arc::new(TokenBucketLimiter::new(amount, refill_rate)))
.clone();
self.rate_access_times
.entry(key.clone())
.or_insert_with(|| AtomicU64::new(now_nanos()));
if self.rate_limiters.len() > CLEANUP_THRESHOLD {
if self
.rate_cleanup_in_progress
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
self.cleanup_rate_limiters();
self.rate_cleanup_in_progress
.store(false, Ordering::Release);
}
}
limiter
}
#[cfg(feature = "quota-control")]
pub fn get_quota_limiter(
&self,
key: &str,
period: std::time::Duration,
max: u64,
) -> Arc<QuotaLimiter> {
let key = key.to_string();
if let Some(existing) = self.quota_limiters.get(&key) {
let existing_limiter = existing.value();
assert!(
existing_limiter.max() == max && existing_limiter.period() == period,
"LimiterManager: quota limiter key '{}' already exists with different params (existing: max={}, period={:?}; new: max={}, period={:?})",
redact_key(&key),
existing_limiter.max(),
existing_limiter.period(),
max,
period
);
if let Some(t) = self.quota_access_times.get(&key) {
t.store(now_nanos(), Ordering::Relaxed);
} else {
self.quota_access_times
.entry(key.clone())
.or_insert_with(|| AtomicU64::new(now_nanos()));
}
return existing_limiter.clone();
}
let config = QuotaConfig {
quota_type: QuotaType::Count,
limit: max,
window_size: period.as_secs(),
allow_overdraft: false,
overdraft_limit_percent: 0,
alert_config: AlertConfig::default(),
};
let limiter = self
.quota_limiters
.entry(key.clone())
.or_insert_with(|| Arc::new(QuotaLimiter::new(config)))
.clone();
self.quota_access_times
.entry(key.clone())
.or_insert_with(|| AtomicU64::new(now_nanos()));
if self.quota_limiters.len() > CLEANUP_THRESHOLD {
if self
.quota_cleanup_in_progress
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
self.cleanup_quota_limiters();
self.quota_cleanup_in_progress
.store(false, Ordering::Release);
}
}
limiter
}
pub fn get_concurrency_limiter(
&self,
key: &str,
max_concurrent: u64,
) -> Arc<ConcurrencyLimiter> {
let key = key.to_string();
if let Some(existing) = self.concurrency_limiters.get(&key) {
let existing_limiter = existing.value();
assert!(
existing_limiter.max_concurrent() == max_concurrent,
"LimiterManager: concurrency limiter key '{}' already exists with different params (existing: max_concurrent={}; new: max_concurrent={})",
redact_key(&key),
existing_limiter.max_concurrent(),
max_concurrent
);
if let Some(t) = self.concurrency_access_times.get(&key) {
t.store(now_nanos(), Ordering::Relaxed);
} else {
self.concurrency_access_times
.entry(key.clone())
.or_insert_with(|| AtomicU64::new(now_nanos()));
}
return existing_limiter.clone();
}
let limiter = self
.concurrency_limiters
.entry(key.clone())
.or_insert_with(|| Arc::new(ConcurrencyLimiter::new(max_concurrent)))
.clone();
self.concurrency_access_times
.entry(key.clone())
.or_insert_with(|| AtomicU64::new(now_nanos()));
if self.concurrency_limiters.len() > CLEANUP_THRESHOLD {
if self
.concurrency_cleanup_in_progress
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
self.cleanup_concurrency_limiters();
self.concurrency_cleanup_in_progress
.store(false, Ordering::Release);
}
}
limiter
}
#[cfg(test)]
pub fn clear_for_test(&self) {
self.rate_limiters.clear();
self.rate_access_times.clear();
#[cfg(feature = "quota-control")]
self.quota_limiters.clear();
#[cfg(feature = "quota-control")]
self.quota_access_times.clear();
self.concurrency_limiters.clear();
self.concurrency_access_times.clear();
}
fn cleanup_rate_limiters(&self) {
self.cleanup_rate_limiters_to(MAX_LIMITER_ENTRIES);
}
fn cleanup_rate_limiters_to(&self, max_entries: usize) {
cleanup_lru(&self.rate_limiters, &self.rate_access_times, max_entries);
}
#[cfg(feature = "quota-control")]
fn cleanup_quota_limiters(&self) {
self.cleanup_quota_limiters_to(MAX_LIMITER_ENTRIES);
}
#[cfg(feature = "quota-control")]
fn cleanup_quota_limiters_to(&self, max_entries: usize) {
cleanup_lru(&self.quota_limiters, &self.quota_access_times, max_entries);
}
fn cleanup_concurrency_limiters(&self) {
self.cleanup_concurrency_limiters_to(MAX_LIMITER_ENTRIES);
}
fn cleanup_concurrency_limiters_to(&self, max_entries: usize) {
cleanup_lru(
&self.concurrency_limiters,
&self.concurrency_access_times,
max_entries,
);
}
#[cfg(test)]
pub fn rate_limiter_count(&self) -> usize {
self.rate_limiters.len()
}
#[cfg(feature = "quota-control")]
#[cfg(test)]
pub fn quota_limiter_count(&self) -> usize {
self.quota_limiters.len()
}
#[cfg(test)]
pub fn concurrency_limiter_count(&self) -> usize {
self.concurrency_limiters.len()
}
}
impl Default for LimiterManager {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for LimiterManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LimiterManager")
.field("rate_limiters_count", &self.rate_limiters.len())
.field(
"quota_limiters_count",
#[cfg(feature = "quota-control")]
&self.quota_limiters.len(),
#[cfg(not(feature = "quota-control"))]
&0usize,
)
.field(
"concurrency_limiters_count",
&self.concurrency_limiters.len(),
)
.finish()
}
}
pub static GLOBAL_LIMITER_MANAGER: LazyLock<LimiterManager> = LazyLock::new(LimiterManager::new);
#[cfg(test)]
mod tests {
use super::*;
use crate::limiters::Limiter;
#[test]
fn test_limiter_manager_new() {
let manager = LimiterManager::new();
assert_eq!(manager.rate_limiter_count(), 0);
assert_eq!(manager.concurrency_limiter_count(), 0);
#[cfg(feature = "quota-control")]
assert_eq!(manager.quota_limiter_count(), 0);
}
#[test]
fn test_limiter_manager_default() {
let manager = LimiterManager::default();
assert_eq!(manager.rate_limiter_count(), 0);
}
#[test]
fn test_get_rate_limiter_caches_by_key() {
let manager = LimiterManager::new();
let l1 = manager.get_rate_limiter("key1", 100, 1);
let l2 = manager.get_rate_limiter("key1", 100, 1);
assert!(Arc::ptr_eq(&l1, &l2));
assert_eq!(manager.rate_limiter_count(), 1);
let l3 = manager.get_rate_limiter("key2", 100, 1);
assert!(!Arc::ptr_eq(&l1, &l3));
assert_eq!(manager.rate_limiter_count(), 2);
}
#[tokio::test]
async fn test_rate_limiter_allow_within_limit() {
let manager = LimiterManager::new();
let limiter = manager.get_rate_limiter("test_allow", 10, 1);
for _ in 0..10 {
assert!(limiter.allow(1).await.unwrap(), "应允许 10 个请求");
}
}
#[tokio::test]
async fn test_rate_limiter_refill_rate_calculation() {
let manager = LimiterManager::new();
let limiter_s = manager.get_rate_limiter("per_sec", 100, 1);
for _ in 0..100 {
assert!(limiter_s.allow(1).await.unwrap());
}
let limiter_m = manager.get_rate_limiter("per_min", 100, 60);
for _ in 0..100 {
assert!(limiter_m.allow(1).await.unwrap());
}
let limiter_h = manager.get_rate_limiter("per_hour", 100, 3600);
for _ in 0..100 {
assert!(limiter_h.allow(1).await.unwrap());
}
}
#[test]
fn test_get_rate_limiter_zero_unit_secs_uses_amount_as_refill() {
let manager = LimiterManager::new();
let limiter = manager.get_rate_limiter("zero_unit", 50, 0);
assert_eq!(manager.rate_limiter_count(), 1);
let _ = limiter;
}
#[cfg(feature = "quota-control")]
#[test]
fn test_get_quota_limiter_caches_by_key() {
let manager = LimiterManager::new();
let l1 = manager.get_quota_limiter("qkey1", std::time::Duration::from_secs(3600), 1000);
let l2 = manager.get_quota_limiter("qkey1", std::time::Duration::from_secs(3600), 1000);
assert!(Arc::ptr_eq(&l1, &l2));
assert_eq!(manager.quota_limiter_count(), 1);
}
#[cfg(feature = "quota-control")]
#[tokio::test]
async fn test_quota_limiter_check_consumes_quota() {
use crate::limiters::Limiter;
let manager = LimiterManager::new();
let limiter =
manager.get_quota_limiter("quota_test", std::time::Duration::from_secs(3600), 3);
for i in 0..3 {
assert!(
limiter.check("quota_test").await.is_ok(),
"请求 {} 应成功",
i
);
}
assert!(
limiter.check("quota_test").await.is_err(),
"第 4 个请求应失败"
);
}
#[test]
fn test_get_concurrency_limiter_caches_by_key() {
let manager = LimiterManager::new();
let l1 = manager.get_concurrency_limiter("ckey1", 10);
let l2 = manager.get_concurrency_limiter("ckey1", 10);
assert!(Arc::ptr_eq(&l1, &l2));
assert_eq!(manager.concurrency_limiter_count(), 1);
}
#[tokio::test]
async fn test_concurrency_limiter_acquire_and_release() {
let manager = LimiterManager::new();
let limiter = manager.get_concurrency_limiter("conc_test_release", 2);
let p1 = limiter.acquire(1).await.unwrap();
let p2 = limiter.acquire(1).await.unwrap();
let result =
tokio::time::timeout(std::time::Duration::from_millis(100), limiter.acquire(1)).await;
assert!(
result.is_err(),
"3rd acquire should block (timeout expected)"
);
drop(p1);
let p3 = limiter
.acquire(1)
.await
.expect("acquire after release should succeed");
drop(p2);
drop(p3);
}
#[test]
fn test_clear_empties_all_caches() {
let manager = LimiterManager::new();
manager.get_rate_limiter("r1", 100, 1);
manager.get_concurrency_limiter("c1", 10);
#[cfg(feature = "quota-control")]
manager.get_quota_limiter("q1", std::time::Duration::from_secs(3600), 100);
assert_eq!(manager.rate_limiter_count(), 1);
assert_eq!(manager.concurrency_limiter_count(), 1);
#[cfg(feature = "quota-control")]
assert_eq!(manager.quota_limiter_count(), 1);
manager.clear_for_test();
assert_eq!(manager.rate_limiter_count(), 0);
assert_eq!(manager.concurrency_limiter_count(), 0);
#[cfg(feature = "quota-control")]
assert_eq!(manager.quota_limiter_count(), 0);
}
#[test]
fn test_debug_format() {
let manager = LimiterManager::new();
manager.get_rate_limiter("debug_test", 100, 1);
let debug_str = format!("{:?}", manager);
assert!(debug_str.contains("LimiterManager"));
assert!(debug_str.contains("rate_limiters_count"));
}
#[test]
fn test_global_limiter_manager_is_accessible() {
let limiter = GLOBAL_LIMITER_MANAGER.get_rate_limiter("global_test", 1, 1);
assert_eq!(GLOBAL_LIMITER_MANAGER.rate_limiter_count(), 1);
GLOBAL_LIMITER_MANAGER.clear_for_test();
assert_eq!(GLOBAL_LIMITER_MANAGER.rate_limiter_count(), 0);
let _ = limiter;
}
#[tokio::test]
async fn test_global_limiter_manager_rate_limiter_works() {
GLOBAL_LIMITER_MANAGER.clear_for_test();
let limiter = GLOBAL_LIMITER_MANAGER.get_rate_limiter("global_rate_test", 5, 1);
for _ in 0..5 {
assert!(limiter.allow(1).await.unwrap());
}
GLOBAL_LIMITER_MANAGER.clear_for_test();
}
#[test]
#[should_panic(expected = "rate limiter key '")]
fn test_get_rate_limiter_param_mismatch_panics() {
let manager = LimiterManager::new();
let _ = manager.get_rate_limiter("mismatch_rate", 100, 1);
let _ = manager.get_rate_limiter("mismatch_rate", 200, 1);
}
#[cfg(feature = "quota-control")]
#[test]
#[should_panic(expected = "quota limiter key '")]
fn test_get_quota_limiter_param_mismatch_panics() {
let manager = LimiterManager::new();
let _ =
manager.get_quota_limiter("mismatch_quota", std::time::Duration::from_secs(3600), 100);
let _ =
manager.get_quota_limiter("mismatch_quota", std::time::Duration::from_secs(3600), 200);
}
#[test]
#[should_panic(expected = "concurrency limiter key '")]
fn test_get_concurrency_limiter_param_mismatch_panics() {
let manager = LimiterManager::new();
let _ = manager.get_concurrency_limiter("mismatch_conc", 10);
let _ = manager.get_concurrency_limiter("mismatch_conc", 20);
}
#[test]
fn test_lru_eviction_when_capacity_exceeded() {
let manager = LimiterManager::new();
for i in 0..11 {
manager.get_rate_limiter(&format!("lru_key_{:02}", i), 100, 1);
std::thread::sleep(std::time::Duration::from_millis(2));
}
assert_eq!(manager.rate_limiter_count(), 11);
assert_eq!(manager.rate_access_times.len(), 11);
manager.cleanup_rate_limiters_to(10);
assert_eq!(
manager.rate_limiter_count(),
9,
"cleanup should leave 9 entries (target = 10 * 0.9)"
);
assert_eq!(manager.rate_access_times.len(), 9);
assert!(
!manager.rate_limiters.contains_key("lru_key_00"),
"lru_key_00 (oldest) should be evicted"
);
assert!(
!manager.rate_limiters.contains_key("lru_key_01"),
"lru_key_01 (2nd oldest) should be evicted"
);
assert!(
manager.rate_limiters.contains_key("lru_key_10"),
"lru_key_10 (newest) should remain"
);
assert!(
manager.rate_limiters.contains_key("lru_key_09"),
"lru_key_09 should remain"
);
assert_eq!(
manager.rate_limiters.len(),
manager.rate_access_times.len(),
"rate_limiters and rate_access_times must stay consistent after cleanup"
);
}
#[test]
fn test_lru_cleanup_noop_when_below_target() {
let manager = LimiterManager::new();
manager.get_rate_limiter("below_1", 100, 1);
manager.get_rate_limiter("below_2", 100, 1);
manager.get_rate_limiter("below_3", 100, 1);
manager.cleanup_rate_limiters_to(10);
assert_eq!(manager.rate_limiter_count(), 3, "no-op when below target");
assert_eq!(manager.rate_access_times.len(), 3);
}
#[test]
fn test_lru_last_access_updated_on_get() {
let manager = LimiterManager::new();
manager.get_rate_limiter("lru_update_test", 100, 1);
let first_access = manager
.rate_access_times
.get("lru_update_test")
.expect("first access_time should be recorded")
.load(Ordering::Relaxed);
std::thread::sleep(std::time::Duration::from_millis(5));
manager.get_rate_limiter("lru_update_test", 100, 1);
let second_access = manager
.rate_access_times
.get("lru_update_test")
.expect("second access_time should be recorded")
.load(Ordering::Relaxed);
assert!(
second_access > first_access,
"access_time should be updated on get: first={}, second={}",
first_access,
second_access
);
assert_eq!(manager.rate_limiter_count(), 1);
assert_eq!(manager.rate_access_times.len(), 1);
}
#[test]
fn test_concurrent_get_rate_limiter_consistency() {
let manager = LimiterManager::new();
let key = "concurrent_test_key";
std::thread::scope(|s| {
let mut handles = vec![];
for _ in 0..10 {
handles.push(s.spawn(|| manager.get_rate_limiter(key, 100, 1)));
}
let limiters: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
for l in &limiters[1..] {
assert!(
Arc::ptr_eq(&limiters[0], l),
"all threads should get same Arc (audit-H-002); got different Arc"
);
}
});
assert_eq!(
manager.rate_limiter_count(),
1,
"concurrent get should not create duplicate entries"
);
assert_eq!(manager.rate_access_times.len(), 1);
}
#[cfg(feature = "quota-control")]
#[test]
fn test_concurrent_get_quota_limiter_consistency() {
let manager = LimiterManager::new();
let key = "concurrent_quota_key";
std::thread::scope(|s| {
let mut handles = vec![];
for _ in 0..10 {
handles.push(s.spawn(|| {
manager.get_quota_limiter(key, std::time::Duration::from_secs(3600), 1000)
}));
}
let limiters: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
for l in &limiters[1..] {
assert!(
Arc::ptr_eq(&limiters[0], l),
"all threads should get same Arc (audit-H-002); got different Arc"
);
}
});
assert_eq!(
manager.quota_limiter_count(),
1,
"concurrent get should not create duplicate entries"
);
assert_eq!(manager.quota_access_times.len(), 1);
}
#[test]
fn test_concurrent_get_concurrency_limiter_consistency() {
let manager = LimiterManager::new();
let key = "concurrent_conc_key";
std::thread::scope(|s| {
let mut handles = vec![];
for _ in 0..10 {
handles.push(s.spawn(|| manager.get_concurrency_limiter(key, 10)));
}
let limiters: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
for l in &limiters[1..] {
assert!(
Arc::ptr_eq(&limiters[0], l),
"all threads should get same Arc (audit-H-002); got different Arc"
);
}
});
assert_eq!(
manager.concurrency_limiter_count(),
1,
"concurrent get should not create duplicate entries"
);
assert_eq!(manager.concurrency_access_times.len(), 1);
}
#[cfg(feature = "quota-control")]
#[test]
fn test_lru_eviction_quota() {
let manager = LimiterManager::new();
for i in 0..11 {
manager.get_quota_limiter(
&format!("quota_lru_{:02}", i),
std::time::Duration::from_secs(3600),
1000,
);
std::thread::sleep(std::time::Duration::from_millis(2));
}
assert_eq!(manager.quota_limiter_count(), 11);
manager.cleanup_quota_limiters_to(10);
assert_eq!(
manager.quota_limiter_count(),
9,
"cleanup should leave 9 entries"
);
assert_eq!(manager.quota_access_times.len(), 9);
assert!(
!manager.quota_limiters.contains_key("quota_lru_00"),
"quota_lru_00 (oldest) should be evicted"
);
assert!(
!manager.quota_limiters.contains_key("quota_lru_01"),
"quota_lru_01 (2nd oldest) should be evicted"
);
assert!(
manager.quota_limiters.contains_key("quota_lru_10"),
"quota_lru_10 (newest) should remain"
);
assert_eq!(
manager.quota_limiters.len(),
manager.quota_access_times.len(),
"quota limiters and access_times must stay consistent after cleanup"
);
}
#[test]
fn test_lru_eviction_concurrency() {
let manager = LimiterManager::new();
for i in 0..11 {
manager.get_concurrency_limiter(&format!("conc_lru_{:02}", i), 10);
std::thread::sleep(std::time::Duration::from_millis(2));
}
assert_eq!(manager.concurrency_limiter_count(), 11);
manager.cleanup_concurrency_limiters_to(10);
assert_eq!(
manager.concurrency_limiter_count(),
9,
"cleanup should leave 9 entries"
);
assert_eq!(manager.concurrency_access_times.len(), 9);
assert!(
!manager.concurrency_limiters.contains_key("conc_lru_00"),
"conc_lru_00 (oldest) should be evicted"
);
assert!(
!manager.concurrency_limiters.contains_key("conc_lru_01"),
"conc_lru_01 (2nd oldest) should be evicted"
);
assert!(
manager.concurrency_limiters.contains_key("conc_lru_10"),
"conc_lru_10 (newest) should remain"
);
assert_eq!(
manager.concurrency_limiters.len(),
manager.concurrency_access_times.len(),
"concurrency limiters and access_times must stay consistent after cleanup"
);
}
#[test]
fn test_redact_key_short() {
let redacted = redact_key("short_key");
assert_eq!(redacted, "<9 chars>");
assert!(!redacted.contains("short_key"));
}
#[test]
fn test_redact_key_long() {
let redacted = redact_key("user_id_123456789_long_key");
assert_eq!(redacted, "user_id_...(26 chars)");
assert!(!redacted.contains("user_id_123456789_long_key"));
}
#[test]
fn test_redact_key_unicode_safe() {
let redacted = redact_key("中文用户_1234567890_extra_long");
assert!(redacted.contains("..."));
assert!(redacted.contains("chars"));
}
#[test]
fn test_redact_key_boundary() {
let key_16 = "0123456789abcdef"; assert_eq!(redact_key(key_16), "<16 chars>");
let key_17 = "0123456789abcdefg"; let redacted = redact_key(key_17);
assert!(redacted.contains("...(17 chars)"));
assert!(redacted.contains("01234567")); }
}