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}