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}