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}