Skip to main content

crates_docs/cache/
memory.rs

1//! Memory cache implementation
2//!
3//! Memory cache using LRU (Least Recently Used) eviction strategy.
4
5use std::sync::Mutex;
6use std::time::{Duration, Instant};
7
8/// Cache entry
9struct CacheEntry {
10    value: String,
11    expires_at: Option<Instant>,
12}
13
14impl CacheEntry {
15    fn new(value: String, ttl: Option<Duration>) -> Self {
16        let expires_at = ttl.map(|duration| Instant::now() + duration);
17        Self { value, expires_at }
18    }
19
20    fn is_expired(&self) -> bool {
21        self.expires_at
22            .is_some_and(|expiry| expiry <= Instant::now())
23    }
24}
25
26/// Memory cache implementation
27///
28/// Uses LRU eviction strategy, removes least recently used entries when cache is full.
29pub struct MemoryCache {
30    /// LRU cache, using Mutex for thread safety
31    cache: Mutex<lru::LruCache<String, CacheEntry>>,
32}
33
34impl MemoryCache {
35    /// Create a new memory cache
36    ///
37    /// # Arguments
38    /// * `max_size` - Maximum number of cache entries
39    #[must_use]
40    pub fn new(max_size: usize) -> Self {
41        // Use non-zero type to ensure cache size is at least 1
42        let cap =
43            std::num::NonZeroUsize::new(max_size.max(1)).expect("cache size must be at least 1");
44        Self {
45            cache: Mutex::new(lru::LruCache::new(cap)),
46        }
47    }
48
49    /// Safely acquire the cache lock, handling lock poisoning gracefully.
50    ///
51    /// When a thread panics while holding the lock, the lock becomes "poisoned".
52    /// Instead of panicking, we recover the data and log a warning.
53    fn acquire_lock(&self) -> std::sync::MutexGuard<'_, lru::LruCache<String, CacheEntry>> {
54        self.cache.lock().unwrap_or_else(|poisoned| {
55            tracing::warn!(
56                "Cache lock was poisoned, recovering data. This indicates a previous panic while holding the lock."
57            );
58            poisoned.into_inner()
59        })
60    }
61
62    /// Clean up expired entries
63    fn cleanup_expired(cache: &mut lru::LruCache<String, CacheEntry>) {
64        // Collect expired keys
65        let expired_keys: Vec<String> = cache
66            .iter()
67            .filter_map(|(k, v)| {
68                if v.is_expired() {
69                    Some(k.clone())
70                } else {
71                    None
72                }
73            })
74            .collect();
75
76        // Remove expired entries
77        for key in expired_keys {
78            cache.pop(&key);
79        }
80    }
81}
82
83#[async_trait::async_trait]
84impl super::Cache for MemoryCache {
85    async fn get(&self, key: &str) -> Option<String> {
86        let mut cache = self.acquire_lock();
87
88        // First check and clean up expired entries
89        Self::cleanup_expired(&mut cache);
90
91        // Get value (LRU automatically moves it to most recently used position)
92        cache.get(key).and_then(|entry| {
93            if entry.is_expired() {
94                None
95            } else {
96                Some(entry.value.clone())
97            }
98        })
99    }
100
101    async fn set(&self, key: String, value: String, ttl: Option<Duration>) {
102        let mut cache = self.acquire_lock();
103
104        // Clean up expired entries
105        Self::cleanup_expired(&mut cache);
106
107        // LRU automatically handles eviction
108        let entry = CacheEntry::new(value, ttl);
109        cache.put(key, entry);
110    }
111
112    async fn delete(&self, key: &str) {
113        let mut cache = self.acquire_lock();
114        cache.pop(key);
115    }
116
117    async fn clear(&self) {
118        let mut cache = self.acquire_lock();
119        cache.clear();
120    }
121
122    async fn exists(&self, key: &str) -> bool {
123        let mut cache = self.acquire_lock();
124        Self::cleanup_expired(&mut cache);
125        cache.contains(key)
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use crate::cache::Cache;
133    use tokio::time::sleep;
134
135    #[tokio::test]
136    async fn test_memory_cache_basic() {
137        let cache = MemoryCache::new(10);
138
139        // 测试设置和获取
140        cache
141            .set("key1".to_string(), "value1".to_string(), None)
142            .await;
143        assert_eq!(cache.get("key1").await, Some("value1".to_string()));
144
145        // 测试删除
146        cache.delete("key1").await;
147        assert_eq!(cache.get("key1").await, None);
148
149        // 测试清空
150        cache
151            .set("key2".to_string(), "value2".to_string(), None)
152            .await;
153        cache.clear().await;
154        assert_eq!(cache.get("key2").await, None);
155    }
156
157    #[tokio::test]
158    async fn test_memory_cache_ttl() {
159        let cache = MemoryCache::new(10);
160
161        // 测试带 TTL 的缓存
162        cache
163            .set(
164                "key1".to_string(),
165                "value1".to_string(),
166                Some(Duration::from_millis(100)),
167            )
168            .await;
169        assert_eq!(cache.get("key1").await, Some("value1".to_string()));
170
171        // 等待过期
172        sleep(Duration::from_millis(150)).await;
173        assert_eq!(cache.get("key1").await, None);
174    }
175
176    #[tokio::test]
177    async fn test_memory_cache_lru_eviction() {
178        let cache = MemoryCache::new(2);
179
180        // 填满缓存
181        cache
182            .set("key1".to_string(), "value1".to_string(), None)
183            .await;
184        cache
185            .set("key2".to_string(), "value2".to_string(), None)
186            .await;
187
188        // 访问 key1,使其成为最近使用
189        let _ = cache.get("key1").await;
190
191        // 添加第三个条目,应该淘汰 key2(最少使用)
192        cache
193            .set("key3".to_string(), "value3".to_string(), None)
194            .await;
195
196        // key1 应该还在(因为刚被访问)
197        assert_eq!(cache.get("key1").await, Some("value1".to_string()));
198        // key2 应该被淘汰
199        assert_eq!(cache.get("key2").await, None);
200        // key3 应该存在
201        assert_eq!(cache.get("key3").await, Some("value3".to_string()));
202    }
203
204    #[tokio::test]
205    async fn test_memory_cache_exists() {
206        let cache = MemoryCache::new(10);
207
208        cache
209            .set("key1".to_string(), "value1".to_string(), None)
210            .await;
211        assert!(cache.exists("key1").await);
212        assert!(!cache.exists("key2").await);
213    }
214}