use std::num::NonZeroUsize;
use crate::daemon::time::{Duration, tsc::Skew};
pub const DEFAULT_MAX_DISPERSION_PPB: u32 = 15_000;
pub const DEFAULT_SKM_WINDOW: Duration = Duration::from_secs(1024);
pub const DEFAULT_ESTIMATE_CAPACITY: NonZeroUsize = NonZeroUsize::new(600).unwrap();
#[derive(Debug, Clone, PartialEq)]
pub struct FfSyncParams {
pub max_dispersion: Skew,
pub skm_window: Duration,
pub estimate_capacity: NonZeroUsize,
pub phc: SourceParams,
pub amazon_time_sync: SourceParams,
pub ntp: SourceParams,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SourceParams {
pub period_window: Duration,
pub theta_window: Duration,
pub rtt_threshold_multiplier: usize,
pub error_normalization_factor: f64,
}
impl SourceParams {
pub const PHC_DEFAULTS: Self = Self {
period_window: Duration::from_secs(100),
theta_window: Duration::from_secs(100),
rtt_threshold_multiplier: 5,
error_normalization_factor: 5e-7,
};
pub const NTP_DEFAULTS: Self = Self {
period_window: DEFAULT_SKM_WINDOW,
theta_window: DEFAULT_SKM_WINDOW,
rtt_threshold_multiplier: 5,
error_normalization_factor: 5e-5,
};
pub fn local_window(&self) -> Duration {
std::cmp::max(self.period_window, self.theta_window)
}
}
impl Default for FfSyncParams {
fn default() -> Self {
Self {
max_dispersion: Skew::from_ppb(f64::from(DEFAULT_MAX_DISPERSION_PPB)),
skm_window: DEFAULT_SKM_WINDOW,
estimate_capacity: DEFAULT_ESTIMATE_CAPACITY,
phc: SourceParams::PHC_DEFAULTS,
amazon_time_sync: SourceParams::NTP_DEFAULTS,
ntp: SourceParams::NTP_DEFAULTS,
}
}
}
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum FfSyncParamsError {
#[error("skm_window must be positive, got {0:?}")]
SkmWindowNotPositive(Duration),
#[error("max_dispersion must be positive")]
MaxDispersionNotPositive,
#[error("{source_type}: {window} ({value:?}) must be positive")]
WindowNotPositive {
source_type: &'static str,
window: &'static str,
value: Duration,
},
#[error("{source_type}: {window} ({value:?}) exceeds skm_window ({skm_window:?})")]
WindowExceedsSkm {
source_type: &'static str,
window: &'static str,
value: Duration,
skm_window: Duration,
},
#[error(
"{source_type}: local window ({value:?}) must hold at least {min_samples} samples at a {poll:?} poll interval, i.e. be at least {min:?}"
)]
WindowTooShort {
source_type: &'static str,
value: Duration,
poll: Duration,
min_samples: u32,
min: Duration,
},
#[error("{source_type}: rtt_threshold_multiplier must be at least 1")]
RttMultiplierZero { source_type: &'static str },
#[error("{source_type}: error_normalization_factor must be positive and finite, got {value}")]
NormalizationFactorInvalid {
source_type: &'static str,
value: f64,
},
}
const MIN_SAMPLES_PER_WINDOW: u32 = 4;
impl FfSyncParams {
pub fn validate(&self) -> Result<(), FfSyncParamsError> {
let polls = super::source::POLL_INTERVALS;
if self.skm_window <= Duration::ZERO {
return Err(FfSyncParamsError::SkmWindowNotPositive(self.skm_window));
}
if self.max_dispersion.get() <= 0.0 {
return Err(FfSyncParamsError::MaxDispersionNotPositive);
}
let sources = [
("phc", &self.phc, polls[0]),
("amazon_time_sync", &self.amazon_time_sync, polls[1]),
("ntp", &self.ntp, polls[2]),
];
for (name, source, poll) in sources {
for (window_name, value) in [
("period_window", source.period_window),
("theta_window", source.theta_window),
] {
if value <= Duration::ZERO {
return Err(FfSyncParamsError::WindowNotPositive {
source_type: name,
window: window_name,
value,
});
}
if value > self.skm_window {
return Err(FfSyncParamsError::WindowExceedsSkm {
source_type: name,
window: window_name,
value,
skm_window: self.skm_window,
});
}
}
let min = Duration::from_nanos(poll.as_nanos() * i64::from(MIN_SAMPLES_PER_WINDOW));
if source.local_window() < min {
return Err(FfSyncParamsError::WindowTooShort {
source_type: name,
value: source.local_window(),
poll,
min_samples: MIN_SAMPLES_PER_WINDOW,
min,
});
}
if source.rtt_threshold_multiplier == 0 {
return Err(FfSyncParamsError::RttMultiplierZero { source_type: name });
}
let f = source.error_normalization_factor;
if !(f.is_finite() && f > 0.0) {
return Err(FfSyncParamsError::NormalizationFactorInvalid {
source_type: name,
value: f,
});
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_are_valid_and_match_production_literals() {
let p = FfSyncParams::default();
p.validate().unwrap();
assert_eq!(p.max_dispersion, Skew::from_ppb(15_000.0));
assert_eq!(p.skm_window, Duration::from_secs(1024));
assert_eq!(p.estimate_capacity.get(), 600);
assert_eq!(p.ntp.period_window, Duration::from_secs(1024));
assert_eq!(p.amazon_time_sync.theta_window, Duration::from_secs(1024));
assert_eq!(p.phc.period_window, Duration::from_secs(100));
assert_eq!(p.phc.theta_window, Duration::from_secs(100));
assert_eq!(p.phc.local_window(), Duration::from_secs(100));
assert_eq!(p.ntp.rtt_threshold_multiplier, 5);
assert_eq!(p.phc.error_normalization_factor, 5e-7);
assert_eq!(p.ntp.error_normalization_factor, 5e-5);
}
#[test]
fn window_exceeding_skm_is_rejected() {
let mut p = FfSyncParams::default();
p.phc.theta_window = Duration::from_secs(2048);
assert!(matches!(
p.validate(),
Err(FfSyncParamsError::WindowExceedsSkm {
source_type: "phc",
window: "theta_window",
..
})
));
}
#[test]
fn window_too_short_for_poll_is_rejected() {
let mut p = FfSyncParams::default();
p.ntp.period_window = Duration::from_secs(30);
p.ntp.theta_window = Duration::from_secs(30);
assert!(matches!(
p.validate(),
Err(FfSyncParamsError::WindowTooShort {
source_type: "ntp",
..
})
));
p.ntp.theta_window = Duration::from_secs(64);
p.validate().unwrap();
}
#[test]
fn zero_and_invalid_values_are_rejected() {
let mut p = FfSyncParams::default();
p.amazon_time_sync.rtt_threshold_multiplier = 0;
assert!(matches!(
p.validate(),
Err(FfSyncParamsError::RttMultiplierZero {
source_type: "amazon_time_sync"
})
));
let mut p = FfSyncParams::default();
p.phc.error_normalization_factor = f64::NAN;
assert!(matches!(
p.validate(),
Err(FfSyncParamsError::NormalizationFactorInvalid {
source_type: "phc",
..
})
));
let mut p = FfSyncParams::default();
p.skm_window = Duration::ZERO;
assert!(matches!(
p.validate(),
Err(FfSyncParamsError::SkmWindowNotPositive(_))
));
}
}