Skip to main content

lsm_db/
error.rs

1//! The crate error type.
2//!
3//! Every fallible operation in `lsm-db` returns [`Result<T>`], whose error is
4//! [`Error`]. The type integrates with the portfolio's `error-forge` framework
5//! — it implements [`error_forge::ForgeError`], so callers get the stable
6//! `kind` / `caption` / `is_fatal` metadata other crates rely on — while still
7//! exposing the underlying [`std::io::Error`] through
8//! [`std::error::Error::source`] for code that needs the OS error kind directly.
9
10use std::{fmt, io};
11
12use error_forge::ForgeError;
13
14/// A specialised [`Result`](std::result::Result) for storage-engine operations.
15///
16/// Defaults its error to [`Error`], so most signatures read `Result<T>`.
17pub type Result<T, E = Error> = std::result::Result<T, E>;
18
19/// Everything that can go wrong while opening, reading from, writing to, or
20/// flushing an [`Lsm`](crate::Lsm) engine.
21///
22/// The type is
23/// [`#[non_exhaustive]`](https://doc.rust-lang.org/reference/attributes/type_system.html#the-non_exhaustive-attribute):
24/// future versions may add variants without a major bump, so a `match` over it
25/// must include a wildcard arm.
26#[non_exhaustive]
27#[derive(Debug)]
28pub enum Error {
29    /// An underlying I/O operation failed.
30    ///
31    /// `context` names the operation that was attempted (for example
32    /// `"open database directory"` or `"flush memtable to disk"`) so the
33    /// message is actionable without a backtrace. The original [`io::Error`] is
34    /// preserved as the [`source`](std::error::Error::source); inspect it when
35    /// the OS error kind (disk full, permission denied, not found) drives the
36    /// recovery decision.
37    Io {
38        /// What the engine was trying to do when the I/O error occurred.
39        context: &'static str,
40        /// The underlying operating-system error.
41        source: io::Error,
42    },
43
44    /// An on-disk sorted run (SSTable) is not intact.
45    ///
46    /// Either a length prefix is implausibly large, or the file ends in the
47    /// middle of a record. A damaged run cannot be trusted, so the read that
48    /// touched it fails rather than returning partial or fabricated data.
49    /// `reason` is a short, human-readable description of the inconsistency.
50    Corruption {
51        /// A short, human-readable reason the run was rejected.
52        reason: &'static str,
53    },
54}
55
56impl Error {
57    /// Wrap an [`io::Error`] with the static context describing the operation.
58    pub(crate) fn io(context: &'static str, source: io::Error) -> Self {
59        Error::Io { context, source }
60    }
61
62    /// Build an [`Error::Corruption`] with a static reason.
63    pub(crate) fn corruption(reason: &'static str) -> Self {
64        Error::Corruption { reason }
65    }
66}
67
68impl fmt::Display for Error {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        match self {
71            Error::Io { context, source } => {
72                write!(f, "i/o error while {context}: {source}")
73            }
74            Error::Corruption { reason } => {
75                write!(f, "sorted-run corruption: {reason}")
76            }
77        }
78    }
79}
80
81impl std::error::Error for Error {
82    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
83        match self {
84            Error::Io { source, .. } => Some(source),
85            Error::Corruption { .. } => None,
86        }
87    }
88}
89
90/// A bare [`io::Error`] converts into [`Error::Io`] with a generic context.
91///
92/// Call sites that know what they were doing attach a specific context instead;
93/// this exists for the `?` ergonomics of code — including doctests and examples
94/// — that does not.
95impl From<io::Error> for Error {
96    fn from(source: io::Error) -> Self {
97        Error::Io {
98            context: "performing a storage i/o operation",
99            source,
100        }
101    }
102}
103
104impl ForgeError for Error {
105    fn kind(&self) -> &'static str {
106        match self {
107            Error::Io { .. } => "Io",
108            Error::Corruption { .. } => "Corruption",
109        }
110    }
111
112    fn caption(&self) -> &'static str {
113        "lsm storage engine error"
114    }
115
116    /// Corruption is unrecoverable by retry: the bytes on disk are already
117    /// damaged. I/O errors are left non-fatal for the caller to judge — a
118    /// transient `Interrupted` or a recoverable `WouldBlock` may be retried.
119    fn is_fatal(&self) -> bool {
120        matches!(self, Error::Corruption { .. })
121    }
122}
123
124#[cfg(test)]
125#[allow(clippy::unwrap_used, clippy::expect_used)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn test_io_error_exposes_source() {
131        let inner = io::Error::new(io::ErrorKind::PermissionDenied, "denied");
132        let err = Error::io("open database directory", inner);
133        let source = std::error::Error::source(&err).expect("io error has a source");
134        let io_source = source
135            .downcast_ref::<io::Error>()
136            .expect("source downcasts to io::Error");
137        assert_eq!(io_source.kind(), io::ErrorKind::PermissionDenied);
138    }
139
140    #[test]
141    fn test_corruption_has_no_source() {
142        let err = Error::corruption("length prefix exceeds file size");
143        assert!(std::error::Error::source(&err).is_none());
144    }
145
146    #[test]
147    fn test_corruption_is_fatal_io_is_not() {
148        assert!(Error::corruption("truncated record").is_fatal());
149        let io = Error::io("read run", io::Error::from(io::ErrorKind::UnexpectedEof));
150        assert!(!io.is_fatal());
151    }
152
153    #[test]
154    fn test_kind_matches_variant() {
155        assert_eq!(Error::corruption("x").kind(), "Corruption");
156        let io = Error::io("x", io::Error::from(io::ErrorKind::Other));
157        assert_eq!(io.kind(), "Io");
158    }
159
160    #[test]
161    fn test_display_is_actionable() {
162        let err = Error::corruption("truncated value");
163        assert_eq!(err.to_string(), "sorted-run corruption: truncated value");
164    }
165}