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}
40
41impl Orchestrator {
42    pub fn new(sources: Vec<Box<dyn TimeSource>>, verbose: bool) -> Self {
43        Self { sources, verbose }
44    }
45
46    /// Try each source in order; return first success with the method name.
47    pub fn resolve(
48        &self,
49        target: SocketAddr,
50        timeout: Duration,
51    ) -> Result<(OffsetMicros, &'static str), OrchestratorError> {
52        let mut last_err: Option<String> = None;
53
54        for src in &self.sources {
55            match src.fetch(target, timeout) {
56                Ok(offset) => {
57                    if self.verbose {
58                        eprintln!("[{}] offset = {}", src.name(), format_offset(offset));
59                    }
60                    return Ok((offset, src.name()));
61                }
62                Err(e) => {
63                    if self.verbose || !matches!(e, TimeSourceError::Config(_)) {
64                        eprintln!("[{}] failed: {}", src.name(), e);
65                    }
66                    last_err = Some(format!("{}: {}", src.name(), e));
67                }
68            }
69        }
70        if let Some(err) = last_err {
71            Err(OrchestratorError::AllSourcesFailed(err))
72        } else {
73            Err(OrchestratorError::NoSourcesConfigured)
74        }
75    }
76}
77
78/// Format offset as "+3.456789s" or "-0.012345s".
79pub fn format_offset(offset_us: OffsetMicros) -> String {
80    let sign = if offset_us >= 0 { "+" } else { "-" };
81    let abs = offset_us.unsigned_abs();
82    format!("{}{}.{:06}s", sign, abs / 1_000_000, abs % 1_000_000)
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn format_positive() {
91        assert_eq!(format_offset(3_456_789), "+3.456789s");
92    }
93
94    #[test]
95    fn format_negative() {
96        assert_eq!(format_offset(-12_345), "-0.012345s");
97    }
98
99    #[test]
100    fn format_zero() {
101        assert_eq!(format_offset(0), "+0.000000s");
102    }
103}