Skip to main content

foxtive_supervisor/
error.rs

1//! Error types for the supervisor system
2
3use thiserror::Error;
4
5/// A type alias for results returned by supervisor operations.
6pub type SupervisorResult<T> = Result<T, SupervisorError>;
7
8/// Error types for the foxtive-supervisor crate.
9#[derive(Error, Debug)]
10pub enum SupervisorError {
11    #[error("Configuration error for task '{task_id}': {reason}")]
12    ConfigurationError { task_id: String, reason: String },
13
14    #[error("Dependency validation failed for task '{task_id}': '{dependency_id}' - {error:?}")]
15    DependencyValidation {
16        task_id: String,
17        dependency_id: String,
18        error: ValidationError,
19    },
20
21    #[error("Circular dependency detected between '{task_a}' and '{task_b}'")]
22    CircularDependency { task_a: String, task_b: String },
23
24    #[error("Prerequisite '{name}' failed")]
25    PrerequisiteFailed {
26        name: String,
27        #[source]
28        source: Box<dyn std::error::Error + Send + Sync>,
29    },
30
31    #[error("Task '{0}' not found")]
32    UnknownTask(String),
33
34    #[error("Persistence error: {message}")]
35    PersistenceError {
36        message: String,
37        #[source]
38        source: std::io::Error,
39    },
40
41    #[error("Serialization error: {0}")]
42    SerializationError(#[from] serde_json::Error),
43
44    #[error("Task panicked: {task_id}")]
45    TaskPanicked {
46        task_id: String,
47        #[source]
48        source: tokio::task::JoinError,
49    },
50
51    #[error("Channel error: {0}")]
52    ChannelError(String),
53
54    #[error("Timeout: {0}")]
55    Timeout(String),
56
57    #[error("Coordination error: {0}")]
58    CoordinationError(String),
59
60    #[error("{0}")]
61    Internal(String),
62
63    /// Wraps infrastructure errors (DB, Redis, IO, serialization, etc.)
64    /// Carries source for error chaining.
65    #[error("{message}")]
66    Infrastructure {
67        message: String,
68        #[source]
69        source: Option<Box<dyn std::error::Error + Send + Sync>>,
70    },
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum ValidationError {
75    UnknownTaskId,
76    CircularDependency,
77}
78
79impl SupervisorError {
80    pub fn config(task_id: impl Into<String>, reason: impl Into<String>) -> Self {
81        Self::ConfigurationError {
82            task_id: task_id.into(),
83            reason: reason.into(),
84        }
85    }
86
87    pub fn dependency_validation(
88        task_id: &str,
89        dependency_id: &str,
90        error: ValidationError,
91    ) -> Self {
92        Self::DependencyValidation {
93            task_id: task_id.to_string(),
94            dependency_id: dependency_id.to_string(),
95            error,
96        }
97    }
98
99    pub fn circular_dependency(task_a: &str, task_b: &str) -> Self {
100        Self::CircularDependency {
101            task_a: task_a.to_string(),
102            task_b: task_b.to_string(),
103        }
104    }
105
106    pub fn prerequisite_failed(
107        name: &str,
108        source: Box<dyn std::error::Error + Send + Sync>,
109    ) -> Self {
110        Self::PrerequisiteFailed {
111            name: name.to_string(),
112            source,
113        }
114    }
115
116    pub fn unknown_task(id: &str) -> Self {
117        Self::UnknownTask(id.to_string())
118    }
119
120    pub fn persistence(message: impl Into<String>, source: std::io::Error) -> Self {
121        Self::PersistenceError {
122            message: message.into(),
123            source,
124        }
125    }
126
127    pub fn task_panicked(task_id: impl Into<String>, source: tokio::task::JoinError) -> Self {
128        Self::TaskPanicked {
129            task_id: task_id.into(),
130            source,
131        }
132    }
133
134    pub fn channel(message: impl Into<String>) -> Self {
135        Self::ChannelError(message.into())
136    }
137
138    pub fn timeout(message: impl Into<String>) -> Self {
139        Self::Timeout(message.into())
140    }
141
142    pub fn coordination(message: impl Into<String>) -> Self {
143        Self::CoordinationError(message.into())
144    }
145
146    pub fn internal(message: impl Into<String>) -> Self {
147        Self::Internal(message.into())
148    }
149
150    /// Wraps any error into an `Infrastructure` SupervisorError.
151    ///
152    /// This is the catch-all constructor for error types that don't have
153    /// a dedicated `From` impl. Use with `.map_err()`:
154    ///
155    /// ```no_run
156    /// use foxtive_supervisor::error::{SupervisorError, SupervisorResult};
157    ///
158    /// fn example() -> SupervisorResult<()> {
159    ///     let flag = "true".parse::<bool>().map_err(SupervisorError::wrap)?;
160    ///     Ok(())
161    /// }
162    /// ```
163    pub fn wrap(e: impl std::error::Error + Send + Sync + 'static) -> Self {
164        Self::Infrastructure {
165            message: format!("{e}"),
166            source: Some(Box::new(e)),
167        }
168    }
169}
170
171impl From<String> for SupervisorError {
172    fn from(msg: String) -> Self {
173        SupervisorError::Internal(msg)
174    }
175}
176
177impl From<&str> for SupervisorError {
178    fn from(msg: &str) -> Self {
179        SupervisorError::Internal(msg.to_string())
180    }
181}
182
183impl From<std::io::Error> for SupervisorError {
184    fn from(err: std::io::Error) -> Self {
185        SupervisorError::PersistenceError {
186            message: err.to_string(),
187            source: err,
188        }
189    }
190}
191
192impl From<tokio::task::JoinError> for SupervisorError {
193    fn from(err: tokio::task::JoinError) -> Self {
194        if err.is_panic() {
195            SupervisorError::TaskPanicked {
196                task_id: "unknown".to_string(),
197                source: err,
198            }
199        } else {
200            SupervisorError::ChannelError(format!("Task cancelled: {err}"))
201        }
202    }
203}
204
205#[cfg(feature = "distributed")]
206impl From<redis::RedisError> for SupervisorError {
207    fn from(err: redis::RedisError) -> Self {
208        SupervisorError::CoordinationError(err.to_string())
209    }
210}