Skip to main content

oxirs_tdb/
query_monitor.rs

1//! Query monitoring for timeout enforcement and slow query logging
2//!
3//! This module provides:
4//! - Query timeout enforcement to prevent runaway queries
5//! - Slow query logging for performance analysis
6//! - Query execution tracking and statistics
7
8use crate::dictionary::Term;
9use crate::error::{Result, TdbError};
10use parking_lot::RwLock;
11use std::collections::VecDeque;
12use std::sync::atomic::{AtomicU64, Ordering};
13use std::sync::Arc;
14use std::time::{Duration, Instant};
15
16/// Configuration for query monitoring
17#[derive(Debug, Clone)]
18pub struct QueryMonitorConfig {
19    /// Global query timeout (0 = no timeout)
20    pub default_timeout: Duration,
21    /// Threshold for logging slow queries
22    pub slow_query_threshold: Duration,
23    /// Maximum number of slow queries to keep in memory
24    pub max_slow_query_history: usize,
25    /// Whether to enable query monitoring
26    pub enabled: bool,
27}
28
29impl Default for QueryMonitorConfig {
30    fn default() -> Self {
31        Self {
32            default_timeout: Duration::from_secs(30),
33            slow_query_threshold: Duration::from_millis(100),
34            max_slow_query_history: 100,
35            enabled: true,
36        }
37    }
38}
39
40/// Query execution context
41pub struct QueryExecution {
42    /// Query ID
43    id: u64,
44    /// Query pattern
45    pattern: QueryPattern,
46    /// Start time
47    start_time: Instant,
48    /// Timeout deadline (if any)
49    deadline: Option<Instant>,
50    /// Whether query is still running
51    running: Arc<AtomicU64>, // 0 = stopped, 1 = running
52}
53
54impl QueryExecution {
55    /// Create a new query execution context
56    fn new(id: u64, pattern: QueryPattern, timeout: Option<Duration>) -> Self {
57        let start_time = Instant::now();
58        let deadline = timeout.map(|t| start_time + t);
59
60        Self {
61            id,
62            pattern,
63            start_time,
64            deadline,
65            running: Arc::new(AtomicU64::new(1)),
66        }
67    }
68
69    /// Check if query has timed out
70    pub fn is_timed_out(&self) -> bool {
71        if let Some(deadline) = self.deadline {
72            Instant::now() >= deadline
73        } else {
74            false
75        }
76    }
77
78    /// Get elapsed time
79    pub fn elapsed(&self) -> Duration {
80        self.start_time.elapsed()
81    }
82
83    /// Mark query as complete
84    fn complete(&self) {
85        self.running.store(0, Ordering::Relaxed);
86    }
87
88    /// Check if still running
89    pub fn is_running(&self) -> bool {
90        self.running.load(Ordering::Relaxed) == 1
91    }
92}
93
94/// Query pattern for logging
95#[derive(Debug, Clone)]
96pub struct QueryPattern {
97    /// Subject pattern
98    pub subject: Option<String>,
99    /// Predicate pattern
100    pub predicate: Option<String>,
101    /// Object pattern
102    pub object: Option<String>,
103}
104
105impl QueryPattern {
106    /// Create from Term options
107    pub fn from_terms(
108        subject: Option<&Term>,
109        predicate: Option<&Term>,
110        object: Option<&Term>,
111    ) -> Self {
112        Self {
113            subject: subject.map(|t| format!("{:?}", t)),
114            predicate: predicate.map(|t| format!("{:?}", t)),
115            object: object.map(|t| format!("{:?}", t)),
116        }
117    }
118
119    /// Get a human-readable description
120    pub fn describe(&self) -> String {
121        format!(
122            "({}, {}, {})",
123            self.subject.as_deref().unwrap_or("?"),
124            self.predicate.as_deref().unwrap_or("?"),
125            self.object.as_deref().unwrap_or("?")
126        )
127    }
128}
129
130/// Slow query record
131#[derive(Debug, Clone)]
132pub struct SlowQueryRecord {
133    /// When the query was executed
134    pub timestamp: Instant,
135    /// Query pattern
136    pub pattern: QueryPattern,
137    /// Execution time
138    pub execution_time: Duration,
139    /// Number of results returned
140    pub result_count: usize,
141    /// Whether the query timed out
142    pub timed_out: bool,
143}
144
145/// Query monitor for timeout enforcement and slow query logging
146pub struct QueryMonitor {
147    /// Configuration
148    config: QueryMonitorConfig,
149    /// Next query ID
150    next_id: AtomicU64,
151    /// Active queries
152    active_queries: Arc<RwLock<Vec<Arc<QueryExecution>>>>,
153    /// Slow query history
154    slow_queries: Arc<RwLock<VecDeque<SlowQueryRecord>>>,
155    /// Statistics
156    stats: QueryMonitorStats,
157}
158
159/// Query monitor statistics
160#[derive(Debug, Default)]
161pub struct QueryMonitorStats {
162    /// Total queries executed
163    pub total_queries: AtomicU64,
164    /// Total queries that timed out
165    pub timed_out_queries: AtomicU64,
166    /// Total slow queries
167    pub slow_queries: AtomicU64,
168    /// Total execution time (microseconds)
169    pub total_execution_time_us: AtomicU64,
170}
171
172impl QueryMonitorStats {
173    /// Get average query execution time (microseconds)
174    pub fn avg_execution_time_us(&self) -> u64 {
175        let total = self.total_queries.load(Ordering::Relaxed);
176        self.total_execution_time_us
177            .load(Ordering::Relaxed)
178            .checked_div(total)
179            .unwrap_or(0)
180    }
181
182    /// Get timeout rate (percentage)
183    pub fn timeout_rate(&self) -> f64 {
184        let total = self.total_queries.load(Ordering::Relaxed) as f64;
185        if total == 0.0 {
186            0.0
187        } else {
188            let timeouts = self.timed_out_queries.load(Ordering::Relaxed) as f64;
189            (timeouts / total) * 100.0
190        }
191    }
192
193    /// Get slow query rate (percentage)
194    pub fn slow_query_rate(&self) -> f64 {
195        let total = self.total_queries.load(Ordering::Relaxed) as f64;
196        if total == 0.0 {
197            0.0
198        } else {
199            let slow = self.slow_queries.load(Ordering::Relaxed) as f64;
200            (slow / total) * 100.0
201        }
202    }
203}
204
205impl QueryMonitor {
206    /// Create a new query monitor
207    pub fn new(config: QueryMonitorConfig) -> Self {
208        Self {
209            config,
210            next_id: AtomicU64::new(1),
211            active_queries: Arc::new(RwLock::new(Vec::new())),
212            slow_queries: Arc::new(RwLock::new(VecDeque::new())),
213            stats: QueryMonitorStats::default(),
214        }
215    }
216
217    /// Begin a monitored query execution
218    pub fn begin_query(
219        &self,
220        subject: Option<&Term>,
221        predicate: Option<&Term>,
222        object: Option<&Term>,
223        timeout_override: Option<Duration>,
224    ) -> Arc<QueryExecution> {
225        if !self.config.enabled {
226            // Create a dummy execution with no monitoring
227            let pattern = QueryPattern::from_terms(subject, predicate, object);
228            return Arc::new(QueryExecution::new(0, pattern, None));
229        }
230
231        let id = self.next_id.fetch_add(1, Ordering::Relaxed);
232        let pattern = QueryPattern::from_terms(subject, predicate, object);
233
234        // Determine timeout
235        let timeout = timeout_override.or({
236            if self.config.default_timeout.as_secs() > 0 {
237                Some(self.config.default_timeout)
238            } else {
239                None
240            }
241        });
242
243        let execution = Arc::new(QueryExecution::new(id, pattern, timeout));
244
245        // Track active query
246        self.active_queries.write().push(Arc::clone(&execution));
247
248        execution
249    }
250
251    /// End a monitored query execution
252    pub fn end_query(&self, execution: Arc<QueryExecution>, result_count: usize) -> Result<()> {
253        if !self.config.enabled {
254            return Ok(());
255        }
256
257        execution.complete();
258
259        // Check for timeout
260        let timed_out = execution.is_timed_out();
261        if timed_out {
262            self.stats.timed_out_queries.fetch_add(1, Ordering::Relaxed);
263            return Err(TdbError::Other(format!(
264                "Query timed out after {:?}",
265                execution.elapsed()
266            )));
267        }
268
269        let execution_time = execution.elapsed();
270
271        // Update statistics
272        self.stats.total_queries.fetch_add(1, Ordering::Relaxed);
273        self.stats
274            .total_execution_time_us
275            .fetch_add(execution_time.as_micros() as u64, Ordering::Relaxed);
276
277        // Check if slow query
278        if execution_time >= self.config.slow_query_threshold {
279            self.log_slow_query(
280                execution.pattern.clone(),
281                execution_time,
282                result_count,
283                timed_out,
284            );
285        }
286
287        // Remove from active queries
288        self.active_queries.write().retain(|e| e.id != execution.id);
289
290        Ok(())
291    }
292
293    /// Check if a query should be cancelled due to timeout
294    pub fn check_timeout(&self, execution: &Arc<QueryExecution>) -> Result<()> {
295        if !self.config.enabled {
296            return Ok(());
297        }
298
299        if execution.is_timed_out() {
300            Err(TdbError::Other(format!(
301                "Query timeout: exceeded {:?}",
302                execution.elapsed()
303            )))
304        } else {
305            Ok(())
306        }
307    }
308
309    /// Log a slow query
310    fn log_slow_query(
311        &self,
312        pattern: QueryPattern,
313        execution_time: Duration,
314        result_count: usize,
315        timed_out: bool,
316    ) {
317        self.stats.slow_queries.fetch_add(1, Ordering::Relaxed);
318
319        let record = SlowQueryRecord {
320            timestamp: Instant::now(),
321            pattern: pattern.clone(),
322            execution_time,
323            result_count,
324            timed_out,
325        };
326
327        // Add to slow query history
328        let mut slow_queries = self.slow_queries.write();
329        slow_queries.push_back(record);
330
331        // Trim if exceeds max size
332        while slow_queries.len() > self.config.max_slow_query_history {
333            slow_queries.pop_front();
334        }
335
336        // Also log to system logger
337        log::warn!(
338            "Slow query detected: pattern={}, time={:?}, results={}{}",
339            pattern.describe(),
340            execution_time,
341            result_count,
342            if timed_out { " (TIMED OUT)" } else { "" }
343        );
344    }
345
346    /// Get slow query history
347    pub fn slow_query_history(&self) -> Vec<SlowQueryRecord> {
348        self.slow_queries.read().iter().cloned().collect()
349    }
350
351    /// Get currently active queries
352    pub fn active_queries(&self) -> Vec<Arc<QueryExecution>> {
353        self.active_queries.read().clone()
354    }
355
356    /// Get statistics
357    pub fn stats(&self) -> &QueryMonitorStats {
358        &self.stats
359    }
360
361    /// Clear slow query history
362    pub fn clear_slow_query_history(&self) {
363        self.slow_queries.write().clear();
364    }
365
366    /// Get number of active queries
367    pub fn active_query_count(&self) -> usize {
368        self.active_queries.read().len()
369    }
370
371    /// Cancel all running queries (emergency stop)
372    pub fn cancel_all(&self) {
373        let active = self.active_queries.read();
374        for query in active.iter() {
375            query.complete();
376        }
377    }
378}
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383    use crate::dictionary::Term;
384    use std::thread;
385
386    #[test]
387    fn test_query_monitor_creation() {
388        let config = QueryMonitorConfig::default();
389        let monitor = QueryMonitor::new(config);
390
391        assert_eq!(monitor.active_query_count(), 0);
392        assert_eq!(monitor.slow_query_history().len(), 0);
393    }
394
395    #[test]
396    fn test_begin_and_end_query() {
397        let monitor = QueryMonitor::new(QueryMonitorConfig::default());
398
399        let s = Term::Iri("http://example.org/s".to_string());
400        let execution = monitor.begin_query(Some(&s), None, None, None);
401
402        assert_eq!(monitor.active_query_count(), 1);
403
404        monitor.end_query(execution, 10).unwrap();
405
406        assert_eq!(monitor.active_query_count(), 0);
407        assert_eq!(monitor.stats().total_queries.load(Ordering::Relaxed), 1);
408    }
409
410    #[test]
411    #[ignore] // Timing-dependent test, not suitable for CI
412    fn test_query_timeout() {
413        let config = QueryMonitorConfig {
414            default_timeout: Duration::from_millis(50),
415            ..Default::default()
416        };
417        let monitor = QueryMonitor::new(config);
418
419        let execution = monitor.begin_query(None, None, None, None);
420
421        // Wait for timeout (longer to ensure reliability)
422        thread::sleep(Duration::from_millis(100));
423
424        // Should detect timeout
425        assert!(execution.is_timed_out());
426
427        let result = monitor.end_query(execution, 0);
428        assert!(result.is_err());
429        assert_eq!(monitor.stats().timed_out_queries.load(Ordering::Relaxed), 1);
430    }
431
432    #[test]
433    fn test_slow_query_logging() {
434        let config = QueryMonitorConfig {
435            slow_query_threshold: Duration::from_millis(10),
436            default_timeout: Duration::from_secs(10),
437            ..Default::default()
438        };
439        let monitor = QueryMonitor::new(config);
440
441        let execution = monitor.begin_query(None, None, None, None);
442
443        // Make it slow
444        thread::sleep(Duration::from_millis(20));
445
446        monitor.end_query(execution, 5).unwrap();
447
448        // Should have logged a slow query
449        assert_eq!(monitor.stats().slow_queries.load(Ordering::Relaxed), 1);
450        assert_eq!(monitor.slow_query_history().len(), 1);
451    }
452
453    #[test]
454    fn test_timeout_override() {
455        let monitor = QueryMonitor::new(QueryMonitorConfig::default());
456
457        let custom_timeout = Duration::from_millis(50);
458        let execution = monitor.begin_query(None, None, None, Some(custom_timeout));
459
460        // Verify custom timeout is used
461        assert!(execution.deadline.is_some());
462    }
463
464    #[test]
465    fn test_multiple_active_queries() {
466        let monitor = QueryMonitor::new(QueryMonitorConfig::default());
467
468        let exec1 = monitor.begin_query(None, None, None, None);
469        let exec2 = monitor.begin_query(None, None, None, None);
470        let exec3 = monitor.begin_query(None, None, None, None);
471
472        assert_eq!(monitor.active_query_count(), 3);
473
474        monitor.end_query(exec1, 1).unwrap();
475        assert_eq!(monitor.active_query_count(), 2);
476
477        monitor.end_query(exec2, 2).unwrap();
478        monitor.end_query(exec3, 3).unwrap();
479        assert_eq!(monitor.active_query_count(), 0);
480    }
481
482    #[test]
483    fn test_slow_query_history_limit() {
484        let config = QueryMonitorConfig {
485            slow_query_threshold: Duration::from_millis(1),
486            max_slow_query_history: 5,
487            default_timeout: Duration::from_secs(10),
488            ..Default::default()
489        };
490        let monitor = QueryMonitor::new(config);
491
492        // Generate 10 slow queries
493        for _ in 0..10 {
494            let execution = monitor.begin_query(None, None, None, None);
495            thread::sleep(Duration::from_millis(2));
496            monitor.end_query(execution, 1).unwrap();
497        }
498
499        // Should only keep 5 most recent
500        assert_eq!(monitor.slow_query_history().len(), 5);
501    }
502
503    #[test]
504    fn test_query_pattern_describe() {
505        let s = Term::Iri("http://example.org/s".to_string());
506        let p = Term::Iri("http://example.org/p".to_string());
507
508        let pattern = QueryPattern::from_terms(Some(&s), Some(&p), None);
509        let desc = pattern.describe();
510
511        assert!(desc.contains("Iri"));
512    }
513
514    #[test]
515    fn test_stats_calculations() {
516        let monitor = QueryMonitor::new(QueryMonitorConfig::default());
517
518        // Execute some queries
519        for _ in 0..10 {
520            let execution = monitor.begin_query(None, None, None, None);
521            monitor.end_query(execution, 1).unwrap();
522        }
523
524        let stats = monitor.stats();
525        assert_eq!(stats.total_queries.load(Ordering::Relaxed), 10);
526        // avg_execution_time_us returns u64, which is always >= 0
527        let _ = stats.avg_execution_time_us(); // Just verify it doesn't panic
528    }
529
530    #[test]
531    #[ignore] // Timing-dependent test, not suitable for CI
532    fn test_timeout_rate() {
533        let config = QueryMonitorConfig {
534            default_timeout: Duration::from_millis(50),
535            ..Default::default()
536        };
537        let monitor = QueryMonitor::new(config);
538
539        // Execute 10 queries, make some timeout
540        for i in 0..10 {
541            let execution = monitor.begin_query(None, None, None, None);
542
543            if i < 5 {
544                // First 5 timeout
545                thread::sleep(Duration::from_millis(100));
546            }
547
548            let _ = monitor.end_query(execution, 1);
549        }
550
551        let stats = monitor.stats();
552        // Should have some timeouts (at least 30% to account for timing variations)
553        assert!(
554            stats.timeout_rate() > 30.0,
555            "Expected some timeouts but got rate: {}%",
556            stats.timeout_rate()
557        );
558    }
559
560    #[test]
561    fn test_disabled_monitor() {
562        let config = QueryMonitorConfig {
563            enabled: false,
564            ..Default::default()
565        };
566        let monitor = QueryMonitor::new(config);
567
568        let execution = monitor.begin_query(None, None, None, None);
569        monitor.end_query(execution, 10).unwrap();
570
571        // Should not track anything when disabled
572        assert_eq!(monitor.stats().total_queries.load(Ordering::Relaxed), 0);
573    }
574
575    #[test]
576    fn test_clear_slow_query_history() {
577        let config = QueryMonitorConfig {
578            slow_query_threshold: Duration::from_millis(1),
579            ..Default::default()
580        };
581        let monitor = QueryMonitor::new(config);
582
583        for _ in 0..5 {
584            let execution = monitor.begin_query(None, None, None, None);
585            thread::sleep(Duration::from_millis(2));
586            monitor.end_query(execution, 1).unwrap();
587        }
588
589        assert_eq!(monitor.slow_query_history().len(), 5);
590
591        monitor.clear_slow_query_history();
592
593        assert_eq!(monitor.slow_query_history().len(), 0);
594    }
595
596    #[test]
597    fn test_cancel_all() {
598        let monitor = QueryMonitor::new(QueryMonitorConfig::default());
599
600        let exec1 = monitor.begin_query(None, None, None, None);
601        let exec2 = monitor.begin_query(None, None, None, None);
602
603        assert!(exec1.is_running());
604        assert!(exec2.is_running());
605
606        monitor.cancel_all();
607
608        assert!(!exec1.is_running());
609        assert!(!exec2.is_running());
610    }
611}