use crate::error::{BanInfo, Decision, RateLimitMetadata, RejectionMetadata};
use oxcache::{Cache, OxCacheError};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub(crate) struct CacheableDecision {
pub decision_type: String,
pub reason: Option<String>,
pub ban_info: Option<CacheableBanInfo>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub(crate) struct CacheableBanInfo {
pub reason: String,
pub banned_until: String,
pub ban_times: u32,
}
impl CacheableDecision {
#[cfg(test)]
pub fn allowed() -> Self {
Self {
decision_type: "allowed".to_string(),
reason: None,
ban_info: None,
}
}
#[cfg(test)]
pub fn rejected(reason: impl Into<String>) -> Self {
Self {
decision_type: "rejected".to_string(),
reason: Some(reason.into()),
ban_info: None,
}
}
pub fn banned(ban_info: &BanInfo) -> Self {
Self {
decision_type: "banned".to_string(),
reason: Some(ban_info.reason().to_string()),
ban_info: Some(CacheableBanInfo {
reason: ban_info.reason().to_string(),
banned_until: ban_info.banned_until().to_rfc3339(),
ban_times: ban_info.ban_times(),
}),
}
}
pub fn from_decision(decision: &Decision) -> Self {
match decision {
Decision::Allowed(metadata) => Self {
decision_type: "allowed".to_string(),
reason: if metadata.policy.is_empty() {
None
} else {
Some(metadata.policy.clone())
},
ban_info: None,
},
Decision::Rejected(metadata) => Self {
decision_type: "rejected".to_string(),
reason: Some(metadata.reason.clone()),
ban_info: None,
},
Decision::Banned(info) => Self::banned(info),
}
}
pub fn to_decision(&self) -> Decision {
match self.decision_type.as_str() {
"allowed" => Decision::Allowed(RateLimitMetadata {
limit: 0,
remaining: 0,
reset_at: 0,
retry_after: None,
policy: self.reason.clone().unwrap_or_default(),
}),
"rejected" => Decision::Rejected(RejectionMetadata {
reason: self.reason.clone().unwrap_or_default(),
retry_after: 0,
limit: 0,
reset_at: 0,
}),
"banned" => {
if let Some(info) = &self.ban_info {
Decision::Banned(BanInfo::new(
info.reason.clone(),
chrono::DateTime::parse_from_rfc3339(&info.banned_until)
.map(|dt| dt.with_timezone(&chrono::Utc))
.unwrap_or_else(|_| chrono::Utc::now()),
info.ban_times,
))
} else {
Decision::Banned(BanInfo::new("unknown".to_string(), chrono::Utc::now(), 0))
}
}
_ => Decision::allowed_default(),
}
}
#[cfg(test)]
pub fn is_allowed(&self) -> bool {
self.decision_type == "allowed"
}
#[cfg(test)]
pub fn is_rejected(&self) -> bool {
self.decision_type == "rejected"
}
#[cfg(test)]
pub fn is_banned(&self) -> bool {
self.decision_type == "banned"
}
}
pub struct RateLimitCacheKey;
impl RateLimitCacheKey {
pub fn user_rate_limit(user_id: &str, rule_id: &str) -> String {
format!("rl:user:{}:{}", user_id, rule_id)
}
pub fn ip_rate_limit(ip: &str, rule_id: &str) -> String {
format!("rl:ip:{}:{}", ip, rule_id)
}
pub fn api_key_rate_limit(api_key: &str, rule_id: &str) -> String {
format!("rl:apikey:{}:{}", api_key, rule_id)
}
pub fn generic(identifier: &str, rule_id: &str) -> String {
format!("rl:generic:{}:{}", identifier, rule_id)
}
pub fn ban_check(identifier: &str) -> String {
format!("ban:{}", identifier)
}
pub fn user_rate_limit_with_ns(namespace: &str, user_id: &str, rule_id: &str) -> String {
format!("{}:rl:user:{}:{}", namespace, user_id, rule_id)
}
pub fn ip_rate_limit_with_ns(namespace: &str, ip: &str, rule_id: &str) -> String {
format!("{}:rl:ip:{}:{}", namespace, ip, rule_id)
}
pub fn api_key_rate_limit_with_ns(namespace: &str, api_key: &str, rule_id: &str) -> String {
format!("{}:rl:apikey:{}:{}", namespace, api_key, rule_id)
}
pub fn generic_with_ns(namespace: &str, identifier: &str, rule_id: &str) -> String {
format!("{}:rl:generic:{}:{}", namespace, identifier, rule_id)
}
pub fn ban_check_with_ns(namespace: &str, identifier: &str) -> String {
format!("{}:ban:{}", namespace, identifier)
}
}
#[derive(Debug, Clone, Default)]
pub struct L1CacheStats {
pub total_lookups: u64,
pub hits: u64,
pub misses: u64,
pub expired_evictions: u64,
pub capacity_evictions: u64,
pub current_size: usize,
pub max_size: usize,
}
impl L1CacheStats {
pub fn hit_rate(&self) -> f64 {
if self.total_lookups == 0 {
return 0.0;
}
(self.hits as f64 / self.total_lookups as f64) * 100.0
}
pub fn miss_rate(&self) -> f64 {
100.0 - self.hit_rate()
}
}
#[derive(Debug, Clone)]
pub struct L1CacheConfig {
pub default_ttl: Duration,
pub max_size: usize,
pub enable_stats: bool,
}
impl Default for L1CacheConfig {
fn default() -> Self {
Self {
default_ttl: Duration::from_secs(60),
max_size: 10_000,
enable_stats: true,
}
}
}
impl L1CacheConfig {
pub fn new(default_ttl: Duration, max_size: usize) -> Self {
Self {
default_ttl,
max_size,
enable_stats: true,
}
}
pub fn with_ttl(mut self, ttl: Duration) -> Self {
self.default_ttl = ttl;
self
}
pub fn with_max_size(mut self, max_size: usize) -> Self {
self.max_size = max_size;
self
}
pub fn with_stats(mut self, enable: bool) -> Self {
self.enable_stats = enable;
self
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum IslandFallbackStrategy {
RejectAll,
AllowAll,
#[default]
LocalDecision,
ConservativeQuota {
max_requests: u32,
window_secs: u64,
},
}
#[derive(Debug, Clone)]
pub struct IslandModeConfig {
pub enabled: bool,
pub fallback_strategy: IslandFallbackStrategy,
pub island_ttl: Duration,
pub auto_exit_on_recovery: bool,
}
impl Default for IslandModeConfig {
fn default() -> Self {
Self {
enabled: false,
fallback_strategy: IslandFallbackStrategy::default(),
island_ttl: Duration::from_secs(300), auto_exit_on_recovery: true,
}
}
}
impl IslandModeConfig {
pub fn new(fallback_strategy: IslandFallbackStrategy) -> Self {
Self {
enabled: true,
fallback_strategy,
..Default::default()
}
}
pub fn with_fallback_strategy(mut self, strategy: IslandFallbackStrategy) -> Self {
self.fallback_strategy = strategy;
self
}
pub fn with_island_ttl(mut self, ttl: Duration) -> Self {
self.island_ttl = ttl;
self
}
pub fn with_auto_exit_on_recovery(mut self, auto_exit: bool) -> Self {
self.auto_exit_on_recovery = auto_exit;
self
}
}
pub struct L1Cache<T>
where
T: Serialize + DeserializeOwned + Send + Sync + 'static,
{
cache: Arc<Cache<String, T>>,
config: L1CacheConfig,
total_lookups: AtomicU64,
hits: AtomicU64,
expired_evictions: AtomicU64,
capacity_evictions: AtomicU64,
island_config: Arc<RwLock<Option<IslandModeConfig>>>,
is_island_mode: AtomicU64,
}
impl<T> Clone for L1Cache<T>
where
T: Serialize + DeserializeOwned + Send + Sync + 'static,
{
fn clone(&self) -> Self {
Self {
cache: Arc::clone(&self.cache),
config: self.config.clone(),
total_lookups: AtomicU64::new(
self.total_lookups
.load(std::sync::atomic::Ordering::Relaxed),
),
hits: AtomicU64::new(self.hits.load(std::sync::atomic::Ordering::Relaxed)),
expired_evictions: AtomicU64::new(
self.expired_evictions
.load(std::sync::atomic::Ordering::Relaxed),
),
capacity_evictions: AtomicU64::new(
self.capacity_evictions
.load(std::sync::atomic::Ordering::Relaxed),
),
island_config: Arc::clone(&self.island_config),
is_island_mode: AtomicU64::new(
self.is_island_mode
.load(std::sync::atomic::Ordering::Relaxed),
),
}
}
}
impl<T> L1Cache<T>
where
T: Serialize + DeserializeOwned + Send + Sync + 'static,
{
pub async fn new() -> Result<Self, OxCacheError> {
Self::with_config(L1CacheConfig::default()).await
}
pub async fn with_config(config: L1CacheConfig) -> Result<Self, OxCacheError> {
let cache = Cache::builder()
.ttl(config.default_ttl)
.capacity(config.max_size as u64)
.build()
.await?;
Ok(Self {
cache: Arc::new(cache),
config,
total_lookups: AtomicU64::new(0),
hits: AtomicU64::new(0),
expired_evictions: AtomicU64::new(0),
capacity_evictions: AtomicU64::new(0),
island_config: Arc::new(RwLock::new(None)),
is_island_mode: AtomicU64::new(0),
})
}
pub async fn with_ttl_and_size(
default_ttl: Duration,
max_size: usize,
) -> Result<Self, OxCacheError> {
Self::with_config(L1CacheConfig::new(default_ttl, max_size)).await
}
pub async fn get(&self, key: &str) -> Result<Option<T>, OxCacheError> {
if self.config.enable_stats {
self.total_lookups.fetch_add(1, Ordering::Relaxed);
}
let result = self.cache.get(&key.to_string()).await?;
if self.config.enable_stats && result.is_some() {
self.hits.fetch_add(1, Ordering::Relaxed);
}
Ok(result)
}
pub async fn set(&self, key: String, value: T) -> Result<(), OxCacheError> {
self.cache.set(&key, &value).await
}
pub async fn set_with_ttl(
&self,
key: String,
value: T,
ttl: Duration,
) -> Result<(), OxCacheError> {
self.cache.set_with_ttl(&key, &value, Some(ttl)).await
}
pub async fn invalidate(&self, key: &str) -> Result<(), OxCacheError> {
self.cache.delete(&key.to_string()).await
}
pub async fn invalidate_by_prefix(&self, prefix: &str) -> Result<(), OxCacheError> {
log::warn!(
"invalidate_by_prefix called with prefix: {}. Note: This is a no-op in oxcache-based L1Cache",
prefix
);
Ok(())
}
pub async fn invalidate_containing(&self, pattern: &str) -> Result<(), OxCacheError> {
log::warn!(
"invalidate_containing called with pattern: {}. Note: This is a no-op in oxcache-based L1Cache",
pattern
);
Ok(())
}
pub async fn clear(&self) -> Result<(), OxCacheError> {
self.cache.clear().await
}
pub async fn evict_expired(&self) -> Result<usize, OxCacheError> {
Ok(0)
}
pub async fn len(&self) -> Result<usize, OxCacheError> {
let len = self.cache.len().await? as usize;
Ok(len)
}
pub async fn is_empty(&self) -> Result<bool, OxCacheError> {
let len = self.len().await?;
Ok(len == 0)
}
pub async fn stats(&self) -> L1CacheStats {
let current_size = self.len().await.unwrap_or(0);
L1CacheStats {
total_lookups: self.total_lookups.load(Ordering::Relaxed),
hits: self.hits.load(Ordering::Relaxed),
misses: self.total_lookups.load(Ordering::Relaxed) - self.hits.load(Ordering::Relaxed),
expired_evictions: self.expired_evictions.load(Ordering::Relaxed),
capacity_evictions: self.capacity_evictions.load(Ordering::Relaxed),
current_size,
max_size: self.config.max_size,
}
}
pub fn reset_stats(&self) {
self.total_lookups.store(0, Ordering::Relaxed);
self.hits.store(0, Ordering::Relaxed);
self.expired_evictions.store(0, Ordering::Relaxed);
self.capacity_evictions.store(0, Ordering::Relaxed);
}
pub async fn contains(&self, key: &str) -> Result<bool, OxCacheError> {
self.cache.exists(&key.to_string()).await
}
pub async fn ttl(&self, _key: &str) -> Result<Option<Duration>, OxCacheError> {
Ok(None)
}
pub fn enable_island_mode(&self, config: IslandModeConfig) {
let was_island = self.is_island_mode.swap(1, Ordering::AcqRel);
if was_island == 0 {
log::warn!(
target: "l1_cache",
"L1 缓存进入孤岛模式: strategy={:?}",
config.fallback_strategy
);
}
let mut island_config = self.island_config.write();
*island_config = Some(config);
}
pub fn disable_island_mode(&self) {
let was_island = self.is_island_mode.swap(0, Ordering::AcqRel);
if was_island == 1 {
log::info!(target: "l1_cache", "L1 缓存退出孤岛模式");
}
let mut island_config = self.island_config.write();
*island_config = None;
}
pub fn is_island_mode(&self) -> bool {
self.is_island_mode.load(Ordering::Acquire) == 1
}
pub fn island_config(&self) -> Option<IslandModeConfig> {
self.island_config.read().clone()
}
#[cfg(test)]
pub(crate) async fn island_stats(&self) -> L1CacheStats {
let mut stats = self.stats().await;
if self.is_island_mode() {
stats.current_size += 1; }
stats
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[tokio::test]
async fn test_cache_basic_operations() {
let cache: L1Cache<String> = L1Cache::new().await.unwrap();
cache
.set("key1".to_string(), "value1".to_string())
.await
.unwrap();
assert_eq!(cache.get("key1").await.unwrap(), Some("value1".to_string()));
assert_eq!(cache.get("key2").await.unwrap(), None);
cache.invalidate("key1").await.unwrap();
assert_eq!(cache.get("key1").await.unwrap(), None);
}
#[tokio::test]
async fn test_cache_custom_ttl() {
let config = L1CacheConfig::new(Duration::from_millis(50), 1000);
let cache: L1Cache<String> = L1Cache::with_config(config).await.unwrap();
cache
.set("key1".to_string(), "value1".to_string())
.await
.unwrap();
assert_eq!(cache.get("key1").await.unwrap(), Some("value1".to_string()));
tokio::time::sleep(Duration::from_millis(100)).await;
assert_eq!(cache.get("key1").await.unwrap(), None);
}
#[tokio::test]
async fn test_cache_stats() {
let cache: L1Cache<String> = L1Cache::new().await.unwrap();
cache
.set("key1".to_string(), "value1".to_string())
.await
.unwrap();
cache.get("key1").await.unwrap();
cache.get("key1").await.unwrap();
cache.get("key2").await.unwrap();
let stats = cache.stats().await;
assert_eq!(stats.total_lookups, 3);
assert_eq!(stats.hits, 2);
assert_eq!(stats.misses, 1);
}
#[tokio::test]
async fn test_cache_contains() {
let cache: L1Cache<String> = L1Cache::new().await.unwrap();
cache
.set("key1".to_string(), "value1".to_string())
.await
.unwrap();
assert!(cache.contains("key1").await.unwrap());
assert!(!cache.contains("key2").await.unwrap());
}
#[tokio::test]
async fn test_cache_reset_stats() {
let cache: L1Cache<String> = L1Cache::new().await.unwrap();
cache
.set("key1".to_string(), "value1".to_string())
.await
.unwrap();
cache.get("key1").await.unwrap();
cache.get("key1").await.unwrap();
let stats = cache.stats().await;
assert_eq!(stats.hits, 2);
cache.reset_stats();
let stats = cache.stats().await;
assert_eq!(stats.hits, 0);
assert_eq!(stats.total_lookups, 0);
}
#[tokio::test]
async fn test_cache_config_builder() {
let config = L1CacheConfig::default()
.with_ttl(Duration::from_secs(120))
.with_max_size(5000)
.with_stats(false);
assert_eq!(config.default_ttl, Duration::from_secs(120));
assert_eq!(config.max_size, 5000);
assert!(!config.enable_stats);
}
#[test]
fn test_cacheable_decision_conversion() {
use crate::error::{BanInfo, Decision, RateLimitMetadata};
let allowed = CacheableDecision::allowed();
assert!(allowed.is_allowed());
let decision = allowed.to_decision();
assert!(matches!(decision, Decision::Allowed(_)));
let rejected = CacheableDecision::rejected("rate limit exceeded");
assert!(rejected.is_rejected());
let decision = rejected.to_decision();
if let Decision::Rejected(metadata) = decision {
assert_eq!(metadata.reason, "rate limit exceeded");
} else {
panic!("Expected Rejected decision");
}
let ban_info = BanInfo::new("spam".to_string(), chrono::Utc::now(), 3);
let banned = CacheableDecision::banned(&ban_info);
assert!(banned.is_banned());
let metadata = RateLimitMetadata {
limit: 100,
remaining: 50,
reset_at: 1234567890,
retry_after: None,
policy: "test".to_string(),
};
let decision = Decision::Allowed(metadata);
let cacheable = CacheableDecision::from_decision(&decision);
assert!(cacheable.is_allowed());
assert_eq!(cacheable.reason, Some("test".to_string()));
}
#[test]
fn test_rate_limit_cache_key() {
let user_key = RateLimitCacheKey::user_rate_limit("user123", "rule1");
assert_eq!(user_key, "rl:user:user123:rule1");
let ip_key = RateLimitCacheKey::ip_rate_limit("192.168.1.1", "rule2");
assert_eq!(ip_key, "rl:ip:192.168.1.1:rule2");
let api_key = RateLimitCacheKey::api_key_rate_limit("api123", "rule3");
assert_eq!(api_key, "rl:apikey:api123:rule3");
let generic_key = RateLimitCacheKey::generic("identifier", "rule4");
assert_eq!(generic_key, "rl:generic:identifier:rule4");
let ban_key = RateLimitCacheKey::ban_check("user123");
assert_eq!(ban_key, "ban:user123");
}
#[test]
fn test_cacheable_decision_from_decision_rejected() {
let metadata = RejectionMetadata {
reason: "too many requests".to_string(),
retry_after: 30,
limit: 100,
reset_at: 1234567890,
};
let decision = Decision::Rejected(metadata);
let cacheable = CacheableDecision::from_decision(&decision);
assert!(cacheable.is_rejected());
assert_eq!(cacheable.reason, Some("too many requests".to_string()));
}
#[test]
fn test_cacheable_decision_from_decision_banned() {
let ban_info = BanInfo::new("spam".to_string(), chrono::Utc::now(), 3);
let decision = Decision::Banned(ban_info);
let cacheable = CacheableDecision::from_decision(&decision);
assert!(cacheable.is_banned());
let cached_ban = cacheable.ban_info.unwrap();
assert_eq!(cached_ban.reason, "spam");
assert_eq!(cached_ban.ban_times, 3);
}
#[test]
fn test_cacheable_decision_from_decision_allowed_empty_policy() {
let metadata = RateLimitMetadata {
limit: 100,
remaining: 50,
reset_at: 1234567890,
retry_after: None,
policy: String::new(),
};
let decision = Decision::Allowed(metadata);
let cacheable = CacheableDecision::from_decision(&decision);
assert!(cacheable.is_allowed());
assert_eq!(cacheable.reason, None);
}
#[test]
fn test_cacheable_decision_to_decision_unknown() {
let cd = CacheableDecision {
decision_type: "unknown".to_string(),
reason: None,
ban_info: None,
};
let decision = cd.to_decision();
assert!(matches!(decision, Decision::Allowed(_)));
}
#[test]
fn test_cacheable_decision_to_decision_banned_no_baninfo() {
let cd = CacheableDecision {
decision_type: "banned".to_string(),
reason: Some("test".to_string()),
ban_info: None,
};
let decision = cd.to_decision();
assert!(matches!(decision, Decision::Banned(_)));
}
#[test]
fn test_cacheable_decision_to_decision_banned_with_baninfo() {
let cd = CacheableDecision {
decision_type: "banned".to_string(),
reason: Some("banned".to_string()),
ban_info: Some(CacheableBanInfo {
reason: "policy violation".to_string(),
banned_until: "2026-12-31T23:59:59Z".to_string(),
ban_times: 5,
}),
};
let decision = cd.to_decision();
match decision {
Decision::Banned(info) => {
assert_eq!(info.reason(), "policy violation");
assert_eq!(info.ban_times(), 5);
}
_ => panic!("expected Decision::Banned"),
}
}
#[test]
fn test_cacheable_decision_to_decision_banned_invalid_date() {
let cd = CacheableDecision {
decision_type: "banned".to_string(),
reason: Some("banned".to_string()),
ban_info: Some(CacheableBanInfo {
reason: "bad date".to_string(),
banned_until: "not-a-date".to_string(),
ban_times: 1,
}),
};
let decision = cd.to_decision();
assert!(matches!(decision, Decision::Banned(_)));
}
#[test]
fn test_rate_limit_cache_key_with_namespace() {
let ns = "tenant:acme:env:prod";
assert_eq!(
RateLimitCacheKey::user_rate_limit_with_ns(ns, "user123", "rule1"),
"tenant:acme:env:prod:rl:user:user123:rule1"
);
assert_eq!(
RateLimitCacheKey::ip_rate_limit_with_ns(ns, "10.0.0.1", "rule2"),
"tenant:acme:env:prod:rl:ip:10.0.0.1:rule2"
);
assert_eq!(
RateLimitCacheKey::api_key_rate_limit_with_ns(ns, "key123", "rule3"),
"tenant:acme:env:prod:rl:apikey:key123:rule3"
);
assert_eq!(
RateLimitCacheKey::generic_with_ns(ns, "ident", "rule4"),
"tenant:acme:env:prod:rl:generic:ident:rule4"
);
assert_eq!(
RateLimitCacheKey::ban_check_with_ns(ns, "user123"),
"tenant:acme:env:prod:ban:user123"
);
}
#[test]
fn test_cache_stats_hit_rate_empty() {
let stats = L1CacheStats::default();
assert_eq!(stats.hit_rate(), 0.0);
assert_eq!(stats.miss_rate(), 100.0);
}
#[test]
fn test_cache_stats_methods() {
let stats = L1CacheStats {
total_lookups: 100,
hits: 75,
misses: 25,
..Default::default()
};
assert_eq!(stats.hit_rate(), 75.0);
assert_eq!(stats.miss_rate(), 25.0);
}
#[test]
fn test_cache_stats_debug() {
let stats = L1CacheStats::default();
let debug = format!("{:?}", stats);
assert!(debug.starts_with("L1CacheStats {"));
assert!(debug.contains("total_lookups"));
}
#[test]
fn test_cache_config_new() {
let config = L1CacheConfig::new(Duration::from_secs(30), 500);
assert_eq!(config.default_ttl, Duration::from_secs(30));
assert_eq!(config.max_size, 500);
assert!(config.enable_stats);
}
#[test]
fn test_island_fallback_strategy_default() {
assert_eq!(
IslandFallbackStrategy::default(),
IslandFallbackStrategy::LocalDecision
);
}
#[test]
fn test_island_mode_config_default_and_builder() {
let default = IslandModeConfig::default();
assert!(!default.enabled);
assert_eq!(
default.fallback_strategy,
IslandFallbackStrategy::LocalDecision
);
assert_eq!(default.island_ttl, Duration::from_secs(300));
assert!(default.auto_exit_on_recovery);
let built = IslandModeConfig::default()
.with_fallback_strategy(IslandFallbackStrategy::AllowAll)
.with_island_ttl(Duration::from_secs(600))
.with_auto_exit_on_recovery(false);
assert_eq!(built.fallback_strategy, IslandFallbackStrategy::AllowAll);
assert_eq!(built.island_ttl, Duration::from_secs(600));
assert!(!built.auto_exit_on_recovery);
}
#[test]
fn test_island_mode_config_new() {
let config = IslandModeConfig::new(IslandFallbackStrategy::RejectAll);
assert!(config.enabled);
assert_eq!(config.fallback_strategy, IslandFallbackStrategy::RejectAll);
let config2 = IslandModeConfig::new(IslandFallbackStrategy::ConservativeQuota {
max_requests: 100,
window_secs: 60,
});
assert!(config2.enabled);
assert!(matches!(
config2.fallback_strategy,
IslandFallbackStrategy::ConservativeQuota { .. }
));
}
#[tokio::test]
async fn test_cache_with_ttl_and_size() {
let cache: L1Cache<String> = L1Cache::with_ttl_and_size(Duration::from_secs(60), 100)
.await
.unwrap();
cache.set("k".to_string(), "v".to_string()).await.unwrap();
assert_eq!(cache.get("k").await.unwrap(), Some("v".to_string()));
}
#[tokio::test]
async fn test_cache_set_with_ttl() {
let cache: L1Cache<String> = L1Cache::new().await.unwrap();
cache
.set_with_ttl("k".to_string(), "v".to_string(), Duration::from_secs(60))
.await
.unwrap();
assert_eq!(cache.get("k").await.unwrap(), Some("v".to_string()));
}
#[tokio::test]
async fn test_cache_clear_and_len() {
let cache: L1Cache<String> = L1Cache::new().await.unwrap();
let _ = cache.len().await.unwrap();
let _ = cache.is_empty().await.unwrap();
cache.set("k1".to_string(), "v1".to_string()).await.unwrap();
cache.set("k2".to_string(), "v2".to_string()).await.unwrap();
assert_eq!(cache.get("k1").await.unwrap(), Some("v1".to_string()));
assert_eq!(cache.get("k2").await.unwrap(), Some("v2".to_string()));
cache.clear().await.unwrap();
assert_eq!(cache.get("k1").await.unwrap(), None);
assert_eq!(cache.get("k2").await.unwrap(), None);
}
#[tokio::test]
async fn test_cache_evict_expired() {
let cache: L1Cache<String> = L1Cache::new().await.unwrap();
assert_eq!(cache.evict_expired().await.unwrap(), 0);
}
#[tokio::test]
async fn test_cache_ttl_method() {
let cache: L1Cache<String> = L1Cache::new().await.unwrap();
cache.set("k".to_string(), "v".to_string()).await.unwrap();
assert!(cache.ttl("k").await.unwrap().is_none());
assert!(cache.ttl("nonexistent").await.unwrap().is_none());
}
#[tokio::test]
async fn test_cache_invalidate_prefix_and_containing() {
let cache: L1Cache<String> = L1Cache::new().await.unwrap();
cache.invalidate_by_prefix("test:").await.unwrap();
cache.invalidate_containing("pattern").await.unwrap();
}
#[tokio::test]
async fn test_cache_empty_key() {
let cache: L1Cache<String> = L1Cache::new().await.unwrap();
cache
.set(String::new(), "empty_val".to_string())
.await
.unwrap();
assert_eq!(cache.get("").await.unwrap(), Some("empty_val".to_string()));
cache.invalidate("").await.unwrap();
assert_eq!(cache.get("").await.unwrap(), None);
}
#[tokio::test]
async fn test_cache_stats_disabled() {
let config = L1CacheConfig::new(Duration::from_secs(60), 1000).with_stats(false);
let cache: L1Cache<String> = L1Cache::with_config(config).await.unwrap();
cache.set("k".to_string(), "v".to_string()).await.unwrap();
let _ = cache.get("k").await.unwrap();
let _ = cache.get("nonexistent").await.unwrap();
let stats = cache.stats().await;
assert_eq!(stats.total_lookups, 0);
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 0);
}
#[tokio::test]
async fn test_cache_island_mode() {
let cache: L1Cache<String> = L1Cache::new().await.unwrap();
assert!(!cache.is_island_mode());
assert!(cache.island_config().is_none());
let island_cfg = IslandModeConfig::new(IslandFallbackStrategy::RejectAll);
cache.enable_island_mode(island_cfg);
assert!(cache.is_island_mode());
let cfg = cache.island_config().unwrap();
assert_eq!(cfg.fallback_strategy, IslandFallbackStrategy::RejectAll);
cache.enable_island_mode(IslandModeConfig::new(IslandFallbackStrategy::AllowAll));
cache.disable_island_mode();
assert!(!cache.is_island_mode());
assert!(cache.island_config().is_none());
cache.disable_island_mode();
}
#[tokio::test]
async fn test_cache_island_stats() {
let cache: L1Cache<String> = L1Cache::new().await.unwrap();
let normal_stats = cache.island_stats().await;
assert_eq!(normal_stats.total_lookups, 0);
cache.enable_island_mode(IslandModeConfig::new(IslandFallbackStrategy::LocalDecision));
let island_stats = cache.island_stats().await;
assert_eq!(island_stats.current_size, normal_stats.current_size + 1);
cache.disable_island_mode();
}
#[tokio::test]
async fn test_cache_clone() {
let cache: L1Cache<String> = L1Cache::new().await.unwrap();
cache.set("k".to_string(), "v".to_string()).await.unwrap();
let _ = cache.get("k").await.unwrap();
let cloned = cache.clone();
assert_eq!(cloned.get("k").await.unwrap(), Some("v".to_string()));
assert_eq!(cache.get("k").await.unwrap(), Some("v".to_string()));
}
#[tokio::test]
async fn test_cache_concurrent_access() {
let cache: L1Cache<String> = L1Cache::new().await.unwrap();
let mut handles = Vec::new();
for i in 0..10 {
let c = cache.clone();
handles.push(tokio::spawn(async move {
let key = format!("concurrent_key_{}", i);
let val = format!("concurrent_val_{}", i);
c.set(key.clone(), val.clone()).await.unwrap();
let result = c.get(&key).await.unwrap();
assert_eq!(result, Some(val));
}));
}
for handle in handles {
handle.await.unwrap();
}
for i in 0..10 {
let key = format!("concurrent_key_{}", i);
assert_eq!(
cache.get(&key).await.unwrap(),
Some(format!("concurrent_val_{}", i))
);
}
}
}