rustkernel_core/resilience/
timeout.rs

1//! Timeout and Deadline Management
2//!
3//! Provides timeout enforcement and deadline propagation for kernel execution.
4//!
5//! # Features
6//!
7//! - Per-kernel timeout configuration
8//! - Deadline propagation in K2K chains
9//! - Cancellation token support
10//!
11//! # Example
12//!
13//! ```rust,ignore
14//! use rustkernel_core::resilience::timeout::{TimeoutConfig, DeadlineContext};
15//!
16//! let config = TimeoutConfig::default()
17//!     .default_timeout(Duration::from_secs(30))
18//!     .max_timeout(Duration::from_secs(300));
19//!
20//! let deadline = DeadlineContext::new(Duration::from_secs(10));
21//! let result = deadline.execute(|| async {
22//!     kernel.execute(input).await
23//! }).await?;
24//! ```
25
26use super::{ResilienceError, ResilienceResult};
27use serde::{Deserialize, Serialize};
28use std::time::{Duration, Instant};
29
30/// Timeout configuration
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct TimeoutConfig {
33    /// Default timeout for kernel execution
34    pub default_timeout: Duration,
35    /// Maximum allowed timeout
36    pub max_timeout: Duration,
37    /// Enable deadline propagation in K2K chains
38    pub propagate_deadline: bool,
39    /// Include queue wait time in timeout
40    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    /// Production configuration
56    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    /// Development configuration
66    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    /// Set default timeout
76    pub fn default_timeout(mut self, timeout: Duration) -> Self {
77        self.default_timeout = timeout;
78        self
79    }
80
81    /// Set max timeout
82    pub fn max_timeout(mut self, timeout: Duration) -> Self {
83        self.max_timeout = timeout;
84        self
85    }
86
87    /// Enable deadline propagation
88    pub fn propagate_deadline(mut self, propagate: bool) -> Self {
89        self.propagate_deadline = propagate;
90        self
91    }
92
93    /// Include queue time in timeout
94    pub fn include_queue_time(mut self, include: bool) -> Self {
95        self.include_queue_time = include;
96        self
97    }
98
99    /// Clamp a timeout to the configured maximum
100    pub fn clamp(&self, timeout: Duration) -> Duration {
101        timeout.min(self.max_timeout)
102    }
103}
104
105/// Timeout error
106#[derive(Debug, thiserror::Error)]
107pub enum TimeoutError {
108    /// Operation timed out
109    #[error("Operation timed out after {timeout:?}")]
110    Timeout {
111        /// The timeout duration that was exceeded
112        timeout: Duration,
113    },
114
115    /// Deadline exceeded
116    #[error("Deadline exceeded (remaining: {remaining:?})")]
117    DeadlineExceeded {
118        /// The remaining time when the deadline was exceeded
119        remaining: Duration,
120    },
121
122    /// Invalid timeout value
123    #[error("Invalid timeout: {reason}")]
124    Invalid {
125        /// The reason the timeout value is invalid
126        reason: String,
127    },
128}
129
130/// Deadline context for propagating deadlines
131#[derive(Debug, Clone)]
132pub struct DeadlineContext {
133    /// Absolute deadline
134    deadline: Instant,
135    /// Original timeout
136    original_timeout: Duration,
137    /// When the context was created
138    created_at: Instant,
139}
140
141impl DeadlineContext {
142    /// Create a new deadline context
143    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    /// Create from an absolute deadline
153    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    /// Get remaining time until deadline
164    pub fn remaining(&self) -> Duration {
165        self.deadline.saturating_duration_since(Instant::now())
166    }
167
168    /// Check if deadline has passed
169    pub fn is_expired(&self) -> bool {
170        Instant::now() >= self.deadline
171    }
172
173    /// Get the original timeout
174    pub fn original_timeout(&self) -> Duration {
175        self.original_timeout
176    }
177
178    /// Get elapsed time since context creation
179    pub fn elapsed(&self) -> Duration {
180        self.created_at.elapsed()
181    }
182
183    /// Create a child context with the same deadline
184    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    /// Create a child context with a reduced timeout
193    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    /// Execute a future with this deadline
204    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    /// Check deadline and return error if exceeded
223    pub fn check(&self) -> ResilienceResult<()> {
224        if self.is_expired() {
225            Err(ResilienceError::DeadlineExceeded)
226        } else {
227            Ok(())
228        }
229    }
230}
231
232/// Timeout guard for tracking execution time
233pub struct TimeoutGuard {
234    start: Instant,
235    timeout: Duration,
236    name: String,
237}
238
239impl TimeoutGuard {
240    /// Create a new timeout guard
241    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    /// Get elapsed time
250    pub fn elapsed(&self) -> Duration {
251        self.start.elapsed()
252    }
253
254    /// Check if timeout is exceeded
255    pub fn is_exceeded(&self) -> bool {
256        self.elapsed() > self.timeout
257    }
258
259    /// Get remaining time
260    pub fn remaining(&self) -> Duration {
261        self.timeout.saturating_sub(self.elapsed())
262    }
263
264    /// Check and log if exceeded
265    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        // Test clamping
317        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        // Child inherits the same deadline as parent, so remaining times should be
336        // approximately equal (with small tolerance for execution time)
337        let parent_remaining = parent.remaining();
338        let child_remaining = child.remaining();
339
340        // Allow 100ms tolerance for test execution time
341        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}