indexedlog/
errors.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8//! Errors used by the crate
9//!
10//! See [`Error`] for the main type.
11
12use std::fmt;
13use std::io;
14use std::path::Path;
15
16// Error design goals:
17// - Callsites can test whether an error is caused by data corruption or other
18//   issues (for example, permission or resource issues).
19//   This is important because it allows callsites (including within the
20//   crate like RotateLog) to decide whether to remove the bad data and
21//   try auto recovery.
22// - The library can change error structure internals. That means accesses
23//   to the error object are via public methods instead of struct or enum
24//   fields. `Error` is the only opaque public error type.
25// - Compatible with std Error. Therefore anyhow::Error is supported too.
26
27/// Represents all possible errors that can occur when using indexedlog.
28pub struct Error {
29    // Boxing makes `Result<Error, _>` smaller.
30    inner: Box<Inner>,
31}
32
33pub type Result<T> = std::result::Result<T, Error>;
34
35#[derive(Default)]
36struct Inner {
37    sources: Vec<Box<dyn std::error::Error + Send + Sync + 'static>>,
38    messages: Vec<String>,
39    is_corruption: bool,
40    io_error_kind: Option<io::ErrorKind>,
41}
42
43impl Error {
44    /// Return `true` if the error is considered as (filesystem) data
45    /// corruption.
46    ///
47    /// Application can use this information to decide whether to try the
48    /// expensive `repair` API.
49    ///
50    /// Data corruption is in general when the data can be confidently read,
51    /// but does not meet integrity expectation. Note: a "file not found" error
52    /// is a piece of information that is "confidently read", because it's
53    /// unexpected to get a different error by using different users, or
54    /// process settings. So "file not found" is not always a corruption.
55    ///
56    /// For example, those are data corruption errors:
57    /// - XxHash checksum does not match the data
58    /// - Some data says "read this file in 100..200 byte range", but logically,
59    ///   the file only has 150 bytes.
60    /// - A byte is expected to only be 1 or 2. But it turns out to be 0.
61    /// - Both file "a" and "b" are expected to exist in a directory, but "a"
62    ///   can be found, while "b" does not exist.
63    ///
64    /// Those are not data corruption:
65    /// - Cannot open a file due to permission issues or exceeding the file
66    ///   descriptor limit.
67    /// - Programming errors and API misuse. For example, a user-provided
68    ///   index function says `data[5..10] is the index key` while `data`
69    /// - Both file "a" and "b" are expected to exist in a directory, but "a"
70    ///   can be found, while "b" cannot be opened due to permission issues.
71    ///
72    /// It's expected that data corruption can *only* happen when the managed
73    /// files or directories are changed without using this crate.  For example,
74    /// a hard reboot, deleting lock files while multiple processes are
75    /// attempting to write, deleting or changing files in some ways. Issues
76    /// like "disk is full", "permission errors", "process killed at random
77    /// time" are expected to not cause data corruption (but only data loss).
78    pub fn is_corruption(&self) -> bool {
79        self.inner.is_corruption
80    }
81
82    pub fn io_error_kind(&self) -> io::ErrorKind {
83        self.inner.io_error_kind.unwrap_or(io::ErrorKind::Other)
84    }
85
86    // Following methods are used by this crate only.
87    // External code should not construct or modify `Error`.
88
89    pub(crate) fn message(mut self, message: impl ToString) -> Self {
90        self.inner.messages.push(message.to_string());
91        self
92    }
93
94    pub(crate) fn source(self, source: impl std::error::Error + Send + Sync + 'static) -> Self {
95        self.source_dyn(Box::new(source))
96    }
97
98    fn source_dyn(mut self, source: Box<dyn std::error::Error + Send + Sync + 'static>) -> Self {
99        // Inherit the data corruption flag.
100        if let Some(err) = source.downcast_ref::<Error>() {
101            if err.is_corruption() {
102                self = self.mark_corruption();
103            }
104        }
105
106        self.inner.sources.push(source);
107        self
108    }
109
110    pub(crate) fn mark_corruption(mut self) -> Self {
111        self.inner.is_corruption = true;
112        self
113    }
114
115    pub(crate) fn blank() -> Self {
116        Error {
117            inner: Default::default(),
118        }
119    }
120
121    /// A ProgrammingError that breaks some internal assumptions.
122    /// For example, passing an invalid parameter to an API.
123    #[inline(never)]
124    pub(crate) fn programming(message: impl ToString) -> Self {
125        Self::blank().message(format!("ProgrammingError: {}", message.to_string()))
126    }
127
128    /// A data corruption error with path.
129    ///
130    /// If there is an [`IOError`], use [`IoResultExt::context`] instead.
131    #[inline(never)]
132    pub(crate) fn corruption(path: &Path, message: impl ToString) -> Self {
133        let message = format!("{:?}: {}", path, message.to_string());
134        Self::blank().mark_corruption().message(message)
135    }
136
137    /// An error with a path that is not a data corruption.
138    ///
139    /// If there is an [`IOError`], use [`IoResultExt::context`] instead.
140    #[inline(never)]
141    pub(crate) fn path(path: &Path, message: impl ToString) -> Self {
142        let message = format!("{:?}: {}", path, message.to_string());
143        Self::blank().message(message)
144    }
145
146    /// Wrap a dynamic stdlib error.
147    #[inline(never)]
148    pub(crate) fn wrap(
149        err: Box<dyn std::error::Error + Send + Sync + 'static>,
150        message: impl LazyToString,
151    ) -> Self {
152        Self::blank()
153            .message(message.to_string_costly())
154            .source_dyn(err)
155    }
156}
157
158/// Create an Error from a message.
159impl From<&str> for Error {
160    fn from(s: &str) -> Self {
161        Self::blank().message(s)
162    }
163}
164
165/// Create an Error from a message and another error.
166impl<E: std::error::Error + Send + Sync + 'static> From<(&str, E)> for Error {
167    fn from(s: (&str, E)) -> Self {
168        Self::blank().message(s.0).source(s.1)
169    }
170}
171
172impl fmt::Display for Error {
173    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
174        let mut lines = Vec::new();
175        for message in &self.inner.messages {
176            lines.push(message.to_string());
177        }
178        if !self.inner.sources.is_empty() {
179            lines.push(format!("Caused by {} errors:", self.inner.sources.len()));
180            for source in &self.inner.sources {
181                lines.push(indent(format!("{}", source), 2, '-'));
182            }
183        }
184        write!(f, "{}", lines.join("\n"))
185    }
186}
187
188impl fmt::Debug for Error {
189    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
190        let mut lines = Vec::new();
191        for message in &self.inner.messages {
192            lines.push(message.to_string());
193        }
194        if self.is_corruption() {
195            lines.push("(This error is considered as a data corruption)".to_string())
196        }
197        if !self.inner.sources.is_empty() {
198            lines.push(format!("Caused by {} errors:", self.inner.sources.len()));
199            for source in &self.inner.sources {
200                lines.push(indent(format!("{:?}", source), 2, '-'));
201            }
202        }
203        write!(f, "{}", lines.join("\n"))
204    }
205}
206
207fn indent(s: String, spaces: usize, first_line_prefix: char) -> String {
208    if spaces == 0 {
209        s
210    } else {
211        format!(
212            "{}{}{}",
213            first_line_prefix,
214            " ".repeat(spaces - 1),
215            s.lines()
216                .collect::<Vec<_>>()
217                .join(&format!("\n{}", " ".repeat(spaces)))
218        )
219    }
220}
221
222pub(crate) trait ResultExt<T> {
223    /// Mark the error as data corruption.
224    fn corruption(self) -> Self;
225
226    /// Add a string message as context.
227    fn context<S: LazyToString>(self, message: S) -> Self;
228
229    /// Add an error source.
230    fn source<E: std::error::Error + Send + Sync + 'static>(self, source: E) -> Self;
231}
232
233impl<T> ResultExt<T> for Result<T> {
234    fn corruption(self) -> Self {
235        self.map_err(|err| err.mark_corruption())
236    }
237
238    fn context<S: LazyToString>(self, message: S) -> Self {
239        self.map_err(|err| err.message(message.to_string_costly()))
240    }
241
242    fn source<E: std::error::Error + Send + Sync + 'static>(self, source: E) -> Self {
243        self.map_err(|err| err.source(source))
244    }
245}
246
247impl std::error::Error for Error {
248    // This 'Error' type is designed to be opaque (internal states are
249    // private, including inner errors), and takes responsibility
250    // of displaying a -chain- tree of errors. So it might be desirable
251    // not implementing `source` here, and expose public APIs for all
252    // use-needs.
253}
254
255pub(crate) trait IoResultExt<T> {
256    /// Wrap [`io::Result`] in [`Result`] with extra context about filesystem
257    /// path and the operation name.
258    ///
259    /// Mark InvalidData and UnexpectedEof as data corruption automatically.
260    ///
261    /// Consider using [`ResultExt::corruption`] to mark the error as data
262    /// corruption if appropriate.
263    fn context<TS: LazyToString>(self, path: &Path, message: TS) -> Result<T>;
264
265    /// Wrap an infallible Result. For example, writing to memory.
266    fn infallible(self) -> Result<T>;
267}
268
269impl<T> IoResultExt<T> for std::io::Result<T> {
270    fn context<TS: LazyToString>(self, path: &Path, message: TS) -> Result<T> {
271        self.map_err(|err| {
272            use std::io::ErrorKind;
273            let kind = err.kind().clone();
274            let corruption = match kind {
275                // For example, try to mmap 200 bytes, but the file
276                // only has 100 bytes. This is unlikely caused by
277                // non-data-corruption issues.
278                ErrorKind::UnexpectedEof | ErrorKind::InvalidData => true,
279                _ => false,
280            };
281            let is_eperm = kind == ErrorKind::PermissionDenied;
282
283            let mut err = Error::blank().source(err).message(format!(
284                "{:?}: {}",
285                path,
286                message.to_string_costly()
287            ));
288            if corruption {
289                err = err.mark_corruption();
290            }
291            err.inner.io_error_kind = Some(kind);
292
293            // Provide more context for PermissionDenied
294            if is_eperm {
295                #[cfg(unix)]
296                #[allow(clippy::redundant_closure_call)]
297                {
298                    let add_stat_context = |path: &Path, err: Error| {
299                        use std::os::unix::fs::MetadataExt;
300                        if let Ok(meta) = path.metadata() {
301                            let msg = format!(
302                                "stat({:?}) = dev:{} ino:{} mode:0o{:o} uid:{} gid:{} mtime:{}",
303                                path,
304                                meta.dev(),
305                                meta.ino(),
306                                meta.mode(),
307                                meta.uid(),
308                                meta.gid(),
309                                meta.mtime()
310                            );
311                            err.message(msg)
312                        } else {
313                            err
314                        }
315                    };
316                    err = add_stat_context(path, err);
317                    // For EPERM on mkdir, parent directory stat is useful.
318                    if let Some(parent) = path.parent() {
319                        err = add_stat_context(parent, err);
320                    }
321                }
322            }
323
324            err
325        })
326    }
327
328    fn infallible(self) -> Result<T> {
329        self.map_err(|err| Error::blank().source(err).message("Unexpected failure"))
330    }
331}
332
333pub(crate) trait LazyToString {
334    fn to_string_costly(&self) -> String;
335}
336
337// &'static str is cheap.
338impl LazyToString for &'static str {
339    fn to_string_costly(&self) -> String {
340        (*self).to_string()
341    }
342}
343
344impl<F: Fn() -> String> LazyToString for F {
345    fn to_string_costly(&self) -> String {
346        self()
347    }
348}
349
350#[cfg(test)]
351mod tests {
352    use super::*;
353
354    #[test]
355    fn test_error_format() {
356        let mut e = Error::blank();
357
358        assert_eq!(format!("{}", &e), "");
359
360        // Attach messages.
361
362        e = e.message("Error Message 1");
363        e = e.message("Error Message 2");
364        assert_eq!(
365            format!("{}", &e),
366            r#"Error Message 1
367Error Message 2"#
368        );
369
370        // Attach error sources.
371        e = e.source(Error::blank().message("Inner Error 1"));
372        e = e.source(
373            Error::blank()
374                .message("Inner Error 2")
375                .source(Error::blank().message("Nested Error 1")),
376        );
377        assert_eq!(
378            format!("{}", &e),
379            r#"Error Message 1
380Error Message 2
381Caused by 2 errors:
382- Inner Error 1
383- Inner Error 2
384  Caused by 1 errors:
385  - Nested Error 1"#
386        );
387
388        // Mark as data corruption.
389        e = e.mark_corruption();
390        assert_eq!(
391            format!("{:?}", &e),
392            r#"Error Message 1
393Error Message 2
394(This error is considered as a data corruption)
395Caused by 2 errors:
396- Inner Error 1
397- Inner Error 2
398  Caused by 1 errors:
399  - Nested Error 1"#
400        );
401    }
402
403    #[test]
404    fn test_result_ext() {
405        let result: Result<()> = Err(Error::blank()).corruption();
406        assert!(result.unwrap_err().is_corruption());
407    }
408
409    #[test]
410    fn test_inherit_corruption() {
411        assert!(!Error::blank().is_corruption());
412        assert!(!Error::blank().source(Error::blank()).is_corruption());
413        assert!(
414            Error::blank()
415                .source(Error::blank().mark_corruption())
416                .is_corruption()
417        );
418        assert!(
419            Error::blank()
420                .source(Error::blank().source(Error::blank().mark_corruption()))
421                .is_corruption()
422        );
423    }
424
425    #[test]
426    fn test_io_result_ext() {
427        let err = io_result().context(Path::new("a.txt"), "cannot open for reading");
428        assert_eq!(
429            format!("{}", err.unwrap_err()),
430            r#""a.txt": cannot open for reading
431Caused by 1 errors:
432- io::Error: something wrong happened"#
433        );
434
435        let name = "b.txt";
436        let err = io_result().context(Path::new(&name), || format!("cannot open {}", &name));
437        assert_eq!(
438            format!("{}", err.unwrap_err()),
439            r#""b.txt": cannot open b.txt
440Caused by 1 errors:
441- io::Error: something wrong happened"#
442        );
443    }
444
445    fn io_result() -> std::io::Result<()> {
446        Err(std::io::Error::new(
447            std::io::ErrorKind::Other,
448            "io::Error: something wrong happened",
449        ))
450    }
451}