Skip to main content

nntp_proxy/types/config/
timeout.rs

1//! Timeout newtypes for type-safe timeout handling
2//!
3//! This module provides strongly-typed wrappers around `std::time::Duration`
4//! to prevent accidentally using the wrong timeout value in the wrong context.
5
6use std::time::Duration;
7
8macro_rules! timeout_newtype {
9    (
10        $(#[$meta:meta])*
11        $vis:vis struct $name:ident($default_secs:expr);
12    ) => {
13        $(#[$meta])*
14        #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
15        $vis struct $name(Duration);
16
17        impl $name {
18            /// Default timeout value
19            pub const DEFAULT: Self = Self(Duration::from_secs($default_secs));
20
21            /// Create a new timeout
22            #[inline]
23            pub const fn new(duration: Duration) -> Self {
24                Self(duration)
25            }
26
27            /// Get the underlying duration
28            #[inline]
29            #[must_use]
30            pub const fn as_duration(self) -> Duration {
31                self.0
32            }
33
34            /// Get timeout in seconds
35            #[inline]
36            #[must_use]
37            pub const fn as_secs(self) -> u64 {
38                self.0.as_secs()
39            }
40        }
41
42        impl From<Duration> for $name {
43            fn from(duration: Duration) -> Self {
44                Self(duration)
45            }
46        }
47
48        impl From<$name> for Duration {
49            fn from(timeout: $name) -> Self {
50                timeout.0
51            }
52        }
53    };
54}
55
56timeout_newtype! {
57    /// Timeout for reading responses from backend servers
58    ///
59    /// This timeout applies to individual read operations from backend connections.
60    /// Per [RFC 3977](https://datatracker.ietf.org/doc/html/rfc3977), NNTP servers
61    /// should respond promptly, but large article transfers may take longer.
62    pub struct BackendReadTimeout(30);
63}
64
65timeout_newtype! {
66    /// Timeout for establishing connections to backend servers
67    ///
68    /// This timeout applies when creating new TCP connections to backend servers.
69    /// Should be relatively short to fail fast on connection issues.
70    pub struct ConnectionTimeout(10);
71}
72
73timeout_newtype! {
74    /// Timeout for executing individual NNTP commands
75    ///
76    /// This timeout applies to the entire request/response cycle for a single command.
77    /// Includes both sending the command and receiving the complete response.
78    pub struct CommandExecutionTimeout(60);
79}
80
81timeout_newtype! {
82    /// Timeout for health check operations
83    ///
84    /// Health checks should complete quickly to avoid blocking pool operations.
85    /// A short timeout ensures unhealthy backends are detected promptly.
86    pub struct HealthCheckTimeout(2);
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use proptest::prelude::*;
93
94    // Property tests for all timeout types
95    proptest! {
96        #[test]
97        fn backend_read_timeout_roundtrip(secs in 0u64..86400u64) {
98            let duration = Duration::from_secs(secs);
99            let timeout = BackendReadTimeout::new(duration);
100            prop_assert_eq!(timeout.as_duration(), duration);
101            prop_assert_eq!(timeout.as_secs(), secs);
102        }
103
104        #[test]
105        fn connection_timeout_roundtrip(secs in 0u64..86400u64) {
106            let duration = Duration::from_secs(secs);
107            let timeout = ConnectionTimeout::new(duration);
108            prop_assert_eq!(timeout.as_duration(), duration);
109            prop_assert_eq!(timeout.as_secs(), secs);
110        }
111
112        #[test]
113        fn command_execution_timeout_roundtrip(secs in 0u64..86400u64) {
114            let duration = Duration::from_secs(secs);
115            let timeout = CommandExecutionTimeout::new(duration);
116            prop_assert_eq!(timeout.as_duration(), duration);
117            prop_assert_eq!(timeout.as_secs(), secs);
118        }
119
120        #[test]
121        fn health_check_timeout_roundtrip(secs in 0u64..86400u64) {
122            let duration = Duration::from_secs(secs);
123            let timeout = HealthCheckTimeout::new(duration);
124            prop_assert_eq!(timeout.as_duration(), duration);
125            prop_assert_eq!(timeout.as_secs(), secs);
126        }
127
128        #[test]
129        fn timeout_from_into_duration(secs in 0u64..86400u64) {
130            let duration = Duration::from_secs(secs);
131            let timeout = BackendReadTimeout::from(duration);
132            let back: Duration = timeout.into();
133            prop_assert_eq!(back, duration);
134        }
135
136        #[test]
137        fn timeout_ordering_property(a in 0u64..1000u64, b in 0u64..1000u64) {
138            let t1 = ConnectionTimeout::new(Duration::from_secs(a));
139            let t2 = ConnectionTimeout::new(Duration::from_secs(b));
140            prop_assert_eq!(t1.cmp(&t2), a.cmp(&b));
141        }
142
143        #[test]
144        fn timeout_clone_equality(secs in 0u64..86400u64) {
145            let timeout = BackendReadTimeout::new(Duration::from_secs(secs));
146            let cloned = timeout;
147            prop_assert_eq!(timeout, cloned);
148        }
149
150        #[test]
151        fn timeout_debug_contains_name(secs in 0u64..100u64) {
152            let timeout = BackendReadTimeout::new(Duration::from_secs(secs));
153            let debug_str = format!("{timeout:?}");
154            prop_assert!(debug_str.contains("BackendReadTimeout"));
155        }
156    }
157
158    // Constant verification tests
159    #[test]
160    fn all_default_constants_correct() {
161        assert_eq!(BackendReadTimeout::DEFAULT.as_secs(), 30);
162        assert_eq!(ConnectionTimeout::DEFAULT.as_secs(), 10);
163        assert_eq!(CommandExecutionTimeout::DEFAULT.as_secs(), 60);
164        assert_eq!(HealthCheckTimeout::DEFAULT.as_secs(), 2);
165    }
166
167    // Edge case tests
168    #[test]
169    fn zero_timeout_is_valid() {
170        let timeout = BackendReadTimeout::new(Duration::from_secs(0));
171        assert_eq!(timeout.as_secs(), 0);
172    }
173
174    #[test]
175    fn large_timeout_one_day() {
176        let large = crate::constants::duration_polyfill::from_hours(24);
177        let timeout = CommandExecutionTimeout::new(large);
178        assert_eq!(timeout.as_secs(), 86400);
179    }
180
181    #[test]
182    fn timeout_hash_works() {
183        use std::collections::HashSet;
184        let mut set = HashSet::new();
185        set.insert(BackendReadTimeout::DEFAULT);
186        assert!(set.contains(&BackendReadTimeout::DEFAULT));
187    }
188}