Skip to main content

aion/durability/
error.rs

1//! `NonDeterminismError` and `DurabilityError` taxonomy.
2
3use aion_core::{SearchAttributeError, WorkflowId};
4use aion_store::StoreError;
5
6/// A deterministic replay mismatch between the workflow command stream and recorded history.
7#[derive(thiserror::Error, Clone, Debug, PartialEq, Eq)]
8#[error(
9    "non-determinism in workflow {workflow_id} at sequence {seq}: expected {expected}, found {found}"
10)]
11pub struct NonDeterminismError {
12    /// Workflow whose recorded history diverged from the replayed command stream.
13    pub workflow_id: WorkflowId,
14    /// Sequence position of the recorded event at the cursor mismatch.
15    pub seq: u64,
16    /// Shape of the command the workflow issued, including family and correlation key.
17    pub expected: String,
18    /// Shape of the recorded event found at the cursor position, including family and key.
19    pub found: String,
20}
21
22/// Errors returned by durability recording, replay, and recovery operations.
23#[derive(thiserror::Error, Debug)]
24pub enum DurabilityError {
25    /// The backing event store rejected or failed a durability operation.
26    #[error("store error: {0}")]
27    Store(#[from] StoreError),
28
29    /// Replay detected that workflow code no longer matches recorded history.
30    #[error("non-determinism violation: {0}")]
31    NonDeterminism(#[from] NonDeterminismError),
32
33    /// Recorded history is malformed or internally inconsistent.
34    #[error("history shape error: {reason}")]
35    HistoryShape {
36        /// Human-readable description of the malformed recorded history.
37        reason: String,
38    },
39
40    /// A search attribute update did not satisfy the registered schema.
41    #[error("search attribute validation error: {0}")]
42    SearchAttribute(#[from] SearchAttributeError),
43
44    /// A durable write was refused because this engine's task epoch has closed.
45    ///
46    /// 🔴 DELIBERATELY NOT [`crate::EngineError::EngineTaskEpochClosed`]. That
47    /// variant carries a documented single-construction-site invariant —
48    /// `aion-client`'s `map_engine_error` gives it no arm on the stated ground
49    /// that it is unreachable through any transport-exposed operation — and a
50    /// second construction site would silently take that transport's catch-all.
51    /// This is a different layer with a different audience: it surfaces as
52    /// `{error, _}` inside running workflow code, never over the wire.
53    #[error("engine task epoch closed: {reason}")]
54    EngineTaskEpochClosed {
55        /// What was refused, and what it means for the run.
56        reason: String,
57    },
58}
59
60#[cfg(test)]
61mod tests {
62    use super::{DurabilityError, NonDeterminismError};
63    use aion_core::WorkflowId;
64    use aion_store::StoreError;
65
66    fn non_determinism_error() -> NonDeterminismError {
67        NonDeterminismError {
68            workflow_id: WorkflowId::new(uuid::Uuid::nil()),
69            seq: 42,
70            expected: "activity schedule ordinal 7".to_owned(),
71            found: "timer fired timer:named:deadline".to_owned(),
72        }
73    }
74
75    #[test]
76    fn non_determinism_display_includes_context() {
77        let error = non_determinism_error();
78
79        let message = error.to_string();
80
81        assert!(message.contains("00000000-0000-0000-0000-000000000000"));
82        assert!(message.contains("42"));
83        assert!(message.contains("activity schedule ordinal 7"));
84        assert!(message.contains("timer fired timer:named:deadline"));
85    }
86
87    #[test]
88    fn durability_error_display_mentions_underlying_cause() {
89        let store = DurabilityError::Store(StoreError::SequenceConflict {
90            expected: 10,
91            found: 11,
92        });
93        let non_determinism = DurabilityError::NonDeterminism(non_determinism_error());
94        let history_shape = DurabilityError::HistoryShape {
95            reason: "activity result without preceding schedule".to_owned(),
96        };
97
98        let store_message = store.to_string();
99        let non_determinism_message = non_determinism.to_string();
100        let history_shape_message = history_shape.to_string();
101
102        assert!(!store_message.is_empty());
103        assert!(store_message.contains("sequence conflict"));
104        assert!(!non_determinism_message.is_empty());
105        assert!(non_determinism_message.contains("activity schedule ordinal 7"));
106        assert!(non_determinism_message.contains("timer fired timer:named:deadline"));
107        assert!(!history_shape_message.is_empty());
108        assert!(history_shape_message.contains("activity result without preceding schedule"));
109    }
110}