vibesql_executor/
timeout.rs

1//! Timeout context for query execution
2//!
3//! This module provides a lightweight timeout checking mechanism that can be
4//! passed to query execution functions (joins, scans, etc.) without requiring
5//! the full `SelectExecutor` reference.
6//!
7//! ## Design Rationale
8//!
9//! The `SelectExecutor` owns the timeout configuration but many query execution
10//! functions (especially joins) don't have access to it. Rather than threading
11//! the executor through all call chains, we extract just the timeout-related
12//! state into this lightweight struct.
13//!
14//! ## Usage Pattern
15//!
16//! ```text
17//! // Create from executor
18//! let timeout_ctx = TimeoutContext::from_executor(executor);
19//!
20//! // Pass to join/scan functions
21//! nested_loop_join(left, right, condition, database, &timeout_ctx)?;
22//!
23//! // Check periodically in hot loops
24//! for row in rows {
25//!     if iteration % CHECK_INTERVAL == 0 {
26//!         timeout_ctx.check()?;
27//!     }
28//!     // ... process row ...
29//! }
30//! ```
31
32use instant::Instant;
33
34use crate::{errors::ExecutorError, limits::MAX_QUERY_EXECUTION_SECONDS};
35
36/// Recommended interval for timeout checks in hot loops.
37/// Checking every 1000 iterations balances responsiveness with overhead.
38pub const CHECK_INTERVAL: usize = 1000;
39
40/// Lightweight timeout context for query execution.
41///
42/// This struct captures the timeout configuration from `SelectExecutor` and can
43/// be passed to functions that need to check for timeouts but don't need the
44/// full executor reference.
45#[derive(Clone, Copy)]
46pub struct TimeoutContext {
47    /// When query execution started
48    start_time: Instant,
49    /// Maximum execution time in seconds
50    timeout_seconds: u64,
51}
52
53impl TimeoutContext {
54    /// Create a new timeout context with the given start time and timeout.
55    pub fn new(start_time: Instant, timeout_seconds: u64) -> Self {
56        Self { start_time, timeout_seconds }
57    }
58
59    /// Create a timeout context from a `SelectExecutor`.
60    pub fn from_executor(executor: &crate::SelectExecutor<'_>) -> Self {
61        Self { start_time: executor.start_time, timeout_seconds: executor.timeout_seconds }
62    }
63
64    /// Create a default timeout context (for use when no executor is available).
65    ///
66    /// This creates a context with the current time as start and default timeout.
67    /// Use sparingly - prefer `from_executor` when an executor is available.
68    pub fn new_default() -> Self {
69        Self { start_time: Instant::now(), timeout_seconds: MAX_QUERY_EXECUTION_SECONDS }
70    }
71
72    /// Check if the timeout has been exceeded.
73    ///
74    /// Returns `Ok(())` if within timeout, `Err(QueryTimeoutExceeded)` if exceeded.
75    ///
76    /// Call this periodically in hot loops (every `CHECK_INTERVAL` iterations).
77    #[inline]
78    pub fn check(&self) -> Result<(), ExecutorError> {
79        let elapsed = self.start_time.elapsed().as_secs();
80        if elapsed >= self.timeout_seconds {
81            return Err(ExecutorError::QueryTimeoutExceeded {
82                elapsed_seconds: elapsed,
83                max_seconds: self.timeout_seconds,
84            });
85        }
86        Ok(())
87    }
88
89    /// Get the start time for this timeout context.
90    pub fn start_time(&self) -> Instant {
91        self.start_time
92    }
93
94    /// Get the timeout in seconds for this timeout context.
95    pub fn timeout_seconds(&self) -> u64 {
96        self.timeout_seconds
97    }
98}
99
100impl Default for TimeoutContext {
101    fn default() -> Self {
102        Self::new_default()
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use std::{thread::sleep, time::Duration};
109
110    use super::*;
111
112    #[test]
113    fn test_timeout_check_passes_within_limit() {
114        let ctx = TimeoutContext::new(Instant::now(), 60);
115        assert!(ctx.check().is_ok());
116    }
117
118    #[test]
119    fn test_timeout_check_fails_after_timeout() {
120        // Use a very short timeout
121        let ctx = TimeoutContext::new(Instant::now(), 0);
122        // Sleep briefly to ensure we're past the 0-second timeout
123        sleep(Duration::from_millis(10));
124        let result = ctx.check();
125        assert!(result.is_err());
126        if let Err(ExecutorError::QueryTimeoutExceeded { .. }) = result {
127            // Expected
128        } else {
129            panic!("Expected QueryTimeoutExceeded error");
130        }
131    }
132
133    #[test]
134    fn test_default_timeout_context() {
135        let ctx = TimeoutContext::default();
136        assert_eq!(ctx.timeout_seconds(), MAX_QUERY_EXECUTION_SECONDS);
137        assert!(ctx.check().is_ok());
138    }
139}