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}