use std::net::{Ipv4Addr, SocketAddrV4};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{mpsc, watch};
use tracing::{debug, error, info};
use super::ntp::{BurstConfig, Ntp, NtpIoError};
use super::{ClockDisruptionEvent, ControlRequest};
use crate::daemon::{async_ring_buffer, event, selected_clock::SelectedClockSource};
pub const BURST_DURATION: Duration = Duration::from_secs(1);
pub const BURST_INTERVAL: Duration = Duration::from_millis(50);
pub const ADDRESS: SocketAddrV4 = SocketAddrV4::new(Ipv4Addr::new(169, 254, 169, 123), 123);
pub const INTERVAL_DURATION: Duration = Duration::from_secs(2);
pub const SAMPLE_TIMEOUT: Duration = Duration::from_millis(100);
#[derive(Debug)]
pub struct AmazonTimeSync {
ntp: Ntp,
}
impl AmazonTimeSync {
pub fn construct(
event_sender: async_ring_buffer::Sender<event::Ntp>,
ctrl_receiver: mpsc::Receiver<ControlRequest>,
clock_disruption_receiver: watch::Receiver<ClockDisruptionEvent>,
selected_clock: Arc<SelectedClockSource>,
) -> Self {
let burst_config = BurstConfig {
duration: BURST_DURATION,
interval: BURST_INTERVAL,
};
let ntp = Ntp::builder()
.event_sender(event_sender)
.ctrl_receiver(ctrl_receiver)
.clock_disruption_receiver(clock_disruption_receiver)
.selected_clock(selected_clock)
.extensions(vec![]) .socket_address(ADDRESS.into())
.timeout(SAMPLE_TIMEOUT)
.interval(INTERVAL_DURATION)
.burst_config(burst_config)
.build();
AmazonTimeSync { ntp }
}
#[tracing::instrument(level = "info", skip_all, fields(identifier = %self.ntp.socket_address()))]
pub async fn run(&mut self) -> Result<(), NtpIoError> {
debug!("Starting Amazon Time Sync sampling loop.");
loop {
let branches = self.ntp.select_branches();
tokio::select! {
biased; val = branches.clock_disruption_receiver.changed() => {
if let Err(e) = val {
error!(?e, "Clock disruption receiver dropped.");
break;
}
if self.ntp.handle_disruption() {
info!("Amazon Time Sync source transitioning from `Normal` mode to `Burst` mode.");
}
}
ctrl_req = branches.ctrl_receiver.recv() => {
match ctrl_req {
None => {
break;
},
Some(ControlRequest::Shutdown) => {
debug!("Received shutdown signal. Exiting.");
break;
},
}
}
_ = branches.interval.tick() => {
self.handle_interval_tick().await;
}
}
}
debug!("Amazon Time Sync runner exiting.");
Ok(())
}
async fn handle_interval_tick(&mut self) {
let ntp_event = match self.ntp.sample().await {
Ok(ntp_event) => ntp_event,
Err(e) => {
tracing::debug!(?e, "Failed to sample Amazon Time Sync source.");
return;
}
};
if let Err(e) = self.ntp.send_event(&ntp_event) {
error!(?e, "channel closed. Not supported on this task");
panic!("unable to communicate with daemon. {e:?}");
}
if self.ntp.handle_burst_expiry() {
info!("Amazon Time Sync source transitioning from `Burst` mode to `Normal` mode.");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
async fn create_amazon_time_sync() -> (
AmazonTimeSync,
watch::Sender<ClockDisruptionEvent>,
Arc<SelectedClockSource>,
) {
let (event_sender, _) = async_ring_buffer::create::<event::Ntp>(1);
let (_, ctrl_receiver) = mpsc::channel::<ControlRequest>(1);
let (clock_disruption_sender, clock_disruption_receiver) =
watch::channel::<ClockDisruptionEvent>(ClockDisruptionEvent {
disruption_marker: None,
});
let selected_clock = Arc::new(SelectedClockSource::default());
(
AmazonTimeSync::construct(
event_sender,
ctrl_receiver,
clock_disruption_receiver,
selected_clock.clone(),
),
clock_disruption_sender,
selected_clock,
)
}
#[tokio::test]
async fn validate_to_burst_mode() {
let (mut amazon_time_sync, clock_disruption_sender, _) = create_amazon_time_sync().await;
clock_disruption_sender
.send(ClockDisruptionEvent {
disruption_marker: Some(1),
})
.unwrap();
amazon_time_sync.ntp.handle_disruption();
let branches = amazon_time_sync.ntp.select_branches();
assert_eq!(branches.interval.period(), BURST_INTERVAL,);
}
#[tokio::test(start_paused = true)]
async fn validate_to_normal_mode() {
let (mut amazon_time_sync, clock_disruption_sender, _) = create_amazon_time_sync().await;
clock_disruption_sender
.send(ClockDisruptionEvent {
disruption_marker: Some(1),
})
.unwrap();
amazon_time_sync.ntp.handle_disruption();
let branches = amazon_time_sync.ntp.select_branches();
assert_eq!(branches.interval.period(), BURST_INTERVAL);
tokio::time::advance(BURST_DURATION + Duration::from_millis(1)).await;
assert!(amazon_time_sync.ntp.handle_burst_expiry());
let branches = amazon_time_sync.ntp.select_branches();
assert_eq!(branches.interval.period(), INTERVAL_DURATION);
}
}