1use std::sync::{
6 atomic::{AtomicBool, Ordering},
7 Arc,
8};
9use std::time::{Duration, Instant};
10
11#[derive(Debug, thiserror::Error)]
13pub enum TimeoutError {
14 #[error("Query exceeded timeout of {duration_ms}ms")]
15 QueryTimeout { duration_ms: u64 },
16 #[error("Query was cancelled")]
17 QueryCancelled,
18}
19
20pub struct QueryTimeout {
25 pub deadline: Instant,
26 pub cancelled: Arc<AtomicBool>,
27 duration: Duration,
28}
29
30impl QueryTimeout {
31 pub fn new(duration: Duration) -> Self {
33 Self {
34 deadline: Instant::now() + duration,
35 cancelled: Arc::new(AtomicBool::new(false)),
36 duration,
37 }
38 }
39
40 pub fn is_expired(&self) -> bool {
42 Instant::now() >= self.deadline
43 }
44
45 pub fn remaining(&self) -> Option<Duration> {
47 self.deadline.checked_duration_since(Instant::now())
48 }
49
50 pub fn cancel(&self) {
53 self.cancelled.store(true, Ordering::Release);
54 }
55
56 pub fn check(&self) -> Result<(), TimeoutError> {
61 if self.cancelled.load(Ordering::Acquire) {
62 return Err(TimeoutError::QueryCancelled);
63 }
64 if self.is_expired() {
65 return Err(TimeoutError::QueryTimeout {
66 duration_ms: self.duration.as_millis() as u64,
67 });
68 }
69 Ok(())
70 }
71
72 pub fn cancellation_handle(&self) -> Arc<AtomicBool> {
74 Arc::clone(&self.cancelled)
75 }
76}
77
78pub struct TimeoutConfig {
80 pub default_timeout: Option<Duration>,
83 pub max_timeout: Duration,
86 pub admin_bypass: bool,
88}
89
90impl Default for TimeoutConfig {
91 fn default() -> Self {
92 Self {
93 default_timeout: Some(Duration::from_secs(30)),
94 max_timeout: Duration::from_secs(300),
95 admin_bypass: true,
96 }
97 }
98}
99
100impl TimeoutConfig {
101 pub fn resolve(&self, requested: Option<Duration>, is_admin: bool) -> Option<Duration> {
109 if is_admin && self.admin_bypass {
110 return None;
111 }
112 let base = requested.or(self.default_timeout)?;
113 Some(base.min(self.max_timeout))
114 }
115}
116
117pub fn parse_timeout_param(value: &str) -> Result<Duration, String> {
125 let trimmed = value.trim();
126
127 if trimmed.is_empty() {
128 return Err("Timeout value is empty".to_string());
129 }
130
131 if trimmed.to_ascii_uppercase().starts_with("PT") {
133 let upper = trimmed.to_ascii_uppercase();
134 let inner = &upper[2..]; if let Some(s_idx) = inner.find('S') {
136 let num_str = &inner[..s_idx];
137 if let Ok(secs) = num_str.parse::<f64>() {
138 if secs < 0.0 {
139 return Err("Timeout must not be negative".to_string());
140 }
141 return Ok(Duration::from_millis((secs * 1000.0) as u64));
142 }
143 }
144 return Err(format!("Cannot parse ISO 8601 duration: {trimmed}"));
145 }
146
147 if let Some(num_str) = trimmed.strip_suffix("ms") {
149 let ms: u64 = num_str
150 .trim()
151 .parse()
152 .map_err(|_| format!("Invalid millisecond value: {num_str}"))?;
153 return Ok(Duration::from_millis(ms));
154 }
155 if let Some(num_str) = trimmed.strip_suffix('s') {
156 let secs: f64 = num_str
157 .trim()
158 .parse()
159 .map_err(|_| format!("Invalid second value: {num_str}"))?;
160 if secs < 0.0 {
161 return Err("Timeout must not be negative".to_string());
162 }
163 return Ok(Duration::from_millis((secs * 1000.0) as u64));
164 }
165 if let Some(num_str) = trimmed.strip_suffix('m') {
166 let mins: f64 = num_str
167 .trim()
168 .parse()
169 .map_err(|_| format!("Invalid minute value: {num_str}"))?;
170 if mins < 0.0 {
171 return Err("Timeout must not be negative".to_string());
172 }
173 return Ok(Duration::from_millis((mins * 60.0 * 1000.0) as u64));
174 }
175
176 if let Ok(ms) = trimmed.parse::<u64>() {
178 return Ok(Duration::from_millis(ms));
179 }
180
181 Err(format!("Unrecognised timeout format: {trimmed}"))
182}
183
184pub struct TimeoutIterator<I: Iterator> {
190 inner: I,
191 timeout: Arc<QueryTimeout>,
192 check_interval: usize,
194 count: usize,
195 terminated: bool,
198}
199
200impl<I: Iterator> TimeoutIterator<I> {
201 pub fn new(iter: I, timeout: Arc<QueryTimeout>, check_interval: usize) -> Self {
203 Self {
204 inner: iter,
205 timeout,
206 check_interval: check_interval.max(1),
207 count: 0,
208 terminated: false,
209 }
210 }
211}
212
213impl<I: Iterator> Iterator for TimeoutIterator<I> {
214 type Item = Result<I::Item, TimeoutError>;
215
216 fn next(&mut self) -> Option<Self::Item> {
217 if self.terminated {
218 return None;
219 }
220 self.count += 1;
221 if self.count % self.check_interval == 0 {
222 if let Err(e) = self.timeout.check() {
223 self.terminated = true;
224 return Some(Err(e));
225 }
226 }
227
228 self.inner.next().map(Ok)
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use std::sync::atomic::Ordering;
236 use std::thread;
237 use std::time::Duration;
238
239 #[test]
242 fn test_not_expired_immediately() {
243 let t = QueryTimeout::new(Duration::from_secs(60));
244 assert!(!t.is_expired(), "Should not be expired immediately");
245 }
246
247 #[test]
248 fn test_expires_after_deadline() {
249 let t = QueryTimeout::new(Duration::from_millis(10));
250 thread::sleep(Duration::from_millis(50));
251 assert!(t.is_expired(), "Should be expired after deadline");
252 }
253
254 #[test]
255 fn test_remaining_positive_before_expiry() {
256 let t = QueryTimeout::new(Duration::from_secs(60));
257 assert!(
258 t.remaining().is_some(),
259 "Remaining should be Some before expiry"
260 );
261 }
262
263 #[test]
264 fn test_remaining_none_after_expiry() {
265 let t = QueryTimeout::new(Duration::from_millis(1));
266 thread::sleep(Duration::from_millis(30));
267 assert!(
268 t.remaining().is_none(),
269 "Remaining should be None after expiry"
270 );
271 }
272
273 #[test]
274 fn test_remaining_decreases_over_time() {
275 let t = QueryTimeout::new(Duration::from_secs(10));
276 let first = t.remaining().expect("first remaining");
277 thread::sleep(Duration::from_millis(50));
278 let second = t.remaining().expect("second remaining");
279 assert!(second < first, "Remaining should decrease");
280 }
281
282 #[test]
283 fn test_check_ok_before_expiry() {
284 let t = QueryTimeout::new(Duration::from_secs(60));
285 assert!(t.check().is_ok(), "check() should be Ok before expiry");
286 }
287
288 #[test]
289 fn test_check_timeout_after_expiry() {
290 let t = QueryTimeout::new(Duration::from_millis(10));
291 thread::sleep(Duration::from_millis(50));
292 match t.check() {
293 Err(TimeoutError::QueryTimeout { .. }) => {}
294 other => panic!("Expected QueryTimeout, got {other:?}"),
295 }
296 }
297
298 #[test]
301 fn test_cancel_not_cancelled_initially() {
302 let t = QueryTimeout::new(Duration::from_secs(60));
303 assert!(!t.cancelled.load(Ordering::Acquire));
304 }
305
306 #[test]
307 fn test_cancel_sets_flag() {
308 let t = QueryTimeout::new(Duration::from_secs(60));
309 t.cancel();
310 assert!(t.cancelled.load(Ordering::Acquire));
311 }
312
313 #[test]
314 fn test_check_returns_cancelled_after_cancel() {
315 let t = QueryTimeout::new(Duration::from_secs(60));
316 t.cancel();
317 match t.check() {
318 Err(TimeoutError::QueryCancelled) => {}
319 other => panic!("Expected QueryCancelled, got {other:?}"),
320 }
321 }
322
323 #[test]
324 fn test_cancel_takes_priority_over_expiry() {
325 let t = QueryTimeout::new(Duration::from_millis(1));
326 t.cancel();
327 thread::sleep(Duration::from_millis(20));
328 match t.check() {
330 Err(TimeoutError::QueryCancelled) => {}
331 other => panic!("Expected QueryCancelled, got {other:?}"),
332 }
333 }
334
335 #[test]
336 fn test_cancellation_handle_shares_flag() {
337 let t = QueryTimeout::new(Duration::from_secs(60));
338 let handle = t.cancellation_handle();
339 handle.store(true, Ordering::Release);
340 assert!(t.cancelled.load(Ordering::Acquire));
341 }
342
343 #[test]
344 fn test_cancellation_handle_from_another_thread() {
345 let t = Arc::new(QueryTimeout::new(Duration::from_secs(60)));
346 let handle = t.cancellation_handle();
347 let t2 = Arc::clone(&t);
348
349 let join = thread::spawn(move || {
350 thread::sleep(Duration::from_millis(10));
351 handle.store(true, Ordering::Release);
352 });
353
354 join.join().expect("thread panicked");
355 match t2.check() {
356 Err(TimeoutError::QueryCancelled) => {}
357 other => panic!("Expected QueryCancelled, got {other:?}"),
358 }
359 }
360
361 #[test]
364 fn test_config_default_values() {
365 let cfg = TimeoutConfig::default();
366 assert_eq!(cfg.default_timeout, Some(Duration::from_secs(30)));
367 assert_eq!(cfg.max_timeout, Duration::from_secs(300));
368 assert!(cfg.admin_bypass);
369 }
370
371 #[test]
372 fn test_config_admin_bypass_returns_none() {
373 let cfg = TimeoutConfig::default();
374 let result = cfg.resolve(Some(Duration::from_secs(10)), true);
375 assert!(result.is_none(), "Admin should bypass timeout");
376 }
377
378 #[test]
379 fn test_config_no_admin_bypass_applies_timeout() {
380 let cfg = TimeoutConfig {
381 admin_bypass: false,
382 ..Default::default()
383 };
384 let result = cfg.resolve(Some(Duration::from_secs(10)), true);
385 assert_eq!(result, Some(Duration::from_secs(10)));
386 }
387
388 #[test]
389 fn test_config_clamps_to_max() {
390 let cfg = TimeoutConfig::default();
391 let result = cfg.resolve(Some(Duration::from_secs(9999)), false);
392 assert_eq!(result, Some(Duration::from_secs(300)));
393 }
394
395 #[test]
396 fn test_config_uses_default_when_no_request() {
397 let cfg = TimeoutConfig::default();
398 let result = cfg.resolve(None, false);
399 assert_eq!(result, Some(Duration::from_secs(30)));
400 }
401
402 #[test]
403 fn test_config_none_default_no_request_returns_none() {
404 let cfg = TimeoutConfig {
405 default_timeout: None,
406 ..Default::default()
407 };
408 let result = cfg.resolve(None, false);
409 assert!(result.is_none());
410 }
411
412 #[test]
413 fn test_config_within_max_passes_through() {
414 let cfg = TimeoutConfig::default();
415 let result = cfg.resolve(Some(Duration::from_secs(60)), false);
416 assert_eq!(result, Some(Duration::from_secs(60)));
417 }
418
419 #[test]
422 fn test_parse_seconds_suffix() {
423 let d = parse_timeout_param("30s").expect("parse 30s");
424 assert_eq!(d, Duration::from_secs(30));
425 }
426
427 #[test]
428 fn test_parse_milliseconds_suffix() {
429 let d = parse_timeout_param("30000ms").expect("parse 30000ms");
430 assert_eq!(d, Duration::from_millis(30_000));
431 }
432
433 #[test]
434 fn test_parse_plain_integer_milliseconds() {
435 let d = parse_timeout_param("30000").expect("parse 30000");
436 assert_eq!(d, Duration::from_millis(30_000));
437 }
438
439 #[test]
440 fn test_parse_minutes_suffix() {
441 let d = parse_timeout_param("1m").expect("parse 1m");
442 assert_eq!(d, Duration::from_secs(60));
443 }
444
445 #[test]
446 fn test_parse_fractional_seconds() {
447 let d = parse_timeout_param("1.5s").expect("parse 1.5s");
448 assert_eq!(d, Duration::from_millis(1500));
449 }
450
451 #[test]
452 fn test_parse_iso8601_pt30s() {
453 let d = parse_timeout_param("PT30S").expect("parse PT30S");
454 assert_eq!(d, Duration::from_secs(30));
455 }
456
457 #[test]
458 fn test_parse_iso8601_lowercase() {
459 let d = parse_timeout_param("pt30s").expect("parse pt30s");
460 assert_eq!(d, Duration::from_secs(30));
461 }
462
463 #[test]
464 fn test_parse_zero_milliseconds() {
465 let d = parse_timeout_param("0ms").expect("parse 0ms");
466 assert_eq!(d, Duration::from_millis(0));
467 }
468
469 #[test]
470 fn test_parse_zero_seconds() {
471 let d = parse_timeout_param("0s").expect("parse 0s");
472 assert_eq!(d, Duration::ZERO);
473 }
474
475 #[test]
476 fn test_parse_empty_string_errors() {
477 assert!(parse_timeout_param("").is_err());
478 }
479
480 #[test]
481 fn test_parse_invalid_string_errors() {
482 assert!(parse_timeout_param("abc").is_err());
483 }
484
485 #[test]
486 fn test_parse_with_leading_whitespace() {
487 let d = parse_timeout_param(" 30s").expect("parse ' 30s'");
488 assert_eq!(d, Duration::from_secs(30));
489 }
490
491 #[test]
492 fn test_parse_large_ms_value() {
493 let d = parse_timeout_param("86400000ms").expect("parse 86400000ms");
494 assert_eq!(d, Duration::from_secs(86400));
495 }
496
497 #[test]
500 fn test_iterator_yields_all_items_before_timeout() {
501 let t = Arc::new(QueryTimeout::new(Duration::from_secs(60)));
502 let iter = TimeoutIterator::new(0..5, Arc::clone(&t), 100);
503 let results: Vec<_> = iter.collect();
504 assert_eq!(results.len(), 5);
505 for r in &results {
506 assert!(r.is_ok());
507 }
508 }
509
510 #[test]
511 fn test_iterator_stops_on_expiry() {
512 let t = Arc::new(QueryTimeout::new(Duration::from_millis(1)));
513 thread::sleep(Duration::from_millis(30));
514 let iter = TimeoutIterator::new(0..1000, Arc::clone(&t), 1);
515 let first = iter.into_iter().next();
517 match first {
518 Some(Err(TimeoutError::QueryTimeout { .. })) => {}
519 other => panic!("Expected QueryTimeout, got {other:?}"),
520 }
521 }
522
523 #[test]
524 fn test_iterator_stops_on_cancellation() {
525 let t = Arc::new(QueryTimeout::new(Duration::from_secs(60)));
526 t.cancel();
527 let iter = TimeoutIterator::new(0..1000, Arc::clone(&t), 1);
528 let first = iter.into_iter().next();
529 match first {
530 Some(Err(TimeoutError::QueryCancelled)) => {}
531 other => panic!("Expected QueryCancelled, got {other:?}"),
532 }
533 }
534
535 #[test]
536 fn test_iterator_checks_at_interval() {
537 let t = Arc::new(QueryTimeout::new(Duration::from_millis(1)));
540 thread::sleep(Duration::from_millis(30));
541 let iter = TimeoutIterator::new(0..100, Arc::clone(&t), 3);
542 let results: Vec<_> = iter.collect();
543 assert_eq!(results.len(), 3);
545 assert!(results[0].is_ok());
546 assert!(results[1].is_ok());
547 match &results[2] {
548 Err(TimeoutError::QueryTimeout { .. }) => {}
549 other => panic!("Expected QueryTimeout at index 2, got {other:?}"),
550 }
551 }
552
553 #[test]
554 fn test_iterator_empty_inner() {
555 let t = Arc::new(QueryTimeout::new(Duration::from_secs(60)));
556 let iter = TimeoutIterator::new(std::iter::empty::<i32>(), t, 1);
557 let results: Vec<_> = iter.collect();
558 assert!(results.is_empty());
559 }
560
561 #[test]
562 fn test_iterator_with_string_items() {
563 let t = Arc::new(QueryTimeout::new(Duration::from_secs(60)));
564 let data = vec!["alpha", "beta", "gamma"];
565 let iter = TimeoutIterator::new(data.into_iter(), t, 10);
566 let results: Vec<_> = iter.collect();
567 assert_eq!(results.len(), 3);
568 assert_eq!(*results[0].as_ref().unwrap(), "alpha");
569 }
570
571 #[test]
574 fn test_timeout_error_display_timeout() {
575 let e = TimeoutError::QueryTimeout { duration_ms: 5000 };
576 let msg = format!("{e}");
577 assert!(msg.contains("5000"));
578 }
579
580 #[test]
581 fn test_timeout_error_display_cancelled() {
582 let e = TimeoutError::QueryCancelled;
583 let msg = format!("{e}");
584 assert!(msg.to_lowercase().contains("cancel"));
585 }
586
587 #[test]
590 fn test_full_scenario_config_parse_enforce() {
591 let cfg = TimeoutConfig::default();
592 let effective = cfg.resolve(Some(Duration::from_secs(45)), false);
594 assert_eq!(effective, Some(Duration::from_secs(45)));
595
596 let t = QueryTimeout::new(effective.unwrap());
598 assert!(t.check().is_ok());
599 assert!(t.remaining().is_some());
600 }
601
602 #[test]
603 fn test_parse_then_new_timeout() {
604 let d = parse_timeout_param("500ms").expect("parse");
605 let t = QueryTimeout::new(d);
606 assert!(t.check().is_ok());
607 thread::sleep(Duration::from_millis(600));
608 assert!(t.is_expired());
609 }
610
611 #[test]
612 fn test_config_non_admin_uses_default_when_none_requested() {
613 let cfg = TimeoutConfig {
614 default_timeout: Some(Duration::from_secs(15)),
615 max_timeout: Duration::from_secs(60),
616 admin_bypass: true,
617 };
618 let result = cfg.resolve(None, false);
619 assert_eq!(result, Some(Duration::from_secs(15)));
620 }
621
622 #[test]
623 fn test_parse_2m() {
624 let d = parse_timeout_param("2m").expect("parse 2m");
625 assert_eq!(d, Duration::from_secs(120));
626 }
627
628 #[test]
629 fn test_parse_pt10s_float() {
630 let d = parse_timeout_param("PT10S").expect("parse PT10S");
631 assert_eq!(d, Duration::from_secs(10));
632 }
633
634 #[test]
635 fn test_iterator_interval_one_checks_every_item() {
636 let t = Arc::new(QueryTimeout::new(Duration::from_secs(60)));
637 let iter = TimeoutIterator::new(0..10, Arc::clone(&t), 1);
638 let results: Vec<_> = iter.collect();
639 assert_eq!(results.len(), 10);
640 for r in &results {
641 assert!(r.is_ok());
642 }
643 }
644
645 #[test]
646 fn test_timeout_duration_stored_correctly() {
647 let d = Duration::from_millis(250);
648 let t = QueryTimeout::new(d);
649 thread::sleep(Duration::from_millis(300));
651 match t.check() {
652 Err(TimeoutError::QueryTimeout { duration_ms }) => {
653 assert_eq!(duration_ms, 250);
654 }
655 other => panic!("Unexpected: {other:?}"),
656 }
657 }
658
659 #[test]
660 fn test_multiple_cancel_calls_idempotent() {
661 let t = QueryTimeout::new(Duration::from_secs(60));
662 t.cancel();
663 t.cancel();
664 t.cancel();
665 assert!(t.cancelled.load(Ordering::Acquire));
666 assert!(matches!(t.check(), Err(TimeoutError::QueryCancelled)));
667 }
668
669 #[test]
670 fn test_config_max_timeout_boundary() {
671 let cfg = TimeoutConfig {
672 default_timeout: Some(Duration::from_secs(10)),
673 max_timeout: Duration::from_secs(100),
674 admin_bypass: false,
675 };
676 assert_eq!(
678 cfg.resolve(Some(Duration::from_secs(100)), false),
679 Some(Duration::from_secs(100))
680 );
681 assert_eq!(
683 cfg.resolve(Some(Duration::from_millis(100_001)), false),
684 Some(Duration::from_secs(100))
685 );
686 }
687}