1use std::net::SocketAddr;
2use std::time::Duration;
3
4use rand::Rng;
5
6pub 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 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 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
87pub 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}