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;
35use crate::limits::MAX_QUERY_EXECUTION_SECONDS;
36
37/// Recommended interval for timeout checks in hot loops.
38/// Checking every 1000 iterations balances responsiveness with overhead.
39pub const CHECK_INTERVAL: usize = 1000;
40
41/// Lightweight timeout context for query execution.
42///
43/// This struct captures the timeout configuration from `SelectExecutor` and can
44/// be passed to functions that need to check for timeouts but don't need the
45/// full executor reference.
46#[derive(Clone, Copy)]
47pub struct TimeoutContext {
48 /// When query execution started
49 start_time: Instant,
50 /// Maximum execution time in seconds
51 timeout_seconds: u64,
52}
53
54impl TimeoutContext {
55 /// Create a new timeout context with the given start time and timeout.
56 pub fn new(start_time: Instant, timeout_seconds: u64) -> Self {
57 Self { start_time, timeout_seconds }
58 }
59
60 /// Create a timeout context from a `SelectExecutor`.
61 pub fn from_executor(executor: &crate::SelectExecutor<'_>) -> Self {
62 Self {
63 start_time: executor.start_time,
64 timeout_seconds: executor.timeout_seconds,
65 }
66 }
67
68 /// Create a default timeout context (for use when no executor is available).
69 ///
70 /// This creates a context with the current time as start and default timeout.
71 /// Use sparingly - prefer `from_executor` when an executor is available.
72 pub fn new_default() -> Self {
73 Self {
74 start_time: Instant::now(),
75 timeout_seconds: MAX_QUERY_EXECUTION_SECONDS,
76 }
77 }
78
79 /// Check if the timeout has been exceeded.
80 ///
81 /// Returns `Ok(())` if within timeout, `Err(QueryTimeoutExceeded)` if exceeded.
82 ///
83 /// Call this periodically in hot loops (every `CHECK_INTERVAL` iterations).
84 #[inline]
85 pub fn check(&self) -> Result<(), ExecutorError> {
86 let elapsed = self.start_time.elapsed().as_secs();
87 if elapsed >= self.timeout_seconds {
88 return Err(ExecutorError::QueryTimeoutExceeded {
89 elapsed_seconds: elapsed,
90 max_seconds: self.timeout_seconds,
91 });
92 }
93 Ok(())
94 }
95
96 /// Get the start time for this timeout context.
97 pub fn start_time(&self) -> Instant {
98 self.start_time
99 }
100
101 /// Get the timeout in seconds for this timeout context.
102 pub fn timeout_seconds(&self) -> u64 {
103 self.timeout_seconds
104 }
105}
106
107impl Default for TimeoutContext {
108 fn default() -> Self {
109 Self::new_default()
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use std::thread::sleep;
117 use std::time::Duration;
118
119 #[test]
120 fn test_timeout_check_passes_within_limit() {
121 let ctx = TimeoutContext::new(Instant::now(), 60);
122 assert!(ctx.check().is_ok());
123 }
124
125 #[test]
126 fn test_timeout_check_fails_after_timeout() {
127 // Use a very short timeout
128 let ctx = TimeoutContext::new(Instant::now(), 0);
129 // Sleep briefly to ensure we're past the 0-second timeout
130 sleep(Duration::from_millis(10));
131 let result = ctx.check();
132 assert!(result.is_err());
133 if let Err(ExecutorError::QueryTimeoutExceeded { .. }) = result {
134 // Expected
135 } else {
136 panic!("Expected QueryTimeoutExceeded error");
137 }
138 }
139
140 #[test]
141 fn test_default_timeout_context() {
142 let ctx = TimeoutContext::default();
143 assert_eq!(ctx.timeout_seconds(), MAX_QUERY_EXECUTION_SECONDS);
144 assert!(ctx.check().is_ok());
145 }
146}