Skip to main content

rustlavel_cache/
store.rs

1//! The [`Cache`] contract every driver implements.
2//!
3//! The trait is deliberately dyn-compatible: the factory in [`crate::config`]
4//! decides at boot which driver an application uses, so the rest of the
5//! framework has to be able to hold an `Arc<dyn Cache>` without knowing which
6//! one it got. That is why every method returns a [`BoxFuture`] instead of
7//! being an `async fn` — `async fn` in a trait is not dyn-compatible.
8//!
9//! The generic conveniences that *cannot* be dyn-compatible (`remember`, which
10//! takes a closure returning a future) live in [`CacheExt`], blanket-implemented
11//! for every `Cache` including `dyn Cache`, so callers never notice the split.
12
13use rustlavel_core::events::{self, Event};
14use rustlavel_core::{Json, Result};
15use std::future::Future;
16use std::pin::Pin;
17use std::time::Duration;
18
19/// A boxed future borrowed from the cache and its key arguments.
20///
21/// Mirrors `rustlavel_http::handler::BoxFuture`, but borrowing rather than
22/// `'static`, so a driver can hold a lock guard or a pooled connection across
23/// the await without cloning the key first.
24pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
25
26/// A cache backend.
27///
28/// Values are [`Json`] rather than a generic `T` for two reasons: it keeps the
29/// trait dyn-compatible, and every rustlavel package already speaks `Json`, so
30/// anything that can be serialised can be cached without a second trait.
31pub trait Cache: Send + Sync + 'static {
32    /// The driver's name, used in `cache.hit` / `cache.miss` events and in
33    /// error messages. Telescope shows it as the store column.
34    fn driver(&self) -> &'static str;
35
36    /// Fetch a value, or `None` when it is missing or expired.
37    ///
38    /// Expiry is checked on read in every driver, so an entry that nobody asks
39    /// for again is never reported as present, even before a sweep removes it.
40    fn get<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<Option<Json>>>;
41
42    /// Store a value that expires after `ttl`.
43    ///
44    /// A zero or negative `ttl` is treated as "already expired": the key is
45    /// forgotten rather than stored, which matches Laravel and avoids leaving
46    /// an entry behind that no read will ever return.
47    fn put<'a>(&'a self, key: &'a str, value: Json, ttl: Duration) -> BoxFuture<'a, Result<()>>;
48
49    /// Store a value with no expiry. It still goes away on [`Cache::flush`].
50    fn forever<'a>(&'a self, key: &'a str, value: Json) -> BoxFuture<'a, Result<()>>;
51
52    /// Remove a key. Returns whether something was actually there.
53    fn forget<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<bool>>;
54
55    /// Remove everything this store owns.
56    fn flush(&self) -> BoxFuture<'_, Result<()>>;
57
58    /// Whether a live (unexpired) value exists.
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    /// Counters are stored as plain JSON numbers so `get` on a counter returns
66    /// something sensible in every driver.
67    fn increment<'a>(&'a self, key: &'a str, by: i64) -> BoxFuture<'a, Result<i64>>;
68
69    /// Subtract `by` from a counter. Returns the new value.
70    fn decrement<'a>(&'a self, key: &'a str, by: i64) -> BoxFuture<'a, Result<i64>> {
71        Box::pin(async move { self.increment(key, -by).await })
72    }
73
74    /// Increment a counter, giving it `ttl` only when this call created it.
75    ///
76    /// Rate limiting needs exactly this and nothing weaker: the first request
77    /// of a window starts the clock, and the ninety-ninth must not restart it.
78    /// Built as a driver method rather than a `get`-then-`put` in the limiter
79    /// because a read-modify-write loses counts under concurrency, which is the
80    /// one thing a rate limiter may not do.
81    fn increment_within<'a>(
82        &'a self,
83        key: &'a str,
84        by: i64,
85        ttl: Duration,
86    ) -> BoxFuture<'a, Result<i64>>;
87
88    /// How long until a key expires: `None` when it is missing or immortal.
89    ///
90    /// The rate limiter reports this as `Retry-After`, so a driver that cannot
91    /// answer would force the caller to guess.
92    fn ttl<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<Option<Duration>>>;
93
94    /// Read a value and remove it in one call — Laravel's `Cache::pull`.
95    fn pull<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<Option<Json>>> {
96        Box::pin(async move {
97            let value = self.get(key).await?;
98            if value.is_some() {
99                self.forget(key).await?;
100            }
101            Ok(value)
102        })
103    }
104}
105
106/// The conveniences that take a closure, and so cannot live on a dyn-compatible
107/// trait. Blanket-implemented, including for `dyn Cache`.
108pub trait CacheExt: Cache {
109    /// Return the cached value, or compute it, store it for `ttl`, and return it.
110    ///
111    /// The closure runs only on a miss. Note that two concurrent misses both
112    /// compute: this is a cache, not a lock, and stampede protection would mean
113    /// holding a lock across arbitrary user code.
114    fn remember<'a, F, Fut>(
115        &'a self,
116        key: &'a str,
117        ttl: Duration,
118        compute: F,
119    ) -> impl Future<Output = Result<Json>> + Send + 'a
120    where
121        F: FnOnce() -> Fut + Send + 'a,
122        Fut: Future<Output = Result<Json>> + Send + 'a,
123    {
124        async move {
125            if let Some(hit) = self.get(key).await? {
126                return Ok(hit);
127            }
128            let value = compute().await?;
129            self.put(key, value.clone(), ttl).await?;
130            Ok(value)
131        }
132    }
133
134    /// [`CacheExt::remember`] with no expiry.
135    fn remember_forever<'a, F, Fut>(
136        &'a self,
137        key: &'a str,
138        compute: F,
139    ) -> impl Future<Output = Result<Json>> + Send + 'a
140    where
141        F: FnOnce() -> Fut + Send + 'a,
142        Fut: Future<Output = Result<Json>> + Send + 'a,
143    {
144        async move {
145            if let Some(hit) = self.get(key).await? {
146                return Ok(hit);
147            }
148            let value = compute().await?;
149            self.forever(key, value.clone()).await?;
150            Ok(value)
151        }
152    }
153}
154
155impl<T: Cache + ?Sized> CacheExt for T {}
156
157/// Report a lookup on the instrumentation bus.
158///
159/// Guarded by `has_subscribers` so an application with no Telescope never pays
160/// for the string allocations behind an event nobody reads.
161pub(crate) fn record(hit: bool, driver: &'static str, key: &str) {
162    if !events::has_subscribers() {
163        return;
164    }
165    let kind = if hit { "cache.hit" } else { "cache.miss" };
166    Event::new(kind).with("key", key).with("store", driver).dispatch();
167}
168
169/// Turn a stored payload back into a value, treating corruption as a miss.
170///
171/// A cache is by definition disposable, so an unreadable entry must never take
172/// the application down: the caller simply recomputes.
173pub(crate) fn decode(payload: &str) -> Option<Json> {
174    Json::parse(payload).ok()
175}
176
177/// Read a counter out of whatever the key currently holds.
178///
179/// A non-numeric value counts as zero rather than an error: `increment` on a
180/// key someone else used for a string should start counting, not explode.
181pub(crate) fn counter_value(value: Option<&Json>) -> i64 {
182    match value {
183        Some(Json::Number(n)) => *n as i64,
184        Some(Json::String(s)) => s.trim().parse().unwrap_or(0),
185        _ => 0,
186    }
187}
188
189/// Prefixes are applied by the driver rather than by a wrapper so that a
190/// driver's native operations (Redis `INCRBY`, a file name) see the final key.
191pub(crate) fn prefixed(prefix: &str, key: &str) -> String {
192    if prefix.is_empty() {
193        return key.to_string();
194    }
195    format!("{prefix}{key}")
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn a_counter_reads_numbers_strings_and_nothing_at_all() {
204        assert_eq!(counter_value(Some(&Json::from(7))), 7);
205        assert_eq!(counter_value(Some(&Json::from("12"))), 12);
206        assert_eq!(counter_value(Some(&Json::Null)), 0);
207        assert_eq!(counter_value(None), 0);
208        assert_eq!(counter_value(Some(&Json::from("not a number"))), 0);
209    }
210
211    #[test]
212    fn a_corrupt_payload_decodes_as_a_miss_rather_than_an_error() {
213        assert_eq!(decode("42"), Some(Json::from(42)));
214        assert_eq!(decode("{oops"), None);
215    }
216
217    #[test]
218    fn an_empty_prefix_leaves_the_key_untouched() {
219        assert_eq!(prefixed("", "users:1"), "users:1");
220        assert_eq!(prefixed("app:", "users:1"), "app:users:1");
221    }
222}