liminal/durability/error.rs
1/// Error taxonomy for haematite-backed durability operations.
2#[derive(Debug, thiserror::Error)]
3pub enum DurabilityError {
4 /// Haematite returned a store-level failure.
5 ///
6 /// The umbrella `From<haematite::ApiError>` conversion lives in
7 /// [`super::store`] because it routes the optimistic-concurrency variants to
8 /// their dedicated cases rather than wrapping them here.
9 #[error("haematite store error: {0}")]
10 StoreError(haematite::ApiError),
11
12 /// An append observed a different stream sequence than the caller expected.
13 #[error("sequence conflict: expected {expected}, actual {actual}")]
14 SequenceConflict {
15 /// Caller-provided expected stream sequence.
16 expected: u64,
17 /// Actual stream sequence reported by haematite.
18 actual: u64,
19 },
20
21 /// A cursor checkpoint attempted to move from a stale stored value.
22 #[error("cursor regression: stored {stored}, attempted {attempted}")]
23 CursorRegression {
24 /// Current value stored for the cursor.
25 stored: u64,
26 /// Value the caller attempted to checkpoint from.
27 attempted: u64,
28 },
29
30 /// A producer idempotency key collided with an existing dedup entry.
31 #[error("dedup key collision for key {key}")]
32 DedupCollision {
33 /// Idempotency key that collided.
34 key: String,
35 },
36
37 /// Durability configuration failed validation.
38 #[error("configuration error: {0}")]
39 ConfigError(String),
40
41 /// An ephemeral durable store could not be opened on disk.
42 ///
43 /// Raised only on the construction path of a self-owned ephemeral store
44 /// (see [`super::store::open_ephemeral`]); the guarding temporary directory
45 /// is already removed by the time this surfaces.
46 #[error("ephemeral store open failed: {0}")]
47 EphemeralStoreOpen(String),
48
49 /// An operation reached an ephemeral store whose backing database was
50 /// already detached by teardown.
51 ///
52 /// Unreachable by construction: the store is detached only inside the
53 /// ephemeral guard's `Drop`, which cannot overlap a live handle's call.
54 /// The variant exists because that detachment is expressed through an
55 /// `Option` the compiler cannot see through, and the fallback must be a
56 /// typed refusal rather than a panic.
57 #[error("ephemeral store already detached by teardown")]
58 EphemeralStoreDetached,
59
60 /// Persisted envelope bytes could not be encoded or decoded.
61 #[error("envelope serialization error: {0}")]
62 EnvelopeError(String),
63}
64
65impl From<haematite::SequenceConflict> for DurabilityError {
66 fn from(error: haematite::SequenceConflict) -> Self {
67 Self::SequenceConflict {
68 expected: error.expected,
69 actual: error.actual,
70 }
71 }
72}
73
74impl From<haematite::CasMismatch> for DurabilityError {
75 fn from(error: haematite::CasMismatch) -> Self {
76 // The real `CasMismatch` carries `Option<u64>` to distinguish absent
77 // (`None`) from stored-zero (`Some(0)`). The cursor contract treats an
78 // absent key as the value 0 (see `HaematiteStore::cas`), so a `None`
79 // collapses to 0 here when reporting a regression.
80 Self::CursorRegression {
81 stored: error.actual.unwrap_or(0),
82 attempted: error.expected.unwrap_or(0),
83 }
84 }
85}