hitbox_backend/composition/policy/read/sequential.rs
1//! Sequential read policy implementation.
2//!
3//! This policy tries L1 first, then falls back to L2 on miss or error.
4//! It's the default and most common strategy for multi-tier caching.
5
6use async_trait::async_trait;
7use hitbox_core::{BoxContext, CacheKey, CacheValue, Offload};
8use std::future::Future;
9
10use super::{CompositionReadPolicy, ReadResult};
11use crate::composition::CompositionLayer;
12
13/// Sequential read policy: Try L1 first, then L2 on miss.
14///
15/// This is the default and most common strategy. It provides:
16/// - Fast reads from L1 when available
17/// - Fallback to L2 if L1 misses or fails
18/// - Graceful degradation if L1 fails
19///
20/// # Behavior
21/// 1. Call `read_l1(key)`
22/// - Hit: Return immediately with L1 context
23/// - Miss or Error: Continue to L2
24/// 2. Call `read_l2(key)`
25/// - Hit: Return value with L2 context (L2 closure handles any L1 population)
26/// - Miss: Return None
27/// - Error: Return error
28///
29/// # Note
30/// The closures passed to `execute_with` are responsible for any post-processing
31/// like L1 population or envelope wrapping. This keeps the policy focused purely
32/// on the control flow strategy.
33#[derive(Debug, Clone, Copy, Default)]
34pub struct SequentialReadPolicy;
35
36impl SequentialReadPolicy {
37 /// Create a new sequential read policy.
38 pub fn new() -> Self {
39 Self
40 }
41}
42
43#[async_trait]
44impl CompositionReadPolicy for SequentialReadPolicy {
45 #[tracing::instrument(skip(self, key, read_l1, read_l2, _offload), level = "trace")]
46 async fn execute_with<T, E, F1, F2, Fut1, Fut2, O>(
47 &self,
48 key: CacheKey,
49 read_l1: F1,
50 read_l2: F2,
51 _offload: &O,
52 ) -> Result<ReadResult<T>, E>
53 where
54 T: Send + 'static,
55 E: Send + std::fmt::Debug + 'static,
56 F1: FnOnce(CacheKey) -> Fut1 + Send,
57 F2: FnOnce(CacheKey) -> Fut2 + Send,
58 Fut1: Future<Output = (Result<Option<CacheValue<T>>, E>, BoxContext)> + Send + 'static,
59 Fut2: Future<Output = (Result<Option<CacheValue<T>>, E>, BoxContext)> + Send + 'static,
60 O: Offload<'static>,
61 {
62 // Try L1 first
63 let (l1_result, l1_ctx) = read_l1(key.clone()).await;
64 match l1_result {
65 Ok(Some(value)) => {
66 // L1 hit - return immediately with L1 context
67 tracing::trace!("L1 hit");
68 return Ok(ReadResult {
69 value: Some(value),
70 source: CompositionLayer::L1,
71 context: l1_ctx,
72 });
73 }
74 Ok(None) => {
75 // L1 miss - continue to L2
76 tracing::trace!("L1 miss");
77 }
78 Err(e) => {
79 // L1 error - log and continue to L2
80 tracing::warn!(error = ?e, "L1 read failed");
81 }
82 }
83
84 // Try L2
85 let (l2_result, l2_ctx) = read_l2(key).await;
86
87 match l2_result {
88 Ok(Some(value)) => {
89 // L2 hit - return with combined context
90 tracing::trace!("L2 hit");
91 Ok(ReadResult {
92 value: Some(value),
93 source: CompositionLayer::L2,
94 context: l2_ctx,
95 })
96 }
97 Ok(None) => {
98 // L2 miss - return combined context
99 tracing::trace!("L2 miss");
100 Ok(ReadResult {
101 value: None,
102 source: CompositionLayer::L2,
103 context: l2_ctx,
104 })
105 }
106 Err(e) => {
107 // L2 error
108 tracing::error!(error = ?e, "L2 read failed");
109 Err(e)
110 }
111 }
112 }
113}