Skip to main content

rainier_cache/
memory.rs

1//! [`MemoryCache`] — a cache in this process's memory.
2
3use 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
12/// A cache held in this process.
13///
14/// Right for development, tests, and anything genuinely per-process. Two
15/// instances behind a load balancer do **not** share it, which for a cache
16/// is a performance question rather than a correctness one — unlike a
17/// [session](https://docs.rs/rainier-session), where it is both.
18///
19/// The exception is anything you cache *for* correctness: a rate-limit counter
20/// or a lock in a memory cache is per-instance, so the limit is really `N ×
21/// limit` and the lock is not one.
22pub 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    /// An empty cache.
49    pub fn new() -> Self {
50        Self { entries: Mutex::new(HashMap::new()) }
51    }
52
53    fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<String, Entry>> {
54        // A panic elsewhere must not make every later cache read fail; the map's
55        // invariants do not depend on the panicking section having finished.
56        self.entries.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
57    }
58
59    /// How many entries are held, expired ones included.
60    pub fn len(&self) -> usize {
61        self.lock().len()
62    }
63
64    /// Whether nothing is held.
65    pub fn is_empty(&self) -> bool {
66        self.len() == 0
67    }
68
69    /// Drop every expired entry. Returns how many.
70    ///
71    /// Reads clean up the key they touch, but a key never read again would
72    /// otherwise sit there — so a long-lived process wants this on a timer.
73    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            // The check and the write under one lock. That is the whole
138            // difference between this and a `has` then a `put`, and it is why
139            // an in-process lock works at all.
140            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                // Absent, expired, or somebody else's — all three mean "not
164                // yours to remove", and the caller wants to know which only in
165                // the sense of "did I still hold it".
166                _ => 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            // Under one lock, so two concurrent increments cannot both read the
174            // same value and lose one of the two.
175            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            // The expiry is preserved, not reset: a counter with a window on it
184            // must not have its window extended by being counted.
185            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        // Nanosecond TTL: already gone.
234        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        // A rate-limit counter whose window resets on every request is not a
281        // rate limit.
282        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        // A deploy that changes a cached shape must not poison every request
342        // until the key expires.
343        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}