Skip to main content

oxirs_tdb/
query_timeout.rs

1//! # Query Timeout Enforcement
2//!
3//! Provides comprehensive query timeout enforcement to prevent runaway queries
4//! from consuming excessive resources in production environments.
5
6use scirs2_core::metrics::{Counter, Histogram};
7use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
8use std::sync::Arc;
9use std::time::{Duration, Instant};
10
11/// Configuration for query timeout enforcement
12#[derive(Debug, Clone)]
13pub struct QueryTimeoutConfig {
14    /// Default query timeout (None = no timeout)
15    pub default_timeout: Option<Duration>,
16    /// Maximum allowed timeout (prevents users from setting arbitrarily high timeouts)
17    pub max_timeout: Duration,
18    /// Grace period after timeout before forceful cancellation
19    pub grace_period: Duration,
20    /// Enable soft timeouts (warnings before hard timeout)
21    pub enable_soft_timeouts: bool,
22    /// Soft timeout threshold (% of total timeout)
23    pub soft_timeout_threshold: f64,
24}
25
26impl Default for QueryTimeoutConfig {
27    fn default() -> Self {
28        Self {
29            default_timeout: Some(Duration::from_secs(30)), // 30 seconds default
30            max_timeout: Duration::from_secs(300),          // 5 minutes max
31            grace_period: Duration::from_secs(5),           // 5 seconds grace
32            enable_soft_timeouts: true,
33            soft_timeout_threshold: 0.8, // Warn at 80% of timeout
34        }
35    }
36}
37
38/// Query timeout status
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum TimeoutStatus {
41    /// Query is within limits
42    Ok,
43    /// Approaching timeout (soft timeout reached)
44    Warning,
45    /// Timeout exceeded
46    Exceeded,
47    /// Forcefully cancelled after grace period
48    Cancelled,
49}
50
51/// Query execution context with timeout tracking
52pub struct QueryContext {
53    /// Unique query ID
54    query_id: u64,
55    /// Start time
56    start_time: Instant,
57    /// Timeout deadline
58    deadline: Option<Instant>,
59    /// Cancellation flag
60    cancelled: Arc<AtomicBool>,
61    /// Soft timeout reached
62    soft_timeout_reached: AtomicBool,
63    /// Hard timeout reached
64    hard_timeout_reached: AtomicBool,
65    /// Configuration
66    config: QueryTimeoutConfig,
67}
68
69impl QueryContext {
70    /// Create a new query context
71    pub fn new(
72        query_id: u64,
73        config: QueryTimeoutConfig,
74        custom_timeout: Option<Duration>,
75    ) -> Self {
76        let timeout = custom_timeout
77            .or(config.default_timeout)
78            .map(|t| t.min(config.max_timeout));
79
80        let deadline = timeout.map(|t| Instant::now() + t);
81
82        Self {
83            query_id,
84            start_time: Instant::now(),
85            deadline,
86            cancelled: Arc::new(AtomicBool::new(false)),
87            soft_timeout_reached: AtomicBool::new(false),
88            hard_timeout_reached: AtomicBool::new(false),
89            config,
90        }
91    }
92
93    /// Check if query should continue execution
94    pub fn check_timeout(&self) -> TimeoutStatus {
95        // Check if cancelled
96        if self.cancelled.load(Ordering::Relaxed) {
97            return TimeoutStatus::Cancelled;
98        }
99
100        let Some(deadline) = self.deadline else {
101            return TimeoutStatus::Ok;
102        };
103
104        let now = Instant::now();
105
106        // Check hard timeout
107        if now >= deadline {
108            self.hard_timeout_reached.store(true, Ordering::Relaxed);
109            return TimeoutStatus::Exceeded;
110        }
111
112        // Check soft timeout if enabled
113        if self.config.enable_soft_timeouts && !self.soft_timeout_reached.load(Ordering::Relaxed) {
114            let elapsed = now.duration_since(self.start_time);
115            let total_timeout = deadline.duration_since(self.start_time);
116            let threshold_duration = Duration::from_secs_f64(
117                total_timeout.as_secs_f64() * self.config.soft_timeout_threshold,
118            );
119
120            if elapsed >= threshold_duration {
121                self.soft_timeout_reached.store(true, Ordering::Relaxed);
122                return TimeoutStatus::Warning;
123            }
124        }
125
126        TimeoutStatus::Ok
127    }
128
129    /// Get elapsed time since query started
130    pub fn elapsed(&self) -> Duration {
131        self.start_time.elapsed()
132    }
133
134    /// Get remaining time until timeout
135    pub fn remaining(&self) -> Option<Duration> {
136        self.deadline.map(|deadline| {
137            let now = Instant::now();
138            if now < deadline {
139                deadline - now
140            } else {
141                Duration::ZERO
142            }
143        })
144    }
145
146    /// Check if timeout has been exceeded
147    pub fn is_timeout_exceeded(&self) -> bool {
148        self.hard_timeout_reached.load(Ordering::Relaxed)
149    }
150
151    /// Check if query was cancelled
152    pub fn is_cancelled(&self) -> bool {
153        self.cancelled.load(Ordering::Relaxed)
154    }
155
156    /// Cancel the query
157    pub fn cancel(&self) {
158        self.cancelled.store(true, Ordering::Relaxed);
159    }
160
161    /// Get cancellation handle for external cancellation
162    pub fn cancellation_handle(&self) -> CancellationHandle {
163        CancellationHandle {
164            query_id: self.query_id,
165            cancelled: Arc::clone(&self.cancelled),
166        }
167    }
168
169    /// Get query ID
170    pub fn query_id(&self) -> u64 {
171        self.query_id
172    }
173}
174
175/// Handle for cancelling a query from another thread
176#[derive(Clone)]
177pub struct CancellationHandle {
178    query_id: u64,
179    cancelled: Arc<AtomicBool>,
180}
181
182impl CancellationHandle {
183    /// Cancel the query
184    pub fn cancel(&self) {
185        self.cancelled.store(true, Ordering::Relaxed);
186    }
187
188    /// Check if query is cancelled
189    pub fn is_cancelled(&self) -> bool {
190        self.cancelled.load(Ordering::Relaxed)
191    }
192
193    /// Get query ID
194    pub fn query_id(&self) -> u64 {
195        self.query_id
196    }
197}
198
199/// Query timeout manager for tracking and enforcing timeouts
200pub struct QueryTimeoutManager {
201    /// Configuration
202    config: QueryTimeoutConfig,
203    /// Next query ID
204    next_query_id: AtomicU64,
205    /// Active query contexts
206    active_queries: parking_lot::RwLock<std::collections::HashMap<u64, Arc<QueryContext>>>,
207    /// Metrics
208    timeout_counter: Counter,
209    cancellation_counter: Counter,
210    query_duration_histogram: Histogram,
211}
212
213impl QueryTimeoutManager {
214    /// Create a new query timeout manager
215    pub fn new(config: QueryTimeoutConfig) -> Self {
216        Self {
217            config,
218            next_query_id: AtomicU64::new(1),
219            active_queries: parking_lot::RwLock::new(std::collections::HashMap::new()),
220            timeout_counter: Counter::new("query_timeouts_total".to_string()),
221            cancellation_counter: Counter::new("query_cancellations_total".to_string()),
222            query_duration_histogram: Histogram::new("query_duration_seconds".to_string()),
223        }
224    }
225
226    /// Start a new query with timeout tracking
227    pub fn start_query(&self, custom_timeout: Option<Duration>) -> Arc<QueryContext> {
228        let query_id = self.next_query_id.fetch_add(1, Ordering::Relaxed);
229        let context = Arc::new(QueryContext::new(
230            query_id,
231            self.config.clone(),
232            custom_timeout,
233        ));
234
235        let mut active = self.active_queries.write();
236        active.insert(query_id, Arc::clone(&context));
237
238        context
239    }
240
241    /// Finish a query and record metrics
242    pub fn finish_query(&self, query_id: u64) {
243        let mut active = self.active_queries.write();
244        if let Some(context) = active.remove(&query_id) {
245            let duration = context.elapsed();
246            self.query_duration_histogram
247                .observe(duration.as_secs_f64());
248
249            if context.is_timeout_exceeded() {
250                self.timeout_counter.inc();
251            }
252
253            if context.is_cancelled() {
254                self.cancellation_counter.inc();
255            }
256        }
257    }
258
259    /// Cancel a specific query
260    pub fn cancel_query(&self, query_id: u64) -> bool {
261        let active = self.active_queries.read();
262        if let Some(context) = active.get(&query_id) {
263            context.cancel();
264            true
265        } else {
266            false
267        }
268    }
269
270    /// Cancel all active queries
271    pub fn cancel_all_queries(&self) {
272        let active = self.active_queries.read();
273        for context in active.values() {
274            context.cancel();
275        }
276    }
277
278    /// Get active query count
279    pub fn active_query_count(&self) -> usize {
280        self.active_queries.read().len()
281    }
282
283    /// Get active query IDs
284    pub fn active_query_ids(&self) -> Vec<u64> {
285        self.active_queries.read().keys().copied().collect()
286    }
287
288    /// Get query context for a specific query ID
289    pub fn get_query_context(&self, query_id: u64) -> Option<Arc<QueryContext>> {
290        self.active_queries.read().get(&query_id).cloned()
291    }
292
293    /// Get metrics
294    pub fn get_metrics(&self) -> QueryTimeoutMetrics {
295        let histogram_stats = self.query_duration_histogram.get_stats();
296
297        QueryTimeoutMetrics {
298            total_timeouts: self.timeout_counter.get(),
299            total_cancellations: self.cancellation_counter.get(),
300            active_queries: self.active_query_count(),
301            avg_query_duration_secs: histogram_stats.mean,
302        }
303    }
304
305    /// Cleanup finished queries (queries that are no longer in the active set)
306    pub fn cleanup(&self) {
307        let mut active = self.active_queries.write();
308        active.retain(|_, context| {
309            // Keep only queries that haven't been explicitly finished
310            // In a real implementation, we'd have a way to mark queries as finished
311            !context.is_cancelled()
312        });
313    }
314}
315
316/// Metrics for query timeout manager
317#[derive(Debug, Clone)]
318pub struct QueryTimeoutMetrics {
319    /// Total queries that timed out
320    pub total_timeouts: u64,
321    /// Total queries that were cancelled
322    pub total_cancellations: u64,
323    /// Currently active queries
324    pub active_queries: usize,
325    /// Average query duration (seconds)
326    pub avg_query_duration_secs: f64,
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332
333    #[test]
334    fn test_query_context_creation() {
335        let config = QueryTimeoutConfig::default();
336        let context = QueryContext::new(1, config, None);
337
338        assert_eq!(context.query_id(), 1);
339        assert!(!context.is_timeout_exceeded());
340        assert!(!context.is_cancelled());
341    }
342
343    #[test]
344    fn test_no_timeout() {
345        let config = QueryTimeoutConfig {
346            default_timeout: None,
347            ..Default::default()
348        };
349        let context = QueryContext::new(1, config, None);
350
351        std::thread::sleep(Duration::from_millis(10));
352        assert_eq!(context.check_timeout(), TimeoutStatus::Ok);
353        assert!(context.remaining().is_none());
354    }
355
356    #[test]
357    fn test_timeout_detection() {
358        let config = QueryTimeoutConfig {
359            default_timeout: Some(Duration::from_millis(50)),
360            enable_soft_timeouts: false,
361            ..Default::default()
362        };
363        let context = QueryContext::new(1, config, None);
364
365        // Should be ok initially
366        assert_eq!(context.check_timeout(), TimeoutStatus::Ok);
367
368        // Wait for timeout
369        std::thread::sleep(Duration::from_millis(60));
370
371        // Should detect timeout
372        assert_eq!(context.check_timeout(), TimeoutStatus::Exceeded);
373        assert!(context.is_timeout_exceeded());
374    }
375
376    #[test]
377    fn test_soft_timeout() {
378        let config = QueryTimeoutConfig {
379            default_timeout: Some(Duration::from_millis(100)),
380            enable_soft_timeouts: true,
381            soft_timeout_threshold: 0.5, // 50ms
382            ..Default::default()
383        };
384        let context = QueryContext::new(1, config, None);
385
386        // Should be ok initially
387        assert_eq!(context.check_timeout(), TimeoutStatus::Ok);
388
389        // Wait for soft timeout (> 50ms but < 100ms)
390        std::thread::sleep(Duration::from_millis(60));
391
392        // Should detect soft timeout
393        assert_eq!(context.check_timeout(), TimeoutStatus::Warning);
394        assert!(!context.is_timeout_exceeded());
395
396        // Wait for hard timeout
397        std::thread::sleep(Duration::from_millis(50));
398        assert_eq!(context.check_timeout(), TimeoutStatus::Exceeded);
399    }
400
401    #[test]
402    fn test_custom_timeout() {
403        let config = QueryTimeoutConfig {
404            default_timeout: Some(Duration::from_secs(10)),
405            ..Default::default()
406        };
407        let custom_timeout = Some(Duration::from_millis(50));
408        let context = QueryContext::new(1, config, custom_timeout);
409
410        std::thread::sleep(Duration::from_millis(60));
411        assert_eq!(context.check_timeout(), TimeoutStatus::Exceeded);
412    }
413
414    #[test]
415    fn test_max_timeout_enforcement() {
416        let config = QueryTimeoutConfig {
417            max_timeout: Duration::from_millis(100),
418            ..Default::default()
419        };
420        let custom_timeout = Some(Duration::from_secs(10)); // Try to set 10 seconds
421        let context = QueryContext::new(1, config, custom_timeout);
422
423        std::thread::sleep(Duration::from_millis(110));
424        // Should timeout at max_timeout (100ms), not custom (10s)
425        assert_eq!(context.check_timeout(), TimeoutStatus::Exceeded);
426    }
427
428    #[test]
429    fn test_cancellation() {
430        let config = QueryTimeoutConfig::default();
431        let context = QueryContext::new(1, config, None);
432
433        assert!(!context.is_cancelled());
434        context.cancel();
435        assert!(context.is_cancelled());
436        assert_eq!(context.check_timeout(), TimeoutStatus::Cancelled);
437    }
438
439    #[test]
440    fn test_cancellation_handle() {
441        let config = QueryTimeoutConfig::default();
442        let context = QueryContext::new(1, config, None);
443        let handle = context.cancellation_handle();
444
445        assert_eq!(handle.query_id(), 1);
446        assert!(!handle.is_cancelled());
447
448        handle.cancel();
449        assert!(handle.is_cancelled());
450        assert!(context.is_cancelled());
451    }
452
453    #[test]
454    fn test_elapsed_and_remaining() {
455        let config = QueryTimeoutConfig {
456            default_timeout: Some(Duration::from_millis(100)),
457            ..Default::default()
458        };
459        let context = QueryContext::new(1, config, None);
460
461        std::thread::sleep(Duration::from_millis(30));
462
463        let elapsed = context.elapsed();
464        assert!(elapsed >= Duration::from_millis(30));
465        assert!(elapsed < Duration::from_millis(100));
466
467        let remaining = context.remaining().unwrap();
468        assert!(remaining > Duration::ZERO);
469        assert!(remaining <= Duration::from_millis(70));
470    }
471
472    #[test]
473    fn test_timeout_manager_creation() {
474        let config = QueryTimeoutConfig::default();
475        let manager = QueryTimeoutManager::new(config);
476
477        assert_eq!(manager.active_query_count(), 0);
478    }
479
480    #[test]
481    fn test_start_and_finish_query() {
482        let manager = QueryTimeoutManager::new(QueryTimeoutConfig::default());
483
484        let context = manager.start_query(None);
485        let query_id = context.query_id();
486
487        assert_eq!(manager.active_query_count(), 1);
488        assert!(manager.active_query_ids().contains(&query_id));
489
490        manager.finish_query(query_id);
491        assert_eq!(manager.active_query_count(), 0);
492    }
493
494    #[test]
495    fn test_cancel_query_by_id() {
496        let manager = QueryTimeoutManager::new(QueryTimeoutConfig::default());
497
498        let context = manager.start_query(None);
499        let query_id = context.query_id();
500
501        assert!(!context.is_cancelled());
502
503        let cancelled = manager.cancel_query(query_id);
504        assert!(cancelled);
505        assert!(context.is_cancelled());
506
507        // Try to cancel non-existent query
508        let cancelled = manager.cancel_query(99999);
509        assert!(!cancelled);
510    }
511
512    #[test]
513    fn test_cancel_all_queries() {
514        let manager = QueryTimeoutManager::new(QueryTimeoutConfig::default());
515
516        let context1 = manager.start_query(None);
517        let context2 = manager.start_query(None);
518        let context3 = manager.start_query(None);
519
520        assert!(!context1.is_cancelled());
521        assert!(!context2.is_cancelled());
522        assert!(!context3.is_cancelled());
523
524        manager.cancel_all_queries();
525
526        assert!(context1.is_cancelled());
527        assert!(context2.is_cancelled());
528        assert!(context3.is_cancelled());
529    }
530
531    #[test]
532    fn test_get_query_context() {
533        let manager = QueryTimeoutManager::new(QueryTimeoutConfig::default());
534
535        let context = manager.start_query(None);
536        let query_id = context.query_id();
537
538        let retrieved = manager.get_query_context(query_id);
539        assert!(retrieved.is_some());
540        assert_eq!(retrieved.unwrap().query_id(), query_id);
541
542        let not_found = manager.get_query_context(99999);
543        assert!(not_found.is_none());
544    }
545
546    #[test]
547    fn test_metrics_tracking() {
548        let config = QueryTimeoutConfig {
549            default_timeout: Some(Duration::from_millis(50)),
550            ..Default::default()
551        };
552        let manager = QueryTimeoutManager::new(config);
553
554        // Start and finish a normal query
555        let context1 = manager.start_query(None);
556        let query_id1 = context1.query_id();
557        std::thread::sleep(Duration::from_millis(10));
558        manager.finish_query(query_id1);
559
560        // Start and timeout a query
561        let context2 = manager.start_query(None);
562        let query_id2 = context2.query_id();
563        std::thread::sleep(Duration::from_millis(60));
564        context2.check_timeout(); // Trigger timeout detection
565        manager.finish_query(query_id2);
566
567        // Start and cancel a query
568        let context3 = manager.start_query(None);
569        let query_id3 = context3.query_id();
570        context3.cancel();
571        manager.finish_query(query_id3);
572
573        let metrics = manager.get_metrics();
574        assert_eq!(metrics.total_timeouts, 1);
575        assert_eq!(metrics.total_cancellations, 1);
576        assert!(metrics.avg_query_duration_secs > 0.0);
577    }
578
579    #[test]
580    fn test_multiple_active_queries() {
581        let manager = QueryTimeoutManager::new(QueryTimeoutConfig::default());
582
583        let _context1 = manager.start_query(None);
584        let _context2 = manager.start_query(None);
585        let _context3 = manager.start_query(None);
586
587        assert_eq!(manager.active_query_count(), 3);
588        assert_eq!(manager.active_query_ids().len(), 3);
589    }
590
591    #[test]
592    fn test_timeout_with_grace_period() {
593        let config = QueryTimeoutConfig {
594            default_timeout: Some(Duration::from_millis(50)),
595            grace_period: Duration::from_millis(20),
596            ..Default::default()
597        };
598
599        let context = QueryContext::new(1, config, None);
600
601        std::thread::sleep(Duration::from_millis(60));
602        assert_eq!(context.check_timeout(), TimeoutStatus::Exceeded);
603
604        // Grace period would be handled by the query executor
605        // The context just reports the status
606    }
607
608    #[test]
609    fn test_remaining_time_zero_after_timeout() {
610        let config = QueryTimeoutConfig {
611            default_timeout: Some(Duration::from_millis(50)),
612            ..Default::default()
613        };
614        let context = QueryContext::new(1, config, None);
615
616        std::thread::sleep(Duration::from_millis(60));
617
618        let remaining = context.remaining().unwrap();
619        assert_eq!(remaining, Duration::ZERO);
620    }
621}