Skip to main content

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}