Skip to main content

oxirs_arq/executor/
adaptive_executor.rs

1//! Adaptive Query Executor with Re-optimization
2//!
3//! Implements adaptive query execution that monitors runtime statistics and
4//! dynamically re-optimizes query plans based on actual execution characteristics.
5//! Supports time-based and deviation-based triggers with checkpointing for plan switching.
6
7use crate::algebra::Algebra;
8use crate::cardinality_estimator::CardinalityEstimator;
9use crate::cost_model::CostModel;
10use anyhow::{anyhow, Result};
11use scirs2_core::metrics::{Counter, Timer};
12use scirs2_core::profiling::Profiler;
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use std::sync::{Arc, RwLock};
16use std::time::{Duration, Instant};
17use tracing::{debug, info};
18
19/// Adaptive query executor that re-optimizes plans based on runtime feedback
20pub struct AdaptiveExecutor {
21    /// Advanced optimizer for re-optimization
22    optimizer: Arc<RwLock<AdaptiveOptimizer>>,
23    /// Configuration
24    config: AdaptiveConfig,
25    /// Performance profiler
26    profiler: Profiler,
27    /// Metrics counters
28    metrics: AdaptiveMetrics,
29}
30
31/// Configuration for adaptive execution
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct AdaptiveConfig {
34    /// Enable adaptive re-optimization
35    pub enable_adaptive: bool,
36    /// Re-optimization trigger as percentage of query execution (0.0-1.0)
37    pub re_opt_trigger_percent: f64,
38    /// Re-optimization trigger in seconds
39    pub re_opt_trigger_seconds: u64,
40    /// Minimum interval between re-optimizations in seconds
41    pub min_reopt_interval_seconds: u64,
42    /// Plan switch threshold (new plan must be N times better)
43    pub plan_switch_threshold: f64,
44    /// Deviation threshold (actual/estimated ratio to trigger re-opt)
45    pub deviation_threshold: f64,
46    /// Maximum number of re-optimizations per query
47    pub max_reoptimizations: usize,
48}
49
50impl Default for AdaptiveConfig {
51    fn default() -> Self {
52        Self {
53            enable_adaptive: true,
54            re_opt_trigger_percent: 0.1, // 10% of execution
55            re_opt_trigger_seconds: 5,
56            min_reopt_interval_seconds: 5,
57            plan_switch_threshold: 2.0, // Must be 2x better
58            deviation_threshold: 5.0,   // 5x deviation triggers re-opt
59            max_reoptimizations: 3,
60        }
61    }
62}
63
64/// Runtime execution statistics
65#[derive(Debug, Clone)]
66pub struct RuntimeStatistics {
67    /// Operator statistics by operator ID
68    pub operator_stats: HashMap<OperatorId, OperatorStats>,
69    /// Total execution time
70    pub execution_time: Duration,
71    /// Total rows processed
72    pub rows_processed: u64,
73    /// Query start time
74    pub start_time: Instant,
75}
76
77impl Default for RuntimeStatistics {
78    fn default() -> Self {
79        Self {
80            operator_stats: HashMap::new(),
81            execution_time: Duration::ZERO,
82            rows_processed: 0,
83            start_time: Instant::now(),
84        }
85    }
86}
87
88impl RuntimeStatistics {
89    /// Update from batch execution
90    pub fn update_from_batch(&mut self, batch: &BatchResult) -> Result<()> {
91        self.rows_processed += batch.rows_produced;
92        self.execution_time = self.start_time.elapsed();
93
94        for (op_id, op_result) in &batch.operator_results {
95            let stats = self
96                .operator_stats
97                .entry(op_id.clone())
98                .or_insert_with(|| OperatorStats::new(op_id.clone()));
99
100            stats.actual_cardinality += op_result.rows_produced;
101            stats.actual_time_ms += op_result.execution_time_ms;
102            stats.update_deviation();
103        }
104
105        Ok(())
106    }
107
108    /// Get maximum deviation across all operators
109    pub fn max_deviation(&self) -> f64 {
110        self.operator_stats
111            .values()
112            .map(|s| s.deviation)
113            .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
114            .unwrap_or(1.0)
115    }
116}
117
118/// Statistics for a single operator
119#[derive(Debug, Clone)]
120pub struct OperatorStats {
121    /// Operator identifier
122    pub operator_id: OperatorId,
123    /// Actual cardinality observed
124    pub actual_cardinality: u64,
125    /// Estimated cardinality (from initial plan)
126    pub estimated_cardinality: u64,
127    /// Actual execution time in milliseconds
128    pub actual_time_ms: f64,
129    /// Estimated execution time in milliseconds
130    pub estimated_time_ms: f64,
131    /// Deviation ratio (actual / estimated)
132    pub deviation: f64,
133}
134
135impl OperatorStats {
136    pub fn new(operator_id: OperatorId) -> Self {
137        Self {
138            operator_id,
139            actual_cardinality: 0,
140            estimated_cardinality: 1,
141            actual_time_ms: 0.0,
142            estimated_time_ms: 1.0,
143            deviation: 1.0,
144        }
145    }
146
147    pub fn update_deviation(&mut self) {
148        if self.estimated_cardinality > 0 {
149            self.deviation = self.actual_cardinality as f64 / self.estimated_cardinality as f64;
150        }
151    }
152
153    pub fn set_estimates(&mut self, cardinality: u64, time_ms: f64) {
154        self.estimated_cardinality = cardinality;
155        self.estimated_time_ms = time_ms;
156    }
157}
158
159/// Operator identifier
160pub type OperatorId = String;
161
162/// Batch execution result
163#[derive(Debug, Clone)]
164pub struct BatchResult {
165    /// Rows produced in this batch
166    pub rows_produced: u64,
167    /// Per-operator results
168    pub operator_results: HashMap<OperatorId, OperatorResult>,
169    /// Is query execution complete?
170    pub is_complete: bool,
171}
172
173/// Result from a single operator
174#[derive(Debug, Clone)]
175pub struct OperatorResult {
176    /// Rows produced
177    pub rows_produced: u64,
178    /// Execution time in milliseconds
179    pub execution_time_ms: f64,
180}
181
182/// Query plan representation
183#[derive(Debug, Clone)]
184pub struct QueryPlan {
185    /// Algebraic representation
186    pub algebra: Algebra,
187    /// Estimated cost
188    pub estimated_cost: f64,
189    /// Estimated total rows
190    pub estimated_total_rows: u64,
191    /// Operator cardinality estimates
192    pub operator_estimates: HashMap<OperatorId, u64>,
193}
194
195/// Adaptive optimizer for query re-optimization
196#[allow(dead_code)]
197pub struct AdaptiveOptimizer {
198    /// Cardinality estimator
199    cardinality_estimator: Arc<RwLock<CardinalityEstimator>>,
200    /// Cost model
201    cost_model: Arc<RwLock<CostModel>>,
202}
203
204impl AdaptiveOptimizer {
205    pub fn new(
206        cardinality_estimator: Arc<RwLock<CardinalityEstimator>>,
207        cost_model: Arc<RwLock<CostModel>>,
208    ) -> Self {
209        Self {
210            cardinality_estimator,
211            cost_model,
212        }
213    }
214
215    /// Update cardinality estimate for an operator
216    pub fn update_cardinality_estimate(&mut self, _op_id: OperatorId, actual: u64) -> Result<()> {
217        // Update the cardinality estimator with actual results
218        // This would feed into learning models in a full implementation
219        debug!("Updated cardinality estimate: actual={}", actual);
220        Ok(())
221    }
222
223    /// Update cost estimate for an operator
224    pub fn update_cost_estimate(&mut self, _op_id: OperatorId, actual_time_ms: f64) -> Result<()> {
225        // Update the cost model with actual timing
226        debug!("Updated cost estimate: actual_time_ms={}", actual_time_ms);
227        Ok(())
228    }
229
230    /// Optimize a query plan
231    pub fn optimize(&self, _algebra: &Algebra) -> Result<QueryPlan> {
232        // In a full implementation, this would use the optimizer
233        // For now, create a simplified plan
234        Ok(QueryPlan {
235            algebra: Algebra::Bgp(vec![]),
236            estimated_cost: 100.0,
237            estimated_total_rows: 1000,
238            operator_estimates: HashMap::new(),
239        })
240    }
241}
242
243/// Metrics for adaptive execution
244pub struct AdaptiveMetrics {
245    /// Number of re-optimizations triggered
246    pub reoptimizations: Counter,
247    /// Number of successful plan switches
248    pub plan_switches: Counter,
249    /// Time spent in re-optimization
250    pub reopt_time: Timer,
251    /// Queries improved by adaptation
252    pub queries_improved: Counter,
253}
254
255impl Default for AdaptiveMetrics {
256    fn default() -> Self {
257        Self {
258            reoptimizations: Counter::new("adaptive.reoptimizations".to_string()),
259            plan_switches: Counter::new("adaptive.plan_switches".to_string()),
260            reopt_time: Timer::new("adaptive.reopt_time".to_string()),
261            queries_improved: Counter::new("adaptive.queries_improved".to_string()),
262        }
263    }
264}
265
266impl AdaptiveExecutor {
267    /// Create a new adaptive executor
268    pub fn new(
269        cardinality_estimator: Arc<RwLock<CardinalityEstimator>>,
270        cost_model: Arc<RwLock<CostModel>>,
271        config: AdaptiveConfig,
272    ) -> Self {
273        let optimizer = Arc::new(RwLock::new(AdaptiveOptimizer::new(
274            cardinality_estimator,
275            cost_model,
276        )));
277
278        Self {
279            optimizer,
280            config,
281            profiler: Profiler::new(),
282            metrics: AdaptiveMetrics::default(),
283        }
284    }
285
286    /// Execute query with adaptive re-optimization
287    pub async fn execute_adaptive(
288        &mut self,
289        query: &Algebra,
290        initial_plan: QueryPlan,
291    ) -> Result<QueryResults> {
292        let mut current_plan = initial_plan;
293        let mut stats = RuntimeStatistics {
294            start_time: Instant::now(),
295            ..Default::default()
296        };
297        let mut last_reopt = Instant::now();
298
299        let start_time = Instant::now();
300
301        // Execute with checkpointing
302        let mut executor = CheckpointedExecutor::new(current_plan.clone())?;
303
304        loop {
305            // Execute batch
306            let batch_result = executor.execute_batch(1000).await?;
307
308            // Collect statistics
309            stats.update_from_batch(&batch_result)?;
310
311            // Check if should re-optimize
312            let elapsed = start_time.elapsed();
313            let should_reopt = self.should_reoptimize(&stats, elapsed, last_reopt.elapsed())?;
314
315            if should_reopt {
316                info!(
317                    "Triggering adaptive re-optimization at {}s",
318                    elapsed.as_secs_f64()
319                );
320
321                self.metrics.reoptimizations.inc();
322                self.profiler.start();
323
324                // Refine cost model with actual statistics
325                let refined_plan = self.reoptimize_with_statistics(query, &stats)?;
326
327                // Check if new plan is significantly better
328                if self.is_plan_significantly_better(&current_plan, &refined_plan, &stats)? {
329                    let improvement =
330                        self.estimate_improvement(&current_plan, &refined_plan, &stats)?;
331                    info!(
332                        "Switching to new plan (estimated {}x improvement)",
333                        improvement
334                    );
335
336                    // Checkpoint current state
337                    let checkpoint = executor.checkpoint()?;
338
339                    // Switch to new plan
340                    current_plan = refined_plan;
341                    executor = CheckpointedExecutor::new_from_checkpoint(
342                        current_plan.clone(),
343                        checkpoint,
344                    )?;
345
346                    self.metrics.plan_switches.inc();
347                    last_reopt = Instant::now();
348                } else {
349                    info!("New plan not significantly better, continuing with current plan");
350                }
351            }
352
353            // Check if done
354            if batch_result.is_complete {
355                break;
356            }
357        }
358
359        executor.finalize()
360    }
361
362    /// Determine if should trigger re-optimization
363    fn should_reoptimize(
364        &self,
365        stats: &RuntimeStatistics,
366        elapsed: Duration,
367        since_last_reopt: Duration,
368    ) -> Result<bool> {
369        if !self.config.enable_adaptive {
370            return Ok(false);
371        }
372
373        // Don't re-optimize too frequently (hysteresis)
374        if since_last_reopt.as_secs() < self.config.min_reopt_interval_seconds {
375            return Ok(false);
376        }
377
378        // Trigger after time threshold
379        if elapsed.as_secs() >= self.config.re_opt_trigger_seconds {
380            debug!("Re-optimization triggered by time threshold");
381            return Ok(true);
382        }
383
384        // Trigger if significant deviation detected
385        let max_deviation = stats.max_deviation();
386
387        if max_deviation > self.config.deviation_threshold {
388            info!("Large deviation detected: {}x", max_deviation);
389            return Ok(true);
390        }
391
392        Ok(false)
393    }
394
395    /// Re-optimize query with runtime statistics
396    fn reoptimize_with_statistics(
397        &self,
398        query: &Algebra,
399        stats: &RuntimeStatistics,
400    ) -> Result<QueryPlan> {
401        // Update cost model with actual cardinalities
402        let mut optimizer = self
403            .optimizer
404            .write()
405            .map_err(|e| anyhow!("Failed to acquire optimizer lock: {}", e))?;
406
407        for (op_id, op_stats) in &stats.operator_stats {
408            optimizer.update_cardinality_estimate(op_id.clone(), op_stats.actual_cardinality)?;
409            optimizer.update_cost_estimate(op_id.clone(), op_stats.actual_time_ms)?;
410        }
411
412        // Re-optimize query
413        let new_plan = optimizer.optimize(query)?;
414        Ok(new_plan)
415    }
416
417    /// Check if new plan is significantly better
418    fn is_plan_significantly_better(
419        &self,
420        current_plan: &QueryPlan,
421        new_plan: &QueryPlan,
422        stats: &RuntimeStatistics,
423    ) -> Result<bool> {
424        // Estimate remaining cost for both plans
425        let current_remaining_cost = self.estimate_remaining_cost(current_plan, stats)?;
426        let new_remaining_cost = self.estimate_remaining_cost(new_plan, stats)?;
427
428        let improvement = current_remaining_cost / new_remaining_cost;
429        Ok(improvement > self.config.plan_switch_threshold)
430    }
431
432    fn estimate_remaining_cost(&self, plan: &QueryPlan, stats: &RuntimeStatistics) -> Result<f64> {
433        // Estimate cost for remaining rows
434        let processed = stats.rows_processed;
435        let total_estimated = plan.estimated_total_rows.max(1);
436        let remaining_percent = if processed < total_estimated {
437            (total_estimated - processed) as f64 / total_estimated as f64
438        } else {
439            0.1 // Still some work remaining
440        };
441
442        Ok(plan.estimated_cost * remaining_percent)
443    }
444
445    fn estimate_improvement(
446        &self,
447        current: &QueryPlan,
448        new: &QueryPlan,
449        stats: &RuntimeStatistics,
450    ) -> Result<f64> {
451        let current_cost = self.estimate_remaining_cost(current, stats)?;
452        let new_cost = self.estimate_remaining_cost(new, stats)?.max(0.1);
453        Ok(current_cost / new_cost)
454    }
455
456    /// Get configuration
457    pub fn get_config(&self) -> &AdaptiveConfig {
458        &self.config
459    }
460
461    /// Get profiler for inspection
462    pub fn get_profiler(&self) -> &Profiler {
463        &self.profiler
464    }
465
466    /// Get metrics
467    pub fn get_metrics(&self) -> &AdaptiveMetrics {
468        &self.metrics
469    }
470}
471
472/// Executor with checkpointing support
473#[allow(dead_code)]
474pub struct CheckpointedExecutor {
475    plan: QueryPlan,
476    state: ExecutorState,
477    rows_produced: u64,
478}
479
480/// Executor state for checkpointing
481#[derive(Debug, Clone, Default)]
482pub struct ExecutorState {
483    /// Operator states by operator ID
484    #[allow(clippy::derivable_impls)]
485    pub operator_states: HashMap<OperatorId, OperatorState>,
486    /// Rows processed so far
487    pub rows_processed: u64,
488    /// Intermediate results
489    pub intermediate_results: Vec<u8>, // Serialized results
490}
491
492/// State for a single operator
493#[derive(Debug, Clone)]
494pub struct OperatorState {
495    /// Operator ID
496    pub operator_id: OperatorId,
497    /// Serialized state (hash tables, sort buffers, etc.)
498    pub data: Vec<u8>,
499    /// Rows processed by this operator
500    pub rows_processed: u64,
501}
502
503impl CheckpointedExecutor {
504    /// Create new executor with a plan
505    pub fn new(plan: QueryPlan) -> Result<Self> {
506        Ok(Self {
507            plan,
508            state: ExecutorState::default(),
509            rows_produced: 0,
510        })
511    }
512
513    /// Create executor from checkpoint
514    pub fn new_from_checkpoint(plan: QueryPlan, checkpoint: ExecutorState) -> Result<Self> {
515        Ok(Self {
516            plan,
517            state: checkpoint,
518            rows_produced: 0,
519        })
520    }
521
522    /// Execute a batch of rows
523    pub async fn execute_batch(&mut self, batch_size: u64) -> Result<BatchResult> {
524        // Simulate batch execution
525        // In a real implementation, this would execute the query plan
526
527        let rows_produced = batch_size.min(100); // Simulate producing rows
528        self.rows_produced += rows_produced;
529        self.state.rows_processed += rows_produced;
530
531        let mut operator_results = HashMap::new();
532        operator_results.insert(
533            "scan_op".to_string(),
534            OperatorResult {
535                rows_produced,
536                execution_time_ms: 10.0,
537            },
538        );
539
540        // Check if complete (simulate)
541        let is_complete = self.rows_produced >= 1000;
542
543        Ok(BatchResult {
544            rows_produced,
545            operator_results,
546            is_complete,
547        })
548    }
549
550    /// Checkpoint current execution state
551    pub fn checkpoint(&self) -> Result<ExecutorState> {
552        Ok(self.state.clone())
553    }
554
555    /// Finalize execution and return results
556    pub fn finalize(self) -> Result<QueryResults> {
557        Ok(QueryResults {
558            rows: self.rows_produced,
559            execution_time: Duration::from_millis(100),
560        })
561    }
562}
563
564/// Query execution results
565#[derive(Debug, Clone)]
566pub struct QueryResults {
567    /// Number of rows returned
568    pub rows: u64,
569    /// Total execution time
570    pub execution_time: Duration,
571}
572
573#[cfg(test)]
574mod tests {
575    use super::*;
576    use crate::cardinality_estimator::EstimatorConfig;
577    use crate::cost_model::CostModelConfig;
578
579    #[tokio::test]
580    async fn test_adaptive_executor_basic() -> Result<()> {
581        let estimator = Arc::new(RwLock::new(CardinalityEstimator::new(
582            EstimatorConfig::default(),
583        )));
584        let cost_model = Arc::new(RwLock::new(CostModel::new(CostModelConfig::default())));
585        let config = AdaptiveConfig::default();
586
587        let mut executor = AdaptiveExecutor::new(estimator, cost_model, config);
588
589        let query = Algebra::Bgp(vec![]);
590        let plan = QueryPlan {
591            algebra: query.clone(),
592            estimated_cost: 1000.0,
593            estimated_total_rows: 10000,
594            operator_estimates: HashMap::new(),
595        };
596
597        let results = executor.execute_adaptive(&query, plan).await?;
598        assert!(results.rows > 0);
599
600        Ok(())
601    }
602
603    #[tokio::test]
604    async fn test_checkpointing() -> Result<()> {
605        let plan = QueryPlan {
606            algebra: Algebra::Bgp(vec![]),
607            estimated_cost: 100.0,
608            estimated_total_rows: 1000,
609            operator_estimates: HashMap::new(),
610        };
611
612        let mut executor = CheckpointedExecutor::new(plan.clone())?;
613
614        // Execute some batches
615        let _batch1 = executor.execute_batch(100).await?;
616        let _batch2 = executor.execute_batch(100).await?;
617
618        // Checkpoint
619        let checkpoint = executor.checkpoint()?;
620        assert_eq!(checkpoint.rows_processed, 200);
621
622        // Create new executor from checkpoint
623        let mut executor2 = CheckpointedExecutor::new_from_checkpoint(plan, checkpoint)?;
624        let _batch3 = executor2.execute_batch(100).await?;
625
626        Ok(())
627    }
628
629    #[test]
630    fn test_runtime_statistics() {
631        let mut stats = RuntimeStatistics {
632            start_time: Instant::now(),
633            ..Default::default()
634        };
635
636        let batch = BatchResult {
637            rows_produced: 100,
638            operator_results: {
639                let mut map = HashMap::new();
640                map.insert(
641                    "op1".to_string(),
642                    OperatorResult {
643                        rows_produced: 100,
644                        execution_time_ms: 50.0,
645                    },
646                );
647                map
648            },
649            is_complete: false,
650        };
651
652        stats.update_from_batch(&batch).ok();
653        assert_eq!(stats.rows_processed, 100);
654    }
655
656    #[test]
657    fn test_deviation_calculation() {
658        let mut op_stats = OperatorStats::new("test_op".to_string());
659        op_stats.set_estimates(100, 10.0);
660        op_stats.actual_cardinality = 500;
661        op_stats.update_deviation();
662
663        assert!((op_stats.deviation - 5.0).abs() < 0.01);
664    }
665
666    #[test]
667    fn test_config_defaults() {
668        let config = AdaptiveConfig::default();
669        assert!(config.enable_adaptive);
670        assert_eq!(config.re_opt_trigger_seconds, 5);
671        assert_eq!(config.min_reopt_interval_seconds, 5);
672        assert_eq!(config.plan_switch_threshold, 2.0);
673        assert_eq!(config.deviation_threshold, 5.0);
674    }
675}