use nix::sys::time::TimeSpec;
use tracing::info;
use crate::{
daemon::{
clock_parameters::ClockParameters,
io::tsc::ReadTscImpl,
time::{
ClockExt, Duration, Instant, TscCount,
clocks::{ClockBound, RealTime},
tsc::{Period, Skew},
},
},
shm::{
ClockErrorBound, ClockErrorBoundGeneric, ClockErrorBoundLayoutVersion, ClockStatus,
ShmWrite, ShmWriter,
},
};
pub struct SafeShmWriter(ShmWriter);
impl SafeShmWriter {
pub fn new(shm_writer: ShmWriter) -> Self {
Self(shm_writer)
}
}
impl ShmWrite for SafeShmWriter {
fn write(&mut self, ceb: &ClockErrorBound) {
self.0.write(ceb);
}
}
unsafe impl Send for SafeShmWriter {}
unsafe impl Sync for SafeShmWriter {}
pub struct ClockStateWriter<T> {
clock_disruption_support_enabled: bool,
shm_writer_0: T,
shm_writer_1: T,
max_drift_ppb: u32,
disruption_marker: u64,
}
#[cfg_attr(test, mockall::automock)]
pub trait ClockStateWrite: Send + Sync {
fn initialize_ceb_v2_shm(&mut self);
fn handle_clock_parameters_shm1(
&mut self,
clock_parameters: &ClockParameters,
clock_status: ClockStatus,
);
fn handle_clock_parameters_shm0(
&mut self,
clock_parameters: &ClockParameters,
clock_status: ClockStatus,
);
fn handle_disruption(&mut self, clock_parameters: &ClockParameters, new_disruption_marker: u64);
}
impl<T: ShmWrite + Send + Sync> ClockStateWrite for ClockStateWriter<T> {
fn initialize_ceb_v2_shm(&mut self) {
info!(
"Initializing SHM segment to status `ClockStatus::Unknown` and zeroing other fields."
);
let clock_parameters = ClockParameters {
tsc_count: TscCount::new(0),
time: Instant::new(0),
clock_error_bound: Duration::new(0),
period: Period::from_seconds(0.0),
period_max_error: Period::from_seconds(0.0),
as_of_monotonic: Instant::new(0),
};
self.write_shm0(&clock_parameters, ClockStatus::Unknown);
}
fn handle_clock_parameters_shm1(
&mut self,
clock_parameters: &ClockParameters,
clock_status: ClockStatus,
) {
self.write_shm1(clock_parameters, clock_status);
}
fn handle_clock_parameters_shm0(
&mut self,
clock_parameters: &ClockParameters,
clock_status: ClockStatus,
) {
let clockbound_clock = ClockBound::new(clock_parameters.clone(), ReadTscImpl);
let offset_and_rtt = clockbound_clock.get_offset_and_rtt(&RealTime);
let clock_error_bound = clock_parameters.clock_error_bound
+ offset_and_rtt.offset().abs()
+ (offset_and_rtt.rtt() / 2);
let shm0_clock_parameters = ClockParameters {
clock_error_bound,
..clock_parameters.clone()
};
self.write_shm0(&shm0_clock_parameters, clock_status);
}
fn handle_disruption(
&mut self,
clock_parameters: &ClockParameters,
new_disruption_marker: u64,
) {
let Self {
clock_disruption_support_enabled: _,
shm_writer_0: _,
shm_writer_1: _,
max_drift_ppb: _,
disruption_marker,
} = self;
*disruption_marker = new_disruption_marker;
self.write_shm1(clock_parameters, ClockStatus::Disrupted);
self.write_shm0(clock_parameters, ClockStatus::Disrupted);
tracing::debug!("Handled clock disruption event.");
}
}
#[bon::bon]
impl<T: ShmWrite> ClockStateWriter<T> {
#[builder]
pub fn new(
clock_disruption_support_enabled: bool,
shm_writer_0: T,
shm_writer_1: T,
max_drift_ppb: u32,
disruption_marker: u64,
) -> Self {
Self {
clock_disruption_support_enabled,
shm_writer_0,
shm_writer_1,
max_drift_ppb,
disruption_marker,
}
}
fn write_shm0(&mut self, clock_parameters: &ClockParameters, clock_status: ClockStatus) {
let as_of = TimeSpec::from(clock_parameters.as_of_monotonic);
let void_after = as_of + TimeSpec::new(1000, 0);
let bound_nsec = clock_parameters.clock_error_bound.as_nanos();
let software_skew =
Skew::from_period_and_error(clock_parameters.period, clock_parameters.period_max_error);
let Some(software_skew_ppb) = software_skew.to_ppb() else {
tracing::error!(
"Software skew is too large to be expressed as a ppb. Skipping writing to SHM."
);
return;
};
let max_drift_ppb = self.max_drift_ppb + software_skew_ppb;
let ceb = ClockErrorBoundGeneric::builder()
.as_of(as_of)
.void_after(void_after)
.bound_nsec(bound_nsec)
.disruption_marker(self.disruption_marker)
.max_drift_ppb(max_drift_ppb)
.clock_status(clock_status)
.clock_disruption_support_enabled(self.clock_disruption_support_enabled)
.build(ClockErrorBoundLayoutVersion::V2);
self.shm_writer_0.write(&ceb);
}
fn write_shm1(&mut self, clock_parameters: &ClockParameters, clock_status: ClockStatus) {
let bound_nsec = clock_parameters.clock_error_bound.as_nanos();
let as_of = TimeSpec::from(clock_parameters.time);
let void_after = as_of + TimeSpec::new(1000, 0);
let period_err =
Skew::from_period_and_error(clock_parameters.period, clock_parameters.period_max_error)
.get();
if !(0.0..1.0).contains(&period_err) {
tracing::error!(
period_err,
"Relative period error is not in [0, 1). Skipping writing to SHM."
);
return;
}
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
let ceb = ClockErrorBoundGeneric::builder()
.as_of_tsc(clock_parameters.tsc_count.get() as u64)
.as_of(as_of)
.void_after(void_after)
.bound_nsec(bound_nsec)
.period(clock_parameters.period.get())
.period_err(period_err)
.disruption_marker(self.disruption_marker)
.max_drift_ppb(self.max_drift_ppb)
.clock_status(clock_status)
.clock_disruption_support_enabled(self.clock_disruption_support_enabled)
.build(ClockErrorBoundLayoutVersion::V3);
self.shm_writer_1.write(&ceb);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::daemon::time::{Duration, Instant, TscCount, tsc::Period};
use mockall::mock;
use rstest::rstest;
mock! {
ShmWriter {}
impl ShmWrite for ShmWriter {
fn write(&mut self, ceb: &ClockErrorBound);
}
}
#[bon::builder]
fn create_test_clock_parameters(
clock_error_bound_nanos: i64,
tsc_count: i64,
time_nanos: i64,
) -> ClockParameters {
ClockParameters {
tsc_count: TscCount::new(tsc_count),
time: Instant::from_nanos(time_nanos),
clock_error_bound: Duration::from_nanos(clock_error_bound_nanos),
period: Period::from_seconds(1e-9),
period_max_error: Period::from_seconds(1e-11),
as_of_monotonic: Instant::from_nanos(time_nanos),
}
}
#[rstest]
#[case::synchronized(ClockStatus::Synchronized)]
#[case::unknown(ClockStatus::Unknown)]
#[case::free_running(ClockStatus::FreeRunning)]
#[case::disrupted(ClockStatus::Disrupted)]
fn test_write_shm(#[case] clock_status: ClockStatus) {
let as_of = Instant::from_nanos(3_000_000_000);
let bound_nsec = 500;
let max_drift_ppb = 15_000;
let disruption_marker = 345;
let clock_disruption_support_enabled = true;
let expected_ceb_v2 = ClockErrorBoundGeneric::builder()
.as_of(TimeSpec::try_from(as_of).unwrap())
.void_after(TimeSpec::try_from(as_of + Duration::from_secs(1000)).unwrap())
.bound_nsec(bound_nsec)
.disruption_marker(disruption_marker)
.max_drift_ppb(max_drift_ppb + 10000000) .clock_status(clock_status)
.clock_disruption_support_enabled(clock_disruption_support_enabled)
.build(ClockErrorBoundLayoutVersion::V2);
let mut shm_writer_0 = MockShmWriter::new();
shm_writer_0
.expect_write()
.withf(move |ceb: &ClockErrorBound| expected_ceb_v2 == *ceb)
.times(1)
.return_const(());
let clock_parameters = create_test_clock_parameters()
.tsc_count(1_000_000)
.clock_error_bound_nanos(500)
.time_nanos(3_000_000_000)
.call();
let expected_ceb_v3 = ClockErrorBoundGeneric::builder()
.as_of_tsc(1_000_000)
.as_of(TimeSpec::try_from(as_of).unwrap())
.void_after(TimeSpec::try_from(as_of + Duration::from_secs(1000)).unwrap())
.period(1e-9)
.period_err(
Skew::from_period_and_error(
Period::from_seconds(1e-9),
Period::from_seconds(1e-11),
)
.get(),
)
.bound_nsec(500)
.disruption_marker(disruption_marker)
.max_drift_ppb(max_drift_ppb)
.clock_status(clock_status)
.clock_disruption_support_enabled(clock_disruption_support_enabled)
.build(ClockErrorBoundLayoutVersion::V3);
let mut shm_writer_1 = MockShmWriter::new();
shm_writer_1
.expect_write()
.withf(move |ceb: &ClockErrorBound| expected_ceb_v3 == *ceb)
.times(1)
.return_const(());
let mut clock_state_writer = ClockStateWriter::builder()
.clock_disruption_support_enabled(clock_disruption_support_enabled)
.shm_writer_0(shm_writer_0)
.shm_writer_1(shm_writer_1)
.max_drift_ppb(max_drift_ppb)
.disruption_marker(disruption_marker)
.build();
clock_state_writer.write_shm0(&clock_parameters, clock_status);
clock_state_writer.write_shm1(&clock_parameters, clock_status);
}
#[test]
fn write_shm1_skips_a_relative_period_error_of_one_or_more() {
let mut clock_parameters = create_test_clock_parameters()
.tsc_count(1_000_000)
.clock_error_bound_nanos(500)
.time_nanos(3_000_000_000)
.call();
clock_parameters.period_max_error = Period::from_seconds(1e-9);
let mut shm_writer_1 = MockShmWriter::new();
shm_writer_1.expect_write().times(0);
let mut clock_state_writer = ClockStateWriter::builder()
.clock_disruption_support_enabled(true)
.shm_writer_0(MockShmWriter::new())
.shm_writer_1(shm_writer_1)
.max_drift_ppb(15_000)
.disruption_marker(0)
.build();
clock_state_writer.write_shm1(&clock_parameters, ClockStatus::Synchronized);
}
#[test]
fn handle_clock_parameters_shm0() {
let clock_parameters = create_test_clock_parameters()
.clock_error_bound_nanos(1000)
.tsc_count(1000)
.time_nanos(123_000)
.call();
let clock_disruption_support_enabled = false;
let max_drift_ppb = 0;
let disruption_marker = 0;
let clock_status = ClockStatus::Synchronized;
let mut shm_writer_0 = MockShmWriter::new();
shm_writer_0
.expect_write()
.withf(move |ceb: &ClockErrorBound| {
ceb.void_after()
== ceb.as_of() + TimeSpec::from_duration(std::time::Duration::from_secs(1000))
&& ceb.disruption_marker() == disruption_marker
&& ceb.clock_status() == clock_status
&& ceb.clock_disruption_support_enabled() == clock_disruption_support_enabled
})
.times(1)
.return_const(());
let mut shm_writer_1 = MockShmWriter::new();
shm_writer_1.expect_write().never();
let mut clock_state_writer = ClockStateWriter::builder()
.clock_disruption_support_enabled(clock_disruption_support_enabled)
.shm_writer_0(shm_writer_0)
.shm_writer_1(shm_writer_1)
.max_drift_ppb(max_drift_ppb)
.disruption_marker(disruption_marker)
.build();
clock_state_writer.handle_clock_parameters_shm0(&clock_parameters, clock_status);
}
#[test]
fn handle_clock_parameters_shm1() {
let clock_parameters = create_test_clock_parameters()
.clock_error_bound_nanos(1000)
.tsc_count(1000)
.time_nanos(123_000)
.call();
let clock_disruption_support_enabled = false;
let max_drift_ppb = 0;
let disruption_marker = 0;
let clock_status = ClockStatus::Synchronized;
let mut shm_writer_0 = MockShmWriter::new();
shm_writer_0.expect_write().never();
let mut shm_writer_1 = MockShmWriter::new();
shm_writer_1
.expect_write()
.withf(move |ceb: &ClockErrorBound| {
ceb.void_after()
== ceb.as_of() + TimeSpec::from_duration(std::time::Duration::from_secs(1000))
&& ceb.bound_nsec() == 1000
&& ceb.disruption_marker() == disruption_marker
&& ceb.clock_status() == clock_status
&& ceb.clock_disruption_support_enabled() == clock_disruption_support_enabled
})
.times(1)
.return_const(());
let mut clock_state_writer = ClockStateWriter::builder()
.clock_disruption_support_enabled(clock_disruption_support_enabled)
.shm_writer_0(shm_writer_0)
.shm_writer_1(shm_writer_1)
.max_drift_ppb(max_drift_ppb)
.disruption_marker(disruption_marker)
.build();
clock_state_writer.handle_clock_parameters_shm1(&clock_parameters, clock_status);
}
#[test]
fn test_initialize_ceb_v2_shm() {
let clock_disruption_support_enabled = false;
let expected_max_drift_ppb = 15_000;
let expected_disruption_marker = 0;
let mut shm_writer_0 = MockShmWriter::new();
shm_writer_0
.expect_write()
.withf(move |ceb: &ClockErrorBound| {
ceb.void_after()
== ceb.as_of() + TimeSpec::from_duration(std::time::Duration::from_secs(1000))
&& ceb.bound_nsec() == 0
&& ceb.disruption_marker() == expected_disruption_marker
&& ceb.max_drift_ppb() == expected_max_drift_ppb
&& ceb.clock_status() == ClockStatus::Unknown
&& ceb.clock_disruption_support_enabled() == clock_disruption_support_enabled
})
.times(1)
.return_const(());
let mut shm_writer_1 = MockShmWriter::new();
shm_writer_1.expect_write().never();
let mut clock_state_writer = ClockStateWriter::builder()
.clock_disruption_support_enabled(clock_disruption_support_enabled)
.shm_writer_0(shm_writer_0)
.shm_writer_1(shm_writer_1)
.max_drift_ppb(expected_max_drift_ppb)
.disruption_marker(expected_disruption_marker)
.build();
assert_eq!(
clock_state_writer.disruption_marker,
expected_disruption_marker
);
clock_state_writer.initialize_ceb_v2_shm();
}
#[test]
fn handle_disruption() {
let clock_disruption_support_enabled = false;
let max_drift_ppb = 0;
let initial_disruption_marker = 0;
let final_disruption_marker = 1;
let clock_parameters = create_test_clock_parameters()
.clock_error_bound_nanos(1000)
.tsc_count(1000)
.time_nanos(123_000)
.call();
let mut shm_writer_0 = MockShmWriter::new();
shm_writer_0
.expect_write()
.withf(move |ceb: &ClockErrorBound| {
ceb.void_after()
== ceb.as_of() + TimeSpec::from_duration(std::time::Duration::from_secs(1000))
&& ceb.disruption_marker() == final_disruption_marker
&& ceb.max_drift_ppb() == 10000000 && ceb.clock_status() == ClockStatus::Disrupted
&& ceb.clock_disruption_support_enabled() == clock_disruption_support_enabled
})
.times(1)
.return_const(());
let mut shm_writer_1 = MockShmWriter::new();
shm_writer_1
.expect_write()
.withf(move |ceb: &ClockErrorBound| {
ceb.void_after()
== ceb.as_of() + TimeSpec::from_duration(std::time::Duration::from_secs(1000))
&& ceb.bound_nsec() == 1000
&& ceb.disruption_marker() == final_disruption_marker
&& ceb.max_drift_ppb() == max_drift_ppb
&& ceb.clock_status() == ClockStatus::Disrupted
&& ceb.clock_disruption_support_enabled() == clock_disruption_support_enabled
})
.times(1)
.return_const(());
let mut clock_state_writer = ClockStateWriter::builder()
.clock_disruption_support_enabled(clock_disruption_support_enabled)
.shm_writer_0(shm_writer_0)
.shm_writer_1(shm_writer_1)
.max_drift_ppb(max_drift_ppb)
.disruption_marker(initial_disruption_marker)
.build();
assert_eq!(
clock_state_writer.disruption_marker,
initial_disruption_marker
);
clock_state_writer.handle_disruption(&clock_parameters, final_disruption_marker);
assert_eq!(
clock_state_writer.disruption_marker,
final_disruption_marker
);
}
}