Skip to main content

hitbox_core/
context.rs

1//! Cache context types for tracking cache operation results.
2
3use std::any::Any;
4
5use smallbox::{SmallBox, smallbox, space::S4};
6
7use crate::label::BackendLabel;
8
9/// Whether the request resulted in a cache hit, miss, or stale data.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11pub enum CacheStatus {
12    /// Cache hit - valid cached data was found and returned.
13    Hit,
14    /// Cache miss - no cached data was found.
15    #[default]
16    Miss,
17    /// Stale data - cached data was found but has exceeded its freshness window.
18    Stale,
19}
20
21impl CacheStatus {
22    /// Returns the status as a string slice.
23    #[inline]
24    pub const fn as_str(&self) -> &'static str {
25        match self {
26            CacheStatus::Hit => "hit",
27            CacheStatus::Miss => "miss",
28            CacheStatus::Stale => "stale",
29        }
30    }
31}
32
33/// Source of the response - either from upstream or from a cache backend.
34#[derive(Debug, Clone, PartialEq, Eq, Default)]
35pub enum ResponseSource {
36    /// Response came from upstream service (cache miss or bypass).
37    #[default]
38    Upstream,
39    /// Response came from cache backend with the given label.
40    Backend(BackendLabel),
41}
42
43impl ResponseSource {
44    /// Returns the source as a string slice.
45    #[inline]
46    pub fn as_str(&self) -> &str {
47        match self {
48            ResponseSource::Upstream => "upstream",
49            ResponseSource::Backend(label) => label.as_str(),
50        }
51    }
52}
53
54/// Mode for cache read operations.
55///
56/// Controls post-read behavior, particularly for composition backends
57/// where data read from one layer may need to be written to another.
58#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
59pub enum ReadMode {
60    /// Direct read - return value without side effects.
61    #[default]
62    Direct,
63    /// Refill mode - write value back to source layer after reading.
64    ///
65    /// Used in composition backends to populate L1 with data read from L2.
66    Refill,
67}
68
69/// Unified context for cache operations.
70///
71/// This trait combines operation tracking (status, source) with backend policy hints.
72/// It allows a single context object to flow through the entire cache pipeline,
73/// being transformed as needed by different layers.
74///
75/// # Usage
76///
77/// - `CacheFuture` creates a `Box<dyn Context>` at the start
78/// - Context is passed as `&mut BoxContext` through backend operations
79/// - Backends can upgrade the context type via `*ctx = Box::new(NewContext { ... })`
80/// - Format uses `&dyn Context` for policy hints during serialization
81/// - At the end, convert to `CacheContext` via `into_cache_context()`
82pub trait Context: Send + Sync {
83    // Operation tracking
84
85    /// Returns the cache status.
86    fn status(&self) -> CacheStatus;
87
88    /// Sets the cache status.
89    fn set_status(&mut self, status: CacheStatus);
90
91    /// Returns the response source.
92    fn source(&self) -> &ResponseSource;
93
94    /// Sets the response source.
95    fn set_source(&mut self, source: ResponseSource);
96
97    // Read mode
98
99    /// Returns the read mode for this context.
100    fn read_mode(&self) -> ReadMode {
101        ReadMode::default()
102    }
103
104    /// Sets the read mode.
105    fn set_read_mode(&mut self, _mode: ReadMode) {
106        // Default implementation does nothing - simple contexts ignore read mode
107    }
108
109    // Type identity and conversion
110
111    /// Returns a reference to self as `Any` for downcasting.
112    fn as_any(&self) -> &dyn Any;
113
114    /// Clone this context into a box.
115    fn clone_box(&self) -> BoxContext;
116
117    /// Consumes boxed self and returns a `CacheContext`.
118    fn into_cache_context(self: Box<Self>) -> CacheContext;
119
120    /// Merge fields from another context into this one.
121    ///
122    /// Used by composition backends to combine results from inner backends.
123    /// The `prefix` is prepended to the source path for hierarchical naming.
124    ///
125    /// # Arguments
126    /// * `other` - The inner context to merge from
127    /// * `prefix` - Label prefix to prepend to source path (e.g., backend label)
128    fn merge_from(&mut self, other: &dyn Context, prefix: &BackendLabel) {
129        // Merge status - take the inner status if it indicates a hit
130        let inner_status = other.status();
131        if inner_status == CacheStatus::Hit || inner_status == CacheStatus::Stale {
132            self.set_status(inner_status);
133        }
134
135        // Merge source with path composition
136        match other.source() {
137            ResponseSource::Backend(inner_label) => {
138                // Compose: prefix.inner_label (e.g., "composition.moka")
139                let composed = prefix.compose(inner_label);
140                self.set_source(ResponseSource::Backend(composed));
141            }
142            ResponseSource::Upstream => {
143                // No backend hit, keep as upstream
144            }
145        }
146    }
147}
148
149/// Boxed context trait object using SmallBox for inline storage.
150///
151/// Uses SmallBox with S4 space (4 * usize = 32 bytes on 64-bit) to avoid
152/// heap allocation for small contexts (like `CacheContext`). Larger contexts
153/// (like `CompositionContext`) fall back to heap allocation automatically.
154///
155/// This optimization reduces allocation overhead in the common case
156/// where only basic cache context tracking is needed.
157pub type BoxContext = SmallBox<dyn Context, S4>;
158
159/// Convert a BoxContext (SmallBox) into a CacheContext.
160///
161/// This function converts the SmallBox to a Box and then calls
162/// `into_cache_context()`. The allocation happens only at the end
163/// of the request lifecycle when the context is finalized.
164pub fn finalize_context(ctx: BoxContext) -> CacheContext {
165    let boxed: Box<dyn Context> = SmallBox::into_box(ctx);
166    boxed.into_cache_context()
167}
168
169/// Context information about a cache operation.
170#[derive(Debug, Clone, Default)]
171pub struct CacheContext {
172    /// Whether the request resulted in a cache hit, miss, or stale data.
173    pub status: CacheStatus,
174    /// Read mode for this operation.
175    pub read_mode: ReadMode,
176    /// Source of the response.
177    pub source: ResponseSource,
178}
179
180impl CacheContext {
181    /// Convert this context into a boxed trait object.
182    ///
183    /// This is a convenience method for creating `BoxContext` from `CacheContext`.
184    /// Uses SmallBox for inline storage, avoiding heap allocation for small contexts.
185    pub fn boxed(self) -> BoxContext {
186        smallbox!(self)
187    }
188}
189
190impl Context for CacheContext {
191    fn status(&self) -> CacheStatus {
192        self.status
193    }
194
195    fn set_status(&mut self, status: CacheStatus) {
196        self.status = status;
197    }
198
199    fn source(&self) -> &ResponseSource {
200        &self.source
201    }
202
203    fn set_source(&mut self, source: ResponseSource) {
204        self.source = source;
205    }
206
207    fn read_mode(&self) -> ReadMode {
208        self.read_mode
209    }
210
211    fn set_read_mode(&mut self, mode: ReadMode) {
212        self.read_mode = mode;
213    }
214
215    fn as_any(&self) -> &dyn Any {
216        self
217    }
218
219    fn clone_box(&self) -> BoxContext {
220        smallbox!(self.clone())
221    }
222
223    fn into_cache_context(self: Box<Self>) -> CacheContext {
224        *self
225    }
226}
227
228/// Extension trait for enriching responses with cache status information.
229///
230/// This trait provides a protocol-agnostic way to attach cache status
231/// metadata to responses. Each protocol (HTTP, gRPC, etc.) implements
232/// this trait with its own configuration type.
233///
234/// # Example
235///
236/// ```ignore
237/// use hitbox_core::{CacheStatus, CacheStatusExt};
238///
239/// // For HTTP responses (implemented in hitbox-http)
240/// response.cache_status(CacheStatus::Hit, &header_name);
241/// ```
242pub trait CacheStatusExt {
243    /// Configuration type for applying cache status (e.g., header name for HTTP).
244    type Config;
245
246    /// Applies cache status information to the response.
247    fn cache_status(&mut self, status: CacheStatus, config: &Self::Config);
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    #[test]
255    fn test_context_sizes() {
256        use std::mem::size_of;
257        let cache_ctx_size = size_of::<CacheContext>();
258        let box_ctx_size = size_of::<BoxContext>();
259        let s4_space = 4 * size_of::<usize>();
260
261        println!("CacheContext size: {} bytes", cache_ctx_size);
262        println!("  - CacheStatus: {} bytes", size_of::<CacheStatus>());
263        println!("  - ResponseSource: {} bytes", size_of::<ResponseSource>());
264        println!("BoxContext size: {} bytes", box_ctx_size);
265        println!("S4 inline space: {} bytes", s4_space);
266
267        // CacheContext should fit in S4 inline storage (32 bytes on 64-bit)
268        assert!(
269            cache_ctx_size <= s4_space,
270            "CacheContext ({} bytes) should fit in S4 ({} bytes)",
271            cache_ctx_size,
272            s4_space
273        );
274    }
275}