Skip to main content

voirs_recognizer/
error_bridge.rs

1//! # Error Bridge
2//!
3//! Standardized error handling patterns for cross-crate error conversion
4//! and unified error reporting across the `VoiRS` ecosystem.
5
6use crate::sdk_bridge::ErrorCode;
7use crate::RecognitionError;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::sync::Arc;
11use thiserror::Error;
12use tracing::{error, warn};
13
14/// Error bridge for cross-crate error handling
15#[derive(Debug, Error)]
16pub enum ErrorBridgeError {
17    /// Error conversion failed
18    #[error("Error conversion failed: {0}")]
19    ConversionFailed(String),
20
21    /// Unknown error type
22    #[error("Unknown error type: {0}")]
23    UnknownErrorType(String),
24
25    /// Error context missing
26    #[error("Error context missing: {0}")]
27    ContextMissing(String),
28}
29
30/// Trait for converting errors to standardized `VoiRS` error format
31pub trait ToVoirsError {
32    /// Convert to `VoiRS` error
33    fn to_voirs_error(&self) -> RecognitionError;
34
35    /// Convert to error code
36    fn to_error_code(&self) -> ErrorCode;
37
38    /// Get error context
39    fn error_context(&self) -> ErrorContext {
40        ErrorContext::default()
41    }
42}
43
44/// Trait for errors that can be recovered from
45pub trait RecoverableError {
46    /// Check if error is recoverable
47    fn is_recoverable(&self) -> bool;
48
49    /// Suggest recovery action
50    fn recovery_action(&self) -> Option<RecoveryAction>;
51
52    /// Retry delay in milliseconds
53    fn retry_delay_ms(&self) -> Option<u64> {
54        None
55    }
56}
57
58/// Error context for detailed error information
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct ErrorContext {
61    /// Operation that caused the error
62    pub operation: String,
63
64    /// Component where error occurred
65    pub component: String,
66
67    /// Additional context data
68    pub context_data: HashMap<String, String>,
69
70    /// Timestamp when error occurred
71    pub timestamp: chrono::DateTime<chrono::Utc>,
72
73    /// Thread/task ID if available
74    pub thread_id: Option<String>,
75
76    /// Stack trace if available
77    pub stack_trace: Option<String>,
78}
79
80impl Default for ErrorContext {
81    fn default() -> Self {
82        Self {
83            operation: "unknown".to_string(),
84            component: "voirs-recognizer".to_string(),
85            context_data: HashMap::new(),
86            timestamp: chrono::Utc::now(),
87            thread_id: None,
88            stack_trace: None,
89        }
90    }
91}
92
93/// Recovery actions for recoverable errors
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
95pub enum RecoveryAction {
96    /// Retry the operation
97    Retry,
98
99    /// Use fallback method
100    UseFallback,
101
102    /// Skip and continue
103    SkipAndContinue,
104
105    /// Reset and retry
106    ResetAndRetry,
107
108    /// Reduce quality/performance and retry
109    ReduceQualityAndRetry,
110
111    /// Clear cache and retry
112    ClearCacheAndRetry,
113
114    /// Manual intervention required
115    ManualInterventionRequired,
116}
117
118/// Standardized error reporting
119pub struct ErrorReporter {
120    /// Error history
121    error_history: parking_lot::RwLock<Vec<ErrorReport>>,
122
123    /// Error count by type
124    error_counts: parking_lot::RwLock<HashMap<String, usize>>,
125
126    /// Maximum history size
127    max_history_size: usize,
128
129    /// Error callbacks
130    callbacks: parking_lot::RwLock<Vec<ErrorCallback>>,
131}
132
133/// Error report for logging and analysis
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct ErrorReport {
136    /// Error code
137    pub error_code: ErrorCode,
138
139    /// Error message
140    pub message: String,
141
142    /// Error context
143    pub context: ErrorContext,
144
145    /// Recovery attempted
146    pub recovery_attempted: Option<RecoveryAction>,
147
148    /// Recovery successful
149    pub recovery_successful: bool,
150
151    /// Error severity
152    pub severity: ErrorSeverity,
153}
154
155/// Error severity levels
156#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
157pub enum ErrorSeverity {
158    /// Debug level - for development
159    Debug,
160
161    /// Info level - informational
162    Info,
163
164    /// Warning level - might cause issues
165    Warning,
166
167    /// Error level - operation failed
168    Error,
169
170    /// Critical level - system unstable
171    Critical,
172
173    /// Fatal level - system must shut down
174    Fatal,
175}
176
177/// Error callback function type
178pub type ErrorCallback = Arc<dyn Fn(&ErrorReport) + Send + Sync>;
179
180impl ErrorReporter {
181    /// Create a new error reporter
182    #[must_use]
183    pub fn new() -> Self {
184        Self {
185            error_history: parking_lot::RwLock::new(Vec::new()),
186            error_counts: parking_lot::RwLock::new(HashMap::new()),
187            max_history_size: 1000,
188            callbacks: parking_lot::RwLock::new(Vec::new()),
189        }
190    }
191
192    /// Report an error
193    pub fn report(&self, report: ErrorReport) {
194        // Log based on severity
195        match report.severity {
196            ErrorSeverity::Debug => tracing::debug!("Error: {}", report.message),
197            ErrorSeverity::Info => tracing::info!("Error: {}", report.message),
198            ErrorSeverity::Warning => warn!("Error: {}", report.message),
199            ErrorSeverity::Error => error!("Error: {}", report.message),
200            ErrorSeverity::Critical => error!("CRITICAL Error: {}", report.message),
201            ErrorSeverity::Fatal => error!("FATAL Error: {}", report.message),
202        }
203
204        // Update error counts
205        {
206            let mut counts = self.error_counts.write();
207            let error_type = format!("{:?}", report.error_code);
208            *counts.entry(error_type).or_insert(0) += 1;
209        }
210
211        // Call registered callbacks
212        {
213            let callbacks = self.callbacks.read();
214            for callback in callbacks.iter() {
215                callback(&report);
216            }
217        }
218
219        // Add to history (with size limit)
220        {
221            let mut history = self.error_history.write();
222            history.push(report);
223
224            // Trim history if needed
225            if history.len() > self.max_history_size {
226                let excess = history.len() - self.max_history_size;
227                history.drain(0..excess);
228            }
229        }
230    }
231
232    /// Register error callback
233    pub fn register_callback(&self, callback: ErrorCallback) {
234        self.callbacks.write().push(callback);
235    }
236
237    /// Get error history
238    pub fn get_history(&self) -> Vec<ErrorReport> {
239        self.error_history.read().clone()
240    }
241
242    /// Get error counts
243    pub fn get_error_counts(&self) -> HashMap<String, usize> {
244        self.error_counts.read().clone()
245    }
246
247    /// Clear error history
248    pub fn clear_history(&self) {
249        self.error_history.write().clear();
250    }
251
252    /// Get error rate (errors per minute)
253    pub fn get_error_rate(&self, window_minutes: u32) -> f64 {
254        let history = self.error_history.read();
255        let now = chrono::Utc::now();
256        let window_start = now - chrono::Duration::minutes(i64::from(window_minutes));
257
258        let recent_errors = history
259            .iter()
260            .filter(|r| r.context.timestamp > window_start)
261            .count();
262
263        recent_errors as f64 / f64::from(window_minutes)
264    }
265
266    /// Get errors by severity
267    pub fn get_errors_by_severity(&self, severity: ErrorSeverity) -> Vec<ErrorReport> {
268        self.error_history
269            .read()
270            .iter()
271            .filter(|r| r.severity == severity)
272            .cloned()
273            .collect()
274    }
275
276    /// Get most common errors
277    pub fn get_most_common_errors(&self, limit: usize) -> Vec<(String, usize)> {
278        let counts = self.error_counts.read();
279        let mut counts_vec: Vec<_> = counts.iter().map(|(k, v)| (k.clone(), *v)).collect();
280        counts_vec.sort_by(|a, b| b.1.cmp(&a.1));
281        counts_vec.truncate(limit);
282        counts_vec
283    }
284}
285
286impl Default for ErrorReporter {
287    fn default() -> Self {
288        Self::new()
289    }
290}
291
292/// Implementation of `ToVoirsError` for `RecognitionError`
293impl ToVoirsError for RecognitionError {
294    fn to_voirs_error(&self) -> RecognitionError {
295        // RecognitionError doesn't derive Clone, so we reconstruct based on type
296        // In a real implementation, you would clone the error or use Arc
297        match self {
298            RecognitionError::ModelLoadError { message, .. } => RecognitionError::ModelLoadError {
299                message: message.clone(),
300                source: None,
301            },
302            RecognitionError::ModelError { message, .. } => RecognitionError::ModelError {
303                message: message.clone(),
304                source: None,
305            },
306            RecognitionError::AudioProcessingError { message, .. } => {
307                RecognitionError::AudioProcessingError {
308                    message: message.clone(),
309                    source: None,
310                }
311            }
312            RecognitionError::TranscriptionError { message, .. } => {
313                RecognitionError::TranscriptionError {
314                    message: message.clone(),
315                    source: None,
316                }
317            }
318            RecognitionError::PhonemeRecognitionError { message, .. } => {
319                RecognitionError::PhonemeRecognitionError {
320                    message: message.clone(),
321                    source: None,
322                }
323            }
324            RecognitionError::AudioAnalysisError { message, .. } => {
325                RecognitionError::AudioAnalysisError {
326                    message: message.clone(),
327                    source: None,
328                }
329            }
330            RecognitionError::ConfigurationError { message } => {
331                RecognitionError::ConfigurationError {
332                    message: message.clone(),
333                }
334            }
335            RecognitionError::FeatureNotSupported { feature } => {
336                RecognitionError::FeatureNotSupported {
337                    feature: feature.clone(),
338                }
339            }
340            RecognitionError::InvalidInput { message } => RecognitionError::InvalidInput {
341                message: message.clone(),
342            },
343            RecognitionError::ResourceError { message, .. } => RecognitionError::ResourceError {
344                message: message.clone(),
345                source: None,
346            },
347            RecognitionError::UnsupportedFormat(msg) => {
348                RecognitionError::UnsupportedFormat(msg.clone())
349            }
350            RecognitionError::InvalidFormat(msg) => RecognitionError::InvalidFormat(msg.clone()),
351            RecognitionError::ModelNotFound {
352                model,
353                available,
354                suggestions,
355            } => RecognitionError::ModelNotFound {
356                model: model.clone(),
357                available: available.clone(),
358                suggestions: suggestions.clone(),
359            },
360            RecognitionError::LanguageNotSupported {
361                language,
362                supported,
363                suggestions,
364            } => RecognitionError::LanguageNotSupported {
365                language: language.clone(),
366                supported: supported.clone(),
367                suggestions: suggestions.clone(),
368            },
369            RecognitionError::DeviceNotAvailable {
370                device,
371                reason,
372                fallback,
373            } => RecognitionError::DeviceNotAvailable {
374                device: device.clone(),
375                reason: reason.clone(),
376                fallback: fallback.clone(),
377            },
378            RecognitionError::InsufficientMemory {
379                required_mb,
380                available_mb,
381                recommendation,
382            } => RecognitionError::InsufficientMemory {
383                required_mb: *required_mb,
384                available_mb: *available_mb,
385                recommendation: recommendation.clone(),
386            },
387            RecognitionError::RecognitionTimeout {
388                timeout_ms,
389                audio_duration_ms,
390                suggestion,
391            } => RecognitionError::RecognitionTimeout {
392                timeout_ms: *timeout_ms,
393                audio_duration_ms: *audio_duration_ms,
394                suggestion: suggestion.clone(),
395            },
396            RecognitionError::MemoryError { message, .. } => RecognitionError::MemoryError {
397                message: message.clone(),
398                source: None,
399            },
400            RecognitionError::TrainingError { message, .. } => RecognitionError::TrainingError {
401                message: message.clone(),
402                source: None,
403            },
404            RecognitionError::SynchronizationError { message } => {
405                RecognitionError::SynchronizationError {
406                    message: message.clone(),
407                }
408            }
409        }
410    }
411
412    fn to_error_code(&self) -> ErrorCode {
413        match self {
414            RecognitionError::ModelLoadError { .. } => ErrorCode::ModelLoadError,
415            RecognitionError::ModelError { .. } => ErrorCode::InferenceError,
416            RecognitionError::AudioProcessingError { .. } => ErrorCode::AudioProcessingError,
417            RecognitionError::TranscriptionError { .. } => ErrorCode::InferenceError,
418            RecognitionError::PhonemeRecognitionError { .. } => ErrorCode::InferenceError,
419            RecognitionError::AudioAnalysisError { .. } => ErrorCode::AudioProcessingError,
420            RecognitionError::ConfigurationError { .. } => ErrorCode::ConfigError,
421            RecognitionError::FeatureNotSupported { .. } => ErrorCode::GenericError,
422            RecognitionError::InvalidInput { .. } => ErrorCode::GenericError,
423            RecognitionError::ResourceError { .. } => ErrorCode::ResourceExhausted,
424            RecognitionError::UnsupportedFormat(_) => ErrorCode::GenericError,
425            RecognitionError::InvalidFormat(_) => ErrorCode::GenericError,
426            RecognitionError::ModelNotFound { .. } => ErrorCode::ModelLoadError,
427            RecognitionError::LanguageNotSupported { .. } => ErrorCode::GenericError,
428            RecognitionError::DeviceNotAvailable { .. } => ErrorCode::GpuError,
429            RecognitionError::InsufficientMemory { .. } => ErrorCode::MemoryError,
430            RecognitionError::RecognitionTimeout { .. } => ErrorCode::TimeoutError,
431            RecognitionError::MemoryError { .. } => ErrorCode::MemoryError,
432            RecognitionError::TrainingError { .. } => ErrorCode::InferenceError,
433            RecognitionError::SynchronizationError { .. } => ErrorCode::GenericError,
434        }
435    }
436
437    fn error_context(&self) -> ErrorContext {
438        ErrorContext {
439            operation: "recognition".to_string(),
440            component: "voirs-recognizer".to_string(),
441            context_data: {
442                let mut data = HashMap::new();
443                data.insert("error_variant".to_string(), self.to_string());
444                data
445            },
446            timestamp: chrono::Utc::now(),
447            thread_id: Some(format!("{:?}", std::thread::current().id())),
448            stack_trace: None,
449        }
450    }
451}
452
453/// Implementation of `RecoverableError` for `RecognitionError`
454impl RecoverableError for RecognitionError {
455    fn is_recoverable(&self) -> bool {
456        match self {
457            RecognitionError::ModelLoadError { .. } => false, // Need manual intervention
458            RecognitionError::ModelError { .. } => true,      // Can retry
459            RecognitionError::AudioProcessingError { .. } => true, // Can retry or skip
460            RecognitionError::TranscriptionError { .. } => true, // Can retry
461            RecognitionError::PhonemeRecognitionError { .. } => true, // Can retry
462            RecognitionError::AudioAnalysisError { .. } => true, // Can retry
463            RecognitionError::ConfigurationError { .. } => false, // Need config fix
464            RecognitionError::FeatureNotSupported { .. } => false, // Feature not available
465            RecognitionError::InvalidInput { .. } => false,   // User error
466            RecognitionError::ResourceError { .. } => true,   // Can retry
467            RecognitionError::UnsupportedFormat(_) => false,  // Format not supported
468            RecognitionError::InvalidFormat(_) => false,      // Invalid format
469            RecognitionError::ModelNotFound { .. } => false,  // Model doesn't exist
470            RecognitionError::LanguageNotSupported { .. } => false, // Language not supported
471            RecognitionError::DeviceNotAvailable { .. } => true, // Can use fallback
472            RecognitionError::InsufficientMemory { .. } => true, // Can reduce batch size
473            RecognitionError::RecognitionTimeout { .. } => true, // Can retry with longer timeout
474            RecognitionError::MemoryError { .. } => true,     // Can reduce batch size
475            RecognitionError::TrainingError { .. } => true,   // Can retry
476            RecognitionError::SynchronizationError { .. } => false, // Indicates critical internal error
477        }
478    }
479
480    fn recovery_action(&self) -> Option<RecoveryAction> {
481        match self {
482            RecognitionError::ModelError { .. } => Some(RecoveryAction::Retry),
483            RecognitionError::AudioProcessingError { .. } => Some(RecoveryAction::UseFallback),
484            RecognitionError::TranscriptionError { .. } => Some(RecoveryAction::Retry),
485            RecognitionError::PhonemeRecognitionError { .. } => Some(RecoveryAction::Retry),
486            RecognitionError::AudioAnalysisError { .. } => Some(RecoveryAction::UseFallback),
487            RecognitionError::ResourceError { .. } => Some(RecoveryAction::Retry),
488            RecognitionError::DeviceNotAvailable { .. } => Some(RecoveryAction::UseFallback),
489            RecognitionError::InsufficientMemory { .. } => {
490                Some(RecoveryAction::ReduceQualityAndRetry)
491            }
492            RecognitionError::RecognitionTimeout { .. } => {
493                Some(RecoveryAction::ReduceQualityAndRetry)
494            }
495            RecognitionError::MemoryError { .. } => Some(RecoveryAction::ReduceQualityAndRetry),
496            RecognitionError::TrainingError { .. } => Some(RecoveryAction::Retry),
497            _ => None,
498        }
499    }
500
501    fn retry_delay_ms(&self) -> Option<u64> {
502        match self {
503            RecognitionError::ModelError { .. } => Some(1000), // 1 second
504            RecognitionError::AudioProcessingError { .. } => Some(500), // 500 ms
505            RecognitionError::TranscriptionError { .. } => Some(1000), // 1 second
506            RecognitionError::PhonemeRecognitionError { .. } => Some(1000), // 1 second
507            RecognitionError::AudioAnalysisError { .. } => Some(500), // 500 ms
508            RecognitionError::ResourceError { .. } => Some(2000), // 2 seconds
509            RecognitionError::DeviceNotAvailable { .. } => Some(500), // 500 ms
510            RecognitionError::InsufficientMemory { .. } => Some(1000), // 1 second
511            RecognitionError::RecognitionTimeout { .. } => Some(5000), // 5 seconds
512            RecognitionError::MemoryError { .. } => Some(1000), // 1 second
513            RecognitionError::TrainingError { .. } => Some(2000), // 2 seconds
514            _ => None,
515        }
516    }
517}
518
519/// Global error reporter instance
520static ERROR_REPORTER: once_cell::sync::Lazy<ErrorReporter> =
521    once_cell::sync::Lazy::new(ErrorReporter::new);
522
523/// Get global error reporter
524#[must_use]
525pub fn global_error_reporter() -> &'static ErrorReporter {
526    &ERROR_REPORTER
527}
528
529/// Helper function to report an error
530pub fn report_error(
531    error: &RecognitionError,
532    severity: ErrorSeverity,
533    recovery_attempted: Option<RecoveryAction>,
534    recovery_successful: bool,
535) {
536    let report = ErrorReport {
537        error_code: error.to_error_code(),
538        message: error.to_string(),
539        context: error.error_context(),
540        recovery_attempted,
541        recovery_successful,
542        severity,
543    };
544
545    global_error_reporter().report(report);
546}
547
548#[cfg(test)]
549mod tests {
550    use super::*;
551
552    #[test]
553    fn test_error_context_creation() {
554        let context = ErrorContext::default();
555        assert_eq!(context.component, "voirs-recognizer");
556        assert!(!context.operation.is_empty());
557    }
558
559    #[test]
560    fn test_error_reporter_creation() {
561        let reporter = ErrorReporter::new();
562        assert_eq!(reporter.get_history().len(), 0);
563        assert_eq!(reporter.get_error_counts().len(), 0);
564    }
565
566    #[test]
567    fn test_error_reporting() {
568        let reporter = ErrorReporter::new();
569
570        let report = ErrorReport {
571            error_code: ErrorCode::InferenceError,
572            message: "Test error".to_string(),
573            context: ErrorContext::default(),
574            recovery_attempted: None,
575            recovery_successful: false,
576            severity: ErrorSeverity::Error,
577        };
578
579        reporter.report(report);
580
581        let history = reporter.get_history();
582        assert_eq!(history.len(), 1);
583        assert_eq!(history[0].message, "Test error");
584    }
585
586    #[test]
587    fn test_error_counts() {
588        let reporter = ErrorReporter::new();
589
590        for _ in 0..3 {
591            let report = ErrorReport {
592                error_code: ErrorCode::InferenceError,
593                message: "Test error".to_string(),
594                context: ErrorContext::default(),
595                recovery_attempted: None,
596                recovery_successful: false,
597                severity: ErrorSeverity::Error,
598            };
599            reporter.report(report);
600        }
601
602        let counts = reporter.get_error_counts();
603        assert_eq!(counts.get("InferenceError"), Some(&3));
604    }
605
606    #[test]
607    fn test_error_rate_calculation() {
608        let reporter = ErrorReporter::new();
609
610        for _ in 0..5 {
611            let report = ErrorReport {
612                error_code: ErrorCode::InferenceError,
613                message: "Test error".to_string(),
614                context: ErrorContext::default(),
615                recovery_attempted: None,
616                recovery_successful: false,
617                severity: ErrorSeverity::Error,
618            };
619            reporter.report(report);
620        }
621
622        let rate = reporter.get_error_rate(1);
623        assert!(rate >= 5.0);
624    }
625
626    #[test]
627    fn test_errors_by_severity() {
628        let reporter = ErrorReporter::new();
629
630        let report1 = ErrorReport {
631            error_code: ErrorCode::InferenceError,
632            message: "Error 1".to_string(),
633            context: ErrorContext::default(),
634            recovery_attempted: None,
635            recovery_successful: false,
636            severity: ErrorSeverity::Error,
637        };
638
639        let report2 = ErrorReport {
640            error_code: ErrorCode::ConfigError,
641            message: "Error 2".to_string(),
642            context: ErrorContext::default(),
643            recovery_attempted: None,
644            recovery_successful: false,
645            severity: ErrorSeverity::Critical,
646        };
647
648        reporter.report(report1);
649        reporter.report(report2);
650
651        let errors = reporter.get_errors_by_severity(ErrorSeverity::Error);
652        assert_eq!(errors.len(), 1);
653        assert_eq!(errors[0].message, "Error 1");
654    }
655
656    #[test]
657    fn test_recognition_error_conversion() {
658        let error = RecognitionError::ModelError {
659            message: "Test".to_string(),
660            source: None,
661        };
662        assert_eq!(error.to_error_code(), ErrorCode::InferenceError);
663        assert!(error.is_recoverable());
664        assert_eq!(error.recovery_action(), Some(RecoveryAction::Retry));
665    }
666
667    #[test]
668    fn test_most_common_errors() {
669        let reporter = ErrorReporter::new();
670
671        // Report different types of errors with different frequencies
672        for _ in 0..5 {
673            reporter.report(ErrorReport {
674                error_code: ErrorCode::InferenceError,
675                message: "Inference error".to_string(),
676                context: ErrorContext::default(),
677                recovery_attempted: None,
678                recovery_successful: false,
679                severity: ErrorSeverity::Error,
680            });
681        }
682
683        for _ in 0..3 {
684            reporter.report(ErrorReport {
685                error_code: ErrorCode::ConfigError,
686                message: "Config error".to_string(),
687                context: ErrorContext::default(),
688                recovery_attempted: None,
689                recovery_successful: false,
690                severity: ErrorSeverity::Error,
691            });
692        }
693
694        let most_common = reporter.get_most_common_errors(5);
695        assert_eq!(most_common.len(), 2);
696        assert_eq!(most_common[0].1, 5); // InferenceError is most common
697        assert_eq!(most_common[1].1, 3); // ConfigError is second
698    }
699
700    #[test]
701    fn test_error_callback() {
702        let reporter = ErrorReporter::new();
703        let called = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
704        let called_clone = called.clone();
705
706        let callback: ErrorCallback = Arc::new(move |_report| {
707            called_clone.store(true, std::sync::atomic::Ordering::SeqCst);
708        });
709
710        reporter.register_callback(callback);
711
712        reporter.report(ErrorReport {
713            error_code: ErrorCode::InferenceError,
714            message: "Test".to_string(),
715            context: ErrorContext::default(),
716            recovery_attempted: None,
717            recovery_successful: false,
718            severity: ErrorSeverity::Error,
719        });
720
721        assert!(called.load(std::sync::atomic::Ordering::SeqCst));
722    }
723}