crates_docs/cache/
memory.rs1use std::sync::Mutex;
6use std::time::{Duration, Instant};
7
8struct 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
26pub struct MemoryCache {
30 cache: Mutex<lru::LruCache<String, CacheEntry>>,
32}
33
34impl MemoryCache {
35 #[must_use]
40 pub fn new(max_size: usize) -> Self {
41 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 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 fn cleanup_expired(cache: &mut lru::LruCache<String, CacheEntry>) {
64 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 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 Self::cleanup_expired(&mut cache);
90
91 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 Self::cleanup_expired(&mut cache);
106
107 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 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 cache.delete("key1").await;
147 assert_eq!(cache.get("key1").await, None);
148
149 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 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 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 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 let _ = cache.get("key1").await;
190
191 cache
193 .set("key3".to_string(), "value3".to_string(), None)
194 .await;
195
196 assert_eq!(cache.get("key1").await, Some("value1".to_string()));
198 assert_eq!(cache.get("key2").await, None);
200 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}