Skip to main content

rainier_cache/
cache.rs

1//! The [`Cache`] port and the conveniences every implementation gets.
2
3use std::time::Duration;
4
5use rainier_support::{BoxFuture, Error, Result};
6use serde::de::DeserializeOwned;
7use serde::Serialize;
8
9/// A key/value store with expiry.
10///
11/// Bytes rather than strings, because the interesting things to cache — a
12/// serialised session, a rendered page, a protobuf — are not all text, and
13/// forcing them through UTF-8 would mean base64 on top of an already-encoded
14/// value.
15///
16/// Every method is fallible and **a miss is not a failure**: `get` returns
17/// `Ok(None)` for an absent key and `Err` only when the cache itself could not
18/// be reached. Conflating the two is how a cache outage becomes an outage.
19pub trait Cache: Send + Sync + 'static {
20    /// A label for diagnostics — `"memory"`, `"redis"`, `"memcached"`.
21    fn name(&self) -> &str;
22
23    /// The value at `key`, or `None` if it is absent or expired.
24    fn get<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<Option<Vec<u8>>>>;
25
26    /// Store `value`, expiring after `ttl`. `None` means no expiry.
27    fn put<'a>(
28        &'a self,
29        key: &'a str,
30        value: &'a [u8],
31        ttl: Option<Duration>,
32    ) -> BoxFuture<'a, Result<()>>;
33
34    /// Remove a key. `true` if it was there.
35    fn forget<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<bool>>;
36
37    /// Remove everything.
38    ///
39    /// On a shared server this removes **everything in the database**, not just
40    /// this application's keys — which is why [`prefix`](crate::PrefixedCache)
41    /// exists and why this is not something to call on a schedule.
42    fn flush(&self) -> BoxFuture<'_, Result<()>>;
43
44    /// Whether this store is visible to other processes.
45    ///
46    /// The question anything cached **for correctness** has to ask: a lock, a
47    /// rate limit, a "has this webhook been handled" marker. Over a per-process
48    /// store each instance answers from its own copy, which for a lock means
49    /// every instance holds it at once.
50    ///
51    /// Defaults to `true`, because almost every implementation is a server
52    /// somewhere. An in-process one **must** override it — silence here is what
53    /// makes the failure quiet.
54    fn is_shared(&self) -> bool {
55        true
56    }
57
58    /// Whether [`add`](Self::add) is genuinely atomic here.
59    ///
60    /// `true` for everything that can compare-and-set — every driver in this
61    /// crate except Cloudflare Workers KV, which has no such operation at all.
62    ///
63    /// It matters because `add` is the primitive a **lock** is built from. A
64    /// store where two callers can both "win" an `add` cannot hold a lock, and
65    /// the failure is silent: both callers proceed, both believe they are the
66    /// only one, and the report goes out twice. So
67    /// [`LockManager::is_shared`](crate::LockManager::is_shared) requires this
68    /// as well as [`is_shared`](Self::is_shared), which is what makes the
69    /// scheduler refuse rather than trusting an operator to have read the
70    /// driver's documentation.
71    fn supports_atomic_add(&self) -> bool {
72        true
73    }
74
75    /// Whether a key is present and unexpired.
76    fn has<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<bool>> {
77        Box::pin(async move { Ok(self.get(key).await?.is_some()) })
78    }
79
80    /// Add `by` to a counter, creating it at zero first. Returns the new value.
81    ///
82    /// Atomic where the driver can be — which is the point of having it on the
83    /// port rather than as a get-then-put in application code, because that
84    /// version loses increments under concurrency.
85    fn increment<'a>(&'a self, key: &'a str, by: i64) -> BoxFuture<'a, Result<i64>>;
86
87    /// Store `value` **only if** `key` is absent. `true` if it was stored.
88    ///
89    /// # This must be atomic
90    ///
91    /// Not "atomic where convenient" — atomic. It is the primitive
92    /// [`Lock`](crate::Lock) is built from, and therefore the primitive behind
93    /// `without_overlapping` and `on_one_server`. A `has` followed by a `put`
94    /// satisfies the signature and satisfies no caller: two processes both see
95    /// the key absent, both write, both believe they hold the lock, and the
96    /// scheduled task runs twice.
97    ///
98    /// It is a **required** method with no default for exactly that reason. A
99    /// default would be inherited silently by the next driver somebody adds,
100    /// and the failure it causes is rare, non-local and expensive.
101    ///
102    /// | Driver | How |
103    /// |---|---|
104    /// | memory | inside the same mutex as the map |
105    /// | Redis | `SET key value NX PX ttl` |
106    /// | Memcached | the `add` command, which is atomic by definition |
107    /// | DynamoDB | `PutItem` with a condition on the key not existing |
108    fn add<'a>(
109        &'a self,
110        key: &'a str,
111        value: &'a [u8],
112        ttl: Option<Duration>,
113    ) -> BoxFuture<'a, Result<bool>>;
114
115    /// Remove `key` **only if** it currently holds `expected`. `true` if it was
116    /// removed.
117    ///
118    /// The other half of a lock, and the half people leave out.
119    ///
120    /// Releasing with a plain [`forget`](Self::forget) is wrong in a way that
121    /// only shows up under load. A holder that stalls past its TTL — a long GC
122    /// pause, a slow query, a suspended VM — has already lost the lock; another
123    /// process has taken it. When the first one finally finishes and deletes
124    /// the key, it deletes *someone else's* lock, and now a third process can
125    /// take it while the second still thinks it holds it.
126    ///
127    /// Comparing the value first closes that. The value is the holder's random
128    /// token, so "is this still mine" has an answer.
129    ///
130    /// Also required, and for the same reason as [`add`](Self::add).
131    fn forget_if<'a>(&'a self, key: &'a str, expected: &'a [u8]) -> BoxFuture<'a, Result<bool>>;
132}
133
134/// The typed conveniences every [`Cache`] gets.
135///
136/// Kept out of the object-safe trait so `Arc<dyn Cache>` still exists.
137#[async_trait::async_trait]
138pub trait CacheExt: Cache {
139    /// A value, deserialised from JSON.
140    async fn get_json<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>> {
141        match self.get(key).await? {
142            Some(bytes) => match serde_json::from_slice(&bytes) {
143                Ok(value) => Ok(Some(value)),
144                Err(e) => {
145                    // A cached value we cannot parse is a value from an older
146                    // shape of the application. Treating it as a miss lets the
147                    // caller recompute; failing would make a deploy poison
148                    // every request until the cache expired.
149                    tracing::warn!(key, error = %e, "discarding an unreadable cached value");
150                    Ok(None)
151                }
152            },
153            None => Ok(None),
154        }
155    }
156
157    /// Store a value as JSON.
158    async fn put_json<T: Serialize + Sync>(
159        &self,
160        key: &str,
161        value: &T,
162        ttl: Option<Duration>,
163    ) -> Result<()> {
164        let bytes = serde_json::to_vec(value)?;
165        self.put(key, &bytes, ttl).await
166    }
167
168    /// A string value.
169    async fn get_string(&self, key: &str) -> Result<Option<String>> {
170        match self.get(key).await? {
171            Some(bytes) => Ok(String::from_utf8(bytes).ok()),
172            None => Ok(None),
173        }
174    }
175
176    /// Store a string.
177    async fn put_string(&self, key: &str, value: &str, ttl: Option<Duration>) -> Result<()> {
178        self.put(key, value.as_bytes(), ttl).await
179    }
180
181    /// Store a string only if the key is absent.
182    ///
183    /// [`Cache::add`] with the encoding done for you. Atomic, like the method
184    /// it calls.
185    async fn add_string(&self, key: &str, value: &str, ttl: Option<Duration>) -> Result<bool> {
186        self.add(key, value.as_bytes(), ttl).await
187    }
188
189    /// Subtract from a counter.
190    async fn decrement(&self, key: &str, by: i64) -> Result<i64> {
191        self.increment(key, -by).await
192    }
193
194    /// The cached value at `key`, or `compute` it and cache that.
195    ///
196    /// Laravel's `Cache::remember`. The pattern nearly every cache use is,
197    /// written once:
198    ///
199    /// ```ignore
200    /// let settings: Settings = cache
201    ///     .remember("settings", Some(Duration::from_secs(300)), || async {
202    ///         load_settings(&database).await
203    ///     })
204    ///     .await?;
205    /// ```
206    ///
207    /// # A failure is never cached
208    ///
209    /// If `compute` returns an error, the error is returned and **nothing is
210    /// stored**. Caching a failure for five minutes turns one bad second into
211    /// five bad minutes, and the request that would have succeeded never gets
212    /// to try. Cache a *value*; let the next caller retry.
213    ///
214    /// A cached value that will not deserialise is treated as a miss and
215    /// recomputed — see [`get_json`](Self::get_json).
216    ///
217    /// # It is not a lock
218    ///
219    /// Ten simultaneous misses run `compute` ten times and the last write
220    /// wins. That is the right trade for a cache: serialising them would mean
221    /// nine requests waiting on a lock held by a tenth that might fail. When
222    /// the computation is expensive enough that a stampede matters, take a
223    /// [`Lock`](crate::Lock) around it explicitly.
224    async fn remember<T, F, Fut>(&self, key: &str, ttl: Option<Duration>, compute: F) -> Result<T>
225    where
226        T: Serialize + DeserializeOwned + Sync + Send,
227        F: FnOnce() -> Fut + Send,
228        Fut: std::future::Future<Output = Result<T>> + Send,
229    {
230        if let Some(cached) = self.get_json::<T>(key).await? {
231            return Ok(cached);
232        }
233
234        let value = compute().await?;
235
236        // A cache that cannot be written to is a slow application, not a
237        // broken one — the value in hand is still the right answer.
238        if let Err(e) = self.put_json(key, &value, ttl).await {
239            tracing::warn!(key, error = %e.message(), "could not cache a computed value");
240        }
241
242        Ok(value)
243    }
244
245    /// [`remember`](Self::remember), with no expiry.
246    ///
247    /// Laravel's `rememberForever`. For something that changes only when the
248    /// application says so — and which therefore needs
249    /// [`forget`](Cache::forget) somewhere, or it is a leak with a nice name.
250    async fn remember_forever<T, F, Fut>(&self, key: &str, compute: F) -> Result<T>
251    where
252        T: Serialize + DeserializeOwned + Sync + Send,
253        F: FnOnce() -> Fut + Send,
254        Fut: std::future::Future<Output = Result<T>> + Send,
255    {
256        self.remember(key, None, compute).await
257    }
258}
259
260impl<C: Cache + ?Sized> CacheExt for C {}
261
262/// Encode a counter for storage.
263///
264/// Decimal text rather than little-endian bytes, so a counter set by Rainier
265/// reads the same as one set by `redis-cli INCR` — and so `INCR` works on it at
266/// all, since Redis parses the stored string.
267pub(crate) fn encode_counter(value: i64) -> Vec<u8> {
268    value.to_string().into_bytes()
269}
270
271/// Decode a counter, treating anything unparseable as zero.
272pub(crate) fn decode_counter(bytes: &[u8]) -> Result<i64> {
273    if bytes.is_empty() {
274        return Ok(0);
275    }
276    std::str::from_utf8(bytes)
277        .ok()
278        .and_then(|text| text.trim().parse::<i64>().ok())
279        .ok_or_else(|| Error::internal("the cached value is not a counter"))
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    #[test]
287    fn counters_round_trip_as_decimal_text() {
288        for value in [0i64, 1, -1, i64::MAX, i64::MIN] {
289            assert_eq!(decode_counter(&encode_counter(value)).unwrap(), value);
290        }
291        assert_eq!(encode_counter(42), b"42".to_vec(), "readable by redis-cli");
292    }
293
294    #[test]
295    fn an_empty_value_counts_as_zero() {
296        assert_eq!(decode_counter(b"").unwrap(), 0);
297    }
298
299    #[test]
300    fn a_non_counter_is_an_error_rather_than_zero() {
301        // Silently treating "hello" as 0 would let an increment overwrite an
302        // unrelated value.
303        assert!(decode_counter(b"hello").is_err());
304    }
305
306    #[tokio::test]
307    async fn remember_computes_once_and_then_reads_the_cache() {
308        use crate::MemoryCache;
309        use std::sync::atomic::{AtomicUsize, Ordering};
310        use std::sync::Arc;
311
312        let cache = MemoryCache::new();
313        let calls = Arc::new(AtomicUsize::new(0));
314
315        for _ in 0..3 {
316            let calls = Arc::clone(&calls);
317            let value: String = cache
318                .remember("settings", None, || async move {
319                    calls.fetch_add(1, Ordering::SeqCst);
320                    Ok("computed".to_string())
321                })
322                .await
323                .unwrap();
324
325            assert_eq!(value, "computed");
326        }
327
328        assert_eq!(calls.load(Ordering::SeqCst), 1, "it recomputed a value it already had");
329    }
330
331    #[tokio::test]
332    async fn remember_does_not_cache_a_failure() {
333        // The whole point. Caching an error for five minutes turns one bad
334        // second into five bad minutes, and the request that would have
335        // succeeded never gets to try.
336        use crate::MemoryCache;
337
338        let cache = MemoryCache::new();
339
340        let failed: Result<String> = cache
341            .remember("thing", None, || async { Err(Error::internal("upstream is down")) })
342            .await;
343        assert!(failed.is_err());
344
345        assert!(cache.get("thing").await.unwrap().is_none(), "the failure was cached");
346
347        // And the next caller gets to succeed.
348        let recovered: String = cache
349            .remember("thing", None, || async { Ok("it works now".to_string()) })
350            .await
351            .unwrap();
352        assert_eq!(recovered, "it works now");
353    }
354
355    #[tokio::test]
356    async fn remember_recomputes_a_value_it_can_no_longer_read() {
357        // A deploy changed the shape of the cached type. Treating that as a
358        // miss is what stops one deploy poisoning every request until the TTL.
359        use crate::MemoryCache;
360
361        let cache = MemoryCache::new();
362        cache.put("count", b"\"not a number\"", None).await.unwrap();
363
364        let count: u32 = cache.remember("count", None, || async { Ok(7) }).await.unwrap();
365        assert_eq!(count, 7);
366    }
367}