Skip to main content

hitbox_backend/
backend.rs

1//! Core backend traits for cache storage implementations.
2//!
3//! This module defines two levels of abstraction:
4//!
5//! - [`Backend`] - Low-level dyn-compatible trait for raw byte operations
6//! - [`CacheBackend`] - High-level trait with typed operations (automatic via blanket impl)
7
8use std::{future::Future, sync::Arc};
9
10use async_trait::async_trait;
11use bytes::Bytes;
12use hitbox_core::{
13    BackendLabel, BoxContext, CacheKey, CacheStatus, CacheValue, Cacheable, CacheableResponse, Raw,
14    ReadMode, ResponseSource,
15};
16
17use crate::{
18    BackendError, CacheKeyFormat, Compressor, PassthroughCompressor,
19    format::{BincodeFormat, Format, FormatExt},
20    metrics::Timer,
21};
22
23/// Status of a delete operation.
24#[derive(Debug, PartialEq, Eq)]
25pub enum DeleteStatus {
26    /// Record successfully deleted.
27    ///
28    /// The `u32` count indicates how many cache layers deleted the key.
29    /// For single backends this is always `1`, but for [`CompositionBackend`]
30    /// the counts are summed (e.g., `Deleted(2)` means both L1 and L2 had the key).
31    ///
32    /// [`CompositionBackend`]: crate::composition::CompositionBackend
33    Deleted(u32),
34
35    /// Record was not found in the cache.
36    Missing,
37}
38
39/// Result type for backend operations.
40pub type BackendResult<T> = Result<T, BackendError>;
41
42/// Type alias for a dynamically dispatched Backend that is Send but not Sync.
43pub type UnsyncBackend = dyn Backend + Send;
44
45/// Type alias for a dynamically dispatched Backend that is Send + Sync.
46pub type SyncBackend = dyn Backend + Send + Sync;
47
48/// Low-level cache storage trait for raw byte operations.
49///
50/// Implement this trait to create a custom cache backend. The trait operates on
51/// raw bytes ([`CacheValue<Raw>`]), with serialization handled by [`CacheBackend`].
52///
53/// # Dyn-Compatibility
54///
55/// This trait is dyn-compatible. Blanket implementations are provided for:
56/// - `&dyn Backend`
57/// - `Box<dyn Backend>`
58/// - `Arc<dyn Backend + Send>` ([`UnsyncBackend`])
59/// - `Arc<dyn Backend + Send + Sync>` ([`SyncBackend`])
60#[async_trait]
61pub trait Backend: Sync + Send {
62    /// Read raw cached data by key.
63    ///
64    /// Returns `Ok(Some(value))` on hit, `Ok(None)` on miss.
65    async fn read(&self, key: &CacheKey) -> BackendResult<Option<CacheValue<Raw>>>;
66
67    /// Write raw data to cache.
68    async fn write(&self, key: &CacheKey, value: CacheValue<Raw>) -> BackendResult<()>;
69
70    /// Remove data from cache.
71    async fn remove(&self, key: &CacheKey) -> BackendResult<DeleteStatus>;
72
73    /// Backend label for metrics and source path composition.
74    ///
75    /// Used to build hierarchical paths like `"composition.moka"` in
76    /// [`CompositionBackend`](crate::CompositionBackend).
77    fn label(&self) -> BackendLabel {
78        BackendLabel::new_static("backend")
79    }
80
81    /// Serialization format for cached values. Default: [`BincodeFormat`].
82    fn value_format(&self) -> &dyn Format {
83        &BincodeFormat
84    }
85
86    /// Key serialization format. Default: [`CacheKeyFormat::Bitcode`].
87    fn key_format(&self) -> &CacheKeyFormat {
88        &CacheKeyFormat::Bitcode
89    }
90
91    /// Compressor for cached values. Default: [`PassthroughCompressor`].
92    fn compressor(&self) -> &dyn Compressor {
93        &PassthroughCompressor
94    }
95}
96
97#[async_trait]
98impl Backend for &dyn Backend {
99    async fn read(&self, key: &CacheKey) -> BackendResult<Option<CacheValue<Raw>>> {
100        (*self).read(key).await
101    }
102
103    async fn write(&self, key: &CacheKey, value: CacheValue<Raw>) -> BackendResult<()> {
104        (*self).write(key, value).await
105    }
106
107    async fn remove(&self, key: &CacheKey) -> BackendResult<DeleteStatus> {
108        (*self).remove(key).await
109    }
110
111    fn label(&self) -> BackendLabel {
112        (*self).label()
113    }
114
115    fn value_format(&self) -> &dyn Format {
116        (*self).value_format()
117    }
118
119    fn key_format(&self) -> &CacheKeyFormat {
120        (*self).key_format()
121    }
122
123    fn compressor(&self) -> &dyn Compressor {
124        (*self).compressor()
125    }
126}
127
128#[async_trait]
129impl Backend for Box<dyn Backend> {
130    async fn read(&self, key: &CacheKey) -> BackendResult<Option<CacheValue<Raw>>> {
131        (**self).read(key).await
132    }
133
134    async fn write(&self, key: &CacheKey, value: CacheValue<Raw>) -> BackendResult<()> {
135        (**self).write(key, value).await
136    }
137
138    async fn remove(&self, key: &CacheKey) -> BackendResult<DeleteStatus> {
139        (**self).remove(key).await
140    }
141
142    fn label(&self) -> BackendLabel {
143        (**self).label()
144    }
145
146    fn value_format(&self) -> &dyn Format {
147        (**self).value_format()
148    }
149
150    fn key_format(&self) -> &CacheKeyFormat {
151        (**self).key_format()
152    }
153
154    fn compressor(&self) -> &dyn Compressor {
155        (**self).compressor()
156    }
157}
158
159#[async_trait]
160impl Backend for Arc<UnsyncBackend> {
161    async fn read(&self, key: &CacheKey) -> BackendResult<Option<CacheValue<Raw>>> {
162        (**self).read(key).await
163    }
164
165    async fn write(&self, key: &CacheKey, value: CacheValue<Raw>) -> BackendResult<()> {
166        (**self).write(key, value).await
167    }
168
169    async fn remove(&self, key: &CacheKey) -> BackendResult<DeleteStatus> {
170        (**self).remove(key).await
171    }
172
173    fn label(&self) -> BackendLabel {
174        (**self).label()
175    }
176
177    fn value_format(&self) -> &dyn Format {
178        (**self).value_format()
179    }
180
181    fn key_format(&self) -> &CacheKeyFormat {
182        (**self).key_format()
183    }
184
185    fn compressor(&self) -> &dyn Compressor {
186        (**self).compressor()
187    }
188}
189
190#[async_trait]
191impl Backend for Arc<SyncBackend> {
192    async fn read(&self, key: &CacheKey) -> BackendResult<Option<CacheValue<Raw>>> {
193        (**self).read(key).await
194    }
195
196    async fn write(&self, key: &CacheKey, value: CacheValue<Raw>) -> BackendResult<()> {
197        (**self).write(key, value).await
198    }
199
200    async fn remove(&self, key: &CacheKey) -> BackendResult<DeleteStatus> {
201        (**self).remove(key).await
202    }
203
204    fn label(&self) -> BackendLabel {
205        (**self).label()
206    }
207
208    fn value_format(&self) -> &dyn Format {
209        (**self).value_format()
210    }
211
212    fn key_format(&self) -> &CacheKeyFormat {
213        (**self).key_format()
214    }
215
216    fn compressor(&self) -> &dyn Compressor {
217        (**self).compressor()
218    }
219}
220
221/// High-level cache backend trait with typed operations.
222///
223/// This trait provides typed `get`, `set`, and `delete` operations that handle
224/// serialization/deserialization and context tracking. The context is passed
225/// as a mutable reference and updated in-place during operations.
226///
227/// Automatically implemented for all [`Backend`] implementations.
228///
229/// <div class="warning">
230///
231/// Typically, you don't need to implement this trait yourself - the default
232/// implementation handles serialization, compression, and metrics automatically.
233///
234/// If you do provide a custom implementation, be aware that when your backend
235/// is used as a trait object (`dyn Backend`, `Box<dyn Backend>`, etc.), the
236/// blanket implementation will be used instead of your custom one.
237///
238/// </div>
239pub trait CacheBackend: Backend {
240    /// Retrieve a typed value from cache.
241    ///
242    /// Handles decompression and deserialization automatically using the
243    /// backend's configured [`Format`] and [`Compressor`].
244    fn get<T>(
245        &self,
246        key: &CacheKey,
247        ctx: &mut BoxContext,
248    ) -> impl Future<Output = BackendResult<Option<CacheValue<T::Cached>>>> + Send
249    where
250        T: CacheableResponse,
251        T::Cached: Cacheable,
252    {
253        async move {
254            let backend_label = self.label();
255
256            let read_timer = Timer::new();
257            let read_result = self.read(key).await;
258            crate::metrics::record_read(backend_label.as_str(), read_timer.elapsed());
259
260            match read_result {
261                Ok(Some(value)) => {
262                    let (meta, raw_data) = value.into_parts();
263                    let raw_len = raw_data.len();
264                    crate::metrics::record_read_bytes(backend_label.as_str(), raw_len);
265
266                    let format = self.value_format();
267
268                    let decompress_timer = Timer::new();
269                    let decompressed = self.compressor().decompress(&raw_data)?;
270                    crate::metrics::record_decompress(
271                        backend_label.as_str(),
272                        decompress_timer.elapsed(),
273                    );
274
275                    let decompressed_bytes = Bytes::from(decompressed);
276
277                    // Deserialize using with_deserializer - context may be upgraded
278                    let deserialize_timer = Timer::new();
279                    let mut deserialized_opt: Option<T::Cached> = None;
280                    format.with_deserializer(
281                        &decompressed_bytes,
282                        &mut |deserializer| {
283                            let value: T::Cached = deserializer.deserialize()?;
284                            deserialized_opt = Some(value);
285                            Ok(())
286                        },
287                        ctx,
288                    )?;
289                    crate::metrics::record_deserialize(
290                        backend_label.as_str(),
291                        deserialize_timer.elapsed(),
292                    );
293
294                    let deserialized = deserialized_opt.ok_or_else(|| {
295                        BackendError::InternalError(Box::new(std::io::Error::other(
296                            "deserialization produced no result",
297                        )))
298                    })?;
299
300                    let cached_value = CacheValue::new(deserialized, meta.expire, meta.stale);
301
302                    // Refill L1 if read mode is Refill (data came from L2).
303                    // CompositionFormat will create L1-only envelope, so only L1 gets populated.
304                    if ctx.read_mode() == ReadMode::Refill {
305                        let _ = self.set::<T>(key, &cached_value, ctx).await;
306                    }
307
308                    ctx.set_status(CacheStatus::Hit);
309                    ctx.set_source(ResponseSource::Backend(backend_label));
310                    Ok(Some(cached_value))
311                }
312                Ok(None) => Ok(None),
313                Err(e) => {
314                    crate::metrics::record_read_error(backend_label.as_str());
315                    Err(e)
316                }
317            }
318        }
319    }
320
321    /// Store a typed value in cache.
322    ///
323    /// Handles serialization and compression automatically using the
324    /// backend's configured [`Format`] and [`Compressor`].
325    fn set<T>(
326        &self,
327        key: &CacheKey,
328        value: &CacheValue<T::Cached>,
329        ctx: &mut BoxContext,
330    ) -> impl Future<Output = BackendResult<()>> + Send
331    where
332        T: CacheableResponse,
333        T::Cached: Cacheable,
334    {
335        async move {
336            // Skip write if this is a refill operation reaching the source backend.
337            // The source backend already has this data - it provided it during get().
338            // CompositionBackend handles L1 refill via its own set() implementation.
339            if ctx.read_mode() == ReadMode::Refill {
340                return Ok(());
341            }
342
343            let backend_label = self.label();
344            let format = self.value_format();
345
346            let serialize_timer = Timer::new();
347            let serialized_value = format.serialize(value.data(), &**ctx)?;
348            crate::metrics::record_serialize(backend_label.as_str(), serialize_timer.elapsed());
349
350            let compress_timer = Timer::new();
351            let compressed_value = self.compressor().compress(&serialized_value)?;
352            crate::metrics::record_compress(backend_label.as_str(), compress_timer.elapsed());
353
354            let compressed_len = compressed_value.len();
355
356            let write_timer = Timer::new();
357            let result = self
358                .write(
359                    key,
360                    CacheValue::new(Bytes::from(compressed_value), value.expire(), value.stale()),
361                )
362                .await;
363            crate::metrics::record_write(backend_label.as_str(), write_timer.elapsed());
364
365            match result {
366                Ok(()) => {
367                    crate::metrics::record_write_bytes(backend_label.as_str(), compressed_len);
368                    Ok(())
369                }
370                Err(e) => {
371                    crate::metrics::record_write_error(backend_label.as_str());
372                    Err(e)
373                }
374            }
375        }
376    }
377
378    /// Delete a value from cache.
379    ///
380    /// Delegates to [`Backend::remove`].
381    fn delete(
382        &self,
383        key: &CacheKey,
384        _ctx: &mut BoxContext,
385    ) -> impl Future<Output = BackendResult<DeleteStatus>> + Send {
386        async move { self.remove(key).await }
387    }
388}
389
390// Explicit CacheBackend implementations for trait objects
391// These use the default implementations from the trait
392impl CacheBackend for &dyn Backend {}
393
394impl CacheBackend for Box<dyn Backend> {}
395
396impl CacheBackend for Arc<UnsyncBackend> {}
397impl CacheBackend for Arc<SyncBackend> {}