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 #[allow(deprecated)]
158 fn spawn<F>(&self, _kind: impl Into<SmolStr>, future: F)
159 where
160 F: Future<Output = ()> + Send + 'static,
161 {
162 tokio::spawn(future);
163 }
164 }
165
166 #[derive(Clone, Debug)]
167 struct TestBackend {
168 store: Arc<Mutex<HashMap<CacheKey, CacheValue<Raw>>>>,
169 }
170
171 impl TestBackend {
172 fn new() -> Self {
173 Self {
174 store: Arc::new(Mutex::new(HashMap::new())),
175 }
176 }
177 }
178
179 #[async_trait]
180 impl Backend for TestBackend {
181 async fn read(&self, key: &CacheKey) -> BackendResult<Option<CacheValue<Raw>>> {
182 Ok(self.store.lock().unwrap().get(key).cloned())
183 }
184
185 async fn write(&self, key: &CacheKey, value: CacheValue<Raw>) -> BackendResult<()> {
186 self.store.lock().unwrap().insert(key.clone(), value);
187 Ok(())
188 }
189
190 async fn remove(&self, key: &CacheKey) -> BackendResult<DeleteStatus> {
191 match self.store.lock().unwrap().remove(key) {
192 Some(_) => Ok(DeleteStatus::Deleted(1)),
193 None => Ok(DeleteStatus::Missing),
194 }
195 }
196
197 fn value_format(&self) -> &dyn Format {
198 &JsonFormat
199 }
200
201 fn key_format(&self) -> &CacheKeyFormat {
202 &CacheKeyFormat::Bitcode
203 }
204
205 fn compressor(&self) -> &dyn Compressor {
206 &PassthroughCompressor
207 }
208 }
209
210 impl CacheBackend for TestBackend {}
211
212 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
213 #[cfg_attr(
214 feature = "rkyv_format",
215 derive(Archive, RkyvSerialize, rkyv::Deserialize)
216 )]
217 struct CachedData {
218 value: String,
219 }
220
221 struct MockResponse;
222
223 impl CacheableResponse for MockResponse {
224 type Cached = CachedData;
225 type Subject = MockResponse;
226 type IntoCachedFuture = std::future::Ready<CachePolicy<Self::Cached, Self>>;
227 type FromCachedFuture = std::future::Ready<Self>;
228
229 async fn cache_policy<P: Predicate<Subject = Self::Subject> + Send + Sync>(
230 self,
231 _predicate: P,
232 _config: &EntityPolicyConfig,
233 ) -> CachePolicy<CacheValue<Self::Cached>, Self> {
234 unimplemented!()
235 }
236
237 fn into_cached(self) -> Self::IntoCachedFuture {
238 unimplemented!()
239 }
240
241 fn from_cached(_cached: Self::Cached) -> Self::FromCachedFuture {
242 unimplemented!()
243 }
244 }
245
246 #[tokio::test]
247 async fn test_compose_basic() {
248 let l1 = TestBackend::new();
249 let l2 = TestBackend::new();
250 let offload = TestOffload;
251
252 let cache = l1.clone().compose(l2.clone(), offload);
254
255 let key = CacheKey::from_str("test", "key1");
256 let value = CacheValue::new(
257 CachedData {
258 value: "test_value".to_string(),
259 },
260 Some(Utc::now() + chrono::Duration::seconds(60)),
261 None,
262 );
263
264 let mut ctx: BoxContext = CacheContext::default().boxed();
266 cache
267 .set::<MockResponse>(&key, &value, &mut ctx)
268 .await
269 .unwrap();
270
271 let mut ctx: BoxContext = CacheContext::default().boxed();
272 let result = cache.get::<MockResponse>(&key, &mut ctx).await.unwrap();
273 assert_eq!(result.unwrap().data().value, "test_value");
274
275 let mut ctx: BoxContext = CacheContext::default().boxed();
277 assert!(
278 l1.get::<MockResponse>(&key, &mut ctx)
279 .await
280 .unwrap()
281 .is_some()
282 );
283 let mut ctx: BoxContext = CacheContext::default().boxed();
284 assert!(
285 l2.get::<MockResponse>(&key, &mut ctx)
286 .await
287 .unwrap()
288 .is_some()
289 );
290 }
291
292 #[tokio::test]
293 async fn test_compose_with_policy() {
294 use super::super::policy::{RaceReadPolicy, RefillPolicy};
295
296 let l1 = TestBackend::new();
297 let l2 = TestBackend::new();
298 let offload = TestOffload;
299
300 let policy = CompositionPolicy::new()
302 .read(RaceReadPolicy::new())
303 .refill(RefillPolicy::Never);
304
305 let cache = l1.clone().compose_with(l2.clone(), offload, policy);
306
307 let key = CacheKey::from_str("test", "key1");
308 let value = CacheValue::new(
309 CachedData {
310 value: "from_l2".to_string(),
311 },
312 Some(Utc::now() + chrono::Duration::seconds(60)),
313 None,
314 );
315
316 let mut ctx: BoxContext = CacheContext::default().boxed();
318 l2.set::<MockResponse>(&key, &value, &mut ctx)
319 .await
320 .unwrap();
321
322 let mut ctx: BoxContext = CacheContext::default().boxed();
324 let result = cache.get::<MockResponse>(&key, &mut ctx).await.unwrap();
325 assert_eq!(result.unwrap().data().value, "from_l2");
326
327 let mut ctx: BoxContext = CacheContext::default().boxed();
329 assert!(
330 l1.get::<MockResponse>(&key, &mut ctx)
331 .await
332 .unwrap()
333 .is_none()
334 );
335 }
336
337 #[tokio::test]
338 async fn test_compose_nested() {
339 let l1 = TestBackend::new();
341 let l2 = TestBackend::new();
342 let l3 = TestBackend::new();
343 let offload = TestOffload;
344
345 let l2_l3 = l2.clone().compose(l3.clone(), offload.clone());
347
348 let cache = l1.clone().compose(l2_l3, offload);
350
351 let key = CacheKey::from_str("test", "nested_key");
352 let value = CacheValue::new(
353 CachedData {
354 value: "nested_value".to_string(),
355 },
356 Some(Utc::now() + chrono::Duration::seconds(60)),
357 None,
358 );
359
360 let mut ctx: BoxContext = CacheContext::default().boxed();
362 cache
363 .set::<MockResponse>(&key, &value, &mut ctx)
364 .await
365 .unwrap();
366
367 let mut ctx: BoxContext = CacheContext::default().boxed();
369 assert!(
370 l1.get::<MockResponse>(&key, &mut ctx)
371 .await
372 .unwrap()
373 .is_some()
374 );
375 let mut ctx: BoxContext = CacheContext::default().boxed();
376 assert!(
377 l2.get::<MockResponse>(&key, &mut ctx)
378 .await
379 .unwrap()
380 .is_some()
381 );
382 let mut ctx: BoxContext = CacheContext::default().boxed();
383 assert!(
384 l3.get::<MockResponse>(&key, &mut ctx)
385 .await
386 .unwrap()
387 .is_some()
388 );
389 }
390
391 #[tokio::test]
392 async fn test_compose_chaining() {
393 use super::super::policy::RaceReadPolicy;
394
395 let l1 = TestBackend::new();
396 let l2 = TestBackend::new();
397 let offload = TestOffload;
398
399 let cache = l1
401 .clone()
402 .compose(l2.clone(), offload)
403 .read(RaceReadPolicy::new());
404
405 let key = CacheKey::from_str("test", "chain_key");
406 let value = CacheValue::new(
407 CachedData {
408 value: "chain_value".to_string(),
409 },
410 Some(Utc::now() + chrono::Duration::seconds(60)),
411 None,
412 );
413
414 let mut ctx: BoxContext = CacheContext::default().boxed();
415 cache
416 .set::<MockResponse>(&key, &value, &mut ctx)
417 .await
418 .unwrap();
419
420 let mut ctx: BoxContext = CacheContext::default().boxed();
421 let result = cache.get::<MockResponse>(&key, &mut ctx).await.unwrap();
422 assert_eq!(result.unwrap().data().value, "chain_value");
423 }
424}