1use std::net::SocketAddr;
2use std::time::Duration;
3
4pub 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 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
78pub 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}