Skip to main content

oxirs_arq/advanced_optimizer/
model_manager.rs

1//! ML Model Lifecycle Management
2//!
3//! This module manages ML model training, quality tracking, retraining,
4//! and rollback capabilities for the query cost predictor.
5
6use std::collections::VecDeque;
7use std::path::Path;
8use std::sync::atomic::{AtomicBool, Ordering};
9use std::sync::{Arc, RwLock};
10use std::time::SystemTime;
11
12use anyhow::{Context, Result};
13use serde::{Deserialize, Serialize};
14
15use crate::advanced_optimizer::ml_predictor::MLPredictor;
16use crate::advanced_optimizer::training_collector::TrainingCollector;
17
18/// Configuration for model manager
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct ManagerConfig {
21    /// Retraining interval in hours
22    pub retraining_interval_hours: u64,
23    /// Minimum examples required before training
24    pub min_examples_for_training: usize,
25    /// Quality threshold for R² score
26    pub quality_threshold_r2: f64,
27    /// Quality threshold for MAE (as percentage)
28    pub quality_threshold_mae: f64,
29    /// Enable automatic retraining
30    pub enable_auto_retraining: bool,
31    /// Enable model rollback on quality degradation
32    pub enable_rollback: bool,
33    /// Maximum prediction tracking buffer size
34    pub max_prediction_buffer: usize,
35}
36
37impl Default for ManagerConfig {
38    fn default() -> Self {
39        Self {
40            retraining_interval_hours: 24,
41            min_examples_for_training: 100,
42            quality_threshold_r2: 0.8,
43            quality_threshold_mae: 0.2, // 20%
44            enable_auto_retraining: true,
45            enable_rollback: true,
46            max_prediction_buffer: 1000,
47        }
48    }
49}
50
51/// Model manager for ML predictor lifecycle
52pub struct ModelManager {
53    active_model: Arc<RwLock<MLPredictor>>,
54    previous_model: Option<Arc<RwLock<MLPredictor>>>,
55    training_collector: Option<Arc<RwLock<TrainingCollector>>>,
56    config: ManagerConfig,
57    performance_tracker: Arc<RwLock<PerformanceTracker>>,
58    retraining_in_progress: Arc<AtomicBool>,
59    last_retraining: Option<SystemTime>,
60}
61
62impl ModelManager {
63    /// Create a new model manager
64    pub fn new(predictor: MLPredictor, config: ManagerConfig) -> Self {
65        let performance_tracker = Arc::new(RwLock::new(PerformanceTracker::new(
66            config.max_prediction_buffer,
67        )));
68
69        Self {
70            active_model: Arc::new(RwLock::new(predictor)),
71            previous_model: None,
72            training_collector: None,
73            config,
74            performance_tracker,
75            retraining_in_progress: Arc::new(AtomicBool::new(false)),
76            last_retraining: None,
77        }
78    }
79
80    /// Create model manager with training collector
81    pub fn with_training_collector(mut self, collector: Arc<RwLock<TrainingCollector>>) -> Self {
82        self.training_collector = Some(collector);
83        self
84    }
85
86    /// Get the active model
87    pub fn get_predictor(&self) -> Arc<RwLock<MLPredictor>> {
88        Arc::clone(&self.active_model)
89    }
90
91    /// Record a prediction result
92    pub fn record_prediction(&self, predicted: f64, actual: f64) -> Result<()> {
93        let mut tracker = self
94            .performance_tracker
95            .write()
96            .map_err(|e| anyhow::anyhow!("Failed to acquire write lock: {}", e))?;
97
98        tracker.record(PredictionResult {
99            predicted_cost: predicted,
100            actual_cost: Some(actual),
101            timestamp: SystemTime::now(),
102            error: Some((predicted - actual).abs()),
103        });
104
105        Ok(())
106    }
107
108    /// Evaluate current model quality
109    pub fn evaluate_model_quality(&self) -> Result<ModelQuality> {
110        let tracker = self
111            .performance_tracker
112            .read()
113            .map_err(|e| anyhow::anyhow!("Failed to acquire read lock: {}", e))?;
114
115        let quality = tracker.calculate_quality();
116
117        Ok(quality)
118    }
119
120    /// Check if ML model should be used
121    pub fn should_use_ml(&self) -> bool {
122        if let Ok(quality) = self.evaluate_model_quality() {
123            quality.is_acceptable
124        } else {
125            false
126        }
127    }
128
129    /// Check if model should be retrained
130    pub fn should_retrain(&self) -> bool {
131        if !self.config.enable_auto_retraining {
132            return false;
133        }
134
135        // Don't retrain if already in progress
136        if self.retraining_in_progress.load(Ordering::Relaxed) {
137            return false;
138        }
139
140        // Check if enough time has passed
141        if let Some(last_training) = self.last_retraining {
142            if let Ok(elapsed) = SystemTime::now().duration_since(last_training) {
143                let hours_elapsed = elapsed.as_secs() / 3600;
144                if hours_elapsed < self.config.retraining_interval_hours {
145                    return false;
146                }
147            }
148        }
149
150        // Check if we have enough training data
151        if let Some(ref collector) = self.training_collector {
152            if let Ok(collector_guard) = collector.read() {
153                if collector_guard.len() < self.config.min_examples_for_training {
154                    return false;
155                }
156            } else {
157                return false;
158            }
159        } else {
160            // No training collector means no training data
161            return false;
162        }
163
164        true
165    }
166
167    /// Trigger model retraining
168    pub fn trigger_retraining(&mut self) -> Result<()> {
169        // Set retraining flag
170        if self
171            .retraining_in_progress
172            .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
173            .is_err()
174        {
175            return Err(anyhow::anyhow!("Retraining already in progress"));
176        }
177
178        // Perform retraining
179        let result = self.retrain_internal();
180
181        // Clear retraining flag
182        self.retraining_in_progress.store(false, Ordering::SeqCst);
183
184        result
185    }
186
187    /// Internal retraining implementation
188    fn retrain_internal(&mut self) -> Result<()> {
189        // Get training data
190        let training_collector = self
191            .training_collector
192            .as_ref()
193            .ok_or_else(|| anyhow::anyhow!("No training collector available"))?;
194
195        let examples = {
196            let collector = training_collector
197                .read()
198                .map_err(|e| anyhow::anyhow!("Failed to acquire read lock: {}", e))?;
199            collector.get_all_examples()?
200        };
201
202        if examples.len() < self.config.min_examples_for_training {
203            return Err(anyhow::anyhow!(
204                "Insufficient training examples: {} < {}",
205                examples.len(),
206                self.config.min_examples_for_training
207            ));
208        }
209
210        // Save current model as previous (for rollback)
211        if self.config.enable_rollback {
212            let current = self
213                .active_model
214                .read()
215                .map_err(|e| anyhow::anyhow!("Failed to acquire read lock: {}", e))?;
216
217            self.previous_model = Some(Arc::new(RwLock::new(current.clone())));
218        }
219
220        // Get current quality before retraining
221        let old_quality = self.evaluate_model_quality()?;
222
223        // Train new model
224        {
225            let mut model = self
226                .active_model
227                .write()
228                .map_err(|e| anyhow::anyhow!("Failed to acquire write lock: {}", e))?;
229
230            // Add training examples to model
231            for example in examples {
232                model.add_training_example(example);
233            }
234
235            // Train
236            model.train_model().context("Failed to train model")?;
237        }
238
239        // Evaluate new model quality
240        let new_quality = self.evaluate_model_quality()?;
241
242        // Check if new model is better
243        if self.config.enable_rollback && new_quality.r_squared < old_quality.r_squared {
244            tracing::warn!(
245                "New model quality degraded (R²: {} → {}). Rolling back.",
246                old_quality.r_squared,
247                new_quality.r_squared
248            );
249            self.rollback_to_previous()?;
250        } else {
251            tracing::info!(
252                "Model retrained successfully. R²: {} → {}, MAE: {} → {}",
253                old_quality.r_squared,
254                new_quality.r_squared,
255                old_quality.mae,
256                new_quality.mae
257            );
258        }
259
260        // Update last retraining time
261        self.last_retraining = Some(SystemTime::now());
262
263        Ok(())
264    }
265
266    /// Rollback to previous model
267    pub fn rollback_to_previous(&mut self) -> Result<()> {
268        let previous = self
269            .previous_model
270            .take()
271            .ok_or_else(|| anyhow::anyhow!("No previous model available for rollback"))?;
272
273        self.active_model = previous;
274
275        tracing::info!("Rolled back to previous model");
276
277        Ok(())
278    }
279
280    /// Save model checkpoint
281    pub fn save_checkpoint(&self, path: &Path) -> Result<()> {
282        let model = self
283            .active_model
284            .read()
285            .map_err(|e| anyhow::anyhow!("Failed to acquire read lock: {}", e))?;
286
287        model
288            .save_model(path)
289            .context("Failed to save model checkpoint")?;
290
291        Ok(())
292    }
293
294    /// Load model from checkpoint
295    pub fn load_checkpoint(path: &Path, config: ManagerConfig) -> Result<Self> {
296        let predictor =
297            MLPredictor::load_model(path).context("Failed to load model from checkpoint")?;
298
299        Ok(Self::new(predictor, config))
300    }
301
302    /// Get performance metrics
303    pub fn get_performance_metrics(&self) -> Result<PerformanceMetrics> {
304        let tracker = self
305            .performance_tracker
306            .read()
307            .map_err(|e| anyhow::anyhow!("Failed to acquire read lock: {}", e))?;
308
309        let model = self
310            .active_model
311            .read()
312            .map_err(|e| anyhow::anyhow!("Failed to acquire read lock: {}", e))?;
313
314        Ok(PerformanceMetrics {
315            predictions_made: model.predictions_count(),
316            training_examples: model.training_data_count(),
317            mae: tracker.mae,
318            rmse: tracker.rmse,
319            r_squared: tracker.r_squared,
320            is_using_ml: self.should_use_ml(),
321        })
322    }
323}
324
325/// Performance tracker for model predictions
326pub struct PerformanceTracker {
327    predictions: VecDeque<PredictionResult>,
328    max_buffer: usize,
329    pub mae: f64,
330    pub rmse: f64,
331    pub r_squared: f64,
332    last_update: SystemTime,
333}
334
335impl PerformanceTracker {
336    /// Create a new performance tracker
337    pub fn new(max_buffer: usize) -> Self {
338        Self {
339            predictions: VecDeque::with_capacity(max_buffer.min(1000)),
340            max_buffer,
341            mae: 0.0,
342            rmse: 0.0,
343            r_squared: 0.0,
344            last_update: SystemTime::now(),
345        }
346    }
347
348    /// Record a prediction result
349    pub fn record(&mut self, result: PredictionResult) {
350        self.predictions.push_back(result);
351
352        // Remove oldest if over capacity
353        if self.predictions.len() > self.max_buffer {
354            self.predictions.pop_front();
355        }
356
357        // Recalculate metrics
358        self.update_metrics();
359    }
360
361    /// Update performance metrics
362    pub fn update_metrics(&mut self) {
363        let valid_predictions: Vec<&PredictionResult> = self
364            .predictions
365            .iter()
366            .filter(|p| p.actual_cost.is_some())
367            .collect();
368
369        if valid_predictions.is_empty() {
370            return;
371        }
372
373        let n = valid_predictions.len() as f64;
374
375        // Calculate MAE
376        let total_error: f64 = valid_predictions.iter().filter_map(|p| p.error).sum();
377        self.mae = total_error / n;
378
379        // Calculate RMSE
380        let squared_errors: f64 = valid_predictions
381            .iter()
382            .filter_map(|p| p.error.map(|e| e * e))
383            .sum();
384        self.rmse = (squared_errors / n).sqrt();
385
386        // Calculate R²
387        let mean_actual: f64 = valid_predictions
388            .iter()
389            .filter_map(|p| p.actual_cost)
390            .sum::<f64>()
391            / n;
392
393        let ss_tot: f64 = valid_predictions
394            .iter()
395            .filter_map(|p| p.actual_cost.map(|a| (a - mean_actual).powi(2)))
396            .sum();
397
398        let ss_res: f64 = valid_predictions
399            .iter()
400            .filter_map(|p| {
401                if let (Some(_actual), Some(error)) = (p.actual_cost, p.error) {
402                    Some(error.powi(2))
403                } else {
404                    None
405                }
406            })
407            .sum();
408
409        self.r_squared = if ss_tot > 1e-10 {
410            1.0 - (ss_res / ss_tot)
411        } else {
412            0.0
413        };
414
415        self.last_update = SystemTime::now();
416    }
417
418    /// Calculate model quality
419    pub fn calculate_quality(&self) -> ModelQuality {
420        let is_acceptable = self.r_squared >= 0.8 && self.mae <= 0.2;
421
422        let recommendation = if self.r_squared < 0.5 {
423            QualityRecommendation::UseFallback
424        } else if self.r_squared < 0.8 {
425            QualityRecommendation::NeedsRetraining
426        } else {
427            QualityRecommendation::UseMl
428        };
429
430        ModelQuality {
431            r_squared: self.r_squared,
432            mae: self.mae,
433            rmse: self.rmse,
434            is_acceptable,
435            recommendation,
436        }
437    }
438
439    /// Get number of tracked predictions
440    pub fn prediction_count(&self) -> usize {
441        self.predictions.len()
442    }
443}
444
445/// Prediction result for tracking
446#[derive(Debug, Clone)]
447pub struct PredictionResult {
448    pub predicted_cost: f64,
449    pub actual_cost: Option<f64>,
450    pub timestamp: SystemTime,
451    pub error: Option<f64>,
452}
453
454/// Model quality assessment
455#[derive(Debug, Clone)]
456pub struct ModelQuality {
457    pub r_squared: f64,
458    pub mae: f64,
459    pub rmse: f64,
460    pub is_acceptable: bool,
461    pub recommendation: QualityRecommendation,
462}
463
464/// Quality-based recommendation
465#[derive(Debug, Clone, PartialEq)]
466pub enum QualityRecommendation {
467    /// Use ML predictor (high quality)
468    UseMl,
469    /// Fall back to heuristic (poor quality)
470    UseFallback,
471    /// Model needs retraining (degraded quality)
472    NeedsRetraining,
473}
474
475/// Performance metrics for monitoring
476#[derive(Debug, Clone)]
477pub struct PerformanceMetrics {
478    pub predictions_made: usize,
479    pub training_examples: usize,
480    pub mae: f64,
481    pub rmse: f64,
482    pub r_squared: f64,
483    pub is_using_ml: bool,
484}
485
486#[cfg(test)]
487mod tests {
488    use super::*;
489    use crate::advanced_optimizer::ml_predictor::MLModelType;
490
491    #[test]
492    fn test_model_manager_creation() -> Result<()> {
493        let predictor = MLPredictor::from_model_type(MLModelType::LinearRegression)?;
494        let config = ManagerConfig::default();
495        let manager = ModelManager::new(predictor, config);
496
497        assert!(!manager.should_retrain()); // No training data yet
498
499        Ok(())
500    }
501
502    #[test]
503    fn test_performance_tracker() {
504        let mut tracker = PerformanceTracker::new(10);
505
506        // Add some predictions
507        for i in 1..=5 {
508            let predicted = i as f64 * 10.0;
509            let actual = i as f64 * 10.0 + 5.0; // Error of 5.0
510
511            tracker.record(PredictionResult {
512                predicted_cost: predicted,
513                actual_cost: Some(actual),
514                timestamp: SystemTime::now(),
515                error: Some(5.0),
516            });
517        }
518
519        assert_eq!(tracker.prediction_count(), 5);
520        assert!((tracker.mae - 5.0).abs() < 1e-6); // MAE should be 5.0
521    }
522
523    #[test]
524    fn test_model_quality_assessment() {
525        let mut tracker = PerformanceTracker::new(10);
526
527        // Add perfect predictions
528        for i in 1..=10 {
529            let cost = i as f64 * 10.0;
530            tracker.record(PredictionResult {
531                predicted_cost: cost,
532                actual_cost: Some(cost),
533                timestamp: SystemTime::now(),
534                error: Some(0.0),
535            });
536        }
537
538        let quality = tracker.calculate_quality();
539        assert_eq!(quality.mae, 0.0);
540        assert_eq!(quality.rmse, 0.0);
541        assert!(quality.is_acceptable);
542        assert_eq!(quality.recommendation, QualityRecommendation::UseMl);
543    }
544
545    #[test]
546    fn test_quality_recommendation_poor() {
547        let mut tracker = PerformanceTracker::new(10);
548
549        // Add predictions with large errors
550        for i in 1..=5 {
551            let predicted = i as f64 * 10.0;
552            let actual = i as f64 * 50.0; // Large difference
553
554            tracker.record(PredictionResult {
555                predicted_cost: predicted,
556                actual_cost: Some(actual),
557                timestamp: SystemTime::now(),
558                error: Some((predicted - actual).abs()),
559            });
560        }
561
562        let quality = tracker.calculate_quality();
563        assert!(!quality.is_acceptable);
564        // R² should be very low or negative
565        assert!(quality.r_squared < 0.8);
566    }
567
568    #[test]
569    fn test_buffer_limit() {
570        let max_buffer = 5;
571        let mut tracker = PerformanceTracker::new(max_buffer);
572
573        // Add more predictions than buffer size
574        for i in 1..=10 {
575            tracker.record(PredictionResult {
576                predicted_cost: i as f64,
577                actual_cost: Some(i as f64),
578                timestamp: SystemTime::now(),
579                error: Some(0.0),
580            });
581        }
582
583        // Should only keep last 5
584        assert_eq!(tracker.prediction_count(), max_buffer);
585    }
586
587    #[test]
588    fn test_record_prediction() -> Result<()> {
589        let predictor = MLPredictor::from_model_type(MLModelType::LinearRegression)?;
590        let config = ManagerConfig::default();
591        let manager = ModelManager::new(predictor, config);
592
593        manager.record_prediction(100.0, 105.0)?;
594        manager.record_prediction(200.0, 195.0)?;
595
596        let metrics = manager.get_performance_metrics()?;
597        assert_eq!(metrics.predictions_made, 0); // Predictor hasn't been used yet
598
599        Ok(())
600    }
601}