Skip to main content

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}