/home/jgrusewski/Work/foxhunt/common/src/error.rs
Line | Count | Source |
1 | | //! Common error types and utilities |
2 | | //! |
3 | | //! This module provides shared error types and utilities used across |
4 | | //! all Foxhunt services. |
5 | | |
6 | | use serde::{Deserialize, Serialize}; |
7 | | use std::fmt; |
8 | | use std::time::Duration; |
9 | | use thiserror::Error; |
10 | | |
11 | | /// Common error type for all Foxhunt services |
12 | | #[derive(Debug, Error)] |
13 | | pub enum CommonError { |
14 | | /// Database operation failed - wraps database-specific errors |
15 | | #[error("Database error: {0}")] |
16 | | Database(#[from] crate::database::DatabaseError), |
17 | | /// Configuration is invalid or missing required parameters |
18 | | #[error("Configuration error: {0}")] |
19 | | Configuration(String), |
20 | | /// Network communication error occurred |
21 | | #[error("Network error: {0}")] |
22 | | Network(String), |
23 | | /// Service-specific error with categorization for metrics |
24 | | #[error("Service error: {category} - {message}")] |
25 | | Service { |
26 | | /// Error category for classification |
27 | | category: ErrorCategory, |
28 | | /// Descriptive error message |
29 | | message: String, |
30 | | }, |
31 | | /// Input validation failed |
32 | | #[error("Validation error: {0}")] |
33 | | Validation(String), |
34 | | /// Operation exceeded maximum allowed execution time |
35 | | #[error("Timeout error: operation took {actual_ms}ms, max allowed {max_ms}ms")] |
36 | | Timeout { |
37 | | /// Actual execution time in milliseconds |
38 | | actual_ms: u64, |
39 | | /// Maximum allowed execution time in milliseconds |
40 | | max_ms: u64, |
41 | | }, |
42 | | } |
43 | | |
44 | | /// Error categories for classification and metrics |
45 | | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] |
46 | | pub enum ErrorCategory { |
47 | | /// Market data related errors |
48 | | MarketData, |
49 | | /// Trading and order management errors |
50 | | Trading, |
51 | | /// Network and communication errors |
52 | | Network, |
53 | | /// System and infrastructure errors |
54 | | System, |
55 | | /// Configuration errors |
56 | | Configuration, |
57 | | /// Validation errors |
58 | | Validation, |
59 | | /// Critical errors requiring immediate attention |
60 | | Critical, |
61 | | /// Connection errors (data providers) |
62 | | Connection, |
63 | | /// Authentication errors |
64 | | Authentication, |
65 | | /// Rate limiting errors |
66 | | RateLimit, |
67 | | /// Data parsing errors |
68 | | Parse, |
69 | | /// Subscription errors |
70 | | Subscription, |
71 | | /// Financial safety and calculation errors |
72 | | FinancialSafety, |
73 | | /// Risk management and circuit breakers |
74 | | RiskManagement, |
75 | | /// Database and persistence layer |
76 | | Database, |
77 | | /// Broker connectivity and execution |
78 | | Broker, |
79 | | /// Machine learning and AI errors |
80 | | MachineLearning, |
81 | | /// Security and authentication errors |
82 | | Security, |
83 | | /// Business logic errors |
84 | | BusinessLogic, |
85 | | /// Resource errors (not found, conflicts) |
86 | | Resource, |
87 | | /// Development and testing errors |
88 | | Development, |
89 | | /// Risk management errors |
90 | | Risk, |
91 | | /// Machine learning errors (alias for MachineLearning) |
92 | | ML, |
93 | | /// Unknown/other errors |
94 | | Other, |
95 | | } |
96 | | |
97 | | impl fmt::Display for ErrorCategory { |
98 | 0 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
99 | 0 | match self { |
100 | 0 | Self::MarketData => write!(f, "MARKET_DATA"), |
101 | 0 | Self::Trading => write!(f, "TRADING"), |
102 | 0 | Self::Network => write!(f, "NETWORK"), |
103 | 0 | Self::System => write!(f, "SYSTEM"), |
104 | 0 | Self::Configuration => write!(f, "CONFIGURATION"), |
105 | 0 | Self::Validation => write!(f, "VALIDATION"), |
106 | 0 | Self::Critical => write!(f, "CRITICAL"), |
107 | 0 | Self::Connection => write!(f, "CONNECTION"), |
108 | 0 | Self::Authentication => write!(f, "AUTHENTICATION"), |
109 | 0 | Self::RateLimit => write!(f, "RATE_LIMIT"), |
110 | 0 | Self::Parse => write!(f, "PARSE"), |
111 | 0 | Self::Subscription => write!(f, "SUBSCRIPTION"), |
112 | 0 | Self::FinancialSafety => write!(f, "FINANCIAL_SAFETY"), |
113 | 0 | Self::RiskManagement => write!(f, "RISK_MANAGEMENT"), |
114 | 0 | Self::Database => write!(f, "DATABASE"), |
115 | 0 | Self::Broker => write!(f, "BROKER"), |
116 | 0 | Self::MachineLearning => write!(f, "MACHINE_LEARNING"), |
117 | 0 | Self::Security => write!(f, "SECURITY"), |
118 | 0 | Self::BusinessLogic => write!(f, "BUSINESS_LOGIC"), |
119 | 0 | Self::Resource => write!(f, "RESOURCE"), |
120 | 0 | Self::Development => write!(f, "DEVELOPMENT"), |
121 | 0 | Self::Risk => write!(f, "RISK"), |
122 | 0 | Self::ML => write!(f, "ML"), |
123 | 0 | Self::Other => write!(f, "OTHER"), |
124 | | } |
125 | 0 | } |
126 | | } |
127 | | |
128 | | /// Error severity levels for prioritization and alerting |
129 | | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] |
130 | | pub enum ErrorSeverity { |
131 | | /// Debug level - for development and troubleshooting |
132 | | Debug, |
133 | | /// Info level - informational messages |
134 | | Info, |
135 | | /// Warning level - potentially problematic situations |
136 | | Warn, |
137 | | /// Error level - error conditions that should be addressed |
138 | | Error, |
139 | | /// Critical level - serious error conditions requiring immediate attention |
140 | | Critical, |
141 | | } |
142 | | |
143 | | impl fmt::Display for ErrorSeverity { |
144 | 0 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
145 | 0 | match self { |
146 | 0 | Self::Debug => write!(f, "DEBUG"), |
147 | 0 | Self::Info => write!(f, "INFO"), |
148 | 0 | Self::Warn => write!(f, "WARN"), |
149 | 0 | Self::Error => write!(f, "ERROR"), |
150 | 0 | Self::Critical => write!(f, "CRITICAL"), |
151 | | } |
152 | 0 | } |
153 | | } |
154 | | |
155 | | /// Retry strategies for error recovery |
156 | | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] |
157 | | pub enum RetryStrategy { |
158 | | /// Do not retry - error is permanent |
159 | | NoRetry, |
160 | | /// Retry immediately without delay |
161 | | Immediate, |
162 | | /// Linear backoff with fixed intervals |
163 | | Linear { |
164 | | /// Base delay in milliseconds between retries |
165 | | base_delay_ms: u64, |
166 | | }, |
167 | | /// Exponential backoff with jitter |
168 | | Exponential { |
169 | | /// Base delay in milliseconds for exponential backoff |
170 | | base_delay_ms: u64, |
171 | | /// Maximum delay cap in milliseconds |
172 | | max_delay_ms: u64, |
173 | | }, |
174 | | /// Wait for circuit breaker to close |
175 | | CircuitBreaker, |
176 | | } |
177 | | |
178 | | impl RetryStrategy { |
179 | | /// Calculate delay for retry attempt |
180 | | #[must_use] |
181 | 19 | pub fn calculate_delay(&self, attempt: u32) -> Option<Duration> { |
182 | 19 | match self { |
183 | 3 | Self::NoRetry => None, |
184 | 2 | Self::Immediate => Some(Duration::from_millis(0)), |
185 | 4 | Self::Linear { base_delay_ms } => { |
186 | 4 | Some(Duration::from_millis(base_delay_ms * u64::from(attempt))) |
187 | | }, |
188 | | Self::Exponential { |
189 | 8 | base_delay_ms, |
190 | 8 | max_delay_ms, |
191 | | } => { |
192 | 8 | let delay_ms = base_delay_ms * 2_u64.pow(attempt.min(10)); |
193 | 8 | let capped_delay = delay_ms.min(*max_delay_ms); |
194 | | |
195 | | // Add simple jitter (±10%) |
196 | 8 | let jitter_ms = capped_delay / 10; |
197 | 8 | let final_delay = capped_delay.saturating_sub(jitter_ms / 2); |
198 | | |
199 | 8 | Some(Duration::from_millis(final_delay)) |
200 | | }, |
201 | 2 | Self::CircuitBreaker => Some(Duration::from_secs(30)), |
202 | | } |
203 | 19 | } |
204 | | |
205 | | /// Get maximum recommended retry attempts |
206 | | #[must_use] |
207 | 0 | pub const fn max_attempts(&self) -> Option<u32> { |
208 | 0 | match self { |
209 | 0 | Self::NoRetry => Some(0), |
210 | 0 | Self::Immediate => Some(3), |
211 | 0 | Self::Linear { .. } => Some(5), |
212 | 0 | Self::Exponential { .. } => Some(7), |
213 | 0 | Self::CircuitBreaker => Some(1), |
214 | | } |
215 | 0 | } |
216 | | } |
217 | | |
218 | | /// Convenience functions for creating common errors |
219 | | impl CommonError { |
220 | | /// Create a configuration error |
221 | 0 | pub fn config<S: Into<String>>(message: S) -> Self { |
222 | 0 | Self::Configuration(message.into()) |
223 | 0 | } |
224 | | |
225 | | /// Create a network error |
226 | 0 | pub fn network<S: Into<String>>(message: S) -> Self { |
227 | 0 | Self::Network(message.into()) |
228 | 0 | } |
229 | | |
230 | | /// Create a service error with category |
231 | 29 | pub fn service<S: Into<String>>(category: ErrorCategory, message: S) -> Self { |
232 | 29 | Self::Service { |
233 | 29 | category, |
234 | 29 | message: message.into(), |
235 | 29 | } |
236 | 29 | } |
237 | | |
238 | | /// Create a validation error |
239 | 0 | pub fn validation<S: Into<String>>(message: S) -> Self { |
240 | 0 | Self::Validation(message.into()) |
241 | 0 | } |
242 | | |
243 | | /// Create a timeout error |
244 | 0 | pub fn timeout(actual_ms: u64, max_ms: u64) -> Self { |
245 | 0 | Self::Timeout { actual_ms, max_ms } |
246 | 0 | } |
247 | | |
248 | | /// Create a machine learning specific service error |
249 | 0 | pub fn ml<S: Into<String>, M: Into<String>>(model_name: S, message: M) -> Self { |
250 | 0 | Self::Service { |
251 | 0 | category: ErrorCategory::MachineLearning, |
252 | 0 | message: format!("{}: {}", model_name.into(), message.into()), |
253 | 0 | } |
254 | 0 | } |
255 | | |
256 | | /// Create a serialization error |
257 | 0 | pub fn serialization<S: Into<String>>(message: S) -> Self { |
258 | 0 | Self::Service { |
259 | 0 | category: ErrorCategory::Parse, |
260 | 0 | message: format!("Serialization error: {}", message.into()), |
261 | 0 | } |
262 | 0 | } |
263 | | |
264 | | /// Create an internal error |
265 | 0 | pub fn internal<S: Into<String>>(message: S) -> Self { |
266 | 0 | Self::Service { |
267 | 0 | category: ErrorCategory::System, |
268 | 0 | message: format!("Internal error: {}", message.into()), |
269 | 0 | } |
270 | 0 | } |
271 | | |
272 | | /// Create a resource exhausted error |
273 | 0 | pub fn resource_exhausted<S: Into<String>>(resource: S) -> Self { |
274 | 0 | Self::Service { |
275 | 0 | category: ErrorCategory::Resource, |
276 | 0 | message: format!("Resource exhausted: {}", resource.into()), |
277 | 0 | } |
278 | 0 | } |
279 | | |
280 | | /// Get the error category for classification and metrics |
281 | 0 | pub fn category(&self) -> ErrorCategory { |
282 | 0 | match self { |
283 | 0 | Self::Database(_) => ErrorCategory::Database, |
284 | 0 | Self::Configuration(_) => ErrorCategory::Configuration, |
285 | 0 | Self::Network(_) => ErrorCategory::Network, |
286 | 0 | Self::Service { category, .. } => *category, |
287 | 0 | Self::Validation(_) => ErrorCategory::Validation, |
288 | 0 | Self::Timeout { .. } => ErrorCategory::System, |
289 | | } |
290 | 0 | } |
291 | | |
292 | | /// Get error severity level |
293 | 28 | pub fn severity(&self) -> ErrorSeverity { |
294 | 28 | match self { |
295 | 1 | Self::Database(_) => ErrorSeverity::Critical, |
296 | 1 | Self::Configuration(_) => ErrorSeverity::Critical, |
297 | 1 | Self::Network(_) => ErrorSeverity::Error, |
298 | 23 | Self::Service { category, .. } => match category { |
299 | | ErrorCategory::Critical |
300 | | | ErrorCategory::FinancialSafety |
301 | 3 | | ErrorCategory::Authentication => ErrorSeverity::Critical, |
302 | | ErrorCategory::Trading |
303 | | | ErrorCategory::RiskManagement |
304 | 3 | | ErrorCategory::Database => ErrorSeverity::Error, |
305 | 17 | _ => ErrorSeverity::Warn, |
306 | | }, |
307 | 1 | Self::Validation(_) => ErrorSeverity::Warn, |
308 | 1 | Self::Timeout { .. } => ErrorSeverity::Error, |
309 | | } |
310 | 28 | } |
311 | | |
312 | | /// Check if the error is retryable |
313 | 11 | pub fn is_retryable(&self) -> bool { |
314 | 11 | match self { |
315 | 1 | Self::Database(_) => true, // Database operations can be retried |
316 | 1 | Self::Configuration(_) => false, // Configuration errors are permanent |
317 | 1 | Self::Network(_) => true, // Network errors are often transient |
318 | 6 | Self::Service { category, .. } => !matches!5 ( |
319 | 6 | category, |
320 | | ErrorCategory::Authentication |
321 | | | ErrorCategory::Configuration |
322 | | | ErrorCategory::Validation |
323 | | ), |
324 | 1 | Self::Validation(_) => false, // Validation errors are permanent |
325 | 1 | Self::Timeout { .. } => true, // Timeouts can be retried |
326 | | } |
327 | 11 | } |
328 | | |
329 | | /// Get retry strategy for this error |
330 | 11 | pub fn retry_strategy(&self) -> RetryStrategy { |
331 | 11 | if !self.is_retryable() { |
332 | 3 | return RetryStrategy::NoRetry; |
333 | 8 | } |
334 | | |
335 | 8 | match self { |
336 | 1 | Self::Database(_) => RetryStrategy::Exponential { |
337 | 1 | base_delay_ms: 1000, |
338 | 1 | max_delay_ms: 10000, |
339 | 1 | }, |
340 | 1 | Self::Network(_) => RetryStrategy::Linear { base_delay_ms: 500 }, |
341 | 5 | Self::Service { category, .. } => match category { |
342 | | ErrorCategory::Network | ErrorCategory::Connection => { |
343 | 2 | RetryStrategy::Linear { base_delay_ms: 500 } |
344 | | }, |
345 | 1 | ErrorCategory::RateLimit => RetryStrategy::Exponential { |
346 | 1 | base_delay_ms: 5000, |
347 | 1 | max_delay_ms: 60000, |
348 | 1 | }, |
349 | 2 | _ => RetryStrategy::Immediate, |
350 | | }, |
351 | 1 | Self::Timeout { .. } => RetryStrategy::Linear { |
352 | 1 | base_delay_ms: 1000, |
353 | 1 | }, |
354 | 0 | _ => RetryStrategy::NoRetry, |
355 | | } |
356 | 11 | } |
357 | | } |
358 | | |
359 | | /// Result type for common operations |
360 | | pub type CommonResult<T> = Result<T, CommonError>; |