1use scirs2_core::metrics::{Counter, Histogram};
7use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
8use std::sync::Arc;
9use std::time::{Duration, Instant};
10
11#[derive(Debug, Clone)]
13pub struct QueryTimeoutConfig {
14 pub default_timeout: Option<Duration>,
16 pub max_timeout: Duration,
18 pub grace_period: Duration,
20 pub enable_soft_timeouts: bool,
22 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)), max_timeout: Duration::from_secs(300), grace_period: Duration::from_secs(5), enable_soft_timeouts: true,
33 soft_timeout_threshold: 0.8, }
35 }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum TimeoutStatus {
41 Ok,
43 Warning,
45 Exceeded,
47 Cancelled,
49}
50
51pub struct QueryContext {
53 query_id: u64,
55 start_time: Instant,
57 deadline: Option<Instant>,
59 cancelled: Arc<AtomicBool>,
61 soft_timeout_reached: AtomicBool,
63 hard_timeout_reached: AtomicBool,
65 config: QueryTimeoutConfig,
67}
68
69impl QueryContext {
70 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 pub fn check_timeout(&self) -> TimeoutStatus {
95 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 if now >= deadline {
108 self.hard_timeout_reached.store(true, Ordering::Relaxed);
109 return TimeoutStatus::Exceeded;
110 }
111
112 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 pub fn elapsed(&self) -> Duration {
131 self.start_time.elapsed()
132 }
133
134 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 pub fn is_timeout_exceeded(&self) -> bool {
148 self.hard_timeout_reached.load(Ordering::Relaxed)
149 }
150
151 pub fn is_cancelled(&self) -> bool {
153 self.cancelled.load(Ordering::Relaxed)
154 }
155
156 pub fn cancel(&self) {
158 self.cancelled.store(true, Ordering::Relaxed);
159 }
160
161 pub fn cancellation_handle(&self) -> CancellationHandle {
163 CancellationHandle {
164 query_id: self.query_id,
165 cancelled: Arc::clone(&self.cancelled),
166 }
167 }
168
169 pub fn query_id(&self) -> u64 {
171 self.query_id
172 }
173}
174
175#[derive(Clone)]
177pub struct CancellationHandle {
178 query_id: u64,
179 cancelled: Arc<AtomicBool>,
180}
181
182impl CancellationHandle {
183 pub fn cancel(&self) {
185 self.cancelled.store(true, Ordering::Relaxed);
186 }
187
188 pub fn is_cancelled(&self) -> bool {
190 self.cancelled.load(Ordering::Relaxed)
191 }
192
193 pub fn query_id(&self) -> u64 {
195 self.query_id
196 }
197}
198
199pub struct QueryTimeoutManager {
201 config: QueryTimeoutConfig,
203 next_query_id: AtomicU64,
205 active_queries: parking_lot::RwLock<std::collections::HashMap<u64, Arc<QueryContext>>>,
207 timeout_counter: Counter,
209 cancellation_counter: Counter,
210 query_duration_histogram: Histogram,
211}
212
213impl QueryTimeoutManager {
214 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 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 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 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 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 pub fn active_query_count(&self) -> usize {
280 self.active_queries.read().len()
281 }
282
283 pub fn active_query_ids(&self) -> Vec<u64> {
285 self.active_queries.read().keys().copied().collect()
286 }
287
288 pub fn get_query_context(&self, query_id: u64) -> Option<Arc<QueryContext>> {
290 self.active_queries.read().get(&query_id).cloned()
291 }
292
293 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 pub fn cleanup(&self) {
307 let mut active = self.active_queries.write();
308 active.retain(|_, context| {
309 !context.is_cancelled()
312 });
313 }
314}
315
316#[derive(Debug, Clone)]
318pub struct QueryTimeoutMetrics {
319 pub total_timeouts: u64,
321 pub total_cancellations: u64,
323 pub active_queries: usize,
325 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 assert_eq!(context.check_timeout(), TimeoutStatus::Ok);
367
368 std::thread::sleep(Duration::from_millis(60));
370
371 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, ..Default::default()
383 };
384 let context = QueryContext::new(1, config, None);
385
386 assert_eq!(context.check_timeout(), TimeoutStatus::Ok);
388
389 std::thread::sleep(Duration::from_millis(60));
391
392 assert_eq!(context.check_timeout(), TimeoutStatus::Warning);
394 assert!(!context.is_timeout_exceeded());
395
396 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)); let context = QueryContext::new(1, config, custom_timeout);
422
423 std::thread::sleep(Duration::from_millis(110));
424 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 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 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 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(); manager.finish_query(query_id2);
566
567 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 }
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}