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 get<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<Option<Vec<u8>>>> {
75        Box::pin(async move {
76            let key = self.key(key);
77            self.inner.get(&key).await
78        })
79    }
80
81    fn put<'a>(
82        &'a self,
83        key: &'a str,
84        value: &'a [u8],
85        ttl: Option<Duration>,
86    ) -> BoxFuture<'a, Result<()>> {
87        Box::pin(async move {
88            let key = self.key(key);
89            self.inner.put(&key, value, ttl).await
90        })
91    }
92
93    fn forget<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<bool>> {
94        Box::pin(async move {
95            let key = self.key(key);
96            self.inner.forget(&key).await
97        })
98    }
99
100    /// **Not implemented as a flush.**
101    ///
102    /// There is no portable way to delete by prefix: Redis needs a `SCAN` loop
103    /// and Memcached cannot do it at all. Delegating to the inner flush would
104    /// empty the whole server, which is precisely what wrapping it was meant to
105    /// prevent — so this refuses instead of doing something surprising.
106    fn flush(&self) -> BoxFuture<'_, Result<()>> {
107        Box::pin(async move {
108            Err(rainier_support::Error::internal(format!(
109                "a prefixed cache cannot flush just `{}:` — no cache backend can delete by \
110                 prefix portably. Flush the underlying cache if you really mean everything, \
111                 or forget the keys you know about.",
112                self.prefix
113            )))
114        })
115    }
116
117    fn increment<'a>(&'a self, key: &'a str, by: i64) -> BoxFuture<'a, Result<i64>> {
118        Box::pin(async move {
119            let key = self.key(key);
120            self.inner.increment(&key, by).await
121        })
122    }
123
124    // Both forward, so the atomicity is whatever the wrapped cache offers —
125    // prefixing a key cannot make an operation less atomic, and a wrapper that
126    // reimplemented either of these would be a wrapper that quietly downgraded
127    // a lock.
128    fn add<'a>(
129        &'a self,
130        key: &'a str,
131        value: &'a [u8],
132        ttl: Option<Duration>,
133    ) -> BoxFuture<'a, Result<bool>> {
134        Box::pin(async move {
135            let key = self.key(key);
136            self.inner.add(&key, value, ttl).await
137        })
138    }
139
140    fn forget_if<'a>(&'a self, key: &'a str, expected: &'a [u8]) -> BoxFuture<'a, Result<bool>> {
141        Box::pin(async move {
142            let key = self.key(key);
143            self.inner.forget_if(&key, expected).await
144        })
145    }
146
147    fn has<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Result<bool>> {
148        Box::pin(async move {
149            let key = self.key(key);
150            self.inner.has(&key).await
151        })
152    }
153}
154
155impl std::fmt::Debug for PrefixedCache {
156    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157        f.debug_struct("PrefixedCache")
158            .field("prefix", &self.prefix)
159            .field("inner", &self.inner.name())
160            .finish()
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use crate::cache::CacheExt;
168    use crate::memory::MemoryCache;
169
170    fn shared() -> Arc<MemoryCache> {
171        Arc::new(MemoryCache::new())
172    }
173
174    #[tokio::test]
175    async fn keys_are_namespaced() {
176        let inner = shared();
177        let cache = PrefixedCache::new(Arc::clone(&inner) as Arc<dyn Cache>, "billing");
178
179        cache.put("total", b"10", None).await.unwrap();
180
181        assert_eq!(cache.get("total").await.unwrap(), Some(b"10".to_vec()));
182        assert_eq!(inner.get("billing:total").await.unwrap(), Some(b"10".to_vec()));
183        assert_eq!(inner.get("total").await.unwrap(), None, "the bare key is untouched");
184    }
185
186    #[tokio::test]
187    async fn two_namespaces_do_not_collide() {
188        let inner = shared() as Arc<dyn Cache>;
189        let billing = PrefixedCache::new(Arc::clone(&inner), "billing");
190        let catalogue = PrefixedCache::new(inner, "catalogue");
191
192        billing.put("total", b"10", None).await.unwrap();
193        catalogue.put("total", b"999", None).await.unwrap();
194
195        assert_eq!(billing.get("total").await.unwrap(), Some(b"10".to_vec()));
196        assert_eq!(catalogue.get("total").await.unwrap(), Some(b"999".to_vec()));
197    }
198
199    #[test]
200    fn a_trailing_separator_is_not_a_second_namespace() {
201        let inner = shared() as Arc<dyn Cache>;
202
203        assert_eq!(
204            PrefixedCache::new(Arc::clone(&inner), "app").prefix(),
205            PrefixedCache::new(inner, "app:").prefix()
206        );
207    }
208
209    #[tokio::test]
210    async fn every_operation_is_namespaced() {
211        let inner = shared();
212        let cache = PrefixedCache::new(Arc::clone(&inner) as Arc<dyn Cache>, "app");
213
214        cache.increment("hits", 3).await.unwrap();
215        assert_eq!(inner.get_string("app:hits").await.unwrap().as_deref(), Some("3"));
216
217        assert!(cache.has("hits").await.unwrap());
218        assert!(cache.forget("hits").await.unwrap());
219        assert!(!inner.has("app:hits").await.unwrap());
220    }
221
222    #[tokio::test]
223    async fn flushing_a_namespace_is_refused_rather_than_emptying_the_server() {
224        let inner = shared();
225        let cache = PrefixedCache::new(Arc::clone(&inner) as Arc<dyn Cache>, "app");
226
227        inner.put("someone-elses-key", b"v", None).await.unwrap();
228
229        let err = cache.flush().await.unwrap_err();
230        assert!(err.message().contains("cannot flush"), "{}", err.message());
231        assert!(
232            inner.get("someone-elses-key").await.unwrap().is_some(),
233            "the other application's keys must survive"
234        );
235    }
236
237    #[tokio::test]
238    async fn the_driver_name_passes_through() {
239        let cache = PrefixedCache::new(shared() as Arc<dyn Cache>, "app");
240        assert_eq!(cache.name(), "memory");
241    }
242}