Skip to main content

aion_core/
error.rs

1//! Error types shared by workflow and activity histories.
2
3use serde::{Deserialize, Serialize};
4
5use crate::Payload;
6
7/// Classification for an activity failure.
8#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Debug, PartialEq, Eq)]
9pub enum ActivityErrorKind {
10    /// The activity failure may be retried according to the activity's retry policy.
11    Retryable,
12    /// The activity failure is terminal and must not be retried.
13    Terminal,
14}
15
16/// Failure reported by an activity execution.
17///
18/// The engine consults [`ActivityError::is_retryable`] to decide whether to
19/// apply the activity's retry policy or fail the workflow.
20#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Debug, PartialEq, Eq, thiserror::Error)]
21#[error("{message}")]
22pub struct ActivityError {
23    /// Explicit retryability classification for this activity failure.
24    pub kind: ActivityErrorKind,
25    /// Human-readable error message.
26    pub message: String,
27    /// Optional structured details carried as an opaque payload.
28    pub details: Option<Payload>,
29}
30
31impl ActivityError {
32    /// Returns whether the engine may retry this activity failure.
33    #[must_use]
34    pub fn is_retryable(&self) -> bool {
35        matches!(self.kind, ActivityErrorKind::Retryable)
36    }
37}
38
39/// Terminal failure reported by a workflow execution.
40#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Debug, PartialEq, Eq, thiserror::Error)]
41#[error("{message}")]
42pub struct WorkflowError {
43    /// Human-readable error message.
44    pub message: String,
45    /// Optional structured details carried as an opaque payload.
46    pub details: Option<Payload>,
47}
48
49impl From<ActivityError> for WorkflowError {
50    fn from(error: ActivityError) -> Self {
51        Self {
52            message: error.message,
53            details: error.details,
54        }
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use serde_json::json;
61
62    use super::{ActivityError, ActivityErrorKind, WorkflowError};
63    use crate::Payload;
64
65    #[test]
66    fn activity_error_reports_retryable_classification() {
67        let error = ActivityError {
68            kind: ActivityErrorKind::Retryable,
69            message: String::from("temporary outage"),
70            details: None,
71        };
72
73        assert!(error.is_retryable());
74    }
75
76    #[test]
77    fn activity_error_reports_terminal_classification() {
78        let error = ActivityError {
79            kind: ActivityErrorKind::Terminal,
80            message: String::from("invalid request"),
81            details: None,
82        };
83
84        assert!(!error.is_retryable());
85    }
86
87    #[test]
88    fn errors_round_trip_through_json() -> Result<(), Box<dyn std::error::Error>> {
89        let activity_error = ActivityError {
90            kind: ActivityErrorKind::Retryable,
91            message: String::from("connection reset"),
92            details: Some(Payload::from_json(&json!({"retry_after_ms": 500}))?),
93        };
94        let json = serde_json::to_string(&activity_error)?;
95        let decoded: ActivityError = serde_json::from_str(&json)?;
96        assert_eq!(activity_error, decoded);
97
98        let workflow_error = WorkflowError {
99            message: String::from("workflow failed"),
100            details: None,
101        };
102        let json = serde_json::to_string(&workflow_error)?;
103        let decoded: WorkflowError = serde_json::from_str(&json)?;
104        assert_eq!(workflow_error, decoded);
105
106        Ok(())
107    }
108
109    #[test]
110    fn workflow_error_from_activity_error_preserves_message_and_details()
111    -> Result<(), Box<dyn std::error::Error>> {
112        let details = Payload::from_json(&json!({"code": "rate_limited", "after_ms": 1000}))?;
113        let activity_error = ActivityError {
114            kind: ActivityErrorKind::Terminal,
115            message: String::from("activity failed permanently"),
116            details: Some(details.clone()),
117        };
118
119        let workflow_error = WorkflowError::from(activity_error);
120
121        assert_eq!(workflow_error.message, "activity failed permanently");
122        assert_eq!(workflow_error.details, Some(details));
123        Ok(())
124    }
125}