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
45#[cfg(test)]
46mod tests {
47    use super::{DurabilityError, NonDeterminismError};
48    use aion_core::WorkflowId;
49    use aion_store::StoreError;
50
51    fn non_determinism_error() -> NonDeterminismError {
52        NonDeterminismError {
53            workflow_id: WorkflowId::new(uuid::Uuid::nil()),
54            seq: 42,
55            expected: "activity schedule ordinal 7".to_owned(),
56            found: "timer fired timer:named:deadline".to_owned(),
57        }
58    }
59
60    #[test]
61    fn non_determinism_display_includes_context() {
62        let error = non_determinism_error();
63
64        let message = error.to_string();
65
66        assert!(message.contains("00000000-0000-0000-0000-000000000000"));
67        assert!(message.contains("42"));
68        assert!(message.contains("activity schedule ordinal 7"));
69        assert!(message.contains("timer fired timer:named:deadline"));
70    }
71
72    #[test]
73    fn durability_error_display_mentions_underlying_cause() {
74        let store = DurabilityError::Store(StoreError::SequenceConflict {
75            expected: 10,
76            found: 11,
77        });
78        let non_determinism = DurabilityError::NonDeterminism(non_determinism_error());
79        let history_shape = DurabilityError::HistoryShape {
80            reason: "activity result without preceding schedule".to_owned(),
81        };
82
83        let store_message = store.to_string();
84        let non_determinism_message = non_determinism.to_string();
85        let history_shape_message = history_shape.to_string();
86
87        assert!(!store_message.is_empty());
88        assert!(store_message.contains("sequence conflict"));
89        assert!(!non_determinism_message.is_empty());
90        assert!(non_determinism_message.contains("activity schedule ordinal 7"));
91        assert!(non_determinism_message.contains("timer fired timer:named:deadline"));
92        assert!(!history_shape_message.is_empty());
93        assert!(history_shape_message.contains("activity result without preceding schedule"));
94    }
95}