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 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 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 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
109pub 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
116pub(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
123fn 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; }
139 (base_ms as f64 * factor).round() as u64
140}
141
142fn backoff_multiplier(failures: u32) -> u64 {
144 3u64.pow(failures.min(5))
145}
146
147const 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
155pub 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
165pub 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 #[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 #[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 #[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 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 #[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 #[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 #[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}