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}