1use dovecote::{DeliveryState, RowId};
4use thiserror::Error;
5
6#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8#[non_exhaustive]
9pub enum TransientKind {
10 BusyExhausted,
13}
14
15impl std::fmt::Display for TransientKind {
16 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17 formatter.write_str("SQLite busy timeout exhausted")
18 }
19}
20
21pub(crate) fn is_busy(source: &sqlx::Error) -> bool {
22 source
23 .as_database_error()
24 .and_then(sqlx::error::DatabaseError::code)
25 .and_then(|code| code.parse::<i32>().ok())
26 .is_some_and(|code| code == 5 || code == 6 || code & 0xff == 5 || code & 0xff == 6)
27}
28
29#[derive(Debug, Error)]
31#[non_exhaustive]
32pub enum EnqueueError {
33 #[error("enqueue requires a SQLite write transaction (BEGIN IMMEDIATE or a prior write)")]
35 WriteTransactionRequired,
36 #[error("invalid SQLite busy configuration: {detail}")]
38 Configuration {
39 detail: String,
41 },
42 #[error("idempotency conflict for existing row {existing_row_id:?}")]
44 IdempotencyConflict {
45 existing_row_id: RowId,
47 },
48 #[error("migration mismatch: {detail}")]
50 MigrationMismatch {
51 detail: String,
53 },
54 #[error("serialization: {detail}")]
56 Serialization {
57 detail: String,
59 },
60 #[error("{operation}: busy lock exhausted by the caller transaction: {source}")]
62 BusyExhausted {
63 operation: &'static str,
65 #[source]
66 source: sqlx::Error,
68 },
69 #[error("{operation}: {source}")]
71 Sql {
72 operation: &'static str,
74 #[source]
75 source: sqlx::Error,
77 },
78}
79
80impl EnqueueError {
81 pub(crate) fn sql(operation: &'static str, source: sqlx::Error) -> Self {
82 if is_busy(&source) {
83 Self::BusyExhausted { operation, source }
84 } else {
85 Self::Sql { operation, source }
86 }
87 }
88
89 pub(crate) fn serialization(detail: impl Into<String>) -> Self {
90 Self::Serialization {
91 detail: detail.into(),
92 }
93 }
94}
95
96#[derive(Debug, Error)]
98#[non_exhaustive]
99pub enum ImportError {
100 #[error("import requires a SQLite write transaction (BEGIN IMMEDIATE or a prior write)")]
102 WriteTransactionRequired,
103 #[error("immutable event identity conflict for existing row {existing_row_id:?}")]
105 IdentityConflict {
106 existing_row_id: RowId,
108 },
109 #[error("imported delivery state conflict for existing row {existing_row_id:?}")]
111 ImportConflict {
112 existing_row_id: RowId,
114 },
115 #[error("invalid SQLite configuration: {detail}")]
117 Configuration {
118 detail: String,
120 },
121 #[error("invalid imported delivery state: {source}")]
123 InvalidState {
124 #[source]
125 source: dovecote::ValidationError,
127 },
128 #[error("migration mismatch: {detail}")]
130 MigrationMismatch {
131 detail: String,
133 },
134 #[error("serialization: {detail}")]
136 Serialization {
137 detail: String,
139 },
140 #[error("{operation}: busy lock exhausted by the caller transaction: {source}")]
142 BusyExhausted {
143 operation: &'static str,
145 #[source]
146 source: sqlx::Error,
148 },
149 #[error("{operation}: {source}")]
151 Sql {
152 operation: &'static str,
154 #[source]
155 source: sqlx::Error,
157 },
158}
159
160#[derive(Debug, Error)]
162#[non_exhaustive]
163pub enum FinalizeError {
164 #[error("finalization requires a SQLite write transaction (BEGIN IMMEDIATE or a prior write)")]
166 WriteTransactionRequired,
167 #[error("event row not found")]
169 NotFound,
170 #[error("delivery row {row_id:?} is not a canonical imported pending delivery")]
172 StateConflict {
173 row_id: RowId,
175 },
176 #[error("invalid authoritative delivery timestamp: {source}")]
178 InvalidTimestamp {
179 #[source]
180 source: dovecote::ValidationError,
182 },
183 #[error("migration mismatch: {detail}")]
185 MigrationMismatch {
186 detail: String,
188 },
189 #[error("serialization: {detail}")]
191 Serialization {
192 detail: String,
194 },
195 #[error("{operation}: busy lock exhausted by the caller transaction: {source}")]
197 BusyExhausted {
198 operation: &'static str,
200 #[source]
201 source: sqlx::Error,
203 },
204 #[error("{operation}: {source}")]
206 Sql {
207 operation: &'static str,
209 #[source]
210 source: sqlx::Error,
212 },
213}
214
215impl FinalizeError {
216 pub(crate) fn sql(operation: &'static str, source: sqlx::Error) -> Self {
217 if is_busy(&source) {
218 Self::BusyExhausted { operation, source }
219 } else {
220 Self::Sql { operation, source }
221 }
222 }
223}
224
225impl ImportError {
226 pub(crate) fn sql(operation: &'static str, source: sqlx::Error) -> Self {
227 if is_busy(&source) {
228 Self::BusyExhausted { operation, source }
229 } else {
230 Self::Sql { operation, source }
231 }
232 }
233
234 pub(crate) fn serialization(detail: impl Into<String>) -> Self {
235 Self::Serialization {
236 detail: detail.into(),
237 }
238 }
239}
240
241#[derive(Debug, Error)]
243#[non_exhaustive]
244pub enum ClaimError {
245 #[cfg(test)]
246 #[error("test claim failpoint triggered after delivery updates")]
248 InjectedFailure,
249 #[error("attempt counter overflow for row {row_id:?}")]
251 CounterOverflow {
252 row_id: RowId,
254 },
255 #[error("operating-system entropy unavailable: {source}")]
257 EntropyUnavailable {
258 #[source]
259 source: getrandom::Error,
261 },
262 #[error("serialization: {detail}")]
264 Serialization {
265 detail: String,
267 },
268 #[error("migration mismatch: {detail}")]
270 MigrationMismatch {
271 detail: String,
273 },
274 #[error("invalid SQLite busy configuration: {detail}")]
276 Configuration {
277 detail: String,
279 },
280 #[error("{operation}: busy lock exhausted after bounded retries: {source}")]
282 BusyExhausted {
283 operation: &'static str,
285 #[source]
286 source: sqlx::Error,
288 },
289 #[error("{operation}: {source}")]
291 Sql {
292 operation: &'static str,
294 #[source]
295 source: sqlx::Error,
297 },
298}
299
300impl ClaimError {
301 pub(crate) const fn sql(operation: &'static str, source: sqlx::Error) -> Self {
302 Self::Sql { operation, source }
303 }
304
305 pub(crate) fn serialization(detail: impl Into<String>) -> Self {
306 Self::Serialization {
307 detail: detail.into(),
308 }
309 }
310
311 pub(crate) fn busy_source(&self) -> Option<&sqlx::Error> {
312 match self {
313 Self::Sql { source, .. } | Self::BusyExhausted { source, .. } if is_busy(source) => {
314 Some(source)
315 }
316 _ => None,
317 }
318 }
319
320 pub(crate) fn into_busy_exhausted(self) -> Self {
321 match self {
322 Self::Sql { operation, source } => Self::BusyExhausted { operation, source },
323 other => other,
324 }
325 }
326}
327
328#[derive(Debug, Error)]
330#[non_exhaustive]
331pub enum MutationError {
332 #[error("event row not found")]
334 NotFound,
335 #[error("illegal delivery transition from {state:?}")]
337 IllegalTransition {
338 state: DeliveryState,
340 },
341 #[error("claim was lost")]
343 LostClaim,
344 #[error("migration mismatch: {detail}")]
346 MigrationMismatch {
347 detail: String,
349 },
350 #[error("invalid SQLite busy configuration: {detail}")]
352 Configuration {
353 detail: String,
355 },
356 #[error("serialization: {detail}")]
358 Serialization {
359 detail: String,
361 },
362 #[error("{operation}: busy lock exhausted after bounded retries: {source}")]
364 BusyExhausted {
365 operation: &'static str,
367 #[source]
368 source: sqlx::Error,
370 },
371 #[error("{operation}: {source}")]
373 Sql {
374 operation: &'static str,
376 #[source]
377 source: sqlx::Error,
379 },
380}
381
382impl MutationError {
383 pub(crate) const fn sql(operation: &'static str, source: sqlx::Error) -> Self {
384 Self::Sql { operation, source }
385 }
386
387 pub(crate) fn serialization(detail: impl Into<String>) -> Self {
388 Self::Serialization {
389 detail: detail.into(),
390 }
391 }
392 pub(crate) fn into_busy_exhausted(self) -> Self {
393 match self {
394 Self::Sql { operation, source } => Self::BusyExhausted { operation, source },
395 other => other,
396 }
397 }
398 pub(crate) fn is_busy(&self) -> bool {
399 matches!(self, Self::Sql { source, .. } if is_busy(source))
400 }
401}
402
403#[derive(Debug, Error)]
405#[non_exhaustive]
406pub enum PageError {
407 #[error("snapshot pager is closed")]
409 Closed,
410 #[error("serialization: {detail}")]
412 Serialization {
413 detail: String,
415 },
416 #[error("{operation}: busy lock exhausted after bounded retries: {source}")]
418 BusyExhausted {
419 operation: &'static str,
421 #[source]
422 source: sqlx::Error,
424 },
425 #[error("{operation}: {source}")]
427 Sql {
428 operation: &'static str,
430 #[source]
431 source: sqlx::Error,
433 },
434}
435
436impl PageError {
437 pub(crate) fn sql(operation: &'static str, source: sqlx::Error) -> Self {
438 if is_busy(&source) {
439 Self::BusyExhausted { operation, source }
440 } else {
441 Self::Sql { operation, source }
442 }
443 }
444
445 pub(crate) fn serialization(detail: impl Into<String>) -> Self {
446 Self::Serialization {
447 detail: detail.into(),
448 }
449 }
450}
451
452#[derive(Debug, Error)]
454#[non_exhaustive]
455pub enum SchemaError {
456 #[error("migration mismatch: {detail}")]
458 MigrationMismatch {
459 detail: String,
461 },
462 #[error("{operation}: busy lock exhausted after bounded retries: {source}")]
464 BusyExhausted {
465 operation: &'static str,
467 #[source]
468 source: sqlx::Error,
470 },
471 #[error("{operation}: {source}")]
473 Sql {
474 operation: &'static str,
476 #[source]
477 source: sqlx::Error,
479 },
480}
481
482impl SchemaError {
483 pub(crate) fn sql(operation: &'static str, source: sqlx::Error) -> Self {
484 if is_busy(&source) {
485 Self::BusyExhausted { operation, source }
486 } else {
487 Self::Sql { operation, source }
488 }
489 }
490}