adaptive_timeout/config.rs
1use std::num::NonZeroU32;
2use std::time::Duration;
3
4use crate::BackoffInterval;
5
6/// Milliseconds stored as a [`NonZeroU32`].
7///
8/// Compact (4-byte) representation for timeout durations that are always
9/// positive. Max representable value is ~49.7 days.
10pub type MillisNonZero = NonZeroU32;
11
12/// Creates a [`MillisNonZero`] from a `u32` literal at compile time.
13///
14/// # Panics
15///
16/// Panics if `ms == 0`.
17#[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
25/// HdrHistogram precision used internally. 2 significant digits gives ~1%
26/// accuracy, which is more than sufficient for latency tracking.
27pub(crate) const SIGNIFICANT_VALUE_DIGITS: u8 = 2;
28
29/// Configuration for [`LatencyTracker`](crate::LatencyTracker).
30///
31/// All duration-like fields use integer milliseconds for compactness.
32///
33/// # Example
34///
35/// ```
36/// use adaptive_timeout::TrackerConfig;
37///
38/// let config = TrackerConfig {
39/// min_samples: 10,
40/// ..TrackerConfig::default()
41/// };
42/// ```
43#[derive(Debug, Clone, Copy)]
44pub struct TrackerConfig {
45 /// Total sliding window duration in milliseconds. This is the full time
46 /// span the histogram remembers — not per sub-window. The window is
47 /// divided into `N` equal sub-windows of `window_ms / N` each.
48 ///
49 /// Default: 60,000 (60s).
50 pub window_ms: NonZeroU32,
51
52 /// Minimum samples before quantile estimates are valid. Below this,
53 /// queries return `None`. Default: 3.
54 pub min_samples: u32,
55
56 /// Maximum trackable latency in milliseconds. Values above this are
57 /// clamped. Default: 60,000 (60s).
58 pub max_trackable_latency_ms: u32,
59}
60
61impl TrackerConfig {
62 /// Returns the sliding window duration as a [`Duration`].
63 #[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/// Configuration for [`AdaptiveTimeout`](crate::AdaptiveTimeout).
80///
81/// The struct fits in 24 bytes.
82///
83/// # Example
84///
85/// ```
86/// use adaptive_timeout::TimeoutConfig;
87///
88/// let config = TimeoutConfig {
89/// quantile: 0.999,
90/// safety_factor: 3.0,
91/// ..TimeoutConfig::default()
92/// };
93/// ```
94#[derive(Debug, Clone, Copy)]
95pub struct TimeoutConfig {
96 /// Timeout floor and ceiling. Default: `250ms..1min`.
97 pub backoff: BackoffInterval,
98
99 /// Quantile of the latency distribution to use (e.g. 0.9999 for P99.99).
100 /// Default: 0.9999.
101 pub quantile: f64,
102
103 /// Multiplier applied to the quantile estimate. A factor of 2.0 means
104 /// the timeout is twice the observed quantile. Default: 2.0.
105 pub safety_factor: f64,
106}
107
108impl TimeoutConfig {
109 /// Returns the minimum timeout as a [`Duration`].
110 #[inline]
111 pub fn min_timeout(&self) -> Duration {
112 Duration::from_millis(self.backoff.min_ms.get() as u64)
113 }
114
115 /// Returns the maximum timeout as a [`Duration`].
116 #[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}