Skip to main content

mecha10_core/
error.rs

1use std::path::PathBuf;
2use thiserror::Error;
3
4#[derive(Error, Debug)]
5pub enum Mecha10Error {
6    #[error("Node initialization failed: {0}")]
7    NodeInit(String),
8
9    #[error("Node '{node_name}' failed: {message}")]
10    NodeFailed { node_name: String, message: String },
11
12    #[error("Node '{0}' not found")]
13    NodeNotFound(String),
14
15    #[error("Configuration error in field '{field}': {message}\n  Expected: {expected}\n  Got: {got}\n  Suggestion: {suggestion}")]
16    ConfigValidation {
17        field: String,
18        message: String,
19        expected: String,
20        got: String,
21        suggestion: String,
22    },
23
24    #[error(
25        "Configuration file not found: {path}\n  Suggestion: Create the file or set environment variable {env_var}"
26    )]
27    ConfigNotFound { path: PathBuf, env_var: String },
28
29    #[error("Invalid configuration format: {0}\n  Suggestion: Validate against schema at {1}")]
30    ConfigInvalid(String, String),
31
32    #[error("Configuration error: {0}")]
33    Configuration(String),
34
35    #[error("{message}\n  Suggestion: {suggestion}")]
36    MessagingError { message: String, suggestion: String },
37    #[error("Failed to connect to message broker at {url}\n  → Tried: {url}\n  → Suggestion: {suggestion}")]
38    MessagingConnectionFailed { url: String, suggestion: String },
39
40    #[error("Failed to publish message to topic '{topic}': {reason}")]
41    PublishFailed { topic: String, reason: String },
42
43    #[error("Failed to subscribe to topic '{topic}': {reason}")]
44    SubscribeFailed { topic: String, reason: String },
45
46    #[error("Message deserialization failed for topic '{topic}'\n  Expected type: {expected_type}\n  Error: {error}")]
47    DeserializationFailed {
48        topic: String,
49        expected_type: String,
50        error: String,
51    },
52
53    #[error("Message serialization failed: {0}")]
54    SerializationFailed(String),
55
56    #[error("Serialization error: {message}\n  Suggestion: {suggestion}")]
57    SerializationError { message: String, suggestion: String },
58
59    #[error("Circuit breaker open for service '{service}': {message}")]
60    CircuitBreakerOpen { service: String, message: String },
61
62    #[error("Failed to load model from '{uri}': {reason}\n  Suggestion: {suggestion}")]
63    ModelLoadFailed {
64        uri: String,
65        reason: String,
66        suggestion: String,
67    },
68
69    #[error("Model inference failed: {0}")]
70    InferenceFailed(String),
71
72    #[error(
73        "Model not found in MLflow registry: {model_name}/{version}\n  Suggestion: Check MLflow UI at {mlflow_url}"
74    )]
75    ModelNotFound {
76        model_name: String,
77        version: String,
78        mlflow_url: String,
79    },
80
81    #[error("Model checksum mismatch\n  Expected: {expected}\n  Got: {got}\n  Suggestion: Re-download the model")]
82    ModelChecksumMismatch { expected: String, got: String },
83
84    #[error("Build failed for node '{node_name}':\n{stderr}\n  Suggestion: Check the error messages above")]
85    BuildFailed { node_name: String, stderr: String },
86
87    #[error("Hot-reload failed: {0}")]
88    HotReloadFailed(String),
89
90    #[error("Runtime error: {0}")]
91    Runtime(String),
92
93    #[error("Timeout after {timeout_ms}ms waiting for: {operation}")]
94    Timeout { timeout_ms: u64, operation: String },
95
96    #[error("Shutdown error: {0}")]
97    Shutdown(String),
98
99    #[error("File not found: {path}\n  Suggestion: {suggestion}")]
100    FileNotFound { path: PathBuf, suggestion: String },
101
102    #[error("IO error: {0}")]
103    Io(#[from] std::io::Error),
104
105    #[error("IO error: {message}\n  Suggestion: {suggestion}")]
106    IoError { message: String, suggestion: String },
107
108    #[error("JSON error: {0}")]
109    Json(#[from] serde_json::Error),
110
111    #[error("YAML error: {0}")]
112    Yaml(String),
113
114    #[error("Redis error: {0}")]
115    Redis(String),
116
117    #[error("Database error: {0}")]
118    Database(String),
119
120    #[error("HTTP error: {0}")]
121    Http(String),
122
123    #[error("{0}")]
124    Other(String),
125}
126
127pub type Result<T> = std::result::Result<T, Mecha10Error>;
128
129impl Mecha10Error {
130    /// Create a connection error with helpful suggestions
131    pub fn connection_failed(url: impl Into<String>) -> Self {
132        let url = url.into();
133
134        let suggestion = if url.contains("localhost") || url.contains("127.0.0.1") {
135            "Start Redis with `redis-server`\n  → Or set REDIS_URL environment variable".to_string()
136        } else {
137            format!("Check network connection and ensure Redis is running at {}", url)
138        };
139
140        Self::MessagingConnectionFailed { url, suggestion }
141    }
142
143    /// Create a config validation error with clear expectations
144    pub fn config_validation(
145        field: impl Into<String>,
146        message: impl Into<String>,
147        expected: impl Into<String>,
148        got: impl Into<String>,
149        suggestion: impl Into<String>,
150    ) -> Self {
151        Self::ConfigValidation {
152            field: field.into(),
153            message: message.into(),
154            expected: expected.into(),
155            got: got.into(),
156            suggestion: suggestion.into(),
157        }
158    }
159
160    /// Create a model load error with helpful suggestions
161    pub fn model_load_failed(uri: impl Into<String>, reason: impl Into<String>) -> Self {
162        let uri_str = uri.into();
163        let reason = reason.into();
164
165        let suggestion = if uri_str.starts_with("mlflow://") {
166            "Check MLflow server is running with `mecha10 mlflow start`\n  → Or verify MLFLOW_TRACKING_URI environment variable".to_string()
167        } else if uri_str.starts_with("http://") || uri_str.starts_with("https://") {
168            "Check network connection and model URL".to_string()
169        } else {
170            format!("Check file exists: {}", uri_str)
171        };
172
173        Self::ModelLoadFailed {
174            uri: uri_str,
175            reason,
176            suggestion,
177        }
178    }
179
180    /// Create a messaging error with a helpful suggestion
181    pub fn messaging_error(message: impl Into<String>, suggestion: impl Into<String>) -> Self {
182        Self::MessagingError {
183            message: message.into(),
184            suggestion: suggestion.into(),
185        }
186    }
187
188    /// Create a configuration error with context
189    pub fn configuration(message: impl Into<String>) -> Self {
190        Self::Configuration(message.into())
191    }
192
193    /// Create a file not found error with a helpful suggestion
194    pub fn file_not_found(path: impl Into<PathBuf>, suggestion: impl Into<String>) -> Self {
195        Self::FileNotFound {
196            path: path.into(),
197            suggestion: suggestion.into(),
198        }
199    }
200
201    /// Create a config not found error
202    pub fn config_not_found(path: impl Into<PathBuf>, env_var: impl Into<String>) -> Self {
203        Self::ConfigNotFound {
204            path: path.into(),
205            env_var: env_var.into(),
206        }
207    }
208}
209
210/// Node-content preflight check: verify a model file exists on disk,
211/// returning a formatted `Mecha10Error::Configuration` with numbered
212/// remediation steps if it doesn't.
213///
214/// Shared by node configs that resolve an ONNX model path before the node
215/// starts (`rl`, `object-detector`), so a missing catalog/registry model
216/// fails fast with consistent, actionable guidance instead of deep inside
217/// ONNX Runtime session creation.
218///
219/// # Errors
220///
221/// Returns an error with remediation steps if `model_path` doesn't exist.
222pub fn preflight_model_exists(model_path: &std::path::Path, label: &str, remediation: &[String]) -> Result<()> {
223    if model_path.exists() {
224        return Ok(());
225    }
226
227    let mut message = format!(
228        "❌ Pre-flight check failed: {} not found at {}\n\nPossible solutions:\n",
229        label,
230        model_path.display()
231    );
232    for (i, step) in remediation.iter().enumerate() {
233        message.push_str(&format!("{}. {}\n", i + 1, step));
234    }
235
236    Err(Mecha10Error::Configuration(message))
237}
238
239impl From<anyhow::Error> for Mecha10Error {
240    fn from(err: anyhow::Error) -> Self {
241        Self::Other(err.to_string())
242    }
243}