Skip to main content

ad_time/
time_src.rs

1use std::net::SocketAddr;
2use std::time::Duration;
3
4/// Signed offset in microseconds: server_time - local_time.
5/// Positive means the server is ahead of us.
6pub type OffsetMicros = i64;
7
8#[derive(Debug, thiserror::Error)]
9pub enum TimeSourceError {
10    #[error("connection timed out")]
11    Timeout,
12    #[error("connection refused")]
13    Refused,
14    #[error("protocol error: {0}")]
15    Protocol(String),
16    #[error("parse error: {0}")]
17    Parse(String),
18    #[error("config error: {0}")]
19    Config(String),
20}
21
22pub trait TimeSource {
23    fn name(&self) -> &'static str;
24    fn fetch(&self, target: SocketAddr, timeout: Duration)
25        -> Result<OffsetMicros, TimeSourceError>;
26}
27
28#[derive(Debug, thiserror::Error)]
29pub enum OrchestratorError {
30    #[error("all time sources failed. Last error: {0}")]
31    AllSourcesFailed(String),
32    #[error("no sources configured")]
33    NoSourcesConfigured,
34}
35
36pub struct Orchestrator {
37    sources: Vec<Box<dyn TimeSource>>,
38    verbose: bool,
39    sigma: f64,
40    base_ms: u64,
41}
42
43impl Orchestrator {
44    pub fn new(sources: Vec<Box<dyn TimeSource>>, verbose: bool) -> Self {
45        Self {
46            sources,
47            verbose,
48            sigma: 0.4,
49            base_ms: 8000,
50        }
51    }
52
53    pub fn with_jitter(mut self, sigma: f64, base_ms: u64) -> Self {
54        self.sigma = sigma;
55        self.base_ms = base_ms;
56        self
57    }
58
59    /// Try each source in order; return first success with the method name.
60    pub fn resolve(
61        &self,
62        target: SocketAddr,
63        timeout: Duration,
64    ) -> Result<(OffsetMicros, &'static str), OrchestratorError> {
65        let mut last_err: Option<String> = None;
66        let mut failures: u32 = 0;
67
68        let n = self.sources.len();
69        for (i, src) in self.sources.iter().enumerate() {
70            match src.fetch(target, timeout) {
71                Ok(offset) => {
72                    if self.verbose {
73                        eprintln!("[{}] offset = {}", src.name(), format_offset(offset));
74                    }
75                    return Ok((offset, src.name()));
76                }
77                Err(e) => {
78                    if self.verbose || !matches!(e, TimeSourceError::Config(_)) {
79                        eprintln!("[{}] failed: {}", src.name(), e);
80                    }
81                    last_err = Some(format!("{}: {}", src.name(), e));
82
83                    let is_config = matches!(e, TimeSourceError::Config(_));
84
85                    if !is_config && i + 1 < n {
86                        let delay = jittered_delay(self.base_ms, self.sigma, failures);
87                        std::thread::sleep(delay);
88                    }
89
90                    // Two-tier backoff: timeouts and config errors are environmental
91                    // (no response received, or not even attempted) — reset counter.
92                    // Refused/Protocol/Parse errors keep incrementing.
93                    if matches!(e, TimeSourceError::Timeout | TimeSourceError::Config(_)) {
94                        failures = 0;
95                    } else {
96                        failures += 1;
97                    }
98                }
99            }
100        }
101        if let Some(err) = last_err {
102            Err(OrchestratorError::AllSourcesFailed(err))
103        } else {
104            Err(OrchestratorError::NoSourcesConfigured)
105        }
106    }
107}
108
109/// Format offset as "+3.456789s" or "-0.012345s".
110pub fn format_offset(offset_us: OffsetMicros) -> String {
111    let sign = if offset_us >= 0 { "+" } else { "-" };
112    let abs = offset_us.unsigned_abs();
113    format!("{}{}.{:06}s", sign, abs / 1_000_000, abs % 1_000_000)
114}
115
116/// Uniform [0, 1) float from CSPRNG (53-bit mantissa).
117pub(crate) fn crypto_uniform_f64() -> f64 {
118    let mut buf = [0u8; 8];
119    getrandom::fill(&mut buf).expect("CSPRNG failure");
120    (u64::from_le_bytes(buf) >> 11) as f64 / (1u64 << 53) as f64
121}
122
123/// Log-normal jitter: median == base_ms, spread controlled by sigma.
124/// Uses Box-Muller to generate standard normal from two uniform samples.
125/// Right tail is heavier than uniform, matching human/background-task timing.
126fn lognormal_jitter_ms(base_ms: u64, sigma: f64) -> u64 {
127    let u1 = loop {
128        let u = crypto_uniform_f64();
129        if u > 0.0 {
130            break u;
131        }
132    };
133    let u2 = crypto_uniform_f64();
134    let z = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
135    let factor = (z * sigma).exp();
136    if !factor.is_finite() || factor > 1_000_000.0 {
137        return u64::MAX; // capped by caller's MAX_DELAY_MS
138    }
139    (base_ms as f64 * factor).round() as u64
140}
141
142/// Exponential backoff: 3^failures, capped at 5 attempts.
143fn backoff_multiplier(failures: u32) -> u64 {
144    3u64.pow(failures.min(5))
145}
146
147/// Absolute maximum delay for any single inter-protocol sleep (30 minutes).
148const MAX_DELAY_MS: u64 = 1_800_000;
149
150fn jittered_delay(base_ms: u64, sigma: f64, failures: u32) -> Duration {
151    let raw = backoff_multiplier(failures).saturating_mul(lognormal_jitter_ms(base_ms, sigma));
152    Duration::from_millis(raw.min(MAX_DELAY_MS))
153}
154
155/// Log-normal jitter for diagnostic probe mode: no backoff.
156pub fn probe_jitter(sigma: f64, base_ms: u64) -> Duration {
157    let raw = if sigma <= 0.0 {
158        base_ms
159    } else {
160        lognormal_jitter_ms(base_ms, sigma)
161    };
162    Duration::from_millis(raw.min(MAX_DELAY_MS))
163}
164
165/// Apply per-invocation randomization to sigma so that the log-normal
166/// distribution shape is not fingerprintable across multiple runs.
167/// Multiplier is uniform in [0.5, 1.5), centered at 1.0.
168pub fn randomize_sigma(sigma: f64) -> f64 {
169    if sigma <= 0.0 || !sigma.is_finite() {
170        return 0.0;
171    }
172    let r = 0.5 + crypto_uniform_f64();
173    sigma * r
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn format_positive() {
182        assert_eq!(format_offset(3_456_789), "+3.456789s");
183    }
184
185    #[test]
186    fn format_negative() {
187        assert_eq!(format_offset(-12_345), "-0.012345s");
188    }
189
190    #[test]
191    fn format_zero() {
192        assert_eq!(format_offset(0), "+0.000000s");
193    }
194
195    // --- backoff_multiplier ---
196
197    #[test]
198    fn backoff_zero_failures_is_one() {
199        assert_eq!(backoff_multiplier(0), 1);
200    }
201
202    #[test]
203    fn backoff_grows_by_powers_of_three() {
204        assert_eq!(backoff_multiplier(1), 3);
205        assert_eq!(backoff_multiplier(2), 9);
206        assert_eq!(backoff_multiplier(3), 27);
207    }
208
209    #[test]
210    fn backoff_capped_at_five() {
211        assert_eq!(backoff_multiplier(5), 243);
212        assert_eq!(backoff_multiplier(6), 243);
213        assert_eq!(backoff_multiplier(100), 243);
214    }
215
216    // --- lognormal_jitter_ms ---
217
218    #[test]
219    fn lognormal_jitter_non_negative() {
220        for _ in 0..100 {
221            let ms = lognormal_jitter_ms(8000, 0.4);
222            assert!(ms > 0, "got zero or negative jitter: {}", ms);
223        }
224    }
225
226    #[test]
227    fn lognormal_jitter_zero_sigma_is_deterministic() {
228        assert_eq!(lognormal_jitter_ms(8000, 0.0), 8000);
229    }
230
231    // --- jittered_delay ---
232
233    #[test]
234    fn jittered_delay_never_exceeds_max() {
235        for _ in 0..100 {
236            let d = jittered_delay(8000, 0.4, 0);
237            assert!(d <= Duration::from_millis(MAX_DELAY_MS));
238        }
239    }
240
241    #[test]
242    fn lognormal_jitter_capped_for_large_factor() {
243        // With sigma=300, ~52% of Box-Muller samples produce z<0 (tiny delay).
244        // Only z>>0 triggers the factor>1e6 guard returning u64::MAX, which
245        // jittered_delay's saturating_mul + .min(MAX_DELAY_MS) absorbs safely.
246        // The test verifies the safety property: delay never exceeds the cap,
247        // regardless of which side of the Box-Muller the RNG lands on.
248        for _ in 0..50 {
249            let d = jittered_delay(8000, 300.0, 5);
250            assert!(
251                d <= Duration::from_millis(MAX_DELAY_MS),
252                "delay {}s exceeded cap {}s for sigma=300",
253                d.as_secs_f64(),
254                MAX_DELAY_MS as f64 / 1000.0
255            );
256        }
257    }
258
259    // --- probe_jitter ---
260
261    #[test]
262    fn probe_jitter_zero_sigma_returns_base() {
263        assert_eq!(probe_jitter(0.0, 500), Duration::from_millis(500));
264        assert_eq!(probe_jitter(0.0, 2000), Duration::from_millis(2000));
265    }
266
267    #[test]
268    fn probe_jitter_nonzero_sigma_respects_base() {
269        for _ in 0..20 {
270            let d = probe_jitter(0.4, 1000);
271            assert!(d > Duration::ZERO);
272            assert!(d <= Duration::from_millis(MAX_DELAY_MS));
273        }
274    }
275
276    // --- crypto_uniform_f64 ---
277
278    #[test]
279    fn crypto_uniform_is_in_unit_interval() {
280        for _ in 0..200 {
281            let v = crypto_uniform_f64();
282            assert!(v >= 0.0 && v < 1.0, "out of [0,1): {}", v);
283        }
284    }
285
286    // --- randomize_sigma ---
287
288    #[test]
289    fn randomize_sigma_zero_returns_zero() {
290        assert_eq!(randomize_sigma(0.0), 0.0);
291    }
292
293    #[test]
294    fn randomize_sigma_is_in_expected_range() {
295        for _ in 0..100 {
296            let s = randomize_sigma(0.4);
297            assert!(s >= 0.2 && s < 0.6, "out of [0.2, 0.6): {}", s);
298        }
299        for _ in 0..100 {
300            let s = randomize_sigma(1.0);
301            assert!(s >= 0.5 && s < 1.5, "out of [0.5, 1.5): {}", s);
302        }
303    }
304}