Skip to main content

kevy_persist/
modes.rs

1//! The choices a caller makes when opening and replaying a log: when to
2//! fsync, what to do at a corrupt record, and whether replay reports on
3//! stderr.
4
5/// When to fsync the AOF to disk. The three values of Redis's
6/// `appendfsync`.
7///
8/// ```
9/// use kevy_persist::Fsync;
10///
11/// assert_eq!(Fsync::parse("EverySec"), Some(Fsync::EverySec));
12/// assert_eq!(Fsync::Always.as_str(), "always");
13/// assert_eq!(Fsync::default(), Fsync::EverySec);
14/// ```
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
16#[non_exhaustive]
17pub enum Fsync {
18    /// fsync after every write — safest, slowest.
19    ///
20    /// ```
21    /// use kevy_persist::{Aof, Argv, Fsync};
22    ///
23    /// let path = std::env::temp_dir().join(format!("fsync-always-doc-{}.aof", std::process::id()));
24    /// let mut aof = Aof::open(&path, Fsync::Always)?;
25    /// aof.append(&Argv::from(vec![b"SET".to_vec(), b"k".to_vec(), b"v".to_vec()]))?;
26    /// // on disk before append returned: no tick, no drop needed
27    /// assert_eq!(std::fs::metadata(&path)?.len(), aof.size_bytes());
28    /// # drop(aof);
29    /// # std::fs::remove_file(&path)?;
30    /// # Ok::<(), std::io::Error>(())
31    /// ```
32    Always,
33    /// fsync about once per second; each [`crate::Aof::tick`] (or
34    /// [`crate::Aof::maybe_sync`]) writes the buffer into the kernel. A
35    /// power loss loses about one second plus the time one fsync takes.
36    /// The default.
37    ///
38    /// ```
39    /// use kevy_persist::{Aof, Fsync};
40    ///
41    /// let path = std::env::temp_dir().join(format!("fsync-everysec-doc-{}.aof", std::process::id()));
42    /// let aof = Aof::open(&path, Fsync::default())?;
43    /// assert_eq!(aof.fsync_policy(), Fsync::EverySec);
44    /// assert_eq!(Fsync::EverySec.as_str(), "everysec");
45    /// # drop(aof);
46    /// # std::fs::remove_file(&path)?;
47    /// # Ok::<(), std::io::Error>(())
48    /// ```
49    #[default]
50    EverySec,
51    /// Never fsync explicitly: each [`crate::Aof::tick`] writes the buffer
52    /// into the kernel, and the OS decides when it reaches the disk.
53    ///
54    /// ```
55    /// use kevy_persist::{Aof, Fsync};
56    ///
57    /// let path = std::env::temp_dir().join(format!("fsync-no-doc-{}.aof", std::process::id()));
58    /// let mut aof = Aof::open(&path, Fsync::Always)?;
59    /// aof.set_fsync(Fsync::parse("no").unwrap_or_default())?; // CONFIG SET appendfsync no
60    /// assert_eq!(aof.fsync_policy(), Fsync::No);
61    /// # drop(aof);
62    /// # std::fs::remove_file(&path)?;
63    /// # Ok::<(), std::io::Error>(())
64    /// ```
65    No,
66}
67
68impl Fsync {
69    /// The Redis spelling (`always` / `everysec` / `no`), as `CONFIG GET
70    /// appendfsync` reports it.
71    ///
72    /// ```
73    /// assert_eq!(kevy_persist::Fsync::No.as_str(), "no");
74    /// ```
75    pub fn as_str(&self) -> &'static str {
76        match self {
77            Self::Always => "always",
78            Self::EverySec => "everysec",
79            Self::No => "no",
80        }
81    }
82
83    /// The inverse of [`Self::as_str`], ignoring ASCII case. `None` for
84    /// any other input.
85    ///
86    /// ```
87    /// assert_eq!(kevy_persist::Fsync::parse("ALWAYS"), Some(kevy_persist::Fsync::Always));
88    /// assert_eq!(kevy_persist::Fsync::parse("sometimes"), None);
89    /// ```
90    pub fn parse(s: &str) -> Option<Self> {
91        [Self::Always, Self::EverySec, Self::No]
92            .into_iter()
93            .find(|f| f.as_str().eq_ignore_ascii_case(s))
94    }
95}
96
97/// What a replay (and the open that follows it) does at a corrupt record
98/// in the middle of a log.
99///
100/// ```
101/// use kevy_persist::ReplayMode;
102///
103/// assert_eq!(ReplayMode::default(), ReplayMode::Strict);
104/// ```
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
106#[non_exhaustive]
107pub enum ReplayMode {
108    /// Stop at the first corrupt record; the open quarantines and
109    /// truncates everything from there.
110    ///
111    /// ```
112    /// use kevy_persist::{Aof, Argv, Fsync, ReplayMode, ReplaySummary, replay_aof_in_place};
113    ///
114    /// let path = std::env::temp_dir().join(format!("mode-strict-doc-{}.aof", std::process::id()));
115    /// let mut aof = Aof::open(&path, Fsync::Always)?;
116    /// aof.append(&Argv::from(vec![b"SET".to_vec(), b"k".to_vec(), b"v".to_vec()]))?;
117    /// drop(aof);
118    /// let mut bytes = std::fs::read(&path)?;
119    /// *bytes.last_mut().unwrap() ^= 1; // flip a payload bit
120    /// std::fs::write(&path, &bytes)?;
121    /// let report = replay_aof_in_place(&path, ReplayMode::Strict, ReplaySummary::Quiet, |_| {})?;
122    /// assert!(report.corrupt);
123    /// assert_eq!(report.commands, 0);
124    /// # std::fs::remove_file(&path)?;
125    /// # Ok::<(), std::io::Error>(())
126    /// ```
127    #[default]
128    Strict,
129    /// Hop over a corrupt region to the next record whose checksum holds
130    /// and keep replaying; the open leaves interior corrupt regions in
131    /// place and quarantines only the bytes after the last recoverable
132    /// record. v1 logs have no checksums to anchor on and replay strictly.
133    ///
134    /// ```
135    /// use kevy_persist::{Aof, Argv, Fsync, ReplayMode, ReplaySummary, replay_aof_in_place};
136    ///
137    /// let path = std::env::temp_dir().join(format!("mode-resync-doc-{}.aof", std::process::id()));
138    /// let mut aof = Aof::open(&path, Fsync::Always)?;
139    /// for key in [b"a", b"b"] {
140    ///     aof.append(&Argv::from(vec![b"SET".to_vec(), key.to_vec(), b"v".to_vec()]))?;
141    /// }
142    /// drop(aof);
143    /// let mut bytes = std::fs::read(&path)?;
144    /// bytes[kevy_persist::AOF2_MAGIC.len() + 12] ^= 1; // damage the first record only
145    /// std::fs::write(&path, &bytes)?;
146    /// let report = replay_aof_in_place(&path, ReplayMode::Resync, ReplaySummary::Quiet, |_| {})?;
147    /// assert!(report.corrupt);
148    /// assert_eq!(report.commands, 1, "the record behind the damage still replays");
149    /// # std::fs::remove_file(&path)?;
150    /// # Ok::<(), std::io::Error>(())
151    /// ```
152    Resync,
153}
154
155/// Whether a replay prints its informational summary lines on stderr. The
156/// corrupt-record WARN prints either way: it is an incident signal, not
157/// information.
158///
159/// ```
160/// use kevy_persist::ReplaySummary;
161///
162/// assert_eq!(ReplaySummary::default(), ReplaySummary::Print);
163/// ```
164#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
165#[non_exhaustive]
166pub enum ReplaySummary {
167    /// Print the summary lines.
168    ///
169    /// ```
170    /// use kevy_persist::{ReplayMode, ReplaySummary, replay_aof_in_place};
171    ///
172    /// let path = std::env::temp_dir().join(format!("summary-print-doc-{}.aof", std::process::id()));
173    /// std::fs::write(&path, kevy_persist::AOF2_MAGIC)?;
174    /// // prints `replayed 0 commands …` on stderr as it returns
175    /// let report = replay_aof_in_place(&path, ReplayMode::Strict, ReplaySummary::Print, |_| {})?;
176    /// assert_eq!(report.commands, 0);
177    /// # std::fs::remove_file(&path)?;
178    /// # Ok::<(), std::io::Error>(())
179    /// ```
180    #[default]
181    Print,
182    /// Suppress them — for a host that receives the same numbers through
183    /// a metric sink, where the line would be a duplicate.
184    ///
185    /// ```
186    /// use kevy_persist::{ReplayMode, ReplaySummary, replay_aof_in_place};
187    ///
188    /// let path = std::env::temp_dir().join(format!("summary-quiet-doc-{}.aof", std::process::id()));
189    /// std::fs::write(&path, kevy_persist::AOF2_MAGIC)?;
190    /// // the same numbers, handed back instead of printed
191    /// let report = replay_aof_in_place(&path, ReplayMode::Strict, ReplaySummary::Quiet, |_| {})?;
192    /// assert_eq!((report.commands, report.dropped_bytes), (0, 0));
193    /// # std::fs::remove_file(&path)?;
194    /// # Ok::<(), std::io::Error>(())
195    /// ```
196    Quiet,
197}