Skip to main content

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}