hitbox_backend/composition/
compose.rs1use super::policy::{CompositionReadPolicy, CompositionWritePolicy};
28use super::{CompositionBackend, CompositionPolicy};
29use crate::Backend;
30use hitbox_core::Offload;
31
32pub trait Compose: Backend + Sized {
53 fn compose<L2, O>(self, l2: L2, offload: O) -> CompositionBackend<Self, L2, O>
83 where
84 L2: Backend,
85 O: Offload<'static>,
86 {
87 CompositionBackend::new(self, l2, offload)
88 }
89
90 fn compose_with<L2, O, R, W>(
111 self,
112 l2: L2,
113 offload: O,
114 policy: CompositionPolicy<R, W>,
115 ) -> CompositionBackend<Self, L2, O, R, W>
116 where
117 L2: Backend,
118 O: Offload<'static>,
119 R: CompositionReadPolicy,
120 W: CompositionWritePolicy,
121 {
122 CompositionBackend::new(self, l2, offload).with_policy(policy)
123 }
124}
125
126impl<T: Backend> Compose for T {}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132 use crate::format::{Format, JsonFormat};
133 use crate::{
134 Backend, BackendResult, CacheBackend, CacheKeyFormat, Compressor, DeleteStatus,
135 PassthroughCompressor,
136 };
137 use async_trait::async_trait;
138 use chrono::Utc;
139 use hitbox_core::{
140 BoxContext, CacheContext, CacheKey, CachePolicy, CacheValue, CacheableResponse,
141 EntityPolicyConfig, Predicate, Raw,
142 };
143 use serde::{Deserialize, Serialize};
144 use smol_str::SmolStr;
145 use std::collections::HashMap;
146 use std::future::Future;
147 use std::sync::{Arc, Mutex};
148
149 #[cfg(feature = "rkyv_format")]
150 use rkyv::{Archive, Serialize as RkyvSerialize};
151
152 #[derive(Clone, Debug)]
154 struct TestOffload;
155
156 impl Offload<'static> for TestOffload {
157 fn spawn<F>(&self, _kind: impl Into<SmolStr>, future: F)
158 where
159 F: Future<Output = ()> + Send + 'static,
160 {
161 tokio::spawn(future);
162 }
163 }
164
165 #[derive(Clone, Debug)]
166 struct TestBackend {
167 store: Arc<Mutex<HashMap<CacheKey, CacheValue<Raw>>>>,
168 }
169
170 impl TestBackend {
171 fn new() -> Self {
172 Self {
173 store: Arc::new(Mutex::new(HashMap::new())),
174 }
175 }
176 }
177
178 #[async_trait]
179 impl Backend for TestBackend {
180 async fn read(&self, key: &CacheKey) -> BackendResult<Option<CacheValue<Raw>>> {
181 Ok(self.store.lock().unwrap().get(key).cloned())
182 }
183
184 async fn write(&self, key: &CacheKey, value: CacheValue<Raw>) -> BackendResult<()> {
185 self.store.lock().unwrap().insert(key.clone(), value);
186 Ok(())
187 }
188
189 async fn remove(&self, key: &CacheKey) -> BackendResult<DeleteStatus> {
190 match self.store.lock().unwrap().remove(key) {
191 Some(_) => Ok(DeleteStatus::Deleted(1)),
192 None => Ok(DeleteStatus::Missing),
193 }
194 }
195
196 fn value_format(&self) -> &dyn Format {
197 &JsonFormat
198 }
199
200 fn key_format(&self) -> &CacheKeyFormat {
201 &CacheKeyFormat::Bitcode
202 }
203
204 fn compressor(&self) -> &dyn Compressor {
205 &PassthroughCompressor
206 }
207 }
208
209 impl CacheBackend for TestBackend {}
210
211 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
212 #[cfg_attr(
213 feature = "rkyv_format",
214 derive(Archive, RkyvSerialize, rkyv::Deserialize)
215 )]
216 struct CachedData {
217 value: String,
218 }
219
220 struct MockResponse;
221
222 impl CacheableResponse for MockResponse {
223 type Cached = CachedData;
224 type Subject = MockResponse;
225 type IntoCachedFuture = std::future::Ready<CachePolicy<Self::Cached, Self>>;
226 type FromCachedFuture = std::future::Ready<Self>;
227
228 async fn cache_policy<P: Predicate<Subject = Self::Subject> + Send + Sync>(
229 self,
230 _predicate: P,
231 _config: &EntityPolicyConfig,
232 ) -> CachePolicy<CacheValue<Self::Cached>, Self> {
233 unimplemented!()
234 }
235
236 fn into_cached(self) -> Self::IntoCachedFuture {
237 unimplemented!()
238 }
239
240 fn from_cached(_cached: Self::Cached) -> Self::FromCachedFuture {
241 unimplemented!()
242 }
243 }
244
245 #[tokio::test]
246 async fn test_compose_basic() {
247 let l1 = TestBackend::new();
248 let l2 = TestBackend::new();
249 let offload = TestOffload;
250
251 let cache = l1.clone().compose(l2.clone(), offload);
253
254 let key = CacheKey::from_str("test", "key1");
255 let value = CacheValue::new(
256 CachedData {
257 value: "test_value".to_string(),
258 },
259 Some(Utc::now() + chrono::Duration::seconds(60)),
260 None,
261 );
262
263 let mut ctx: BoxContext = CacheContext::default().boxed();
265 cache
266 .set::<MockResponse>(&key, &value, &mut ctx)
267 .await
268 .unwrap();
269
270 let mut ctx: BoxContext = CacheContext::default().boxed();
271 let result = cache.get::<MockResponse>(&key, &mut ctx).await.unwrap();
272 assert_eq!(result.unwrap().data().value, "test_value");
273
274 let mut ctx: BoxContext = CacheContext::default().boxed();
276 assert!(
277 l1.get::<MockResponse>(&key, &mut ctx)
278 .await
279 .unwrap()
280 .is_some()
281 );
282 let mut ctx: BoxContext = CacheContext::default().boxed();
283 assert!(
284 l2.get::<MockResponse>(&key, &mut ctx)
285 .await
286 .unwrap()
287 .is_some()
288 );
289 }
290
291 #[tokio::test]
292 async fn test_compose_with_policy() {
293 use super::super::policy::{RaceReadPolicy, RefillPolicy};
294
295 let l1 = TestBackend::new();
296 let l2 = TestBackend::new();
297 let offload = TestOffload;
298
299 let policy = CompositionPolicy::new()
301 .read(RaceReadPolicy::new())
302 .refill(RefillPolicy::Never);
303
304 let cache = l1.clone().compose_with(l2.clone(), offload, policy);
305
306 let key = CacheKey::from_str("test", "key1");
307 let value = CacheValue::new(
308 CachedData {
309 value: "from_l2".to_string(),
310 },
311 Some(Utc::now() + chrono::Duration::seconds(60)),
312 None,
313 );
314
315 let mut ctx: BoxContext = CacheContext::default().boxed();
317 l2.set::<MockResponse>(&key, &value, &mut ctx)
318 .await
319 .unwrap();
320
321 let mut ctx: BoxContext = CacheContext::default().boxed();
323 let result = cache.get::<MockResponse>(&key, &mut ctx).await.unwrap();
324 assert_eq!(result.unwrap().data().value, "from_l2");
325
326 let mut ctx: BoxContext = CacheContext::default().boxed();
328 assert!(
329 l1.get::<MockResponse>(&key, &mut ctx)
330 .await
331 .unwrap()
332 .is_none()
333 );
334 }
335
336 #[tokio::test]
337 async fn test_compose_nested() {
338 let l1 = TestBackend::new();
340 let l2 = TestBackend::new();
341 let l3 = TestBackend::new();
342 let offload = TestOffload;
343
344 let l2_l3 = l2.clone().compose(l3.clone(), offload.clone());
346
347 let cache = l1.clone().compose(l2_l3, offload);
349
350 let key = CacheKey::from_str("test", "nested_key");
351 let value = CacheValue::new(
352 CachedData {
353 value: "nested_value".to_string(),
354 },
355 Some(Utc::now() + chrono::Duration::seconds(60)),
356 None,
357 );
358
359 let mut ctx: BoxContext = CacheContext::default().boxed();
361 cache
362 .set::<MockResponse>(&key, &value, &mut ctx)
363 .await
364 .unwrap();
365
366 let mut ctx: BoxContext = CacheContext::default().boxed();
368 assert!(
369 l1.get::<MockResponse>(&key, &mut ctx)
370 .await
371 .unwrap()
372 .is_some()
373 );
374 let mut ctx: BoxContext = CacheContext::default().boxed();
375 assert!(
376 l2.get::<MockResponse>(&key, &mut ctx)
377 .await
378 .unwrap()
379 .is_some()
380 );
381 let mut ctx: BoxContext = CacheContext::default().boxed();
382 assert!(
383 l3.get::<MockResponse>(&key, &mut ctx)
384 .await
385 .unwrap()
386 .is_some()
387 );
388 }
389
390 #[tokio::test]
391 async fn test_compose_chaining() {
392 use super::super::policy::RaceReadPolicy;
393
394 let l1 = TestBackend::new();
395 let l2 = TestBackend::new();
396 let offload = TestOffload;
397
398 let cache = l1
400 .clone()
401 .compose(l2.clone(), offload)
402 .read(RaceReadPolicy::new());
403
404 let key = CacheKey::from_str("test", "chain_key");
405 let value = CacheValue::new(
406 CachedData {
407 value: "chain_value".to_string(),
408 },
409 Some(Utc::now() + chrono::Duration::seconds(60)),
410 None,
411 );
412
413 let mut ctx: BoxContext = CacheContext::default().boxed();
414 cache
415 .set::<MockResponse>(&key, &value, &mut ctx)
416 .await
417 .unwrap();
418
419 let mut ctx: BoxContext = CacheContext::default().boxed();
420 let result = cache.get::<MockResponse>(&key, &mut ctx).await.unwrap();
421 assert_eq!(result.unwrap().data().value, "chain_value");
422 }
423}