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}