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    #[cfg(all(not(target_arch = "wasm32"), feature = "sqlite"))]
51    #[error("Database error: {0}")]
52    Database(#[from] rusqlite::Error),
53
54    /// Database error (non-sqlite or wasm32 variant).
55    #[cfg(any(target_arch = "wasm32", not(feature = "sqlite")))]
56    #[error("Database error: {0}")]
57    Database(String),
58
59    /// Serialization error.
60    #[error("Serialization error: {0}")]
61    Serialization(#[from] serde_json::Error),
62
63    /// YAML parsing error.
64    #[error("YAML parsing error: {0}")]
65    YamlParsing(#[from] serde_yaml::Error),
66
67    /// I/O error.
68    #[error("I/O error: {0}")]
69    Io(#[from] std::io::Error),
70
71    /// File watcher error.
72    #[error("File watcher error: {0}")]
73    FileWatcher(String),
74
75    /// Invalid task type.
76    #[error("Invalid task type: {0}")]
77    InvalidTaskType(String),
78
79    /// Missing required parameter.
80    #[error("Missing required parameter: {0}")]
81    MissingParameter(String),
82
83    /// Invalid parameter value.
84    #[error("Invalid parameter value: {param}, value: {value}")]
85    InvalidParameter {
86        /// Parameter name.
87        param: String,
88        /// Invalid value.
89        value: String,
90    },
91
92    /// Resource limit exceeded.
93    #[error("Resource limit exceeded: {resource}, limit: {limit}")]
94    ResourceLimitExceeded {
95        /// Resource type.
96        resource: String,
97        /// Limit value.
98        limit: String,
99    },
100
101    /// Worker pool error.
102    #[error("Worker pool error: {0}")]
103    WorkerPool(String),
104
105    /// Scheduler error.
106    #[error("Scheduler error: {0}")]
107    Scheduler(String),
108
109    /// Invalid cron expression.
110    #[error("Invalid cron expression: {0}")]
111    InvalidCronExpression(String),
112
113    /// File not found.
114    #[error("File not found: {0}")]
115    FileNotFound(PathBuf),
116
117    /// Invalid URL.
118    #[error("Invalid URL: {0}")]
119    InvalidUrl(#[from] url::ParseError),
120
121    /// HTTP error.
122    #[error("HTTP error: {0}")]
123    Http(String),
124
125    /// WebSocket error.
126    #[error("WebSocket error: {0}")]
127    WebSocket(String),
128
129    /// Lock poisoned.
130    #[error("Lock poisoned")]
131    LockPoisoned,
132
133    /// Shutdown in progress.
134    #[error("Shutdown in progress")]
135    ShutdownInProgress,
136
137    /// Already running.
138    #[error("Workflow already running: {0}")]
139    AlreadyRunning(String),
140
141    /// Not running.
142    #[error("Workflow not running: {0}")]
143    NotRunning(String),
144
145    /// Invalid state transition.
146    #[error("Invalid state transition: from {from} to {to}")]
147    InvalidStateTransition {
148        /// Current state.
149        from: String,
150        /// Target state.
151        to: String,
152    },
153
154    /// Generic error.
155    #[error("{0}")]
156    Generic(String),
157}
158
159impl WorkflowError {
160    /// Create a generic error with a message.
161    pub fn generic(msg: impl Into<String>) -> Self {
162        Self::Generic(msg.into())
163    }
164
165    /// Check if error is recoverable.
166    #[must_use]
167    pub fn is_recoverable(&self) -> bool {
168        matches!(
169            self,
170            Self::TaskTimeout(_)
171                | Self::ResourceLimitExceeded { .. }
172                | Self::WorkerPool(_)
173                | Self::Http(_)
174        )
175    }
176
177    /// Check if error should trigger retry.
178    #[must_use]
179    pub fn should_retry(&self) -> bool {
180        matches!(
181            self,
182            Self::TaskExecutionFailed { .. }
183                | Self::TaskTimeout(_)
184                | Self::Http(_)
185                | Self::WorkerPool(_)
186        )
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn test_error_display() {
196        let err = WorkflowError::WorkflowNotFound("test-workflow".to_string());
197        assert_eq!(err.to_string(), "Workflow not found: test-workflow");
198    }
199
200    #[test]
201    fn test_task_execution_failed() {
202        let err = WorkflowError::TaskExecutionFailed {
203            task_id: "task-1".to_string(),
204            reason: "connection timeout".to_string(),
205        };
206        assert!(err.to_string().contains("task-1"));
207        assert!(err.to_string().contains("connection timeout"));
208    }
209
210    #[test]
211    fn test_is_recoverable() {
212        assert!(WorkflowError::TaskTimeout("task-1".to_string()).is_recoverable());
213        assert!(!WorkflowError::CycleDetected.is_recoverable());
214    }
215
216    #[test]
217    fn test_should_retry() {
218        assert!(WorkflowError::TaskTimeout("task-1".to_string()).should_retry());
219        assert!(!WorkflowError::CycleDetected.should_retry());
220    }
221
222    #[test]
223    fn test_generic_error() {
224        let err = WorkflowError::generic("custom error");
225        assert_eq!(err.to_string(), "custom error");
226    }
227
228    #[test]
229    fn test_invalid_state_transition() {
230        let err = WorkflowError::InvalidStateTransition {
231            from: "running".to_string(),
232            to: "pending".to_string(),
233        };
234        assert!(err.to_string().contains("running"));
235        assert!(err.to_string().contains("pending"));
236    }
237}