Skip to main content

ad_time/
time_src.rs

1use std::net::SocketAddr;
2use std::time::Duration;
3
4use rand::Rng;
5
6/// Signed offset in microseconds: server_time - local_time.
7/// Positive means the server is ahead of us.
8pub type OffsetMicros = i64;
9
10#[derive(Debug, thiserror::Error)]
11pub enum TimeSourceError {
12    #[error("connection timed out")]
13    Timeout,
14    #[error("connection refused")]
15    Refused,
16    #[error("protocol error: {0}")]
17    Protocol(String),
18    #[error("parse error: {0}")]
19    Parse(String),
20    #[error("config error: {0}")]
21    Config(String),
22}
23
24pub trait TimeSource {
25    fn name(&self) -> &'static str;
26    fn fetch(&self, target: SocketAddr, timeout: Duration)
27        -> Result<OffsetMicros, TimeSourceError>;
28}
29
30#[derive(Debug, thiserror::Error)]
31pub enum OrchestratorError {
32    #[error("all time sources failed. Last error: {0}")]
33    AllSourcesFailed(String),
34    #[error("no sources configured")]
35    NoSourcesConfigured,
36}
37
38pub struct Orchestrator {
39    sources: Vec<Box<dyn TimeSource>>,
40    verbose: bool,
41}
42
43impl Orchestrator {
44    pub fn new(sources: Vec<Box<dyn TimeSource>>, verbose: bool) -> Self {
45        Self { sources, verbose }
46    }
47
48    /// Try each source in order; return first success with the method name.
49    pub fn resolve(
50        &self,
51        target: SocketAddr,
52        timeout: Duration,
53    ) -> Result<(OffsetMicros, &'static str), OrchestratorError> {
54        let mut last_err: Option<String> = None;
55
56        let n = self.sources.len();
57        for (i, src) in self.sources.iter().enumerate() {
58            match src.fetch(target, timeout) {
59                Ok(offset) => {
60                    if self.verbose {
61                        eprintln!("[{}] offset = {}", src.name(), format_offset(offset));
62                    }
63                    return Ok((offset, src.name()));
64                }
65                Err(e) => {
66                    // Always show protocol failures (Timeout, Refused, Parse) so user knows why it failed.
67                    // Only suppress Config errors when not verbose.
68                    if self.verbose || !matches!(e, TimeSourceError::Config(_)) {
69                        eprintln!("[{}] failed: {}", src.name(), e);
70                    }
71                    last_err = Some(format!("{}: {}", src.name(), e));
72                    if i + 1 < n {
73                        let jitter_ms: u64 = rand::thread_rng().gen_range(500..=5_000);
74                        std::thread::sleep(Duration::from_millis(jitter_ms));
75                    }
76                }
77            }
78        }
79        if let Some(err) = last_err {
80            Err(OrchestratorError::AllSourcesFailed(err))
81        } else {
82            Err(OrchestratorError::NoSourcesConfigured)
83        }
84    }
85}
86
87/// Format offset as "+3.456789s" or "-0.012345s".
88pub fn format_offset(offset_us: OffsetMicros) -> String {
89    let sign = if offset_us >= 0 { "+" } else { "-" };
90    let abs = offset_us.unsigned_abs();
91    format!("{}{}.{:06}s", sign, abs / 1_000_000, abs % 1_000_000)
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn format_positive() {
100        assert_eq!(format_offset(3_456_789), "+3.456789s");
101    }
102
103    #[test]
104    fn format_negative() {
105        assert_eq!(format_offset(-12_345), "-0.012345s");
106    }
107
108    #[test]
109    fn format_zero() {
110        assert_eq!(format_offset(0), "+0.000000s");
111    }
112}