Skip to main content

AirLibrary/Resilience/
Timeout.rs

1//! Timeout management with cascading deadlines.
2//!
3//! `TimeoutManager` tracks a per-operation timeout and an optional global
4//! deadline. `effective_timeout()` returns the minimum of the two so every
5//! nested operation respects both the local and cascade budgets.
6//!
7//! All public methods have panic-safe variants (`Remaining`,
8//! `EffectiveTimeout`, `IsExceeded`) that catch panics via `catch_unwind` and
9//! return conservative fallback values so a transient panic never propagates
10//! into the caller.
11
12use std::time::{Duration, Instant};
13
14use crate::dev_log;
15
16/// Timeout manager with optional cascading global deadline.
17pub struct TimeoutManager {
18	global_deadline:Option<Instant>,
19
20	operation_timeout:Duration,
21}
22
23impl TimeoutManager {
24	/// Create with an operation-scoped timeout and no global deadline.
25	pub fn new(operation_timeout:Duration) -> Self { Self { global_deadline:None, operation_timeout } }
26
27	/// Create with both a global deadline and an operation timeout.
28	pub fn with_deadline(global_deadline:Instant, operation_timeout:Duration) -> Self {
29		Self { global_deadline:Some(global_deadline), operation_timeout }
30	}
31
32	/// Return an error if `timeout` is zero or exceeds one hour.
33	pub fn ValidateTimeout(timeout:Duration) -> Result<(), String> {
34		if timeout.is_zero() {
35			return Err("Timeout must be greater than 0".to_string());
36		}
37
38		if timeout.as_secs() > 3600 {
39			return Err("Timeout cannot exceed 1 hour".to_string());
40		}
41
42		Ok(())
43	}
44
45	/// Return `Ok(timeout)` or an error string; used by fallback paths.
46	pub fn ValidateTimeoutResult(timeout:Duration) -> Result<Duration, String> {
47		if timeout.is_zero() {
48			return Err("Timeout must be greater than 0".to_string());
49		}
50
51		if timeout.as_secs() > 3600 {
52			return Err("Timeout cannot exceed 1 hour".to_string());
53		}
54
55		Ok(timeout)
56	}
57
58	/// Time remaining until the global deadline, or `None` if none is set.
59	pub fn remaining(&self) -> Option<Duration> {
60		self.global_deadline.map(|deadline| {
61			deadline
62				.checked_duration_since(Instant::now())
63				.unwrap_or(Duration::from_secs(0))
64		})
65	}
66
67	/// Panic-safe `remaining()`. Returns `None` on panic (fail-open).
68	pub fn Remaining(&self) -> Option<Duration> {
69		std::panic::catch_unwind(|| self.remaining()).unwrap_or_else(|e| {
70			dev_log!("resilience", "error: [TimeoutManager] Panic in Remaining: {:?}", e);
71
72			None
73		})
74	}
75
76	/// Minimum of `operation_timeout` and remaining deadline time.
77	pub fn effective_timeout(&self) -> Duration {
78		match self.remaining() {
79			Some(remaining) => self.operation_timeout.min(remaining),
80
81			None => self.operation_timeout,
82		}
83	}
84
85	/// Panic-safe `effective_timeout()`. Falls back to 30 s on invalid/panic.
86	pub fn EffectiveTimeout(&self) -> Duration {
87		std::panic::catch_unwind(|| {
88			let timeout = self.effective_timeout();
89
90			match Self::ValidateTimeoutResult(timeout) {
91				Ok(valid_timeout) => valid_timeout,
92				Err(_) => Duration::from_secs(30),
93			}
94		})
95		.unwrap_or_else(|e| {
96			dev_log!("resilience", "error: [TimeoutManager] Panic in EffectiveTimeout: {:?}", e);
97
98			Duration::from_secs(30)
99		})
100	}
101
102	/// `true` when the global deadline has passed.
103	pub fn is_exceeded(&self) -> bool { self.global_deadline.map_or(false, |deadline| Instant::now() >= deadline) }
104
105	/// Panic-safe `is_exceeded()`. Returns `true` on panic (fail-safe).
106	pub fn IsExceeded(&self) -> bool {
107		std::panic::catch_unwind(|| self.is_exceeded()).unwrap_or_else(|e| {
108			dev_log!("resilience", "error: [TimeoutManager] Panic in IsExceeded: {:?}", e);
109
110			true
111		})
112	}
113
114	pub fn GetGlobalDeadline(&self) -> Option<Instant> { self.global_deadline }
115
116	pub fn GetOperationTimeout(&self) -> Duration { self.operation_timeout }
117}