Skip to main content

rainier_cache/
prefixed.rs

1//! [`PrefixedCache`] — namespacing a shared cache.
2
3use std::sync::Arc;
4use std::time::Duration;
5
6use rainier_support::{BoxFuture, Result};
7
8use crate::cache::Cache;
9
10/// Prefixes every key, so several applications can share one cache server.
11///
12/// Worth reaching for whenever the cache is not exclusively yours. Two
13/// applications caching `user:1` on one Redis database will read each other's
14/// values — and the symptom is a user seeing another application's data, not a
15/// crash.
16///
17/// It also makes [`flush`](Cache::flush) safe to mean "mine": the inner cache's
18/// flush would empty the whole database, including the other application's
19/// keys and anybody's sessions.
20///
21/// ```
22/// # use rainier_cache::{Cache, CacheExt, MemoryCache, PrefixedCache};
23/// # use std::sync::Arc;
24/// # #[tokio::main] async fn main() -> rainier_support::Result<()> {
25/// let shared: Arc<dyn Cache> = Arc::new(MemoryCache::new());
26///
27/// let billing = PrefixedCache::new(Arc::clone(&shared), "billing");
28/// let catalogue = PrefixedCache::new(Arc::clone(&shared), "catalogue");
29///
30/// billing.put_string("total", "10", None).await?;
31/// catalogue.put_string("total", "999", None).await?;
32///
33/// assert_eq!(billing.get_string("total").await?.as_deref(), Some("10"));
34/// # Ok(()) }
35/// ```
36pub struct PrefixedCache {
37    inner: Arc<dyn Cache>,
38    prefix: String,
39    /// The full prefix including the separator, precomputed.
40    with_separator: String,
41}
42
43impl PrefixedCache {
44    /// Wrap `inner`, prefixing every key with `prefix:`.
45    pub fn new(inner: Arc<dyn Cache>, prefix: impl Into<String>) -> Self {
46        let prefix = prefix.into();
47        // Trailing separators are trimmed so `new(c, "app")` and
48        // `new(c, "app:")` address the same keys rather than two namespaces
49        // that differ by a character nobody notices.
50        let trimmed = prefix.trim_end_matches(':').to_string();
51        Self { with_separator: format!("{trimmed}:"), prefix: trimmed, inner }
52    }
53
54    /// The prefix, without its separator.
55    pub fn prefix(&self) -> &str {
56        &self.prefix
57    }
58
59    /// The cache underneath.
60    pub fn inner(&self) -> &Arc<dyn Cache> {
61        &self.inner
62    }
63
64    fn key(&self, key: &str) -> String {
65        format!("{}{key}", self.with_separator)
66    }
67}
68
69impl Cache for PrefixedCache {
70    fn name(&self) -> &str {
71        self.inner.name()
72    }
73
74    fn is_shared(&self) -> bool {
75        self.inner.is_shared()
76    }
77
78    fn get<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<Option<Vec<u8>>>> {
79        Box::pin(async move {
80            let key = self.key(key);
81            self.inner.get(&key).await
82        })
83    }
84
85    fn put<'a>(
86        &'a self,
87        key: &'a str,
88        value: &'a [u8],
89        ttl: Option<Duration>,
90    ) -> BoxFuture<'a, Result<()>> {
91        Box::pin(async move {
92            let key = self.key(key);
93            self.inner.put(&key, value, ttl).await
94        })
95    }
96
97    fn forget<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<bool>> {
98        Box::pin(async move {
99            let key = self.key(key);
100            self.inner.forget(&key).await
101        })
102    }
103
104    /// **Not implemented as a flush.**
105    ///
106    /// There is no portable way to delete by prefix: Redis needs a `SCAN` loop
107    /// and Memcached cannot do it at all. Delegating to the inner flush would
108    /// empty the whole server, which is precisely what wrapping it was meant to
109    /// prevent — so this refuses instead of doing something surprising.
110    fn flush(&self) -> BoxFuture<'_, Result<()>> {
111        Box::pin(async move {
112            Err(rainier_support::Error::internal(format!(
113                "a prefixed cache cannot flush just `{}:` — no cache backend can delete by \
114                 prefix portably. Flush the underlying cache if you really mean everything, \
115                 or forget the keys you know about.",
116                self.prefix
117            )))
118        })
119    }
120
121    fn increment<'a>(&'a self, key: &'a str, by: i64) -> BoxFuture<'a, Result<i64>> {
122        Box::pin(async move {
123            let key = self.key(key);
124            self.inner.increment(&key, by).await
125        })
126    }
127
128    // Both forward, so the atomicity is whatever the wrapped cache offers —
129    // prefixing a key cannot make an operation less atomic, and a wrapper that
130    // reimplemented either of these would be a wrapper that quietly downgraded
131    // a lock.
132    fn add<'a>(
133        &'a self,
134        key: &'a str,
135        value: &'a [u8],
136        ttl: Option<Duration>,
137    ) -> BoxFuture<'a, Result<bool>> {
138        Box::pin(async move {
139            let key = self.key(key);
140            self.inner.add(&key, value, ttl).await
141        })
142    }
143
144    fn forget_if<'a>(&'a self, key: &'a str, expected: &'a [u8]) -> BoxFuture<'a, Result<bool>> {
145        Box::pin(async move {
146            let key = self.key(key);
147            self.inner.forget_if(&key, expected).await
148        })
149    }
150
151    fn has<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<bool>> {
152        Box::pin(async move {
153            let key = self.key(key);
154            self.inner.has(&key).await
155        })
156    }
157}
158
159impl std::fmt::Debug for PrefixedCache {
160    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161        f.debug_struct("PrefixedCache")
162            .field("prefix", &self.prefix)
163            .field("inner", &self.inner.name())
164            .finish()
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use crate::cache::CacheExt;
172    use crate::memory::MemoryCache;
173
174    fn shared() -> Arc<MemoryCache> {
175        Arc::new(MemoryCache::new())
176    }
177
178    #[tokio::test]
179    async fn keys_are_namespaced() {
180        let inner = shared();
181        let cache = PrefixedCache::new(Arc::clone(&inner) as Arc<dyn Cache>, "billing");
182
183        cache.put("total", b"10", None).await.unwrap();
184
185        assert_eq!(cache.get("total").await.unwrap(), Some(b"10".to_vec()));
186        assert_eq!(inner.get("billing:total").await.unwrap(), Some(b"10".to_vec()));
187        assert_eq!(inner.get("total").await.unwrap(), None, "the bare key is untouched");
188    }
189
190    #[tokio::test]
191    async fn two_namespaces_do_not_collide() {
192        let inner = shared() as Arc<dyn Cache>;
193        let billing = PrefixedCache::new(Arc::clone(&inner), "billing");
194        let catalogue = PrefixedCache::new(inner, "catalogue");
195
196        billing.put("total", b"10", None).await.unwrap();
197        catalogue.put("total", b"999", None).await.unwrap();
198
199        assert_eq!(billing.get("total").await.unwrap(), Some(b"10".to_vec()));
200        assert_eq!(catalogue.get("total").await.unwrap(), Some(b"999".to_vec()));
201    }
202
203    #[test]
204    fn a_trailing_separator_is_not_a_second_namespace() {
205        let inner = shared() as Arc<dyn Cache>;
206
207        assert_eq!(
208            PrefixedCache::new(Arc::clone(&inner), "app").prefix(),
209            PrefixedCache::new(inner, "app:").prefix()
210        );
211    }
212
213    #[tokio::test]
214    async fn every_operation_is_namespaced() {
215        let inner = shared();
216        let cache = PrefixedCache::new(Arc::clone(&inner) as Arc<dyn Cache>, "app");
217
218        cache.increment("hits", 3).await.unwrap();
219        assert_eq!(inner.get_string("app:hits").await.unwrap().as_deref(), Some("3"));
220
221        assert!(cache.has("hits").await.unwrap());
222        assert!(cache.forget("hits").await.unwrap());
223        assert!(!inner.has("app:hits").await.unwrap());
224    }
225
226    #[tokio::test]
227    async fn flushing_a_namespace_is_refused_rather_than_emptying_the_server() {
228        let inner = shared();
229        let cache = PrefixedCache::new(Arc::clone(&inner) as Arc<dyn Cache>, "app");
230
231        inner.put("someone-elses-key", b"v", None).await.unwrap();
232
233        let err = cache.flush().await.unwrap_err();
234        assert!(err.message().contains("cannot flush"), "{}", err.message());
235        assert!(
236            inner.get("someone-elses-key").await.unwrap().is_some(),
237            "the other application's keys must survive"
238        );
239    }
240
241    #[tokio::test]
242    async fn the_driver_name_passes_through() {
243        let cache = PrefixedCache::new(shared() as Arc<dyn Cache>, "app");
244        assert_eq!(cache.name(), "memory");
245    }
246}