Skip to main content

amethystate_core/
failure.rs

1use error_stack::Report;
2use std::error::Error;
3use std::fmt;
4
5/// What a store operation failed at.
6///
7/// Each variant names the operation that failed. Which engine said what is the
8/// frame below this one in the report, and the particulars - which path, which
9/// file, which prefix - are attachments put there by whoever knew them. Two
10/// engines failing to write are the same kind of failure, told apart by the
11/// frames underneath.
12///
13/// `non_exhaustive` where the sets that carry it are not: this one is the
14/// disk's own list and grows with the engines, and nobody is meant to match it
15/// arm by arm. The sets are the opposite - a `match` over one is meant to be
16/// complete, and gaining a way to fail is meant to break it.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18#[non_exhaustive]
19pub enum StorageError {
20    /// Opening or creating the store.
21    Open,
22
23    /// Reading a value.
24    Read,
25
26    /// Writing a value.
27    Write,
28
29    /// Removing a key or a subtree.
30    Delete,
31
32    /// Listing what is under a prefix.
33    Scan,
34
35    /// Getting what is buffered onto disk.
36    Flush,
37
38    /// Turning a value into the store's format, or reading it back.
39    Codec,
40
41    /// Reading or writing the schema bookkeeping - versions, snapshots, the
42    /// migration log, the initialization markers.
43    Meta,
44
45    /// Bringing stored data up to the schema the code declares.
46    Migrate,
47
48    /// A name that cannot be a level, so nothing can be stored under it.
49    Path,
50
51    /// A path or a value that nests deeper than this store reads back.
52    ///
53    /// Told apart from [`StorageError::Path`] because the names are all fine:
54    /// what is wrong is how many of them there are, and the facts say which
55    /// budget ran out and by how much.
56    Depth,
57
58    /// The flush this commit was waiting on did not complete.
59    CommitFailed,
60
61    /// Two owners want the same place, so one would write over the other.
62    Taken,
63
64    /// The store was closed and has let go of its file.
65    ///
66    /// A close hands the file to whoever asked for it - another process, a
67    /// backup, a rename - and it stays theirs, so every later read, write,
68    /// scan and delete answers with this.
69    Closed,
70
71    /// The change was stored, and somebody subscribed to it could not take it.
72    ///
73    /// The write landed: this is about what happened while telling people. A
74    /// field that cannot read back what was just written to its path answers
75    /// with this, which is how the writer finds out that what it stored is not
76    /// what that field holds.
77    Notify,
78
79    /// Asked for from inside something the store is already doing, where doing
80    /// it would mean waiting for the caller to finish.
81    ///
82    /// Closing from `on_persist_failure` is the one that reaches here: that
83    /// callback runs on the thread a close has to wait for.
84    Reentrant,
85}
86
87impl fmt::Display for StorageError {
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        f.write_str(match self {
90            StorageError::Open => "the store could not be opened",
91            StorageError::Read => "the store could not read",
92            StorageError::Write => "the store could not write",
93            StorageError::Delete => "the store could not delete",
94            StorageError::Scan => "the store could not list a prefix",
95            StorageError::Flush => "the store could not commit what it had buffered",
96            StorageError::Codec => "the value could not be encoded or decoded",
97            StorageError::Meta => "the schema bookkeeping could not be read or written",
98            StorageError::Migrate => "the data could not be brought up to the declared schema",
99            StorageError::Path => "a name that cannot be a level",
100            StorageError::Depth => "deeper than this store reads back",
101            StorageError::CommitFailed => "the flush this commit was waiting on did not complete",
102            StorageError::Taken => "two schemas want the same place",
103            StorageError::Closed => "the store was closed and has let go of its file",
104            StorageError::Notify => "the change was stored, and a subscriber could not take it",
105            StorageError::Reentrant => {
106                "the store cannot do this from inside what it is already doing"
107            }
108        })
109    }
110}
111
112impl Error for StorageError {}
113
114pub type StorageResult<T> = Result<T, Report<StorageError>>;
115
116/// A write a document engine refuses because its tree cannot represent the
117/// result.
118///
119/// A tree holds a value at a node or values under it, never both, so the second
120/// write is refused and the first survives. Only the document engines - json,
121/// toml, ron - report this, and only inside the part of the file a schema
122/// declares: what nothing declares is written whole, one key to a path, where
123/// there is no level for a value to be at odds with.
124///
125/// Sits below [`StorageError::Write`] so that a caller who has to tell this
126/// apart - a seeding write, which nobody asked for, backs off where a real one
127/// propagates - can do it without knowing which engine is underneath.
128#[derive(Debug, PartialEq, Eq)]
129#[non_exhaustive]
130pub enum Occupied {
131    /// A value is stored at a level the write needs as a branch.
132    Value { level: String },
133
134    /// Values are stored under the level a plain value would replace.
135    ///
136    /// Only a value that is not itself a map is refused here. A serialized
137    /// struct is a map with children, indistinguishable in a document from a
138    /// level with values under it, so writing one over another is taken as the
139    /// update it almost always is.
140    Branch { level: String },
141}
142
143impl fmt::Display for Occupied {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        match self {
146            Occupied::Value { level } => {
147                write!(
148                    f,
149                    "`{level}` holds a value, so nothing can be stored under it"
150                )
151            }
152            Occupied::Branch { level } => write!(
153                f,
154                "`{level}` holds values under it, so a value cannot be stored at it"
155            ),
156        }
157    }
158}
159
160impl Error for Occupied {}
161
162/// A report's chain of contexts on one line, for a log record.
163///
164/// A report's `Display` shows only the outermost context, which is the one that
165/// says least: "the store could not write" without what refused it. This keeps
166/// the causes and drops the attachments, so the line stays greppable.
167pub fn one_line<C: fmt::Display + Send + Sync + 'static>(report: &Report<C>) -> String {
168    report
169        .frames()
170        .filter_map(|frame| match frame.kind() {
171            error_stack::FrameKind::Context(context) => Some(context.to_string()),
172            error_stack::FrameKind::Attachment(_) => None,
173        })
174        .collect::<Vec<_>>()
175        .join(" <- ")
176}
177
178/// A report's frames as an ordinary error chain.
179///
180/// A [`Report`] keeps its frames itself, and [`Error::source`] has to hand back
181/// something with a life of its own, so the chain is copied out where a report
182/// crosses into a set. One link per context, carrying the attachments that were
183/// put on it - the key, the file, the engine's own words - so a caller who
184/// walks the chain and prints it gets what the report holds without knowing
185/// there is a report.
186///
187/// The outermost context is left out: whoever carries this says it already, and
188/// printing it again is the duplicate line this exists to remove. What was
189/// attached to it is kept and moves down onto the first link that is a cause,
190/// so no link is facts with nothing that failed above them - a reader walking
191/// causes should meet causes. Only where the outermost is the sole context do
192/// the facts stand on their own, because then there is nothing to move them to.
193#[derive(Debug)]
194pub struct Caused {
195    said: String,
196    under: Option<Box<Caused>>,
197}
198
199impl Caused {
200    /// The chain under `report`'s outermost context, or `None` when the
201    /// outermost is all there is and carries nothing.
202    pub fn under<C>(report: &Report<C>) -> Option<Self> {
203        let mut links: Vec<Link> = Vec::new();
204        let mut waiting: Vec<String> = Vec::new();
205
206        for frame in report.frames() {
207            match frame.kind() {
208                error_stack::FrameKind::Context(context) => links.push(Link {
209                    said: Some(context.to_string()),
210                    carried: std::mem::take(&mut waiting),
211                }),
212                error_stack::FrameKind::Attachment(error_stack::AttachmentKind::Printable(
213                    shown,
214                )) => waiting.push(shown.to_string()),
215                _ => {}
216            }
217        }
218
219        match links.len() {
220            0 => {}
221            1 => links[0].said = None,
222            _ => {
223                let outermost = links.remove(0);
224                let mut carried = outermost.carried;
225                carried.append(&mut links[0].carried);
226                links[0].carried = carried;
227            }
228        }
229
230        links
231            .into_iter()
232            .rev()
233            .filter(|link| !link.is_empty())
234            .fold(None, |under, link| {
235                Some(Caused {
236                    said: link.spelled(),
237                    under: under.map(Box::new),
238                })
239            })
240    }
241}
242
243/// One context and what was attached to it, before it is spelled out.
244struct Link {
245    said: Option<String>,
246    carried: Vec<String>,
247}
248
249impl Link {
250    fn is_empty(&self) -> bool {
251        self.said.is_none() && self.carried.is_empty()
252    }
253
254    fn spelled(self) -> String {
255        match (self.said, self.carried.is_empty()) {
256            (Some(said), true) => said,
257            (Some(said), false) => format!("{said} ({})", self.carried.join("; ")),
258            (None, _) => self.carried.join("; "),
259        }
260    }
261}
262
263impl fmt::Display for Caused {
264    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
265        f.write_str(&self.said)
266    }
267}
268
269impl Error for Caused {
270    fn source(&self) -> Option<&(dyn Error + 'static)> {
271        self.under.as_deref().map(|under| under as &dyn Error)
272    }
273}
274
275/// A set's own sentence and the chain under it, laid out the way `anyhow` lays
276/// one out.
277///
278/// What every set's [`fmt::Debug`] is. A library error and the `anyhow::Error`
279/// it becomes should not print two different ways: whoever wrote `{err:?}`
280/// learnt the shape from one of them, and `fn main() -> anyhow::Result<()>`
281/// renders with `Debug`.
282pub fn spelled<E>(why: &E, f: &mut fmt::Formatter<'_>) -> fmt::Result
283where
284    E: Error + ?Sized,
285{
286    write!(f, "{why}")?;
287
288    let mut under = why.source();
289
290    if under.is_some() {
291        f.write_str("\n\nCaused by:")?;
292    }
293
294    while let Some(step) = under {
295        write!(f, "\n    {step}")?;
296        under = step.source();
297    }
298
299    Ok(())
300}
301
302/// A report on its way out of the library, with the chain a caller can walk.
303///
304/// The report is still there whole - [`Deref`](std::ops::Deref) reaches it, and
305/// so do [`facts::all`](crate::facts::all) and a `{:?}` of it - and beside it
306/// sits the same thing as an ordinary error chain, built the first time
307/// somebody asks and not before. A failure that nobody looks into costs the
308/// report it was already carrying and nothing more.
309pub struct Because {
310    why: Report<StorageError>,
311    chain: std::sync::OnceLock<Option<Caused>>,
312}
313
314impl Because {
315    pub fn new(why: Report<StorageError>) -> Self {
316        Self {
317            why,
318            chain: std::sync::OnceLock::new(),
319        }
320    }
321
322    /// The report, for the plumbing under the boundary.
323    pub fn into_report(self) -> Report<StorageError> {
324        self.why
325    }
326
327    /// What is under the outermost context, as an error chain.
328    pub fn caused(&self) -> Option<&Caused> {
329        self.chain.get_or_init(|| Caused::under(&self.why)).as_ref()
330    }
331
332    /// The whole report as a string, facts and all.
333    ///
334    /// For a caller who is handing this to `anyhow` and wants everything in one
335    /// place: `.context(why.explain())`.
336    pub fn explain(&self) -> String {
337        format!("{:?}", self.why)
338    }
339}
340
341impl std::ops::Deref for Because {
342    type Target = Report<StorageError>;
343
344    fn deref(&self) -> &Self::Target {
345        &self.why
346    }
347}
348
349impl fmt::Debug for Because {
350    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
351        fmt::Debug::fmt(&self.why, f)
352    }
353}
354
355/// What the report says, which is its outermost context. The chain under it is
356/// [`caused`](Self::caused) and the whole of it is [`explain`](Self::explain).
357impl fmt::Display for Because {
358    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
359        fmt::Display::fmt(&self.why, f)
360    }
361}
362
363impl From<Report<StorageError>> for Because {
364    fn from(why: Report<StorageError>) -> Self {
365        Self::new(why)
366    }
367}
368
369/// What a migration step returns when it decides to fail.
370///
371/// A step is written by whoever uses the library, and the frames around it -
372/// which prefix, which version, which store - are put there by the engine that
373/// called it. So a step has nothing to add and only needs to say that this is
374/// its refusal rather than the store's.
375pub trait IntoStorageReport {
376    fn into_report(self) -> Report<StorageError>;
377}
378
379impl IntoStorageReport for crate::path::StorePathError {
380    fn into_report(self) -> Report<StorageError> {
381        Report::new(self).change_context(StorageError::Path)
382    }
383}