adaptive_timeout/
config.rs1use std::num::NonZeroU32;
2use std::time::Duration;
3
4use crate::BackoffInterval;
5
6pub type MillisNonZero = NonZeroU32;
11
12#[inline]
18pub(crate) const fn millis(ms: u32) -> MillisNonZero {
19 match NonZeroU32::new(ms) {
20 Some(v) => v,
21 None => panic!("millis value must be non-zero"),
22 }
23}
24
25pub(crate) const SIGNIFICANT_VALUE_DIGITS: u8 = 2;
28
29#[derive(Debug, Clone, Copy)]
44pub struct TrackerConfig {
45 pub window_ms: NonZeroU32,
51
52 pub min_samples: u32,
55
56 pub max_trackable_latency_ms: u32,
59}
60
61impl TrackerConfig {
62 #[inline]
64 pub fn window(&self) -> Duration {
65 Duration::from_millis(self.window_ms.get() as u64)
66 }
67}
68
69impl Default for TrackerConfig {
70 fn default() -> Self {
71 Self {
72 window_ms: millis(60_000),
73 min_samples: 3,
74 max_trackable_latency_ms: 60_000,
75 }
76 }
77}
78
79#[derive(Debug, Clone, Copy)]
95pub struct TimeoutConfig {
96 pub backoff: BackoffInterval,
98
99 pub quantile: f64,
102
103 pub safety_factor: f64,
106}
107
108impl TimeoutConfig {
109 #[inline]
111 pub fn min_timeout(&self) -> Duration {
112 Duration::from_millis(self.backoff.min_ms.get() as u64)
113 }
114
115 #[inline]
117 pub fn max_timeout(&self) -> Duration {
118 Duration::from_millis(self.backoff.max_ms.get() as u64)
119 }
120}
121
122impl Default for TimeoutConfig {
123 fn default() -> Self {
124 Self {
125 backoff: BackoffInterval::default(),
126 quantile: 0.9999,
127 safety_factor: 2.0,
128 }
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use std::mem;
136
137 #[test]
138 fn timeout_config_is_compact() {
139 assert_eq!(mem::size_of::<TimeoutConfig>(), 24);
140 }
141
142 #[test]
143 fn tracker_config_is_compact() {
144 assert!(mem::size_of::<TrackerConfig>() <= 12);
145 }
146
147 #[test]
148 fn millis_helper() {
149 let m = millis(42);
150 assert_eq!(m.get(), 42);
151 }
152
153 #[test]
154 #[should_panic(expected = "non-zero")]
155 fn millis_zero_panics() {
156 let _ = millis(0);
157 }
158
159 #[test]
160 fn timeout_config_conversions() {
161 let cfg = TimeoutConfig::default();
162 assert_eq!(cfg.min_timeout(), Duration::from_millis(250));
163 assert_eq!(cfg.max_timeout(), Duration::from_millis(60_000));
164 }
165
166 #[test]
167 fn tracker_config_window_conversion() {
168 let cfg = TrackerConfig::default();
169 assert_eq!(cfg.window(), Duration::from_secs(60));
170 }
171
172 #[test]
173 fn timeout_config_from_str() {
174 let cfg: TimeoutConfig = "10ms..60s".parse().unwrap();
175 assert_eq!(cfg.backoff.min_ms.get(), 10);
176 assert_eq!(cfg.backoff.max_ms.get(), 60_000);
177 assert_eq!(cfg.quantile, 0.9999);
178 assert_eq!(cfg.safety_factor, 2.0);
179 }
180
181 #[test]
182 fn timeout_config_from_str_error() {
183 assert!("not-a-range".parse::<TimeoutConfig>().is_err());
184 }
185
186 #[cfg(feature = "serde")]
187 mod serde_tests {
188 use super::*;
189
190 #[test]
191 fn serialize_as_backoff_string() {
192 let cfg: TimeoutConfig = "10ms..60s".parse().unwrap();
193 let json = serde_json::to_string(&cfg).unwrap();
194 assert_eq!(json, r#""10ms..1m""#);
195 }
196
197 #[test]
198 fn deserialize_from_string() {
199 let cfg: serde_json::Result<TimeoutConfig> = serde_json::from_str(r#""250ms..1m""#);
200 let cfg = cfg.unwrap();
201 assert_eq!(cfg.backoff.min_ms.get(), 250);
202 assert_eq!(cfg.backoff.max_ms.get(), 60_000);
203 assert_eq!(cfg.quantile, 0.9999);
204 assert_eq!(cfg.safety_factor, 2.0);
205 }
206
207 #[test]
208 fn serde_round_trip() {
209 let original: TimeoutConfig = "100ms..30s".parse().unwrap();
210 let json = serde_json::to_string(&original).unwrap();
211 let restored: TimeoutConfig = serde_json::from_str(&json).unwrap();
212 assert_eq!(original.backoff, restored.backoff);
213 assert_eq!(original.quantile, restored.quantile);
214 assert_eq!(original.safety_factor, restored.safety_factor);
215 }
216
217 #[test]
218 fn deserialize_error_propagated() {
219 assert!(serde_json::from_str::<TimeoutConfig>(r#""bad""#).is_err());
220 }
221 }
222}