Skip to main content

zeppelin_embed/lifecycle/
durability.rs

1//! Explicit per-store and per-commit durability policy.
2
3use crate::vfs::SyncKind;
4
5/// Selects whether this store must make acknowledged writes durable on its own.
6///
7/// The default is [`Derived`](Self::Derived). In that mode [`CommitTier`] is
8/// ignored and no synchronization primitive is issued at any tier. An
9/// application crash or process kill does not lose acknowledged writes because
10/// the operating-system page cache retains them and writes them out afterward.
11/// A power cut or kernel panic can lose recently acknowledged writes. Recovery
12/// truncates the log at the first record whose checksum fails, so the store
13/// remains structurally valid: the failure is a missing recent tail, never
14/// silently wrong data.
15///
16/// Selecting `Derived` asserts that another store is authoritative and this
17/// store can be rebuilt from it. A caller for whom this store is the only copy
18/// of the data must select [`Durable`](Self::Durable) explicitly.
19#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
20pub enum DurabilityMode {
21    /// The default: another store is authoritative, this store is rebuildable,
22    /// and synchronization is skipped regardless of [`CommitTier`].
23    #[default]
24    Derived,
25    /// This store is the source of truth and applies the selected [`CommitTier`].
26    Durable,
27    /// Participates in a host transaction; v1 returns a typed unsupported error.
28    Attached,
29}
30
31/// Synchronization strength for one commit.
32///
33/// This setting has no effect unless [`DurabilityMode::Durable`] is selected.
34/// In the default [`DurabilityMode::Derived`] mode it is ignored entirely and
35/// no synchronization primitive is issued, including when this value is
36/// [`Ordered`](Self::Ordered) or [`Durable`](Self::Durable).
37#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
38pub enum CommitTier {
39    /// Buffer writes without synchronization; the page cache covers an
40    /// application crash, but not a power cut.
41    None,
42    /// Order earlier writes before later writes with the platform barrier.
43    ///
44    /// This default tier chooses a cheaper median than full durability on the
45    /// measured development device, with a materially wider latency tail:
46    /// barrier p95 varied 7.40x while full-sync p95 varied 1.02x. That spread
47    /// is a device property, not buffering or timing behavior in this code.
48    #[default]
49    Ordered,
50    /// Flush writes through to durable media with the platform full sync.
51    /// Its measured 4 KiB median was 20.8x the ordered barrier median, but its
52    /// latency distribution was substantially more stable on the same device.
53    Durable,
54}
55
56/// An explicit synchronization decision at one protocol boundary.
57#[derive(Clone, Copy, Debug, Eq, PartialEq)]
58pub enum SyncRequirement {
59    /// Deliberately issue no synchronization primitive.
60    Skip,
61    /// Issue the named synchronization primitive.
62    Sync(SyncKind),
63}
64
65/// Validated synchronization decisions for one store and commit.
66///
67/// Data-file and directory synchronization remain separate questions even
68/// when the current policy resolves them to the same requirement.
69#[derive(Clone, Copy, Debug, Eq, PartialEq)]
70pub struct DurabilityPolicy {
71    data_file: SyncRequirement,
72    directory: SyncRequirement,
73}
74
75impl DurabilityPolicy {
76    /// Resolves one mode/tier pair without compatibility fallback.
77    pub const fn new(
78        mode: DurabilityMode,
79        tier: CommitTier,
80    ) -> Result<Self, DurabilityPolicyError> {
81        let requirement = match (mode, tier) {
82            (DurabilityMode::Attached, _) => {
83                return Err(DurabilityPolicyError::AttachedNotYetSupported);
84            }
85            (DurabilityMode::Derived, _) | (DurabilityMode::Durable, CommitTier::None) => {
86                SyncRequirement::Skip
87            }
88            (DurabilityMode::Durable, CommitTier::Ordered) => {
89                SyncRequirement::Sync(SyncKind::Barrier)
90            }
91            (DurabilityMode::Durable, CommitTier::Durable) => SyncRequirement::Sync(SyncKind::Full),
92        };
93        Ok(Self {
94            data_file: requirement,
95            directory: requirement,
96        })
97    }
98
99    /// Returns the synchronization requirement for a written data file.
100    #[must_use]
101    pub const fn data_file_sync(self) -> SyncRequirement {
102        self.data_file
103    }
104
105    /// Returns the synchronization requirement for its containing directory.
106    #[must_use]
107    pub const fn directory_sync(self) -> SyncRequirement {
108        self.directory
109    }
110}
111
112/// A durability policy could not be constructed.
113#[derive(Clone, Copy, Debug, Eq, PartialEq)]
114pub enum DurabilityPolicyError {
115    /// Attached host transactions are reserved but are not implemented in v1.
116    AttachedNotYetSupported,
117}
118
119impl std::fmt::Display for DurabilityPolicyError {
120    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        match self {
122            Self::AttachedNotYetSupported => {
123                formatter.write_str("attached durability mode is not yet supported")
124            }
125        }
126    }
127}
128
129impl std::error::Error for DurabilityPolicyError {}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    #[allow(clippy::expect_used)]
137    fn default_policy_skips_every_synchronization() {
138        let policy = DurabilityPolicy::new(DurabilityMode::default(), CommitTier::default())
139            .expect("default policy");
140
141        assert_eq!(policy.data_file_sync(), SyncRequirement::Skip);
142        assert_eq!(policy.directory_sync(), SyncRequirement::Skip);
143    }
144
145    #[test]
146    #[allow(clippy::expect_used)]
147    fn derived_and_durable_none_resolve_to_the_same_policy() {
148        let durable_none = DurabilityPolicy::new(DurabilityMode::Durable, CommitTier::None)
149            .expect("durable none policy");
150
151        for tier in [CommitTier::None, CommitTier::Ordered, CommitTier::Durable] {
152            let derived =
153                DurabilityPolicy::new(DurabilityMode::Derived, tier).expect("derived policy");
154            assert_eq!(
155                derived, durable_none,
156                "Derived with {tier:?} must match Durable with None"
157            );
158        }
159    }
160
161    #[test]
162    #[allow(clippy::expect_used)]
163    fn maps_every_mode_and_tier_without_attached_fallback() {
164        let cases = [
165            (
166                DurabilityMode::Derived,
167                CommitTier::None,
168                SyncRequirement::Skip,
169            ),
170            (
171                DurabilityMode::Derived,
172                CommitTier::Ordered,
173                SyncRequirement::Skip,
174            ),
175            (
176                DurabilityMode::Derived,
177                CommitTier::Durable,
178                SyncRequirement::Skip,
179            ),
180            (
181                DurabilityMode::Durable,
182                CommitTier::None,
183                SyncRequirement::Skip,
184            ),
185            (
186                DurabilityMode::Durable,
187                CommitTier::Ordered,
188                SyncRequirement::Sync(SyncKind::Barrier),
189            ),
190            (
191                DurabilityMode::Durable,
192                CommitTier::Durable,
193                SyncRequirement::Sync(SyncKind::Full),
194            ),
195        ];
196        for (mode, tier, expected) in cases {
197            let policy = DurabilityPolicy::new(mode, tier).expect("supported policy");
198            assert_eq!(
199                (policy.data_file_sync(), policy.directory_sync()),
200                (expected, expected),
201                "policy mismatch for ({mode:?}, {tier:?})"
202            );
203        }
204        for tier in [CommitTier::None, CommitTier::Ordered, CommitTier::Durable] {
205            let error = DurabilityPolicy::new(DurabilityMode::Attached, tier)
206                .expect_err("Attached must fail rather than fall back");
207            assert_eq!(error, DurabilityPolicyError::AttachedNotYetSupported);
208            assert_eq!(
209                error.to_string(),
210                "attached durability mode is not yet supported"
211            );
212        }
213        assert_eq!(DurabilityMode::default(), DurabilityMode::Derived);
214        assert_eq!(CommitTier::default(), CommitTier::Ordered);
215    }
216}