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    /// Persisted envelope bytes could not be encoded or decoded.
42    #[error("envelope serialization error: {0}")]
43    EnvelopeError(String),
44}
45
46impl From<haematite::SequenceConflict> for DurabilityError {
47    fn from(error: haematite::SequenceConflict) -> Self {
48        Self::SequenceConflict {
49            expected: error.expected,
50            actual: error.actual,
51        }
52    }
53}
54
55impl From<haematite::CasMismatch> for DurabilityError {
56    fn from(error: haematite::CasMismatch) -> Self {
57        // The real `CasMismatch` carries `Option<u64>` to distinguish absent
58        // (`None`) from stored-zero (`Some(0)`). The cursor contract treats an
59        // absent key as the value 0 (see `HaematiteStore::cas`), so a `None`
60        // collapses to 0 here when reporting a regression.
61        Self::CursorRegression {
62            stored: error.actual.unwrap_or(0),
63            attempted: error.expected.unwrap_or(0),
64        }
65    }
66}