Skip to main content

oximedia_workflow/
error.rs

1//! Error types for workflow orchestration.
2
3use std::path::PathBuf;
4use thiserror::Error;
5
6/// Result type for workflow operations.
7pub type Result<T> = std::result::Result<T, WorkflowError>;
8
9/// Errors that can occur during workflow orchestration.
10#[derive(Debug, Error)]
11pub enum WorkflowError {
12    /// Workflow not found.
13    #[error("Workflow not found: {0}")]
14    WorkflowNotFound(String),
15
16    /// Task not found.
17    #[error("Task not found: {0}")]
18    TaskNotFound(String),
19
20    /// Cycle detected in workflow DAG.
21    #[error("Cycle detected in workflow DAG")]
22    CycleDetected,
23
24    /// Invalid workflow configuration.
25    #[error("Invalid workflow configuration: {0}")]
26    InvalidConfiguration(String),
27
28    /// Task execution failed.
29    #[error("Task execution failed: {task_id}, reason: {reason}")]
30    TaskExecutionFailed {
31        /// Task identifier.
32        task_id: String,
33        /// Failure reason.
34        reason: String,
35    },
36
37    /// Task timeout.
38    #[error("Task timeout: {0}")]
39    TaskTimeout(String),
40
41    /// Task cancelled.
42    #[error("Task cancelled: {0}")]
43    TaskCancelled(String),
44
45    /// Dependency failure.
46    #[error("Dependency failed: {0}")]
47    DependencyFailed(String),
48
49    /// Database error.
50    #[error("Database error: {0}")]
51    Database(String),
52
53    /// Serialization error.
54    #[error("Serialization error: {0}")]
55    Serialization(#[from] serde_json::Error),
56
57    /// YAML parsing error.
58    #[error("YAML parsing error: {0}")]
59    YamlParsing(#[from] serde_yaml::Error),
60
61    /// I/O error.
62    #[error("I/O error: {0}")]
63    Io(#[from] std::io::Error),
64
65    /// File watcher error.
66    #[error("File watcher error: {0}")]
67    FileWatcher(String),
68
69    /// Invalid task type.
70    #[error("Invalid task type: {0}")]
71    InvalidTaskType(String),
72
73    /// Missing required parameter.
74    #[error("Missing required parameter: {0}")]
75    MissingParameter(String),
76
77    /// Invalid parameter value.
78    #[error("Invalid parameter value: {param}, value: {value}")]
79    InvalidParameter {
80        /// Parameter name.
81        param: String,
82        /// Invalid value.
83        value: String,
84    },
85
86    /// Resource limit exceeded.
87    #[error("Resource limit exceeded: {resource}, limit: {limit}")]
88    ResourceLimitExceeded {
89        /// Resource type.
90        resource: String,
91        /// Limit value.
92        limit: String,
93    },
94
95    /// Worker pool error.
96    #[error("Worker pool error: {0}")]
97    WorkerPool(String),
98
99    /// Scheduler error.
100    #[error("Scheduler error: {0}")]
101    Scheduler(String),
102
103    /// Invalid cron expression.
104    #[error("Invalid cron expression: {0}")]
105    InvalidCronExpression(String),
106
107    /// File not found.
108    #[error("File not found: {0}")]
109    FileNotFound(PathBuf),
110
111    /// Invalid URL.
112    #[error("Invalid URL: {0}")]
113    InvalidUrl(#[from] url::ParseError),
114
115    /// HTTP error.
116    #[error("HTTP error: {0}")]
117    Http(String),
118
119    /// WebSocket error.
120    #[error("WebSocket error: {0}")]
121    WebSocket(String),
122
123    /// Lock poisoned.
124    #[error("Lock poisoned")]
125    LockPoisoned,
126
127    /// Shutdown in progress.
128    #[error("Shutdown in progress")]
129    ShutdownInProgress,
130
131    /// Already running.
132    #[error("Workflow already running: {0}")]
133    AlreadyRunning(String),
134
135    /// Not running.
136    #[error("Workflow not running: {0}")]
137    NotRunning(String),
138
139    /// Invalid state transition.
140    #[error("Invalid state transition: from {from} to {to}")]
141    InvalidStateTransition {
142        /// Current state.
143        from: String,
144        /// Target state.
145        to: String,
146    },
147
148    /// Generic error.
149    #[error("{0}")]
150    Generic(String),
151}
152
153impl WorkflowError {
154    /// Create a generic error with a message.
155    pub fn generic(msg: impl Into<String>) -> Self {
156        Self::Generic(msg.into())
157    }
158
159    /// Check if error is recoverable.
160    #[must_use]
161    pub fn is_recoverable(&self) -> bool {
162        matches!(
163            self,
164            Self::TaskTimeout(_)
165                | Self::ResourceLimitExceeded { .. }
166                | Self::WorkerPool(_)
167                | Self::Http(_)
168        )
169    }
170
171    /// Check if error should trigger retry.
172    #[must_use]
173    pub fn should_retry(&self) -> bool {
174        matches!(
175            self,
176            Self::TaskExecutionFailed { .. }
177                | Self::TaskTimeout(_)
178                | Self::Http(_)
179                | Self::WorkerPool(_)
180        )
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn test_error_display() {
190        let err = WorkflowError::WorkflowNotFound("test-workflow".to_string());
191        assert_eq!(err.to_string(), "Workflow not found: test-workflow");
192    }
193
194    #[test]
195    fn test_task_execution_failed() {
196        let err = WorkflowError::TaskExecutionFailed {
197            task_id: "task-1".to_string(),
198            reason: "connection timeout".to_string(),
199        };
200        assert!(err.to_string().contains("task-1"));
201        assert!(err.to_string().contains("connection timeout"));
202    }
203
204    #[test]
205    fn test_is_recoverable() {
206        assert!(WorkflowError::TaskTimeout("task-1".to_string()).is_recoverable());
207        assert!(!WorkflowError::CycleDetected.is_recoverable());
208    }
209
210    #[test]
211    fn test_should_retry() {
212        assert!(WorkflowError::TaskTimeout("task-1".to_string()).should_retry());
213        assert!(!WorkflowError::CycleDetected.should_retry());
214    }
215
216    #[test]
217    fn test_generic_error() {
218        let err = WorkflowError::generic("custom error");
219        assert_eq!(err.to_string(), "custom error");
220    }
221
222    #[test]
223    fn test_invalid_state_transition() {
224        let err = WorkflowError::InvalidStateTransition {
225            from: "running".to_string(),
226            to: "pending".to_string(),
227        };
228        assert!(err.to_string().contains("running"));
229        assert!(err.to_string().contains("pending"));
230    }
231}