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}