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}