Skip to main content

hitbox_backend/composition/policy/write/
sequential.rs

1//! Sequential write policy implementation (Write-Through).
2//!
3//! This policy writes to L1 first, then writes to L2 sequentially.
4//! It's the classic write-through strategy with strong consistency.
5
6use async_trait::async_trait;
7use hitbox_core::{CacheKey, Offload};
8use std::future::Future;
9
10use super::CompositionWritePolicy;
11use crate::BackendError;
12
13/// Sequential write policy: Write to L1, then L2 (write-through).
14///
15/// This is the default and most common strategy. It provides:
16/// - Strong consistency (both layers updated before returning)
17/// - Atomic updates from caller's perspective
18/// - Graceful degradation if L1 succeeds but L2 fails
19///
20/// # Behavior
21/// 1. Call `write_l1(key)`
22///    - If fails: Return error immediately (don't write to L2)
23///    - If succeeds: Continue to L2
24/// 2. Call `write_l2(key)`
25///    - If fails: Return error (L1 has data, L2 doesn't - inconsistent state)
26///    - If succeeds: Return success
27///
28/// # Consistency Guarantees
29///
30/// **Success case (`Ok(())`)**: Both L1 and L2 have been updated successfully.
31///
32/// **Failure cases (`Err`)**:
33/// - **L1 write failed**: Neither layer updated - cache remains consistent
34/// - **L2 write failed**: L1 updated, L2 not updated - **inconsistent state**
35///
36/// ## Inconsistent State Handling
37///
38/// When L1 succeeds but L2 fails, the cache enters an inconsistent state where:
39/// - **L1 contains the new value** - subsequent reads from this client will hit L1
40/// - **L2 may contain stale data or no data** - other clients may see stale values
41/// - **The error is logged** with tracing::error for monitoring
42///
43/// ### Mitigation Strategies:
44///
45/// 1. **Accept inconsistency** - If L1 is much faster and L2 failures are rare,
46///    the inconsistency may be acceptable as L1 will mask it for most reads
47///
48/// 2. **Retry logic** - Implement retry at application level or use a RetryBackend
49///    wrapper to retry failed L2 writes
50///
51/// 3. **Use OptimisticParallelWritePolicy** - Succeeds if either L1 or L2 succeeds,
52///    providing better availability at the cost of potential inconsistency
53///
54/// 4. **Monitor and alert** - Track L2 write failures via metrics and investigate
55///    persistent failures that could indicate L2 capacity or connectivity issues
56///
57/// ### When L2 Failures Are Acceptable:
58/// - L2 is a persistent cache for cold starts (L1 mask inconsistency during normal operation)
59/// - Cache data is regeneratable from source of truth
60/// - Read-heavy workload where L1 hit rate is very high
61#[derive(Debug, Clone, Copy, Default)]
62pub struct SequentialWritePolicy;
63
64impl SequentialWritePolicy {
65    /// Create a new sequential write policy.
66    pub fn new() -> Self {
67        Self
68    }
69}
70
71#[async_trait]
72impl CompositionWritePolicy for SequentialWritePolicy {
73    #[tracing::instrument(skip(self, key, write_l1, write_l2, _offload), level = "trace")]
74    async fn execute_with<F1, F2, Fut1, Fut2, O>(
75        &self,
76        key: CacheKey,
77        write_l1: F1,
78        write_l2: F2,
79        _offload: &O,
80    ) -> Result<(), BackendError>
81    where
82        F1: FnOnce(CacheKey) -> Fut1 + Send,
83        F2: FnOnce(CacheKey) -> Fut2 + Send,
84        Fut1: Future<Output = Result<(), BackendError>> + Send + 'static,
85        Fut2: Future<Output = Result<(), BackendError>> + Send + 'static,
86        O: Offload<'static>,
87    {
88        // Write to L1 first
89        match write_l1(key.clone()).await {
90            Ok(()) => {
91                tracing::trace!("L1 write succeeded");
92            }
93            Err(e) => {
94                // L1 failed - don't write to L2
95                tracing::error!(error = ?e, "L1 write failed");
96                return Err(e);
97            }
98        }
99
100        // Write to L2
101        match write_l2(key).await {
102            Ok(()) => {
103                tracing::trace!("L2 write succeeded");
104                Ok(())
105            }
106            Err(e) => {
107                // L2 failed - inconsistent state (L1 has data, L2 doesn't)
108                tracing::error!(
109                    error = ?e,
110                    "L2 write failed after L1 succeeded - inconsistent state"
111                );
112                Err(e)
113            }
114        }
115    }
116}