rainier_cache/
prefixed.rs1use std::sync::Arc;
4use std::time::Duration;
5
6use rainier_support::{BoxFuture, Result};
7
8use crate::cache::Cache;
9
10pub struct PrefixedCache {
37 inner: Arc<dyn Cache>,
38 prefix: String,
39 with_separator: String,
41}
42
43impl PrefixedCache {
44 pub fn new(inner: Arc<dyn Cache>, prefix: impl Into<String>) -> Self {
46 let prefix = prefix.into();
47 let trimmed = prefix.trim_end_matches(':').to_string();
51 Self { with_separator: format!("{trimmed}:"), prefix: trimmed, inner }
52 }
53
54 pub fn prefix(&self) -> &str {
56 &self.prefix
57 }
58
59 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 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 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}