Skip to main content

diskann_record/load/
error.rs

1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6//! Load-side error type and classification.
7//!
8//! The [`Error`] type wraps [`anyhow::Error`] for rich diagnostics and carries a
9//! recoverable / critical bit used by probing call sites. The [`Kind`] enum enumerates
10//! the well-known structural failure modes; [`Kind::is_recoverable`] is the canonical
11//! source of truth for the recoverable / critical classification.
12
13use std::fmt::{Debug, Display};
14
15/// A specialized [`std::result::Result`] for load-side operations.
16pub type Result<T> = ::std::result::Result<T, Error>;
17
18/// Load-side error.
19///
20/// Carries an inner [`anyhow::Error`] for rich diagnostics (chained context,
21/// backtraces) along with a single `recoverable` bit. Recoverable errors are
22/// the contract for probing APIs: a caller that tries multiple load strategies
23/// (e.g. current version, then legacy) can distinguish "this attempt didn't
24/// match, try another" from "the data is broken, stop now".
25///
26/// Most constructors produce *critical* (non-recoverable) errors. Probing
27/// call sites use the explicit `*_recoverable` constructors, or rely on the
28/// [`From<Kind>`] impl which classifies each [`Kind`] variant according to
29/// [`Kind::is_recoverable`].
30#[derive(Debug)]
31pub struct Error {
32    inner: anyhow::Error,
33    recoverable: bool,
34}
35
36impl Error {
37    /// Construct a critical error from an underlying source error.
38    pub fn new<E>(err: E) -> Self
39    where
40        E: std::error::Error + Send + Sync + 'static,
41    {
42        Self {
43            inner: anyhow::Error::new(err),
44            recoverable: false,
45        }
46    }
47
48    /// Construct a critical error from a display message.
49    pub fn message<D>(message: D) -> Self
50    where
51        D: Display + Debug + Send + Sync + 'static,
52    {
53        Self {
54            inner: anyhow::Error::msg(message),
55            recoverable: false,
56        }
57    }
58
59    /// Construct a recoverable error from an underlying source. Suitable for
60    /// probing APIs that may attempt an alternative load strategy.
61    pub fn new_recoverable<E>(err: E) -> Self
62    where
63        E: std::error::Error + Send + Sync + 'static,
64    {
65        Self {
66            inner: anyhow::Error::new(err),
67            recoverable: true,
68        }
69    }
70
71    /// Construct a recoverable error from a display message. Suitable for
72    /// probing APIs that may attempt an alternative load strategy.
73    pub fn message_recoverable<D>(message: D) -> Self
74    where
75        D: Display + Debug + Send + Sync + 'static,
76    {
77        Self {
78            inner: anyhow::Error::msg(message),
79            recoverable: true,
80        }
81    }
82
83    /// Attach additional context. The `recoverable` flag is preserved.
84    pub fn context<D>(self, message: D) -> Self
85    where
86        D: Display + Send + Sync + 'static,
87    {
88        Self {
89            inner: self.inner.context(message),
90            recoverable: self.recoverable,
91        }
92    }
93
94    /// Returns `true` if this error is recoverable. Probing call sites should
95    /// only fall back to alternative load strategies when this is `true`.
96    pub fn is_recoverable(&self) -> bool {
97        self.recoverable
98    }
99}
100
101impl Display for Error {
102    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103        write!(f, "Load Error: {:?}", self.inner)
104    }
105}
106
107impl std::error::Error for Error {
108    /// Returns the lower-level source of this error, if it exists.
109    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
110        Some(self.inner.as_ref())
111    }
112}
113
114/// Well-known classes of load-side failure.
115///
116/// Used in two roles:
117///
118/// * As the source of an [`Error`] via `From<Kind>` (and the matching `From<Kind>` for
119///   [`Error`] which classifies recoverable / critical according to
120///   [`Kind::is_recoverable`]).
121/// * As a probe value in error chains — high-level callers can introspect the kind to
122///   decide whether to try a fallback loader.
123#[derive(Debug, Clone, Copy)]
124#[non_exhaustive]
125pub enum Kind {
126    /// The manifest's `$version` does not match the loader's expected
127    /// [`Load::VERSION`](crate::load::Load::VERSION).
128    VersionMismatch,
129    /// A required field is absent from the record.
130    MissingField,
131    /// The shape of the saved value does not match what the loader expected (e.g. found
132    /// an array where an object was needed).
133    TypeMismatch,
134    /// The manifest's version is recognized as not matching the current schema, and the
135    /// type's [`Load::load_legacy`](crate::load::Load::load_legacy) has no upgrade path
136    /// for it.
137    UnknownVersion,
138    /// The variant tag read from the wire format does not match any known
139    /// variant of the target enum.
140    UnknownVariant,
141    /// A numeric value in the manifest does not fit in the requested Rust type
142    /// (either out of range or would lose precision).
143    NumberOutOfRange,
144    /// A `$handle` references a file name that is not registered in the
145    /// manifest's `files` set.
146    MissingFile,
147}
148
149impl Kind {
150    /// Stable, human-readable description of this kind. Used as the default error
151    /// message when constructing an [`Error`] from a `Kind`.
152    pub const fn as_str(self) -> &'static str {
153        match self {
154            Self::VersionMismatch => "version mismatch",
155            Self::MissingField => "missing field",
156            Self::TypeMismatch => "type mismatch",
157            Self::UnknownVersion => "unknown version",
158            Self::UnknownVariant => "unknown variant",
159            Self::NumberOutOfRange => "number out of range for target type",
160            Self::MissingFile => "handle references a file not present in the manifest",
161        }
162    }
163
164    /// Whether an error of this kind should be treated as recoverable by
165    /// probing APIs (i.e., suitable for triggering a fallback to an alternative
166    /// load strategy).
167    ///
168    /// Recoverable kinds describe "the data did not match what this loader
169    /// expected" (a different version or shape might still succeed). Critical
170    /// kinds describe structural or integrity problems where retrying would be
171    /// pointless or unsafe.
172    pub const fn is_recoverable(self) -> bool {
173        match self {
174            // Shape/version probing signals — another loader might succeed.
175            Self::VersionMismatch | Self::MissingField | Self::TypeMismatch => true,
176            // Structural / integrity failures — give up.
177            Self::UnknownVersion
178            | Self::UnknownVariant
179            | Self::NumberOutOfRange
180            | Self::MissingFile => false,
181        }
182    }
183}
184
185impl std::error::Error for Kind {}
186
187impl std::fmt::Display for Kind {
188    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189        f.write_str(self.as_str())
190    }
191}
192
193impl From<Kind> for Error {
194    fn from(kind: Kind) -> Self {
195        Self {
196            inner: anyhow::Error::new(kind),
197            recoverable: kind.is_recoverable(),
198        }
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[test]
207    fn context_preserves_recoverable_flag() {
208        assert!(
209            Error::from(Kind::TypeMismatch)
210                .context("extra")
211                .is_recoverable()
212        );
213        assert!(
214            !Error::from(Kind::MissingFile)
215                .context("extra")
216                .is_recoverable()
217        );
218    }
219}