Skip to main content

grafton_visca/
timeout.rs

1//! Unified timeout configuration and management for VISCA commands.
2//!
3//! This module provides configurable timeout support for different categories of VISCA commands,
4//! allowing fine-tuned control over command execution timeouts based on the expected duration
5//! of each operation type. It also includes socket-level timeout management to prevent
6//! duplication across transport implementations.
7
8use std::{
9    io,
10    net::{TcpStream, UdpSocket},
11    sync::MutexGuard,
12    time::{Duration, Instant},
13};
14
15use crate::Error;
16
17/// Categories of VISCA commands with different timeout requirements.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19#[non_exhaustive]
20pub enum CommandCategory {
21    /// Quick commands like inquiry, power status (1-2 seconds).
22    Quick,
23    /// Movement commands like pan/tilt/zoom (5-10 seconds).
24    Movement,
25    /// Preset operations like recall/save (30-60 seconds).
26    Preset,
27    /// Long operations like preset discovery (2-5 minutes).
28    LongRunning,
29    /// Network commands like multicast/Ndi settings (1-2 seconds).
30    Network,
31    /// Custom timeout for specific commands.
32    Custom,
33}
34
35impl CommandCategory {
36    /// Returns the default timeout for this category.
37    #[must_use]
38    pub const fn default_timeout(&self) -> Duration {
39        match self {
40            Self::Quick => Duration::from_secs(5), // Increased from 2s for network delays
41            Self::Movement => Duration::from_secs(30), // Increased from 10s for full-range movements
42            Self::Preset => Duration::from_secs(60), // Reduced from 90s to prevent excessive waits
43            Self::LongRunning => Duration::from_secs(300), // Keep at 5 minutes for discovery
44            Self::Network => Duration::from_secs(5), // Increased from 2s for network operations
45            Self::Custom => Duration::from_secs(60), // Increased from 30s as general fallback
46        }
47    }
48}
49
50/// Configuration for command timeouts.
51#[derive(Debug, Copy, Clone)]
52#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
53#[cfg_attr(feature = "serde", serde_with::serde_as)]
54#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
55pub struct TimeoutConfig {
56    /// Timeout for ACK responses from the camera (default 500ms)
57    #[cfg_attr(
58        feature = "serde",
59        serde_as(as = "serde_with::DurationMilliSeconds<u64>")
60    )]
61    #[cfg_attr(
62        feature = "schemars",
63        schemars(with = "u64", description = "Timeout in milliseconds")
64    )]
65    pub ack_timeout: Duration,
66    /// Timeout for quick commands (inquiry, power status)
67    #[cfg_attr(
68        feature = "serde",
69        serde_as(as = "serde_with::DurationMilliSeconds<u64>")
70    )]
71    #[cfg_attr(
72        feature = "schemars",
73        schemars(with = "u64", description = "Timeout in milliseconds")
74    )]
75    pub quick_timeout: Duration,
76    /// Timeout for movement commands (pan/tilt/zoom)
77    #[cfg_attr(
78        feature = "serde",
79        serde_as(as = "serde_with::DurationMilliSeconds<u64>")
80    )]
81    #[cfg_attr(
82        feature = "schemars",
83        schemars(with = "u64", description = "Timeout in milliseconds")
84    )]
85    pub movement_timeout: Duration,
86    /// Timeout for preset operations
87    #[cfg_attr(
88        feature = "serde",
89        serde_as(as = "serde_with::DurationMilliSeconds<u64>")
90    )]
91    #[cfg_attr(
92        feature = "schemars",
93        schemars(with = "u64", description = "Timeout in milliseconds")
94    )]
95    pub preset_timeout: Duration,
96    /// Timeout for long-running operations
97    #[cfg_attr(
98        feature = "serde",
99        serde_as(as = "serde_with::DurationMilliSeconds<u64>")
100    )]
101    #[cfg_attr(
102        feature = "schemars",
103        schemars(with = "u64", description = "Timeout in milliseconds")
104    )]
105    pub long_timeout: Duration,
106    /// Timeout for network commands (multicast, Ndi)
107    #[cfg_attr(
108        feature = "serde",
109        serde_as(as = "serde_with::DurationMilliSeconds<u64>")
110    )]
111    #[cfg_attr(
112        feature = "schemars",
113        schemars(with = "u64", description = "Timeout in milliseconds")
114    )]
115    pub network_timeout: Duration,
116    /// Default timeout for uncategorized commands
117    #[cfg_attr(
118        feature = "serde",
119        serde_as(as = "serde_with::DurationMilliSeconds<u64>")
120    )]
121    #[cfg_attr(
122        feature = "schemars",
123        schemars(with = "u64", description = "Timeout in milliseconds")
124    )]
125    pub default_timeout: Duration,
126}
127
128impl Default for TimeoutConfig {
129    fn default() -> Self {
130        Self {
131            ack_timeout: Duration::from_millis(500), // Default ACK timeout as specified in epic
132            quick_timeout: Duration::from_secs(5),   // Increased for network delays
133            movement_timeout: Duration::from_secs(30), // Increased for full-range movements
134            preset_timeout: Duration::from_secs(60), // Reduced from 90s to prevent excessive waits
135            long_timeout: Duration::from_secs(300),  // Keep at 5 minutes for discovery
136            network_timeout: Duration::from_secs(5), // Increased for network operations
137            default_timeout: Duration::from_secs(60), // Increased as general fallback
138        }
139    }
140}
141
142impl TimeoutConfig {
143    /// Creates a new timeout configuration with all timeouts set to the same value.
144    #[must_use]
145    pub const fn uniform(timeout: Duration) -> Self {
146        Self {
147            ack_timeout: timeout,
148            quick_timeout: timeout,
149            movement_timeout: timeout,
150            preset_timeout: timeout,
151            long_timeout: timeout,
152            network_timeout: timeout,
153            default_timeout: timeout,
154        }
155    }
156
157    /// Gets the timeout for a specific command category.
158    #[must_use]
159    pub const fn get_timeout(&self, category: CommandCategory) -> Duration {
160        match category {
161            CommandCategory::Quick => self.quick_timeout,
162            CommandCategory::Movement => self.movement_timeout,
163            CommandCategory::Preset => self.preset_timeout,
164            CommandCategory::LongRunning => self.long_timeout,
165            CommandCategory::Network => self.network_timeout,
166            CommandCategory::Custom => self.default_timeout,
167        }
168    }
169
170    /// Creates a builder for timeout configuration.
171    #[must_use]
172    pub fn builder() -> TimeoutConfigBuilder {
173        TimeoutConfigBuilder::default()
174    }
175}
176
177/// Builder for creating custom timeout configurations.
178#[derive(Debug, Clone, Copy, Default)]
179pub struct TimeoutConfigBuilder {
180    config: TimeoutConfig,
181}
182
183impl TimeoutConfigBuilder {
184    /// Sets the timeout for ACK responses.
185    #[allow(clippy::missing_const_for_fn)]
186    #[must_use]
187    pub fn ack_timeout(mut self, timeout: Duration) -> Self {
188        self.config.ack_timeout = timeout;
189        self
190    }
191
192    /// Sets the timeout for quick commands.
193    #[allow(clippy::missing_const_for_fn)]
194    #[must_use]
195    pub fn quick_timeout(mut self, timeout: Duration) -> Self {
196        self.config.quick_timeout = timeout;
197        self
198    }
199
200    /// Sets the timeout for movement commands.
201    #[allow(clippy::missing_const_for_fn)]
202    #[must_use]
203    pub fn movement_timeout(mut self, timeout: Duration) -> Self {
204        self.config.movement_timeout = timeout;
205        self
206    }
207
208    /// Sets the timeout for preset operations.
209    #[allow(clippy::missing_const_for_fn)]
210    #[must_use]
211    pub fn preset_timeout(mut self, timeout: Duration) -> Self {
212        self.config.preset_timeout = timeout;
213        self
214    }
215
216    /// Sets the timeout for long-running operations.
217    #[allow(clippy::missing_const_for_fn)]
218    #[must_use]
219    pub fn long_timeout(mut self, timeout: Duration) -> Self {
220        self.config.long_timeout = timeout;
221        self
222    }
223
224    /// Sets the timeout for network commands.
225    #[allow(clippy::missing_const_for_fn)]
226    #[must_use]
227    pub fn network_timeout(mut self, timeout: Duration) -> Self {
228        self.config.network_timeout = timeout;
229        self
230    }
231
232    /// Sets the default timeout for uncategorized commands.
233    #[allow(clippy::missing_const_for_fn)]
234    #[must_use]
235    pub fn default_timeout(mut self, timeout: Duration) -> Self {
236        self.config.default_timeout = timeout;
237        self
238    }
239
240    /// Builds the timeout configuration.
241    #[must_use]
242    pub const fn build(self) -> TimeoutConfig {
243        self.config
244    }
245}
246
247#[cfg(test)]
248#[allow(clippy::expect_used)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn test_command_category_defaults() {
254        assert_eq!(
255            CommandCategory::Quick.default_timeout(),
256            Duration::from_secs(5)
257        );
258        assert_eq!(
259            CommandCategory::Movement.default_timeout(),
260            Duration::from_secs(30)
261        );
262        assert_eq!(
263            CommandCategory::Preset.default_timeout(),
264            Duration::from_secs(60)
265        );
266        assert_eq!(
267            CommandCategory::LongRunning.default_timeout(),
268            Duration::from_secs(300)
269        );
270        assert_eq!(
271            CommandCategory::Network.default_timeout(),
272            Duration::from_secs(5)
273        );
274        assert_eq!(
275            CommandCategory::Custom.default_timeout(),
276            Duration::from_secs(60)
277        );
278    }
279
280    #[test]
281    fn test_timeout_config_default() {
282        let config = TimeoutConfig::default();
283        assert_eq!(config.ack_timeout, Duration::from_millis(500));
284        assert_eq!(config.quick_timeout, Duration::from_secs(5));
285        assert_eq!(config.movement_timeout, Duration::from_secs(30));
286        assert_eq!(config.preset_timeout, Duration::from_secs(60));
287        assert_eq!(config.long_timeout, Duration::from_secs(300));
288        assert_eq!(config.network_timeout, Duration::from_secs(5));
289        assert_eq!(config.default_timeout, Duration::from_secs(60));
290    }
291
292    #[test]
293    fn test_timeout_config_uniform() {
294        let timeout = Duration::from_secs(5);
295        let config = TimeoutConfig::uniform(timeout);
296        assert_eq!(config.ack_timeout, timeout);
297        assert_eq!(config.quick_timeout, timeout);
298        assert_eq!(config.movement_timeout, timeout);
299        assert_eq!(config.preset_timeout, timeout);
300        assert_eq!(config.long_timeout, timeout);
301        assert_eq!(config.network_timeout, timeout);
302        assert_eq!(config.default_timeout, timeout);
303    }
304
305    #[test]
306    fn test_get_timeout() {
307        let config = TimeoutConfig::default();
308        assert_eq!(
309            config.get_timeout(CommandCategory::Quick),
310            Duration::from_secs(5)
311        );
312        assert_eq!(
313            config.get_timeout(CommandCategory::Movement),
314            Duration::from_secs(30)
315        );
316        assert_eq!(
317            config.get_timeout(CommandCategory::Preset),
318            Duration::from_secs(60)
319        );
320        assert_eq!(
321            config.get_timeout(CommandCategory::LongRunning),
322            Duration::from_secs(300)
323        );
324        assert_eq!(
325            config.get_timeout(CommandCategory::Network),
326            Duration::from_secs(5)
327        );
328        assert_eq!(
329            config.get_timeout(CommandCategory::Custom),
330            Duration::from_secs(60)
331        );
332    }
333
334    #[test]
335    fn test_timeout_config_builder() {
336        let config = TimeoutConfig::builder()
337            .quick_timeout(Duration::from_secs(1))
338            .movement_timeout(Duration::from_secs(5))
339            .preset_timeout(Duration::from_secs(30))
340            .long_timeout(Duration::from_secs(120))
341            .default_timeout(Duration::from_secs(15))
342            .build();
343
344        assert_eq!(config.quick_timeout, Duration::from_secs(1));
345        assert_eq!(config.movement_timeout, Duration::from_secs(5));
346        assert_eq!(config.preset_timeout, Duration::from_secs(30));
347        assert_eq!(config.long_timeout, Duration::from_secs(120));
348        assert_eq!(config.default_timeout, Duration::from_secs(15));
349    }
350}
351
352/// A trait for managing timeouts on socket operations.
353///
354/// This trait provides a consistent interface for saving, setting, and
355/// restoring timeout values across different socket types.
356pub trait TimeoutManager {
357    /// Execute a function with a temporary timeout.
358    ///
359    /// This method saves the current timeout, sets a new timeout for the
360    /// duration of the function execution, then restores the original timeout.
361    ///
362    /// # Arguments
363    ///
364    /// * `timeout` - The timeout duration to use
365    /// * `f` - The function to execute with the timeout
366    ///
367    /// # Returns
368    ///
369    /// The result of the function execution.
370    ///
371    /// # Errors
372    ///
373    /// Returns an error if:
374    /// - Getting or setting timeouts fails
375    /// - The provided function returns an error
376    fn with_timeout<F, R>(&mut self, timeout: Duration, f: F) -> Result<R, Error>
377    where
378        F: FnOnce(&mut Self) -> Result<R, Error>;
379
380    /// Get the current read timeout.
381    fn get_read_timeout(&self) -> io::Result<Option<Duration>>;
382
383    /// Set the read timeout.
384    fn set_read_timeout(&mut self, timeout: Option<Duration>) -> io::Result<()>;
385
386    /// Get the current write timeout.
387    fn get_write_timeout(&self) -> io::Result<Option<Duration>>;
388
389    /// Set the write timeout.
390    fn set_write_timeout(&mut self, timeout: Option<Duration>) -> io::Result<()>;
391
392    /// Execute a function with a temporary read timeout.
393    ///
394    /// This is a convenience method that specifically manages read timeouts.
395    fn with_read_timeout<F, R>(&mut self, timeout: Duration, f: F) -> Result<R, Error>
396    where
397        F: FnOnce(&mut Self) -> Result<R, Error>,
398    {
399        let original_timeout = self.get_read_timeout()?;
400        self.set_read_timeout(Some(timeout))?;
401        let result = f(self);
402        self.set_read_timeout(original_timeout)?;
403
404        result
405    }
406
407    /// Execute a function with a temporary write timeout.
408    ///
409    /// This is a convenience method that specifically manages write timeouts.
410    fn with_write_timeout<F, R>(&mut self, timeout: Duration, f: F) -> Result<R, Error>
411    where
412        F: FnOnce(&mut Self) -> Result<R, Error>,
413    {
414        let original_timeout = self.get_write_timeout()?;
415        self.set_write_timeout(Some(timeout))?;
416        let result = f(self);
417        self.set_write_timeout(original_timeout)?;
418
419        result
420    }
421}
422
423/// Implement TimeoutManager for TcpStream.
424impl TimeoutManager for TcpStream {
425    fn with_timeout<F, R>(&mut self, timeout: Duration, f: F) -> Result<R, Error>
426    where
427        F: FnOnce(&mut Self) -> Result<R, Error>,
428    {
429        self.with_read_timeout(timeout, f)
430    }
431
432    fn get_read_timeout(&self) -> io::Result<Option<Duration>> {
433        self.read_timeout()
434    }
435
436    fn set_read_timeout(&mut self, timeout: Option<Duration>) -> io::Result<()> {
437        TcpStream::set_read_timeout(self, timeout)
438    }
439
440    fn get_write_timeout(&self) -> io::Result<Option<Duration>> {
441        self.write_timeout()
442    }
443
444    fn set_write_timeout(&mut self, timeout: Option<Duration>) -> io::Result<()> {
445        TcpStream::set_write_timeout(self, timeout)
446    }
447}
448
449/// Implement TimeoutManager for UdpSocket.
450impl TimeoutManager for UdpSocket {
451    fn with_timeout<F, R>(&mut self, timeout: Duration, f: F) -> Result<R, Error>
452    where
453        F: FnOnce(&mut Self) -> Result<R, Error>,
454    {
455        self.with_read_timeout(timeout, f)
456    }
457
458    fn get_read_timeout(&self) -> io::Result<Option<Duration>> {
459        self.read_timeout()
460    }
461
462    fn set_read_timeout(&mut self, timeout: Option<Duration>) -> io::Result<()> {
463        UdpSocket::set_read_timeout(self, timeout)
464    }
465
466    fn get_write_timeout(&self) -> io::Result<Option<Duration>> {
467        self.write_timeout()
468    }
469
470    fn set_write_timeout(&mut self, timeout: Option<Duration>) -> io::Result<()> {
471        UdpSocket::set_write_timeout(self, timeout)
472    }
473}
474
475/// A helper struct to manage timeouts on a socket within a closure.
476///
477/// This struct ensures that timeouts are properly restored even if the
478/// operation fails or panics.
479pub struct TimeoutGuard<'a, T: TimeoutManager> {
480    socket: &'a mut T,
481    original_read_timeout: Option<Duration>,
482    original_write_timeout: Option<Duration>,
483    restored: bool,
484}
485
486impl<T: TimeoutManager> std::fmt::Debug for TimeoutGuard<'_, T> {
487    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
488        f.debug_struct("TimeoutGuard")
489            .field("original_read_timeout", &self.original_read_timeout)
490            .field("original_write_timeout", &self.original_write_timeout)
491            .field("restored", &self.restored)
492            .finish()
493    }
494}
495
496impl<'a, T: TimeoutManager> TimeoutGuard<'a, T> {
497    /// Create a new timeout guard with specified timeouts.
498    pub fn new(
499        socket: &'a mut T,
500        read_timeout: Option<Duration>,
501        write_timeout: Option<Duration>,
502    ) -> Result<Self, Error> {
503        let original_read_timeout = socket.get_read_timeout()?;
504        let original_write_timeout = socket.get_write_timeout()?;
505
506        if let Some(timeout) = read_timeout {
507            socket.set_read_timeout(Some(timeout))?;
508        }
509
510        if let Some(timeout) = write_timeout {
511            socket.set_write_timeout(Some(timeout))?;
512        }
513
514        Ok(Self {
515            socket,
516            original_read_timeout,
517            original_write_timeout,
518            restored: false,
519        })
520    }
521
522    /// Restore the original timeouts.
523    pub fn restore(&mut self) -> Result<(), Error> {
524        if !self.restored {
525            self.socket.set_read_timeout(self.original_read_timeout)?;
526            self.socket.set_write_timeout(self.original_write_timeout)?;
527            self.restored = true;
528        }
529        Ok(())
530    }
531}
532
533impl<T: TimeoutManager> Drop for TimeoutGuard<'_, T> {
534    fn drop(&mut self) {
535        // Best effort to restore timeouts
536        let _ = self.restore();
537    }
538}
539
540/// Execute a function with a temporary timeout on a MutexGuard-wrapped socket.
541///
542/// This is a utility function for working with sockets that are protected
543/// by a Mutex, which is common in the transport implementations.
544pub fn with_timeout_on_guard<T, F, R>(
545    guard: &mut MutexGuard<'_, T>,
546    timeout: Duration,
547    f: F,
548) -> Result<R, Error>
549where
550    T: TimeoutManager,
551    F: FnOnce(&mut T) -> Result<R, Error>,
552{
553    guard.with_read_timeout(timeout, f)
554}
555
556/// Deadline for timeout operations.
557///
558/// Provides a consistent way to calculate and check deadlines across
559/// different operation types in the library.
560///
561/// # Clock-Agnostic Design
562///
563/// This type supports both wall-clock and executor-driven time sources.
564/// In async code, prefer the `*_at` variants that accept an explicit `now`
565/// parameter to ensure compatibility with deterministic executors that use
566/// virtual time. The parameterless methods are provided for convenience in
567/// blocking code where wall-clock time is appropriate.
568#[derive(Debug, Clone, Copy)]
569pub struct Deadline {
570    /// The point in time when the operation should timeout.
571    pub deadline: Instant,
572    /// The original timeout duration.
573    pub timeout: Duration,
574}
575
576impl Deadline {
577    /// Create a new deadline from a timeout duration using wall-clock time.
578    ///
579    /// # Note
580    ///
581    /// This method uses `Instant::now()` internally. For async code that needs
582    /// to work with deterministic executors, prefer [`Deadline::from_timeout_at`].
583    #[must_use]
584    pub fn from_timeout(timeout: Duration) -> Self {
585        Self::from_timeout_at(Instant::now(), timeout)
586    }
587
588    /// Create a new deadline from a timeout duration at a specific instant.
589    ///
590    /// This is the clock-agnostic constructor that should be used in async code.
591    /// The `now` parameter should come from `Executor::now()` to ensure
592    /// compatibility with deterministic executors.
593    ///
594    /// # Example
595    ///
596    /// ```ignore
597    /// let deadline = Deadline::from_timeout_at(executor.now(), Duration::from_secs(5));
598    /// ```
599    #[must_use]
600    pub fn from_timeout_at(now: Instant, timeout: Duration) -> Self {
601        let deadline = now + timeout;
602        Self { deadline, timeout }
603    }
604
605    /// Create a new deadline from a specific instant.
606    #[must_use]
607    pub const fn from_instant(deadline: Instant, timeout: Duration) -> Self {
608        Self { deadline, timeout }
609    }
610
611    /// Check if the deadline has been exceeded using wall-clock time.
612    ///
613    /// # Note
614    ///
615    /// This method uses `Instant::now()` internally. For async code that needs
616    /// to work with deterministic executors, prefer [`Deadline::is_expired_at`].
617    #[must_use]
618    pub fn is_expired(&self) -> bool {
619        self.is_expired_at(Instant::now())
620    }
621
622    /// Check if the deadline has been exceeded at a specific instant.
623    ///
624    /// This is the clock-agnostic version that should be used in async code.
625    /// The `now` parameter should come from `Executor::now()` to ensure
626    /// compatibility with deterministic executors.
627    #[must_use]
628    pub fn is_expired_at(&self, now: Instant) -> bool {
629        now > self.deadline
630    }
631
632    /// Get the remaining time until the deadline using wall-clock time.
633    ///
634    /// # Note
635    ///
636    /// This method uses `Instant::now()` internally. For async code that needs
637    /// to work with deterministic executors, prefer [`Deadline::remaining_at`].
638    #[must_use]
639    pub fn remaining(&self) -> Duration {
640        self.remaining_at(Instant::now())
641    }
642
643    /// Get the remaining time until the deadline at a specific instant.
644    ///
645    /// This is the clock-agnostic version that should be used in async code.
646    /// The `now` parameter should come from `Executor::now()` to ensure
647    /// compatibility with deterministic executors.
648    #[must_use]
649    pub fn remaining_at(&self, now: Instant) -> Duration {
650        self.deadline.saturating_duration_since(now)
651    }
652
653    /// Get the elapsed time since the deadline was created using wall-clock time.
654    ///
655    /// # Note
656    ///
657    /// This method uses `Instant::now()` internally. For async code that needs
658    /// to work with deterministic executors, prefer [`Deadline::elapsed_at`].
659    #[must_use]
660    pub fn elapsed(&self) -> Duration {
661        self.elapsed_at(Instant::now())
662    }
663
664    /// Get the elapsed time since the deadline was created at a specific instant.
665    ///
666    /// This is the clock-agnostic version that should be used in async code.
667    /// The `now` parameter should come from `Executor::now()` to ensure
668    /// compatibility with deterministic executors.
669    #[must_use]
670    pub fn elapsed_at(&self, now: Instant) -> Duration {
671        self.timeout.saturating_sub(self.remaining_at(now))
672    }
673}
674
675/// Trait for commands that can provide timeout classification.
676///
677/// This trait allows each command to specify its expected timeout category,
678/// enabling appropriate timeout and retry behavior.
679pub trait CommandTimeout {
680    /// Get the timeout class for this command.
681    ///
682    /// This determines how long to wait for the command to complete
683    /// and affects retry behavior.
684    fn timeout_class(&self) -> CommandCategory;
685}
686
687/// Blanket implementation for all commands that implement ViscaCommand.
688///
689/// This automatically provides timeout classification for all VISCA commands
690/// based on their TIMEOUT_CATEGORY constant.
691impl<T> CommandTimeout for T
692where
693    T: crate::command::ViscaCommand,
694{
695    fn timeout_class(&self) -> CommandCategory {
696        T::TIMEOUT_CATEGORY
697    }
698}
699
700/// Timeout policy configuration that maps classes to durations.
701///
702/// This provides a runtime-configurable way to adjust timeout behavior
703/// across different command categories.
704///
705/// # Clock-Agnostic Design
706///
707/// This type supports both wall-clock and executor-driven time sources.
708/// In async code, prefer [`TimeoutPolicy::deadline_for_at`] which accepts
709/// an explicit `now` parameter to ensure compatibility with deterministic
710/// executors that use virtual time.
711#[derive(Debug, Clone, Copy, Default)]
712pub struct TimeoutPolicy {
713    config: TimeoutConfig,
714}
715
716impl TimeoutPolicy {
717    /// Create a new timeout policy from a timeout configuration.
718    #[must_use]
719    pub const fn new(config: TimeoutConfig) -> Self {
720        Self { config }
721    }
722
723    /// Get the timeout duration for a specific class.
724    #[must_use]
725    pub const fn get_timeout(&self, class: CommandCategory) -> Duration {
726        self.config.get_timeout(class)
727    }
728
729    /// Create a deadline for a specific timeout class using wall-clock time.
730    ///
731    /// # Note
732    ///
733    /// This method uses `Instant::now()` internally. For async code that needs
734    /// to work with deterministic executors, prefer [`TimeoutPolicy::deadline_for_at`].
735    #[must_use]
736    pub fn deadline_for(&self, class: CommandCategory) -> Deadline {
737        Deadline::from_timeout(self.get_timeout(class))
738    }
739
740    /// Create a deadline for a specific timeout class at a specific instant.
741    ///
742    /// This is the clock-agnostic version that should be used in async code.
743    /// The `now` parameter should come from `Executor::now()` to ensure
744    /// compatibility with deterministic executors.
745    ///
746    /// # Example
747    ///
748    /// ```ignore
749    /// let deadline = timeout_policy.deadline_for_at(executor.now(), CommandCategory::Quick);
750    /// ```
751    #[must_use]
752    pub fn deadline_for_at(&self, now: Instant, class: CommandCategory) -> Deadline {
753        Deadline::from_timeout_at(now, self.get_timeout(class))
754    }
755
756    /// Update the timeout configuration.
757    pub fn update_config(&mut self, config: TimeoutConfig) {
758        self.config = config;
759    }
760}
761
762#[cfg(test)]
763#[allow(clippy::expect_used)]
764mod timeout_manager_tests {
765    use std::{
766        io,
767        net::{TcpListener, TcpStream, UdpSocket},
768        sync::Mutex,
769        thread,
770    };
771
772    use super::*;
773
774    #[derive(Debug, Default)]
775    struct MockTimeoutSocket {
776        read_timeout: Option<Duration>,
777        write_timeout: Option<Duration>,
778    }
779
780    impl MockTimeoutSocket {
781        const fn new(read_timeout: Option<Duration>, write_timeout: Option<Duration>) -> Self {
782            Self {
783                read_timeout,
784                write_timeout,
785            }
786        }
787    }
788
789    impl TimeoutManager for MockTimeoutSocket {
790        fn with_timeout<F, R>(&mut self, timeout: Duration, f: F) -> Result<R, Error>
791        where
792            F: FnOnce(&mut Self) -> Result<R, Error>,
793        {
794            self.with_read_timeout(timeout, f)
795        }
796
797        fn get_read_timeout(&self) -> io::Result<Option<Duration>> {
798            Ok(self.read_timeout)
799        }
800
801        fn set_read_timeout(&mut self, timeout: Option<Duration>) -> io::Result<()> {
802            self.read_timeout = timeout;
803            Ok(())
804        }
805
806        fn get_write_timeout(&self) -> io::Result<Option<Duration>> {
807            Ok(self.write_timeout)
808        }
809
810        fn set_write_timeout(&mut self, timeout: Option<Duration>) -> io::Result<()> {
811            self.write_timeout = timeout;
812            Ok(())
813        }
814    }
815
816    #[test]
817    fn test_with_read_timeout_restores_original_timeout_on_success() {
818        let original_timeout = Some(Duration::from_secs(5));
819        let requested_timeout = Duration::from_secs(1);
820        let mut socket = MockTimeoutSocket::new(original_timeout, None);
821
822        let result = socket.with_read_timeout(requested_timeout, |socket| {
823            assert_eq!(socket.read_timeout, Some(requested_timeout));
824            Ok(42)
825        });
826
827        assert_eq!(result.expect("temporary read timeout should succeed"), 42);
828        assert_eq!(socket.read_timeout, original_timeout);
829    }
830
831    #[test]
832    fn test_with_read_timeout_restores_original_timeout_on_error() {
833        let original_timeout = Some(Duration::from_secs(5));
834        let requested_timeout = Duration::from_secs(1);
835        let mut socket = MockTimeoutSocket::new(original_timeout, None);
836
837        let error = socket
838            .with_read_timeout(requested_timeout, |socket| {
839                assert_eq!(socket.read_timeout, Some(requested_timeout));
840                Err::<(), Error>(Error::from(io::Error::other(
841                    "expected timeout test failure",
842                )))
843            })
844            .expect_err("closure failure should be propagated");
845
846        assert!(matches!(error, Error::Io(_)));
847        assert_eq!(socket.read_timeout, original_timeout);
848    }
849
850    #[test]
851    fn test_with_write_timeout_restores_original_timeout() {
852        let original_timeout = Some(Duration::from_secs(10));
853        let requested_timeout = Duration::from_secs(2);
854        let mut socket = MockTimeoutSocket::new(None, original_timeout);
855
856        let result = socket.with_write_timeout(requested_timeout, |socket| {
857            assert_eq!(socket.write_timeout, Some(requested_timeout));
858            Ok("ok")
859        });
860
861        assert_eq!(
862            result.expect("temporary write timeout should succeed"),
863            "ok"
864        );
865        assert_eq!(socket.write_timeout, original_timeout);
866    }
867
868    #[test]
869    fn test_timeout_guard_restores_timeouts() {
870        let original_read_timeout = Some(Duration::from_secs(5));
871        let original_write_timeout = Some(Duration::from_secs(10));
872        let requested_read_timeout = Duration::from_secs(1);
873        let requested_write_timeout = Duration::from_secs(2);
874        let mut socket = MockTimeoutSocket::new(original_read_timeout, original_write_timeout);
875
876        {
877            let mut guard = TimeoutGuard::new(
878                &mut socket,
879                Some(requested_read_timeout),
880                Some(requested_write_timeout),
881            )
882            .expect("Failed to create timeout guard");
883
884            assert_eq!(guard.socket.read_timeout, Some(requested_read_timeout));
885            assert_eq!(guard.socket.write_timeout, Some(requested_write_timeout));
886
887            guard
888                .restore()
889                .expect("Failed to restore original timeouts");
890            assert_eq!(guard.socket.read_timeout, original_read_timeout);
891            assert_eq!(guard.socket.write_timeout, original_write_timeout);
892
893            guard
894                .restore()
895                .expect("timeout restoration should be idempotent");
896        }
897
898        assert_eq!(socket.read_timeout, original_read_timeout);
899        assert_eq!(socket.write_timeout, original_write_timeout);
900    }
901
902    #[test]
903    fn test_with_timeout_on_guard_restores_read_timeout() {
904        let original_timeout = Some(Duration::from_secs(5));
905        let requested_timeout = Duration::from_secs(3);
906        let socket = Mutex::new(MockTimeoutSocket::new(original_timeout, None));
907        let mut guard = socket.lock().expect("mutex should not be poisoned");
908
909        let result = with_timeout_on_guard(&mut guard, requested_timeout, |socket| {
910            assert_eq!(socket.read_timeout, Some(requested_timeout));
911            Ok("ok")
912        });
913
914        assert_eq!(
915            result.expect("temporary timeout through mutex guard should succeed"),
916            "ok"
917        );
918        assert_eq!(guard.read_timeout, original_timeout);
919    }
920
921    #[test]
922    #[cfg_attr(miri, ignore = "requires real TCP sockets")]
923    fn test_tcp_timeout_manager_smoke() {
924        // Start a TCP server
925        let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind test listener");
926        let addr = listener
927            .local_addr()
928            .expect("Failed to get listener address");
929
930        // Spawn a thread to accept connections
931        thread::spawn(move || {
932            let (_stream, _) = listener.accept().expect("Failed to accept connection");
933            // Keep the connection open
934            thread::sleep(Duration::from_secs(10));
935        });
936
937        // Connect to the server
938        let mut stream = TcpStream::connect(addr).expect("Failed to connect to test server");
939
940        // Test setting and getting timeouts
941        assert_eq!(
942            stream.get_read_timeout().expect("Failed to get timeout"),
943            None
944        );
945
946        stream
947            .set_read_timeout(Some(Duration::from_secs(5)))
948            .expect("Failed to set timeout");
949        assert_eq!(
950            stream.get_read_timeout().expect("Failed to get timeout"),
951            Some(Duration::from_secs(5))
952        );
953
954        // Test with_read_timeout
955        let result = stream.with_read_timeout(Duration::from_secs(1), |s| {
956            // Verify timeout is set
957            assert_eq!(
958                s.get_read_timeout()
959                    .expect("Failed to get timeout in guard"),
960                Some(Duration::from_secs(1))
961            );
962            Ok(42)
963        });
964
965        assert_eq!(result.expect("Test operation failed"), 42);
966        // Verify timeout is restored
967        assert_eq!(
968            stream
969                .get_read_timeout()
970                .expect("Failed to get timeout after guard"),
971            Some(Duration::from_secs(5))
972        );
973    }
974
975    #[test]
976    #[cfg_attr(miri, ignore = "requires real UDP sockets")]
977    fn test_udp_timeout_manager_smoke() {
978        let mut socket = UdpSocket::bind("127.0.0.1:0").expect("Failed to bind UDP socket");
979
980        // Test setting and getting timeouts
981        assert_eq!(
982            socket
983                .get_read_timeout()
984                .expect("Failed to get UDP timeout"),
985            None
986        );
987
988        socket
989            .set_read_timeout(Some(Duration::from_secs(3)))
990            .expect("Failed to set UDP timeout");
991        assert_eq!(
992            socket
993                .get_read_timeout()
994                .expect("Failed to get UDP timeout"),
995            Some(Duration::from_secs(3))
996        );
997
998        // Test with_read_timeout
999        let result = socket.with_read_timeout(Duration::from_secs(2), |s| {
1000            // Verify timeout is set
1001            assert_eq!(
1002                s.get_read_timeout()
1003                    .expect("Failed to get UDP timeout in guard"),
1004                Some(Duration::from_secs(2))
1005            );
1006            Ok("success")
1007        });
1008
1009        assert_eq!(result.expect("UDP test operation failed"), "success");
1010        // Verify timeout is restored
1011        assert_eq!(
1012            socket
1013                .get_read_timeout()
1014                .expect("Failed to get UDP timeout after guard"),
1015            Some(Duration::from_secs(3))
1016        );
1017    }
1018
1019    #[test]
1020    fn test_timeout_guard_restores_timeouts_on_drop() {
1021        let original_read_timeout = Some(Duration::from_secs(5));
1022        let original_write_timeout = Some(Duration::from_secs(10));
1023        let mut socket = MockTimeoutSocket::new(original_read_timeout, original_write_timeout);
1024
1025        {
1026            let guard = TimeoutGuard::new(
1027                &mut socket,
1028                Some(Duration::from_secs(1)),
1029                Some(Duration::from_secs(2)),
1030            )
1031            .expect("Failed to create timeout guard");
1032
1033            assert_eq!(guard.socket.read_timeout, Some(Duration::from_secs(1)));
1034            assert_eq!(guard.socket.write_timeout, Some(Duration::from_secs(2)));
1035        }
1036
1037        assert_eq!(socket.read_timeout, original_read_timeout);
1038        assert_eq!(socket.write_timeout, original_write_timeout);
1039    }
1040
1041    #[test]
1042    fn test_deadline() {
1043        let timeout = Duration::from_millis(100);
1044        let now = Instant::now();
1045        let deadline = Deadline::from_timeout_at(now, timeout);
1046
1047        assert!(!deadline.is_expired_at(now));
1048        assert_eq!(deadline.timeout, timeout);
1049        assert_eq!(deadline.remaining_at(now), timeout);
1050        assert_eq!(deadline.elapsed_at(now), Duration::ZERO);
1051
1052        let during_timeout = now + Duration::from_millis(10);
1053        assert!(!deadline.is_expired_at(during_timeout));
1054        assert_eq!(
1055            deadline.remaining_at(during_timeout),
1056            Duration::from_millis(90)
1057        );
1058        assert_eq!(
1059            deadline.elapsed_at(during_timeout),
1060            Duration::from_millis(10)
1061        );
1062
1063        let at_deadline = now + timeout;
1064        assert!(!deadline.is_expired_at(at_deadline));
1065        assert_eq!(deadline.remaining_at(at_deadline), Duration::ZERO);
1066
1067        let after_deadline = at_deadline + Duration::from_nanos(1);
1068        assert!(deadline.is_expired_at(after_deadline));
1069        assert_eq!(deadline.remaining_at(after_deadline), Duration::ZERO);
1070    }
1071
1072    #[test]
1073    fn test_timeout_policy() {
1074        let policy = TimeoutPolicy::default();
1075
1076        assert_eq!(
1077            policy.get_timeout(CommandCategory::Quick),
1078            Duration::from_secs(5)
1079        );
1080        assert_eq!(
1081            policy.get_timeout(CommandCategory::Movement),
1082            Duration::from_secs(30)
1083        );
1084
1085        let deadline = policy.deadline_for(CommandCategory::Quick);
1086        assert!(!deadline.is_expired());
1087        assert_eq!(deadline.timeout, Duration::from_secs(5));
1088
1089        // Test config update
1090        let mut policy = TimeoutPolicy::default();
1091        let new_config = TimeoutConfig::uniform(Duration::from_secs(10));
1092        policy.update_config(new_config);
1093
1094        assert_eq!(
1095            policy.get_timeout(CommandCategory::Quick),
1096            Duration::from_secs(10)
1097        );
1098    }
1099}