Skip to main content

eventsdb_core/
error.rs

1//! The error a store call can fail with.
2//!
3//! The variants are split by what a caller can *do* about them, not by where
4//! they were raised. `Busy` says another call is worth making; `Validation`
5//! says the event was wrong and no retry will change that; `Storage` says the
6//! database itself failed; `Unsupported` says the request was well-formed and
7//! this backend has no answer for it.
8
9use thiserror::Error;
10
11/// The result of every fallible store operation.
12pub type Result<T> = std::result::Result<T, Error>;
13
14/// `#[non_exhaustive]` because this list is not finished.
15///
16/// Several open questions end in "add a variant", and each of those is a
17/// breaking change to every caller that matches exhaustively — unless the
18/// attribute is there first. It costs a `_` arm today and buys the right to
19/// name a new failure later without a major version.
20#[derive(Debug, Error)]
21#[non_exhaustive]
22pub enum Error {
23    /// The event did not satisfy the envelope contract. The message names the
24    /// offending key and says what belongs there instead, because a caller
25    /// that has to guess will guess the same way twice.
26    #[error("invalid event: {0}")]
27    Validation(String),
28
29    /// The write could not take the lock in time. Retrying is meaningful:
30    /// this is contention, not a defect in the request.
31    #[error("store busy: {0}")]
32    Busy(String),
33
34    /// A deadline the caller set was reached, and the statement was
35    /// interrupted.
36    ///
37    /// Deliberately not [`Error::Busy`]. Busy means someone else holds the
38    /// lock, and the same call may well succeed next time; this means the work
39    /// itself was longer than the caller allowed, and repeating it unchanged
40    /// will take just as long. Nothing retries it automatically.
41    #[error("exceeded the deadline: {0}")]
42    Timeout(String),
43
44    /// The database failed: it could not read, could not write, could not open.
45    ///
46    /// Retry-or-call-someone. Contrast [`Error::Corruption`], which is the
47    /// same call site's other outcome and wants the opposite response.
48    #[error("storage failure: {0}")]
49    Storage(String),
50
51    /// The read succeeded and the bytes do not mean what they should.
52    ///
53    /// Split from [`Error::Storage`] because this module's rule is that
54    /// variants divide by what a caller can *do*, and these two divide
55    /// cleanly: a failing disk is worth retrying and worth paging someone
56    /// about; bytes that do not decode are worth stopping for. Retrying reads
57    /// the same bytes again.
58    ///
59    /// Two things arrive here, and the second is the common one:
60    ///
61    /// - a stored `meta` or `data` column that will not parse as JSON — out-of-
62    ///   band tampering, an interrupted maintenance job, a version skew;
63    /// - an event that does not read as an event *after the upcaster chain
64    ///   ran*. That is almost always a bug in the chain rather than in the
65    ///   file: a step dropped a field the next one needs. It was reported as a
66    ///   storage failure before, which sent readers to look at the disk.
67    ///
68    /// Either way a dropped row must never read as an empty stream — a fold
69    /// over a silently truncated log produces a wrong state rather than an
70    /// obvious failure.
71    #[error("stored data does not decode: {0}")]
72    Corruption(String),
73
74    /// The request is well-formed and this backend cannot serve it: an
75    /// in-memory store asked to write a second stream, a store that is not a
76    /// database asked to answer SQL.
77    #[error("unsupported by this backend: {0}")]
78    Unsupported(String),
79
80    /// The history the caller asked for is partly gone: retention removed
81    /// events at or below `removed_up_to`, and a fold starting at `requested`
82    /// would be missing them.
83    ///
84    /// Loud on purpose. Retention is the one operation that can make a
85    /// correct-looking read wrong, so a consumer whose cursor sits behind the
86    /// watermark is told rather than handed a short answer it cannot tell
87    /// from a complete one.
88    #[error(
89        "history at or below position {removed_up_to} has been removed; \
90         reading from {requested} would be incomplete"
91    )]
92    Truncated { requested: u64, removed_up_to: u64 },
93
94    /// The stream had moved on: the head the caller expected is not the head
95    /// the write found, so nothing was appended.
96    ///
97    /// Both coordinates are given because the caller's next move needs both —
98    /// `expected` is where it left off and `actual` is what to read up to, so
99    /// it can fold only what it missed rather than the stream from the start.
100    ///
101    /// Deliberately not [`Error::Busy`]: nothing is contended, and repeating
102    /// the same call unchanged will fail the same way. The caller has to
103    /// decide again against a state it has not seen.
104    #[error("stream head is {actual:?}, not the expected {expected:?}")]
105    HeadMismatch {
106        expected: crate::store::Expected,
107        actual: crate::store::Expected,
108    },
109
110    /// Retention would have removed events a registered consumer has not seen.
111    #[error(
112        "retention would remove up to position {up_to}, past consumer `{consumer}` at {cursor}"
113    )]
114    ConsumerBehind {
115        consumer: String,
116        cursor: u64,
117        up_to: u64,
118    },
119
120    /// Retention would have removed events no confirmed export covers.
121    ///
122    /// `exported_through` is how far the chain of landed, unfiltered exports
123    /// reaches from the beginning; the plan wanted to remove up to `up_to`.
124    /// The gap between them is history that would exist nowhere afterwards.
125    #[error(
126        "retention would remove up to position {up_to}, but confirmed exports \
127         reach only {exported_through}"
128    )]
129    NotExported { up_to: u64, exported_through: u64 },
130}
131
132impl Error {
133    /// Whether another attempt at the same call is worth making.
134    ///
135    /// Contention only. A [`Error::Timeout`] is not busy: the work was longer
136    /// than the caller allowed, and repeating it unchanged will be too.
137    pub fn is_busy(&self) -> bool {
138        matches!(self, Error::Busy(_))
139    }
140
141    /// Whether a deadline was reached.
142    pub fn is_timeout(&self) -> bool {
143        matches!(self, Error::Timeout(_))
144    }
145
146    /// Public so a backend crate builds the same refusals this one does.
147    pub fn validation(message: impl Into<String>) -> Self {
148        Error::Validation(message.into())
149    }
150
151    /// Public for the same reason as [`Error::validation`].
152    pub fn storage(message: impl Into<String>) -> Self {
153        Error::Storage(message.into())
154    }
155
156    /// Public for the same reason as [`Error::validation`].
157    pub fn corruption(message: impl Into<String>) -> Self {
158        Error::Corruption(message.into())
159    }
160
161    /// Whether the bytes read back are the problem, rather than the reading.
162    pub fn is_corruption(&self) -> bool {
163        matches!(self, Error::Corruption(_))
164    }
165}