1use std::collections::HashMap;
4use std::sync::Mutex;
5use std::time::Duration;
6
7use chrono::{DateTime, Utc};
8use rainier_support::{BoxFuture, Result};
9
10use crate::cache::{decode_counter, encode_counter, Cache};
11
12pub struct MemoryCache {
23 entries: Mutex<HashMap<String, Entry>>,
24}
25
26#[derive(Debug, Clone)]
27struct Entry {
28 value: Vec<u8>,
29 expires_at: Option<DateTime<Utc>>,
30}
31
32impl Entry {
33 fn is_live(&self) -> bool {
34 match self.expires_at {
35 Some(at) => at > Utc::now(),
36 None => true,
37 }
38 }
39}
40
41impl Default for MemoryCache {
42 fn default() -> Self {
43 Self::new()
44 }
45}
46
47impl MemoryCache {
48 pub fn new() -> Self {
50 Self { entries: Mutex::new(HashMap::new()) }
51 }
52
53 fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<String, Entry>> {
54 self.entries.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
57 }
58
59 pub fn len(&self) -> usize {
61 self.lock().len()
62 }
63
64 pub fn is_empty(&self) -> bool {
66 self.len() == 0
67 }
68
69 pub fn purge(&self) -> u64 {
74 let mut entries = self.lock();
75 let before = entries.len();
76 entries.retain(|_, entry| entry.is_live());
77 (before - entries.len()) as u64
78 }
79
80 fn expiry(ttl: Option<Duration>) -> Option<DateTime<Utc>> {
81 ttl.and_then(|ttl| chrono::Duration::from_std(ttl).ok()).map(|ttl| Utc::now() + ttl)
82 }
83}
84
85impl Cache for MemoryCache {
86 fn name(&self) -> &str {
87 "memory"
88 }
89
90 fn is_shared(&self) -> bool {
92 false
93 }
94
95 fn get<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<Option<Vec<u8>>>> {
96 Box::pin(async move {
97 let mut entries = self.lock();
98 match entries.get(key) {
99 Some(entry) if entry.is_live() => Ok(Some(entry.value.clone())),
100 Some(_) => {
101 entries.remove(key);
102 Ok(None)
103 }
104 None => Ok(None),
105 }
106 })
107 }
108
109 fn put<'a>(
110 &'a self,
111 key: &'a str,
112 value: &'a [u8],
113 ttl: Option<Duration>,
114 ) -> BoxFuture<'a, Result<()>> {
115 Box::pin(async move {
116 self.lock().insert(
117 key.to_string(),
118 Entry { value: value.to_vec(), expires_at: Self::expiry(ttl) },
119 );
120 Ok(())
121 })
122 }
123
124 fn forget<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<bool>> {
125 Box::pin(async move { Ok(self.lock().remove(key).is_some()) })
126 }
127
128 fn flush(&self) -> BoxFuture<'_, Result<()>> {
129 Box::pin(async move {
130 self.lock().clear();
131 Ok(())
132 })
133 }
134
135 fn add<'a>(
136 &'a self,
137 key: &'a str,
138 value: &'a [u8],
139 ttl: Option<Duration>,
140 ) -> BoxFuture<'a, Result<bool>> {
141 Box::pin(async move {
142 let mut entries = self.lock();
146
147 if entries.get(key).is_some_and(Entry::is_live) {
148 return Ok(false);
149 }
150
151 entries.insert(
152 key.to_string(),
153 Entry { value: value.to_vec(), expires_at: Self::expiry(ttl) },
154 );
155 Ok(true)
156 })
157 }
158
159 fn forget_if<'a>(&'a self, key: &'a str, expected: &'a [u8]) -> BoxFuture<'a, Result<bool>> {
160 Box::pin(async move {
161 let mut entries = self.lock();
162
163 match entries.get(key) {
164 Some(entry) if entry.is_live() && entry.value == expected => {
165 entries.remove(key);
166 Ok(true)
167 }
168 _ => Ok(false),
172 }
173 })
174 }
175
176 fn increment<'a>(&'a self, key: &'a str, by: i64) -> BoxFuture<'a, Result<i64>> {
177 Box::pin(async move {
178 let mut entries = self.lock();
181
182 let current = match entries.get(key) {
183 Some(entry) if entry.is_live() => decode_counter(&entry.value)?,
184 _ => 0,
185 };
186 let next = current.saturating_add(by);
187
188 let expires_at = entries.get(key).filter(|e| e.is_live()).and_then(|e| e.expires_at);
191 entries.insert(key.to_string(), Entry { value: encode_counter(next), expires_at });
192
193 Ok(next)
194 })
195 }
196}
197
198impl std::fmt::Debug for MemoryCache {
199 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200 f.debug_struct("MemoryCache").field("entries", &self.len()).finish()
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use crate::cache::CacheExt;
208
209 #[tokio::test]
210 async fn a_value_round_trips() {
211 let cache = MemoryCache::new();
212 cache.put("k", b"v", None).await.unwrap();
213
214 assert_eq!(cache.get("k").await.unwrap(), Some(b"v".to_vec()));
215 assert!(cache.has("k").await.unwrap());
216 }
217
218 #[tokio::test]
219 async fn a_miss_is_none_not_an_error() {
220 assert_eq!(MemoryCache::new().get("absent").await.unwrap(), None);
221 assert!(!MemoryCache::new().has("absent").await.unwrap());
222 }
223
224 #[tokio::test]
225 async fn forgetting_reports_whether_it_was_there() {
226 let cache = MemoryCache::new();
227 cache.put("k", b"v", None).await.unwrap();
228
229 assert!(cache.forget("k").await.unwrap());
230 assert!(!cache.forget("k").await.unwrap());
231 }
232
233 #[tokio::test]
234 async fn an_expired_value_reads_as_absent_and_is_dropped() {
235 let cache = MemoryCache::new();
236 cache.put("k", b"v", Some(Duration::from_nanos(1))).await.unwrap();
237
238 assert_eq!(cache.get("k").await.unwrap(), None);
240 assert!(cache.is_empty(), "reading an expired key should clean it up");
241 }
242
243 #[tokio::test]
244 async fn no_ttl_means_no_expiry() {
245 let cache = MemoryCache::new();
246 cache.put("k", b"v", None).await.unwrap();
247
248 assert_eq!(cache.purge(), 0);
249 assert!(cache.get("k").await.unwrap().is_some());
250 }
251
252 #[tokio::test]
253 async fn purge_removes_only_the_expired() {
254 let cache = MemoryCache::new();
255 cache.put("gone", b"v", Some(Duration::from_nanos(1))).await.unwrap();
256 cache.put("kept", b"v", None).await.unwrap();
257
258 assert_eq!(cache.purge(), 1);
259 assert_eq!(cache.len(), 1);
260 assert!(cache.get("kept").await.unwrap().is_some());
261 }
262
263 #[tokio::test]
264 async fn flush_empties_everything() {
265 let cache = MemoryCache::new();
266 cache.put("a", b"1", None).await.unwrap();
267 cache.put("b", b"2", None).await.unwrap();
268
269 cache.flush().await.unwrap();
270 assert!(cache.is_empty());
271 }
272
273 #[tokio::test]
274 async fn increment_starts_from_zero() {
275 let cache = MemoryCache::new();
276
277 assert_eq!(cache.increment("hits", 1).await.unwrap(), 1);
278 assert_eq!(cache.increment("hits", 1).await.unwrap(), 2);
279 assert_eq!(cache.increment("hits", 5).await.unwrap(), 7);
280 assert_eq!(cache.decrement("hits", 3).await.unwrap(), 4);
281 }
282
283 #[tokio::test]
284 async fn increment_does_not_extend_the_window() {
285 let cache = MemoryCache::new();
288 cache.put("hits", b"0", Some(Duration::from_secs(60))).await.unwrap();
289 let before = cache.lock().get("hits").unwrap().expires_at;
290
291 cache.increment("hits", 1).await.unwrap();
292
293 assert_eq!(cache.lock().get("hits").unwrap().expires_at, before);
294 }
295
296 #[tokio::test]
297 async fn increment_saturates_rather_than_wrapping() {
298 let cache = MemoryCache::new();
299 cache.put("k", i64::MAX.to_string().as_bytes(), None).await.unwrap();
300
301 assert_eq!(cache.increment("k", 1).await.unwrap(), i64::MAX);
302 }
303
304 #[tokio::test]
305 async fn incrementing_a_non_counter_errors_rather_than_overwriting() {
306 let cache = MemoryCache::new();
307 cache.put("name", b"Ada", None).await.unwrap();
308
309 assert!(cache.increment("name", 1).await.is_err());
310 assert_eq!(cache.get("name").await.unwrap(), Some(b"Ada".to_vec()));
311 }
312
313 #[tokio::test]
314 async fn concurrent_increments_do_not_lose_any() {
315 let cache = std::sync::Arc::new(MemoryCache::new());
316
317 let mut handles = Vec::new();
318 for _ in 0..50 {
319 let cache = std::sync::Arc::clone(&cache);
320 handles.push(tokio::spawn(async move { cache.increment("hits", 1).await }));
321 }
322 for handle in handles {
323 handle.await.unwrap().unwrap();
324 }
325
326 assert_eq!(cache.get_string("hits").await.unwrap().as_deref(), Some("50"));
327 }
328
329 #[tokio::test]
330 async fn json_round_trips() {
331 #[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)]
332 struct Post {
333 id: u64,
334 title: String,
335 }
336
337 let cache = MemoryCache::new();
338 let post = Post { id: 1, title: "Hello".into() };
339
340 cache.put_json("post", &post, None).await.unwrap();
341 assert_eq!(cache.get_json::<Post>("post").await.unwrap(), Some(post));
342 }
343
344 #[tokio::test]
345 async fn an_unreadable_cached_value_reads_as_a_miss() {
346 let cache = MemoryCache::new();
349 cache.put("post", b"not json at all", None).await.unwrap();
350
351 assert_eq!(cache.get_json::<Vec<u8>>("post").await.unwrap(), None);
352 }
353
354 #[tokio::test]
355 async fn strings_round_trip() {
356 let cache = MemoryCache::new();
357 cache.put_string("name", "Ada", None).await.unwrap();
358
359 assert_eq!(cache.get_string("name").await.unwrap().as_deref(), Some("Ada"));
360 }
361
362 #[tokio::test]
363 async fn add_only_stores_when_absent() {
364 let cache = MemoryCache::new();
365
366 assert!(cache.add("k", b"first", None).await.unwrap());
367 assert!(!cache.add("k", b"second", None).await.unwrap());
368 assert_eq!(cache.get("k").await.unwrap(), Some(b"first".to_vec()));
369 }
370
371 #[tokio::test]
372 async fn the_port_is_object_safe() {
373 let cache: std::sync::Arc<dyn Cache> = std::sync::Arc::new(MemoryCache::new());
374
375 cache.put("k", b"v", None).await.unwrap();
376 assert_eq!(cache.get_string("k").await.unwrap().as_deref(), Some("v"));
377 }
378}