use super::cache_trait::Cache;
use super::entry::CacheEntry;
use super::layered::LayeredCacheStore;
use super::statistics::{CacheEntryInfo, CacheStatistics};
use async_trait::async_trait;
use reinhardt_core::exception::{Error, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, SystemTime};
use tokio::sync::RwLock;
use tokio::task::AbortHandle;
#[derive(Clone, Copy, Debug)]
pub enum CleanupStrategy {
Naive,
Layered,
}
#[derive(Clone)]
pub struct InMemoryCache {
store: Arc<RwLock<HashMap<String, CacheEntry>>>,
layered_store: Option<LayeredCacheStore>,
cleanup_strategy: CleanupStrategy,
default_ttl: Option<Duration>,
hits: Arc<AtomicU64>,
misses: Arc<AtomicU64>,
cleanup_interval: Option<Duration>,
cleanup_handle: Arc<std::sync::Mutex<Option<AbortHandle>>>,
}
impl InMemoryCache {
pub fn new() -> Self {
Self {
store: Arc::new(RwLock::new(HashMap::new())),
layered_store: None,
cleanup_strategy: CleanupStrategy::Naive,
default_ttl: None,
hits: Arc::new(AtomicU64::new(0)),
misses: Arc::new(AtomicU64::new(0)),
cleanup_interval: None,
cleanup_handle: Arc::new(std::sync::Mutex::new(None)),
}
}
pub fn with_layered_cleanup() -> Self {
Self {
store: Arc::new(RwLock::new(HashMap::new())),
layered_store: Some(LayeredCacheStore::new()),
cleanup_strategy: CleanupStrategy::Layered,
default_ttl: None,
hits: Arc::new(AtomicU64::new(0)),
misses: Arc::new(AtomicU64::new(0)),
cleanup_interval: None,
cleanup_handle: Arc::new(std::sync::Mutex::new(None)),
}
}
pub fn with_custom_layered_cleanup(sample_size: usize, threshold: f32) -> Self {
Self {
store: Arc::new(RwLock::new(HashMap::new())),
layered_store: Some(LayeredCacheStore::with_sampler(sample_size, threshold)),
cleanup_strategy: CleanupStrategy::Layered,
default_ttl: None,
hits: Arc::new(AtomicU64::new(0)),
misses: Arc::new(AtomicU64::new(0)),
cleanup_interval: None,
cleanup_handle: Arc::new(std::sync::Mutex::new(None)),
}
}
pub fn with_default_ttl(mut self, ttl: Duration) -> Self {
self.default_ttl = Some(ttl);
self
}
pub async fn cleanup_expired(&self) {
match self.cleanup_strategy {
CleanupStrategy::Naive => {
let mut store = self.store.write().await;
store.retain(|_, entry| !entry.is_expired());
}
CleanupStrategy::Layered => {
if let Some(ref layered_store) = self.layered_store {
layered_store.cleanup().await;
}
}
}
}
pub async fn get_statistics(&self) -> CacheStatistics {
let hits = self.hits.load(Ordering::Relaxed);
let misses = self.misses.load(Ordering::Relaxed);
let (entry_count, memory_usage) = match self.cleanup_strategy {
CleanupStrategy::Naive => {
let store = self.store.read().await;
let entry_count = store.len() as u64;
let memory_usage = store
.values()
.map(|entry| entry.value.len() as u64)
.sum::<u64>();
(entry_count, memory_usage)
}
CleanupStrategy::Layered => {
if let Some(ref layered_store) = self.layered_store {
let store_clone = layered_store.get_store_clone().await;
let entry_count = store_clone.len() as u64;
let memory_usage = store_clone
.values()
.map(|entry| entry.value.len() as u64)
.sum::<u64>();
(entry_count, memory_usage)
} else {
(0, 0)
}
}
};
CacheStatistics {
hits,
misses,
total_requests: hits + misses,
entry_count,
memory_usage,
}
}
pub async fn list_keys(&self) -> Vec<String> {
match self.cleanup_strategy {
CleanupStrategy::Naive => {
let store = self.store.read().await;
store.keys().cloned().collect()
}
CleanupStrategy::Layered => {
if let Some(ref layered_store) = self.layered_store {
layered_store.keys().await
} else {
Vec::new()
}
}
}
}
pub async fn inspect_entry_with_timestamps(
&self,
key: &str,
) -> Result<Option<(SystemTime, Option<SystemTime>)>> {
match self.cleanup_strategy {
CleanupStrategy::Naive => {
let store = self.store.read().await;
if let Some(entry) = store.get(key) {
if entry.is_expired() {
return Ok(None);
}
Ok(Some((entry.created_at, entry.accessed_at)))
} else {
Ok(None)
}
}
CleanupStrategy::Layered => {
if let Some(ref layered_store) = self.layered_store {
Ok(layered_store.get_entry_timestamps(key).await)
} else {
Ok(None)
}
}
}
}
pub async fn inspect_entry(&self, key: &str) -> Option<CacheEntryInfo> {
let entry = match self.cleanup_strategy {
CleanupStrategy::Naive => {
let store = self.store.read().await;
store.get(key).cloned()
}
CleanupStrategy::Layered => {
if let Some(ref layered_store) = self.layered_store {
layered_store.get_entry(key).await
} else {
None
}
}
};
entry.map(|entry| {
let ttl_seconds = entry.expires_at.and_then(|expires_at| {
expires_at
.duration_since(SystemTime::now())
.ok()
.map(|d| d.as_secs())
});
CacheEntryInfo {
key: key.to_string(),
size: entry.value.len(),
has_expiry: entry.expires_at.is_some(),
ttl_seconds,
}
})
}
pub fn start_auto_cleanup(&self, interval: Duration) {
let mut handle_guard = self
.cleanup_handle
.lock()
.unwrap_or_else(|e| e.into_inner());
if let Some(existing) = handle_guard.take() {
existing.abort();
}
let cache = self.clone();
let abort_handle = tokio::spawn(async move {
let mut interval_timer = tokio::time::interval(interval);
loop {
interval_timer.tick().await;
cache.cleanup_expired().await;
}
})
.abort_handle();
*handle_guard = Some(abort_handle);
}
pub fn stop_auto_cleanup(&self) {
let mut handle_guard = self
.cleanup_handle
.lock()
.unwrap_or_else(|e| e.into_inner());
if let Some(handle) = handle_guard.take() {
handle.abort();
}
}
pub fn with_auto_cleanup(mut self, interval: Duration) -> Self {
self.cleanup_interval = Some(interval);
self.start_auto_cleanup(interval);
self
}
}
impl Default for InMemoryCache {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Cache for InMemoryCache {
async fn get<T>(&self, key: &str) -> Result<Option<T>>
where
T: for<'de> Deserialize<'de> + Send,
{
match self.cleanup_strategy {
CleanupStrategy::Naive => {
let mut store = self.store.write().await;
if let Some(entry) = store.get_mut(key) {
if entry.is_expired() {
self.misses.fetch_add(1, Ordering::Relaxed);
return Ok(None);
}
entry.touch();
self.hits.fetch_add(1, Ordering::Relaxed);
let value = serde_json::from_slice(&entry.value)
.map_err(|e| Error::Serialization(e.to_string()))?;
Ok(Some(value))
} else {
self.misses.fetch_add(1, Ordering::Relaxed);
Ok(None)
}
}
CleanupStrategy::Layered => {
if let Some(ref layered_store) = self.layered_store {
if let Some(data) = layered_store.get(key).await {
self.hits.fetch_add(1, Ordering::Relaxed);
let value = serde_json::from_slice(&data)
.map_err(|e| Error::Serialization(e.to_string()))?;
Ok(Some(value))
} else {
self.misses.fetch_add(1, Ordering::Relaxed);
Ok(None)
}
} else {
self.misses.fetch_add(1, Ordering::Relaxed);
Ok(None)
}
}
}
}
async fn set<T>(&self, key: &str, value: &T, ttl: Option<Duration>) -> Result<()>
where
T: Serialize + Send + Sync,
{
let serialized =
serde_json::to_vec(value).map_err(|e| Error::Serialization(e.to_string()))?;
let ttl = ttl.or(self.default_ttl);
match self.cleanup_strategy {
CleanupStrategy::Naive => {
let entry = CacheEntry::new(serialized, ttl);
let mut store = self.store.write().await;
store.insert(key.to_string(), entry);
}
CleanupStrategy::Layered => {
if let Some(ref layered_store) = self.layered_store {
layered_store.set(key.to_string(), serialized, ttl).await;
}
}
}
Ok(())
}
async fn delete(&self, key: &str) -> Result<()> {
match self.cleanup_strategy {
CleanupStrategy::Naive => {
let mut store = self.store.write().await;
store.remove(key);
}
CleanupStrategy::Layered => {
if let Some(ref layered_store) = self.layered_store {
layered_store.delete(key).await;
}
}
}
Ok(())
}
async fn has_key(&self, key: &str) -> Result<bool> {
match self.cleanup_strategy {
CleanupStrategy::Naive => {
let store = self.store.read().await;
if let Some(entry) = store.get(key) {
Ok(!entry.is_expired())
} else {
Ok(false)
}
}
CleanupStrategy::Layered => {
if let Some(ref layered_store) = self.layered_store {
Ok(layered_store.has_key(key).await)
} else {
Ok(false)
}
}
}
}
async fn clear(&self) -> Result<()> {
match self.cleanup_strategy {
CleanupStrategy::Naive => {
let mut store = self.store.write().await;
store.clear();
}
CleanupStrategy::Layered => {
if let Some(ref layered_store) = self.layered_store {
layered_store.clear().await;
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
async fn poll_until<F, Fut>(
timeout: std::time::Duration,
interval: std::time::Duration,
mut condition: F,
) -> std::result::Result<(), String>
where
F: FnMut() -> Fut,
Fut: std::future::Future<Output = bool>,
{
let start = std::time::Instant::now();
while start.elapsed() < timeout {
if condition().await {
return Ok(());
}
tokio::time::sleep(interval).await;
}
Err(format!("Timeout after {:?} waiting for condition", timeout))
}
#[tokio::test]
async fn test_in_memory_cache_basic() {
let cache = InMemoryCache::new();
cache.set("key1", &"value1", None).await.unwrap();
let value: Option<String> = cache.get("key1").await.unwrap();
assert_eq!(value, Some("value1".to_string()));
assert!(cache.has_key("key1").await.unwrap());
assert!(!cache.has_key("key2").await.unwrap());
cache.delete("key1").await.unwrap();
let value: Option<String> = cache.get("key1").await.unwrap();
assert_eq!(value, None);
}
#[tokio::test]
async fn test_in_memory_cache_ttl() {
let cache = InMemoryCache::new();
cache
.set("key1", &"value1", Some(Duration::from_millis(100)))
.await
.unwrap();
let value: Option<String> = cache.get("key1").await.unwrap();
assert_eq!(value, Some("value1".to_string()));
poll_until(
Duration::from_millis(200),
Duration::from_millis(10),
|| async {
let value: Option<String> = cache.get("key1").await.unwrap();
value.is_none()
},
)
.await
.expect("Key should expire within 200ms");
}
#[tokio::test]
async fn test_in_memory_cache_many() {
let cache = InMemoryCache::new();
let mut values = HashMap::new();
values.insert("key1".to_string(), "value1".to_string());
values.insert("key2".to_string(), "value2".to_string());
cache.set_many(values, None).await.unwrap();
let results: HashMap<String, String> =
cache.get_many(&["key1", "key2", "key3"]).await.unwrap();
assert_eq!(results.len(), 2);
assert_eq!(results.get("key1"), Some(&"value1".to_string()));
assert_eq!(results.get("key2"), Some(&"value2".to_string()));
cache.delete_many(&["key1", "key2"]).await.unwrap();
assert!(!cache.has_key("key1").await.unwrap());
assert!(!cache.has_key("key2").await.unwrap());
}
#[tokio::test]
async fn test_in_memory_cache_incr_decr() {
let cache = InMemoryCache::new();
let value = cache.incr("counter", 5).await.unwrap();
assert_eq!(value, 5);
let value = cache.incr("counter", 3).await.unwrap();
assert_eq!(value, 8);
let value = cache.decr("counter", 2).await.unwrap();
assert_eq!(value, 6);
}
#[tokio::test]
async fn test_in_memory_cache_clear() {
let cache = InMemoryCache::new();
cache.set("key1", &"value1", None).await.unwrap();
cache.set("key2", &"value2", None).await.unwrap();
cache.clear().await.unwrap();
assert!(!cache.has_key("key1").await.unwrap());
assert!(!cache.has_key("key2").await.unwrap());
}
#[tokio::test]
async fn test_cache_cleanup_expired() {
let cache = InMemoryCache::new();
cache
.set("key1", &"value1", Some(Duration::from_millis(100)))
.await
.unwrap();
cache.set("key2", &"value2", None).await.unwrap();
poll_until(
Duration::from_millis(200),
Duration::from_millis(10),
|| async {
let value: Option<String> = cache.get("key1").await.unwrap();
value.is_none()
},
)
.await
.expect("Key1 should expire within 200ms");
cache.cleanup_expired().await;
assert!(!cache.has_key("key1").await.unwrap());
assert!(cache.has_key("key2").await.unwrap());
}
#[tokio::test]
async fn test_cache_statistics_basic() {
let cache = InMemoryCache::new();
let stats = cache.get_statistics().await;
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 0);
assert_eq!(stats.total_requests, 0);
assert_eq!(stats.entry_count, 0);
assert_eq!(stats.memory_usage, 0);
cache.set("key1", &"value1", None).await.unwrap();
cache.set("key2", &"value2", None).await.unwrap();
let stats = cache.get_statistics().await;
assert_eq!(stats.entry_count, 2);
assert!(stats.memory_usage > 0);
let _: Option<String> = cache.get("key1").await.unwrap();
let _: Option<String> = cache.get("key2").await.unwrap();
let stats = cache.get_statistics().await;
assert_eq!(stats.hits, 2);
assert_eq!(stats.misses, 0);
assert_eq!(stats.total_requests, 2);
let _: Option<String> = cache.get("key3").await.unwrap();
let stats = cache.get_statistics().await;
assert_eq!(stats.hits, 2);
assert_eq!(stats.misses, 1);
assert_eq!(stats.total_requests, 3);
}
#[tokio::test]
async fn test_cache_statistics_hit_miss_rate() {
let cache = InMemoryCache::new();
cache.set("key1", &"value1", None).await.unwrap();
cache.set("key2", &"value2", None).await.unwrap();
let _: Option<String> = cache.get("key1").await.unwrap();
let _: Option<String> = cache.get("key2").await.unwrap();
let _: Option<String> = cache.get("key3").await.unwrap();
let stats = cache.get_statistics().await;
assert_eq!(stats.hit_rate(), 2.0 / 3.0);
assert_eq!(stats.miss_rate(), 1.0 / 3.0);
}
#[tokio::test]
async fn test_cache_statistics_expired_counts_as_miss() {
let cache = InMemoryCache::new();
cache
.set("key1", &"value1", Some(Duration::from_millis(10)))
.await
.unwrap();
tokio::time::sleep(Duration::from_millis(15)).await;
let value: Option<String> = cache.get("key1").await.unwrap();
assert!(value.is_none(), "Key should have expired");
let stats = cache.get_statistics().await;
assert_eq!(stats.hits, 0, "Expected 0 hits, got {}", stats.hits);
assert_eq!(stats.misses, 1, "Expected 1 miss, got {}", stats.misses);
}
#[tokio::test]
async fn test_cache_statistics_memory_usage() {
let cache = InMemoryCache::new();
cache.set("key1", &"short", None).await.unwrap();
cache.set("key2", &"a longer value", None).await.unwrap();
let stats = cache.get_statistics().await;
assert!(stats.memory_usage > 0);
let initial_usage = stats.memory_usage;
cache
.set("key3", &"even longer value here", None)
.await
.unwrap();
let stats = cache.get_statistics().await;
assert!(stats.memory_usage > initial_usage);
}
#[tokio::test]
async fn test_list_keys() {
let cache = InMemoryCache::new();
let keys = cache.list_keys().await;
assert_eq!(keys.len(), 0);
cache.set("key1", &"value1", None).await.unwrap();
cache.set("key2", &"value2", None).await.unwrap();
cache.set("key3", &"value3", None).await.unwrap();
let keys = cache.list_keys().await;
assert_eq!(keys.len(), 3);
assert!(keys.contains(&"key1".to_string()));
assert!(keys.contains(&"key2".to_string()));
assert!(keys.contains(&"key3".to_string()));
cache.delete("key2").await.unwrap();
let keys = cache.list_keys().await;
assert_eq!(keys.len(), 2);
assert!(keys.contains(&"key1".to_string()));
assert!(!keys.contains(&"key2".to_string()));
assert!(keys.contains(&"key3".to_string()));
}
#[tokio::test]
async fn test_list_keys_includes_expired() {
let cache = InMemoryCache::new();
cache
.set("expired_key", &"value", Some(Duration::from_millis(10)))
.await
.unwrap();
cache.set("valid_key", &"value", None).await.unwrap();
poll_until(
Duration::from_millis(50),
Duration::from_millis(5),
|| async {
let value: Option<String> = cache.get("expired_key").await.unwrap();
value.is_none()
},
)
.await
.expect("Expired key should expire within 50ms");
let keys = cache.list_keys().await;
assert_eq!(keys.len(), 2);
assert!(keys.contains(&"expired_key".to_string()));
assert!(keys.contains(&"valid_key".to_string()));
cache.cleanup_expired().await;
let keys = cache.list_keys().await;
assert_eq!(keys.len(), 1);
assert!(!keys.contains(&"expired_key".to_string()));
assert!(keys.contains(&"valid_key".to_string()));
}
#[tokio::test]
async fn test_inspect_entry_basic() {
let cache = InMemoryCache::new();
let info = cache.inspect_entry("nonexistent").await;
assert!(info.is_none());
cache.set("key1", &"value1", None).await.unwrap();
let info = cache.inspect_entry("key1").await;
let info = info.unwrap();
assert_eq!(info.key, "key1");
assert!(!info.has_expiry);
assert!(info.ttl_seconds.is_none());
assert!(info.size > 0);
}
#[tokio::test]
async fn test_inspect_entry_with_ttl() {
let cache = InMemoryCache::new();
cache
.set("key1", &"value1", Some(Duration::from_secs(300)))
.await
.unwrap();
let info = cache.inspect_entry("key1").await;
let info = info.unwrap();
assert_eq!(info.key, "key1");
assert!(info.has_expiry);
assert!(info.ttl_seconds.is_some());
let ttl = info.ttl_seconds.unwrap();
assert!(ttl <= 300);
assert!(ttl > 0);
}
#[tokio::test]
async fn test_inspect_entry_size() {
let cache = InMemoryCache::new();
cache.set("small", &"x", None).await.unwrap();
cache.set("large", &"x".repeat(1000), None).await.unwrap();
let small_info = cache.inspect_entry("small").await.unwrap();
let large_info = cache.inspect_entry("large").await.unwrap();
assert!(large_info.size > small_info.size);
}
#[tokio::test]
async fn test_inspect_entry_expired() {
let cache = InMemoryCache::new();
cache
.set("key1", &"value1", Some(Duration::from_millis(10)))
.await
.unwrap();
let info = cache.inspect_entry("key1").await;
assert!(info.is_some());
poll_until(
Duration::from_millis(50),
Duration::from_millis(5),
|| async {
let value: Option<String> = cache.get("key1").await.unwrap();
value.is_none()
},
)
.await
.expect("Key should expire within 50ms");
let info = cache.inspect_entry("key1").await;
let info = info.unwrap();
assert!(info.has_expiry);
assert!(info.ttl_seconds.is_none());
cache.cleanup_expired().await;
let info = cache.inspect_entry("key1").await;
assert!(info.is_none());
}
#[tokio::test]
async fn test_start_auto_cleanup() {
let cache = InMemoryCache::new();
cache
.set("key1", &"value1", Some(Duration::from_millis(50)))
.await
.unwrap();
cache
.set("key2", &"value2", Some(Duration::from_millis(50)))
.await
.unwrap();
cache.start_auto_cleanup(Duration::from_millis(30));
assert!(cache.has_key("key1").await.unwrap());
assert!(cache.has_key("key2").await.unwrap());
poll_until(
Duration::from_millis(200),
Duration::from_millis(10),
|| async {
!cache.has_key("key1").await.unwrap() && !cache.has_key("key2").await.unwrap()
},
)
.await
.expect("Keys should be auto-cleaned within 200ms");
assert!(!cache.has_key("key1").await.unwrap());
assert!(!cache.has_key("key2").await.unwrap());
}
#[tokio::test]
async fn test_with_auto_cleanup() {
let cache = InMemoryCache::new().with_auto_cleanup(Duration::from_millis(30));
cache
.set("key1", &"value1", Some(Duration::from_millis(50)))
.await
.unwrap();
cache
.set("key2", &"value2", Some(Duration::from_millis(50)))
.await
.unwrap();
assert!(cache.has_key("key1").await.unwrap());
assert!(cache.has_key("key2").await.unwrap());
poll_until(
Duration::from_millis(200),
Duration::from_millis(10),
|| async {
!cache.has_key("key1").await.unwrap() && !cache.has_key("key2").await.unwrap()
},
)
.await
.expect("Keys should be auto-cleaned within 200ms");
}
#[tokio::test]
async fn test_stop_auto_cleanup() {
let cache = InMemoryCache::new();
cache.start_auto_cleanup(Duration::from_millis(30));
cache
.set("key1", &"value1", Some(Duration::from_millis(50)))
.await
.unwrap();
cache.stop_auto_cleanup();
tokio::time::sleep(Duration::from_millis(150)).await;
let value: Option<String> = cache.get("key1").await.unwrap();
assert!(value.is_none(), "Key should be expired");
}
#[tokio::test]
async fn test_start_auto_cleanup_replaces_previous() {
let cache = InMemoryCache::new();
cache.start_auto_cleanup(Duration::from_millis(30));
cache.start_auto_cleanup(Duration::from_millis(30));
cache
.set("key1", &"value1", Some(Duration::from_millis(50)))
.await
.unwrap();
poll_until(
Duration::from_millis(200),
Duration::from_millis(10),
|| async { !cache.has_key("key1").await.unwrap() },
)
.await
.expect("Key should be cleaned up");
}
#[tokio::test]
async fn test_auto_cleanup_preserves_non_expired() {
let cache = InMemoryCache::new();
cache.start_auto_cleanup(Duration::from_millis(30));
cache
.set("short_lived", &"value1", Some(Duration::from_millis(50)))
.await
.unwrap();
cache.set("long_lived", &"value2", None).await.unwrap();
assert!(cache.has_key("short_lived").await.unwrap());
assert!(cache.has_key("long_lived").await.unwrap());
poll_until(
Duration::from_millis(200),
Duration::from_millis(10),
|| async {
!cache.has_key("short_lived").await.unwrap()
&& cache.has_key("long_lived").await.unwrap()
},
)
.await
.expect("Short-lived key should be cleaned, long-lived should remain");
}
#[tokio::test]
async fn test_layered_cache_basic() {
let cache = InMemoryCache::with_layered_cleanup();
cache.set("key1", &"value1", None).await.unwrap();
let value: Option<String> = cache.get("key1").await.unwrap();
assert_eq!(value, Some("value1".to_string()));
assert!(cache.has_key("key1").await.unwrap());
assert!(!cache.has_key("key2").await.unwrap());
cache.delete("key1").await.unwrap();
let value: Option<String> = cache.get("key1").await.unwrap();
assert_eq!(value, None);
}
#[tokio::test]
async fn test_layered_cache_ttl() {
let cache = InMemoryCache::with_layered_cleanup();
cache
.set("key1", &"value1", Some(Duration::from_millis(100)))
.await
.unwrap();
let value: Option<String> = cache.get("key1").await.unwrap();
assert_eq!(value, Some("value1".to_string()));
poll_until(
Duration::from_millis(200),
Duration::from_millis(10),
|| async {
let value: Option<String> = cache.get("key1").await.unwrap();
value.is_none()
},
)
.await
.expect("Key should expire within 200ms");
}
#[tokio::test]
async fn test_layered_cleanup_expired() {
let cache = InMemoryCache::with_layered_cleanup();
cache
.set("key1", &"value1", Some(Duration::from_millis(50)))
.await
.unwrap();
cache.set("key2", &"value2", None).await.unwrap();
tokio::time::sleep(Duration::from_millis(100)).await;
cache.cleanup_expired().await;
assert!(!cache.has_key("key1").await.unwrap());
assert!(cache.has_key("key2").await.unwrap());
}
#[tokio::test]
async fn test_layered_cache_statistics() {
let cache = InMemoryCache::with_layered_cleanup();
cache.set("key1", &"value1", None).await.unwrap();
cache.set("key2", &"value2", None).await.unwrap();
let stats = cache.get_statistics().await;
assert_eq!(stats.entry_count, 2);
let _: Option<String> = cache.get("key1").await.unwrap();
let _: Option<String> = cache.get("key2").await.unwrap();
let stats = cache.get_statistics().await;
assert_eq!(stats.hits, 2);
assert_eq!(stats.misses, 0);
let _: Option<String> = cache.get("key3").await.unwrap();
let stats = cache.get_statistics().await;
assert_eq!(stats.hits, 2);
assert_eq!(stats.misses, 1);
}
#[tokio::test]
async fn test_layered_list_keys() {
let cache = InMemoryCache::with_layered_cleanup();
let keys = cache.list_keys().await;
assert_eq!(keys.len(), 0);
cache.set("key1", &"value1", None).await.unwrap();
cache.set("key2", &"value2", None).await.unwrap();
cache.set("key3", &"value3", None).await.unwrap();
let keys = cache.list_keys().await;
assert_eq!(keys.len(), 3);
assert!(keys.contains(&"key1".to_string()));
assert!(keys.contains(&"key2".to_string()));
assert!(keys.contains(&"key3".to_string()));
}
#[tokio::test]
async fn test_layered_inspect_entry() {
let cache = InMemoryCache::with_layered_cleanup();
let info = cache.inspect_entry("nonexistent").await;
assert!(info.is_none());
cache
.set("key1", &"value1", Some(Duration::from_secs(300)))
.await
.unwrap();
let info = cache.inspect_entry("key1").await;
let info = info.unwrap();
assert_eq!(info.key, "key1");
assert!(info.has_expiry);
assert!(info.ttl_seconds.is_some());
assert!(info.ttl_seconds.unwrap() <= 300);
}
#[tokio::test]
async fn test_layered_large_dataset() {
let cache = InMemoryCache::with_layered_cleanup();
let num_keys = 1000;
for i in 0..num_keys {
cache
.set(
&format!("key{}", i),
&format!("value{}", i),
Some(Duration::from_millis(50)),
)
.await
.unwrap();
}
let stats = cache.get_statistics().await;
assert_eq!(stats.entry_count, num_keys);
tokio::time::sleep(Duration::from_millis(60)).await;
cache.cleanup_expired().await;
let stats = cache.get_statistics().await;
assert_eq!(stats.entry_count, 0);
}
#[tokio::test]
async fn test_custom_layered_cleanup() {
let cache = InMemoryCache::with_custom_layered_cleanup(50, 0.30);
for i in 0..100 {
cache
.set(
&format!("key{}", i),
&format!("value{}", i),
Some(Duration::from_millis(50)),
)
.await
.unwrap();
}
tokio::time::sleep(Duration::from_millis(100)).await;
cache.cleanup_expired().await;
let stats = cache.get_statistics().await;
assert_eq!(stats.entry_count, 0);
}
}