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 a key is present and unexpired.
45    fn has<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<bool>> {
46        Box::pin(async move { Ok(self.get(key).await?.is_some()) })
47    }
48
49    /// Add `by` to a counter, creating it at zero first. Returns the new value.
50    ///
51    /// Atomic where the driver can be — which is the point of having it on the
52    /// port rather than as a get-then-put in application code, because that
53    /// version loses increments under concurrency.
54    fn increment<'a>(&'a self, key: &'a str, by: i64) -> BoxFuture<'a, Result<i64>>;
55
56    /// Store `value` **only if** `key` is absent. `true` if it was stored.
57    ///
58    /// # This must be atomic
59    ///
60    /// Not "atomic where convenient" — atomic. It is the primitive
61    /// [`Lock`](crate::Lock) is built from, and therefore the primitive behind
62    /// `without_overlapping` and `on_one_server`. A `has` followed by a `put`
63    /// satisfies the signature and satisfies no caller: two processes both see
64    /// the key absent, both write, both believe they hold the lock, and the
65    /// scheduled task runs twice.
66    ///
67    /// It is a **required** method with no default for exactly that reason. A
68    /// default would be inherited silently by the next driver somebody adds,
69    /// and the failure it causes is rare, non-local and expensive.
70    ///
71    /// | Driver | How |
72    /// |---|---|
73    /// | memory | inside the same mutex as the map |
74    /// | Redis | `SET key value NX PX ttl` |
75    /// | Memcached | the `add` command, which is atomic by definition |
76    /// | DynamoDB | `PutItem` with a condition on the key not existing |
77    fn add<'a>(
78        &'a self,
79        key: &'a str,
80        value: &'a [u8],
81        ttl: Option<Duration>,
82    ) -> BoxFuture<'a, Result<bool>>;
83
84    /// Remove `key` **only if** it currently holds `expected`. `true` if it was
85    /// removed.
86    ///
87    /// The other half of a lock, and the half people leave out.
88    ///
89    /// Releasing with a plain [`forget`](Self::forget) is wrong in a way that
90    /// only shows up under load. A holder that stalls past its TTL — a long GC
91    /// pause, a slow query, a suspended VM — has already lost the lock; another
92    /// process has taken it. When the first one finally finishes and deletes
93    /// the key, it deletes *someone else's* lock, and now a third process can
94    /// take it while the second still thinks it holds it.
95    ///
96    /// Comparing the value first closes that. The value is the holder's random
97    /// token, so "is this still mine" has an answer.
98    ///
99    /// Also required, and for the same reason as [`add`](Self::add).
100    fn forget_if<'a>(&'a self, key: &'a str, expected: &'a [u8]) -> BoxFuture<'a, Result<bool>>;
101}
102
103/// The typed conveniences every [`Cache`] gets.
104///
105/// Kept out of the object-safe trait so `Arc<dyn Cache>` still exists.
106#[async_trait::async_trait]
107pub trait CacheExt: Cache {
108    /// A value, deserialised from JSON.
109    async fn get_json<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>> {
110        match self.get(key).await? {
111            Some(bytes) => match serde_json::from_slice(&bytes) {
112                Ok(value) => Ok(Some(value)),
113                Err(e) => {
114                    // A cached value we cannot parse is a value from an older
115                    // shape of the application. Treating it as a miss lets the
116                    // caller recompute; failing would make a deploy poison
117                    // every request until the cache expired.
118                    tracing::warn!(key, error = %e, "discarding an unreadable cached value");
119                    Ok(None)
120                }
121            },
122            None => Ok(None),
123        }
124    }
125
126    /// Store a value as JSON.
127    async fn put_json<T: Serialize + Sync>(
128        &self,
129        key: &str,
130        value: &T,
131        ttl: Option<Duration>,
132    ) -> Result<()> {
133        let bytes = serde_json::to_vec(value)?;
134        self.put(key, &bytes, ttl).await
135    }
136
137    /// A string value.
138    async fn get_string(&self, key: &str) -> Result<Option<String>> {
139        match self.get(key).await? {
140            Some(bytes) => Ok(String::from_utf8(bytes).ok()),
141            None => Ok(None),
142        }
143    }
144
145    /// Store a string.
146    async fn put_string(&self, key: &str, value: &str, ttl: Option<Duration>) -> Result<()> {
147        self.put(key, value.as_bytes(), ttl).await
148    }
149
150    /// Store a string only if the key is absent.
151    ///
152    /// [`Cache::add`] with the encoding done for you. Atomic, like the method
153    /// it calls.
154    async fn add_string(&self, key: &str, value: &str, ttl: Option<Duration>) -> Result<bool> {
155        self.add(key, value.as_bytes(), ttl).await
156    }
157
158    /// Subtract from a counter.
159    async fn decrement(&self, key: &str, by: i64) -> Result<i64> {
160        self.increment(key, -by).await
161    }
162}
163
164impl<C: Cache + ?Sized> CacheExt for C {}
165
166/// Encode a counter for storage.
167///
168/// Decimal text rather than little-endian bytes, so a counter set by Rainier
169/// reads the same as one set by `redis-cli INCR` — and so `INCR` works on it at
170/// all, since Redis parses the stored string.
171pub(crate) fn encode_counter(value: i64) -> Vec<u8> {
172    value.to_string().into_bytes()
173}
174
175/// Decode a counter, treating anything unparseable as zero.
176pub(crate) fn decode_counter(bytes: &[u8]) -> Result<i64> {
177    if bytes.is_empty() {
178        return Ok(0);
179    }
180    std::str::from_utf8(bytes)
181        .ok()
182        .and_then(|text| text.trim().parse::<i64>().ok())
183        .ok_or_else(|| Error::internal("the cached value is not a counter"))
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn counters_round_trip_as_decimal_text() {
192        for value in [0i64, 1, -1, i64::MAX, i64::MIN] {
193            assert_eq!(decode_counter(&encode_counter(value)).unwrap(), value);
194        }
195        assert_eq!(encode_counter(42), b"42".to_vec(), "readable by redis-cli");
196    }
197
198    #[test]
199    fn an_empty_value_counts_as_zero() {
200        assert_eq!(decode_counter(b"").unwrap(), 0);
201    }
202
203    #[test]
204    fn a_non_counter_is_an_error_rather_than_zero() {
205        // Silently treating "hello" as 0 would let an increment overwrite an
206        // unrelated value.
207        assert!(decode_counter(b"hello").is_err());
208    }
209}