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 get<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<Option<Vec<u8>>>> {
91 Box::pin(async move {
92 let mut entries = self.lock();
93 match entries.get(key) {
94 Some(entry) if entry.is_live() => Ok(Some(entry.value.clone())),
95 Some(_) => {
96 entries.remove(key);
97 Ok(None)
98 }
99 None => Ok(None),
100 }
101 })
102 }
103
104 fn put<'a>(
105 &'a self,
106 key: &'a str,
107 value: &'a [u8],
108 ttl: Option<Duration>,
109 ) -> BoxFuture<'a, Result<()>> {
110 Box::pin(async move {
111 self.lock().insert(
112 key.to_string(),
113 Entry { value: value.to_vec(), expires_at: Self::expiry(ttl) },
114 );
115 Ok(())
116 })
117 }
118
119 fn forget<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<bool>> {
120 Box::pin(async move { Ok(self.lock().remove(key).is_some()) })
121 }
122
123 fn flush(&self) -> BoxFuture<'_, Result<()>> {
124 Box::pin(async move {
125 self.lock().clear();
126 Ok(())
127 })
128 }
129
130 fn add<'a>(
131 &'a self,
132 key: &'a str,
133 value: &'a [u8],
134 ttl: Option<Duration>,
135 ) -> BoxFuture<'a, Result<bool>> {
136 Box::pin(async move {
137 let mut entries = self.lock();
141
142 if entries.get(key).is_some_and(Entry::is_live) {
143 return Ok(false);
144 }
145
146 entries.insert(
147 key.to_string(),
148 Entry { value: value.to_vec(), expires_at: Self::expiry(ttl) },
149 );
150 Ok(true)
151 })
152 }
153
154 fn forget_if<'a>(&'a self, key: &'a str, expected: &'a [u8]) -> BoxFuture<'a, Result<bool>> {
155 Box::pin(async move {
156 let mut entries = self.lock();
157
158 match entries.get(key) {
159 Some(entry) if entry.is_live() && entry.value == expected => {
160 entries.remove(key);
161 Ok(true)
162 }
163 _ => Ok(false),
167 }
168 })
169 }
170
171 fn increment<'a>(&'a self, key: &'a str, by: i64) -> BoxFuture<'a, Result<i64>> {
172 Box::pin(async move {
173 let mut entries = self.lock();
176
177 let current = match entries.get(key) {
178 Some(entry) if entry.is_live() => decode_counter(&entry.value)?,
179 _ => 0,
180 };
181 let next = current.saturating_add(by);
182
183 let expires_at = entries.get(key).filter(|e| e.is_live()).and_then(|e| e.expires_at);
186 entries.insert(key.to_string(), Entry { value: encode_counter(next), expires_at });
187
188 Ok(next)
189 })
190 }
191}
192
193impl std::fmt::Debug for MemoryCache {
194 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195 f.debug_struct("MemoryCache").field("entries", &self.len()).finish()
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202 use crate::cache::CacheExt;
203
204 #[tokio::test]
205 async fn a_value_round_trips() {
206 let cache = MemoryCache::new();
207 cache.put("k", b"v", None).await.unwrap();
208
209 assert_eq!(cache.get("k").await.unwrap(), Some(b"v".to_vec()));
210 assert!(cache.has("k").await.unwrap());
211 }
212
213 #[tokio::test]
214 async fn a_miss_is_none_not_an_error() {
215 assert_eq!(MemoryCache::new().get("absent").await.unwrap(), None);
216 assert!(!MemoryCache::new().has("absent").await.unwrap());
217 }
218
219 #[tokio::test]
220 async fn forgetting_reports_whether_it_was_there() {
221 let cache = MemoryCache::new();
222 cache.put("k", b"v", None).await.unwrap();
223
224 assert!(cache.forget("k").await.unwrap());
225 assert!(!cache.forget("k").await.unwrap());
226 }
227
228 #[tokio::test]
229 async fn an_expired_value_reads_as_absent_and_is_dropped() {
230 let cache = MemoryCache::new();
231 cache.put("k", b"v", Some(Duration::from_nanos(1))).await.unwrap();
232
233 assert_eq!(cache.get("k").await.unwrap(), None);
235 assert!(cache.is_empty(), "reading an expired key should clean it up");
236 }
237
238 #[tokio::test]
239 async fn no_ttl_means_no_expiry() {
240 let cache = MemoryCache::new();
241 cache.put("k", b"v", None).await.unwrap();
242
243 assert_eq!(cache.purge(), 0);
244 assert!(cache.get("k").await.unwrap().is_some());
245 }
246
247 #[tokio::test]
248 async fn purge_removes_only_the_expired() {
249 let cache = MemoryCache::new();
250 cache.put("gone", b"v", Some(Duration::from_nanos(1))).await.unwrap();
251 cache.put("kept", b"v", None).await.unwrap();
252
253 assert_eq!(cache.purge(), 1);
254 assert_eq!(cache.len(), 1);
255 assert!(cache.get("kept").await.unwrap().is_some());
256 }
257
258 #[tokio::test]
259 async fn flush_empties_everything() {
260 let cache = MemoryCache::new();
261 cache.put("a", b"1", None).await.unwrap();
262 cache.put("b", b"2", None).await.unwrap();
263
264 cache.flush().await.unwrap();
265 assert!(cache.is_empty());
266 }
267
268 #[tokio::test]
269 async fn increment_starts_from_zero() {
270 let cache = MemoryCache::new();
271
272 assert_eq!(cache.increment("hits", 1).await.unwrap(), 1);
273 assert_eq!(cache.increment("hits", 1).await.unwrap(), 2);
274 assert_eq!(cache.increment("hits", 5).await.unwrap(), 7);
275 assert_eq!(cache.decrement("hits", 3).await.unwrap(), 4);
276 }
277
278 #[tokio::test]
279 async fn increment_does_not_extend_the_window() {
280 let cache = MemoryCache::new();
283 cache.put("hits", b"0", Some(Duration::from_secs(60))).await.unwrap();
284 let before = cache.lock().get("hits").unwrap().expires_at;
285
286 cache.increment("hits", 1).await.unwrap();
287
288 assert_eq!(cache.lock().get("hits").unwrap().expires_at, before);
289 }
290
291 #[tokio::test]
292 async fn increment_saturates_rather_than_wrapping() {
293 let cache = MemoryCache::new();
294 cache.put("k", i64::MAX.to_string().as_bytes(), None).await.unwrap();
295
296 assert_eq!(cache.increment("k", 1).await.unwrap(), i64::MAX);
297 }
298
299 #[tokio::test]
300 async fn incrementing_a_non_counter_errors_rather_than_overwriting() {
301 let cache = MemoryCache::new();
302 cache.put("name", b"Ada", None).await.unwrap();
303
304 assert!(cache.increment("name", 1).await.is_err());
305 assert_eq!(cache.get("name").await.unwrap(), Some(b"Ada".to_vec()));
306 }
307
308 #[tokio::test]
309 async fn concurrent_increments_do_not_lose_any() {
310 let cache = std::sync::Arc::new(MemoryCache::new());
311
312 let mut handles = Vec::new();
313 for _ in 0..50 {
314 let cache = std::sync::Arc::clone(&cache);
315 handles.push(tokio::spawn(async move { cache.increment("hits", 1).await }));
316 }
317 for handle in handles {
318 handle.await.unwrap().unwrap();
319 }
320
321 assert_eq!(cache.get_string("hits").await.unwrap().as_deref(), Some("50"));
322 }
323
324 #[tokio::test]
325 async fn json_round_trips() {
326 #[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)]
327 struct Post {
328 id: u64,
329 title: String,
330 }
331
332 let cache = MemoryCache::new();
333 let post = Post { id: 1, title: "Hello".into() };
334
335 cache.put_json("post", &post, None).await.unwrap();
336 assert_eq!(cache.get_json::<Post>("post").await.unwrap(), Some(post));
337 }
338
339 #[tokio::test]
340 async fn an_unreadable_cached_value_reads_as_a_miss() {
341 let cache = MemoryCache::new();
344 cache.put("post", b"not json at all", None).await.unwrap();
345
346 assert_eq!(cache.get_json::<Vec<u8>>("post").await.unwrap(), None);
347 }
348
349 #[tokio::test]
350 async fn strings_round_trip() {
351 let cache = MemoryCache::new();
352 cache.put_string("name", "Ada", None).await.unwrap();
353
354 assert_eq!(cache.get_string("name").await.unwrap().as_deref(), Some("Ada"));
355 }
356
357 #[tokio::test]
358 async fn add_only_stores_when_absent() {
359 let cache = MemoryCache::new();
360
361 assert!(cache.add("k", b"first", None).await.unwrap());
362 assert!(!cache.add("k", b"second", None).await.unwrap());
363 assert_eq!(cache.get("k").await.unwrap(), Some(b"first".to_vec()));
364 }
365
366 #[tokio::test]
367 async fn the_port_is_object_safe() {
368 let cache: std::sync::Arc<dyn Cache> = std::sync::Arc::new(MemoryCache::new());
369
370 cache.put("k", b"v", None).await.unwrap();
371 assert_eq!(cache.get_string("k").await.unwrap().as_deref(), Some("v"));
372 }
373}