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