rustkernel_core/resilience/
timeout.rs1use super::{ResilienceError, ResilienceResult};
27use serde::{Deserialize, Serialize};
28use std::time::{Duration, Instant};
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct TimeoutConfig {
33 pub default_timeout: Duration,
35 pub max_timeout: Duration,
37 pub propagate_deadline: bool,
39 pub include_queue_time: bool,
41}
42
43impl Default for TimeoutConfig {
44 fn default() -> Self {
45 Self {
46 default_timeout: Duration::from_secs(30),
47 max_timeout: Duration::from_secs(300),
48 propagate_deadline: true,
49 include_queue_time: false,
50 }
51 }
52}
53
54impl TimeoutConfig {
55 pub fn production() -> Self {
57 Self {
58 default_timeout: Duration::from_secs(60),
59 max_timeout: Duration::from_secs(600),
60 propagate_deadline: true,
61 include_queue_time: true,
62 }
63 }
64
65 pub fn development() -> Self {
67 Self {
68 default_timeout: Duration::from_secs(300),
69 max_timeout: Duration::from_secs(3600),
70 propagate_deadline: false,
71 include_queue_time: false,
72 }
73 }
74
75 pub fn default_timeout(mut self, timeout: Duration) -> Self {
77 self.default_timeout = timeout;
78 self
79 }
80
81 pub fn max_timeout(mut self, timeout: Duration) -> Self {
83 self.max_timeout = timeout;
84 self
85 }
86
87 pub fn propagate_deadline(mut self, propagate: bool) -> Self {
89 self.propagate_deadline = propagate;
90 self
91 }
92
93 pub fn include_queue_time(mut self, include: bool) -> Self {
95 self.include_queue_time = include;
96 self
97 }
98
99 pub fn clamp(&self, timeout: Duration) -> Duration {
101 timeout.min(self.max_timeout)
102 }
103}
104
105#[derive(Debug, thiserror::Error)]
107pub enum TimeoutError {
108 #[error("Operation timed out after {timeout:?}")]
110 Timeout {
111 timeout: Duration,
113 },
114
115 #[error("Deadline exceeded (remaining: {remaining:?})")]
117 DeadlineExceeded {
118 remaining: Duration,
120 },
121
122 #[error("Invalid timeout: {reason}")]
124 Invalid {
125 reason: String,
127 },
128}
129
130#[derive(Debug, Clone)]
132pub struct DeadlineContext {
133 deadline: Instant,
135 original_timeout: Duration,
137 created_at: Instant,
139}
140
141impl DeadlineContext {
142 pub fn new(timeout: Duration) -> Self {
144 let now = Instant::now();
145 Self {
146 deadline: now + timeout,
147 original_timeout: timeout,
148 created_at: now,
149 }
150 }
151
152 pub fn from_deadline(deadline: Instant) -> Self {
154 let now = Instant::now();
155 let remaining = deadline.saturating_duration_since(now);
156 Self {
157 deadline,
158 original_timeout: remaining,
159 created_at: now,
160 }
161 }
162
163 pub fn remaining(&self) -> Duration {
165 self.deadline.saturating_duration_since(Instant::now())
166 }
167
168 pub fn is_expired(&self) -> bool {
170 Instant::now() >= self.deadline
171 }
172
173 pub fn original_timeout(&self) -> Duration {
175 self.original_timeout
176 }
177
178 pub fn elapsed(&self) -> Duration {
180 self.created_at.elapsed()
181 }
182
183 pub fn child(&self) -> Self {
185 Self {
186 deadline: self.deadline,
187 original_timeout: self.remaining(),
188 created_at: Instant::now(),
189 }
190 }
191
192 pub fn child_with_timeout(&self, max_timeout: Duration) -> Self {
194 let remaining = self.remaining();
195 let timeout = remaining.min(max_timeout);
196 Self {
197 deadline: Instant::now() + timeout,
198 original_timeout: timeout,
199 created_at: Instant::now(),
200 }
201 }
202
203 pub async fn execute<F, Fut, T, E>(&self, f: F) -> ResilienceResult<T>
205 where
206 F: FnOnce() -> Fut,
207 Fut: std::future::Future<Output = Result<T, E>>,
208 E: Into<crate::error::KernelError>,
209 {
210 if self.is_expired() {
211 return Err(ResilienceError::DeadlineExceeded);
212 }
213
214 let remaining = self.remaining();
215 match tokio::time::timeout(remaining, f()).await {
216 Ok(Ok(result)) => Ok(result),
217 Ok(Err(e)) => Err(ResilienceError::KernelError(e.into())),
218 Err(_elapsed) => Err(ResilienceError::Timeout { timeout: remaining }),
219 }
220 }
221
222 pub fn check(&self) -> ResilienceResult<()> {
224 if self.is_expired() {
225 Err(ResilienceError::DeadlineExceeded)
226 } else {
227 Ok(())
228 }
229 }
230}
231
232pub struct TimeoutGuard {
234 start: Instant,
235 timeout: Duration,
236 name: String,
237}
238
239impl TimeoutGuard {
240 pub fn new(name: impl Into<String>, timeout: Duration) -> Self {
242 Self {
243 start: Instant::now(),
244 timeout,
245 name: name.into(),
246 }
247 }
248
249 pub fn elapsed(&self) -> Duration {
251 self.start.elapsed()
252 }
253
254 pub fn is_exceeded(&self) -> bool {
256 self.elapsed() > self.timeout
257 }
258
259 pub fn remaining(&self) -> Duration {
261 self.timeout.saturating_sub(self.elapsed())
262 }
263
264 pub fn check(&self) -> ResilienceResult<()> {
266 if self.is_exceeded() {
267 tracing::warn!(
268 name = %self.name,
269 elapsed = ?self.elapsed(),
270 timeout = ?self.timeout,
271 "Timeout exceeded"
272 );
273 Err(ResilienceError::Timeout {
274 timeout: self.timeout,
275 })
276 } else {
277 Ok(())
278 }
279 }
280}
281
282impl Drop for TimeoutGuard {
283 fn drop(&mut self) {
284 let elapsed = self.elapsed();
285 if elapsed > self.timeout {
286 tracing::warn!(
287 name = %self.name,
288 elapsed = ?elapsed,
289 timeout = ?self.timeout,
290 "Operation exceeded timeout"
291 );
292 } else if elapsed > self.timeout / 2 {
293 tracing::debug!(
294 name = %self.name,
295 elapsed = ?elapsed,
296 timeout = ?self.timeout,
297 "Operation took >50% of timeout"
298 );
299 }
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306
307 #[test]
308 fn test_timeout_config() {
309 let config = TimeoutConfig::default()
310 .default_timeout(Duration::from_secs(60))
311 .max_timeout(Duration::from_secs(120));
312
313 assert_eq!(config.default_timeout, Duration::from_secs(60));
314 assert_eq!(config.max_timeout, Duration::from_secs(120));
315
316 assert_eq!(
318 config.clamp(Duration::from_secs(300)),
319 Duration::from_secs(120)
320 );
321 }
322
323 #[test]
324 fn test_deadline_context() {
325 let ctx = DeadlineContext::new(Duration::from_secs(10));
326 assert!(!ctx.is_expired());
327 assert!(ctx.remaining() <= Duration::from_secs(10));
328 }
329
330 #[test]
331 fn test_deadline_child() {
332 let parent = DeadlineContext::new(Duration::from_secs(10));
333 let child = parent.child();
334
335 let parent_remaining = parent.remaining();
338 let child_remaining = child.remaining();
339
340 let tolerance = Duration::from_millis(100);
342 assert!(
343 child_remaining <= parent_remaining + tolerance,
344 "Child remaining {:?} should be <= parent {:?} + tolerance {:?}",
345 child_remaining,
346 parent_remaining,
347 tolerance
348 );
349 }
350
351 #[test]
352 fn test_timeout_guard() {
353 let guard = TimeoutGuard::new("test", Duration::from_secs(10));
354 assert!(!guard.is_exceeded());
355 assert!(guard.remaining() <= Duration::from_secs(10));
356 }
357
358 #[tokio::test]
359 async fn test_deadline_expired() {
360 let ctx = DeadlineContext::new(Duration::from_nanos(1));
361 std::thread::sleep(Duration::from_millis(1));
362
363 assert!(ctx.is_expired());
364 assert!(ctx.check().is_err());
365 }
366}