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}