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    /// One process. Two instances of the application share nothing here.
91    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            // The check and the write under one lock. That is the whole
143            // difference between this and a `has` then a `put`, and it is why
144            // an in-process lock works at all.
145            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                // Absent, expired, or somebody else's — all three mean "not
169                // yours to remove", and the caller wants to know which only in
170                // the sense of "did I still hold it".
171                _ => 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            // Under one lock, so two concurrent increments cannot both read the
179            // same value and lose one of the two.
180            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            // The expiry is preserved, not reset: a counter with a window on it
189            // must not have its window extended by being counted.
190            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        // Nanosecond TTL: already gone.
239        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        // A rate-limit counter whose window resets on every request is not a
286        // rate limit.
287        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        // A deploy that changes a cached shape must not poison every request
347        // until the key expires.
348        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}