use std::{net::SocketAddr, sync::Arc};
use tokio::{
sync::{mpsc, watch},
time::Duration,
};
use tracing::{debug, error, info};
use super::resolver;
use crate::daemon::io::ntp::{DaemonInfo, ExtensionField, Ntp, NtpIoError};
use crate::daemon::io::{ClockDisruptionEvent, ControlRequest};
use crate::daemon::{async_ring_buffer, event, selected_clock::SelectedClockSource};
const INTERVAL: Duration = Duration::from_secs(16);
const SAMPLE_TIMEOUT: Duration = Duration::from_millis(100);
const MAX_CONSECUTIVE_TIMEOUTS: u32 = 5;
#[derive(Debug)]
pub struct NtpSource {
pool_domain: String,
ntp: Ntp,
consecutive_timeout_count: u32,
resolver_tx: mpsc::Sender<resolver::Message>,
sent_unreachable_msg: bool,
}
impl NtpSource {
#[expect(clippy::too_many_arguments)]
pub fn construct(
pool_domain: String,
address: SocketAddr,
event_sender: async_ring_buffer::Sender<event::Ntp>,
ctrl_receiver: mpsc::Receiver<ControlRequest>,
clock_disruption_receiver: watch::Receiver<ClockDisruptionEvent>,
selected_clock: Arc<SelectedClockSource>,
daemon_info: DaemonInfo,
resolver_tx: mpsc::Sender<resolver::Message>,
) -> Self {
let extensions = vec![ExtensionField::Fec2V1(daemon_info)];
let ntp = Ntp::builder()
.event_sender(event_sender)
.ctrl_receiver(ctrl_receiver)
.clock_disruption_receiver(clock_disruption_receiver)
.selected_clock(selected_clock)
.extensions(extensions)
.socket_address(address)
.timeout(SAMPLE_TIMEOUT)
.interval(INTERVAL)
.build();
NtpSource {
pool_domain,
ntp,
resolver_tx,
consecutive_timeout_count: 0,
sent_unreachable_msg: false,
}
}
#[tracing::instrument(level = "info", skip_all, fields(pool = self.pool_domain, identifier = %self.ntp.socket_address()))]
pub async fn run(&mut self) -> Result<(), NtpIoError> {
debug!("Starting DNS NTP Source IO sampling loop.");
self.register_with_resolver().await;
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;
}
info!("Received clock disruption signal.");
self.ntp.handle_disruption();
}
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!("DNS NTP Source IO runner exiting.");
self.notify_resolver_shutdown().await;
Ok(())
}
async fn handle_interval_tick(&mut self) {
let sample_result = self.ntp.sample().await;
self.handle_ntp_sample_result(sample_result).await;
}
async fn handle_ntp_sample_result(&mut self, result: Result<event::Ntp, NtpIoError>) {
match result {
Err(e @ NtpIoError::Timeout(_)) => {
debug!(?e, "DNS NTP source timed out.");
self.consecutive_timeout_count += 1;
if !self.sent_unreachable_msg
&& self.consecutive_timeout_count >= MAX_CONSECUTIVE_TIMEOUTS
{
self.sent_unreachable_msg = true;
self.notify_resolver_unreachable().await;
}
}
Err(e) => {
debug!(?e, "Failed to sample DNS NTP source.");
}
Ok(ntp_event) => {
self.consecutive_timeout_count = 0;
if self.ntp.send_event(&ntp_event).is_err() {
debug!("Attempted to send ntp event to buffer when it's shutting down");
}
}
}
}
async fn register_with_resolver(&self) {
let addr = self.ntp.socket_address().ip();
if self
.resolver_tx
.send(resolver::Message::new_register_addr(addr))
.await
.is_err()
{
info!("Resolver channel closed during registration, source will exit.");
}
}
async fn notify_resolver_unreachable(&self) {
let addr = self.ntp.socket_address().ip();
if self
.resolver_tx
.send(resolver::Message::new_unreachable_addr(addr))
.await
.is_err()
{
info!("Resolver channel closed during unreachable notification, source will exit.");
}
}
async fn notify_resolver_shutdown(&mut self) {
let addr = self.ntp.socket_address().ip();
if self
.resolver_tx
.send(resolver::Message::new_ntp_task_shutdown(addr))
.await
.is_err()
{
debug!("Resolver channel closed during unregister notification, source will exit.");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::daemon::event::{Ntp as NtpEvent, NtpData, Stratum};
use crate::daemon::io::ClockDisruptionEvent;
use crate::daemon::io::ntp::DaemonInfo;
use crate::daemon::selected_clock::SelectedClockSource;
use crate::daemon::time::{Duration as CbDuration, Instant as CbInstant, TscCount};
use std::net::{Ipv4Addr, SocketAddrV4};
fn create_test_ntp_source() -> (
NtpSource,
async_ring_buffer::Receiver<event::Ntp>,
mpsc::Receiver<resolver::Message>,
) {
let (event_sender, event_receiver) = async_ring_buffer::create::<event::Ntp>(4);
let (_, ctrl_receiver) = mpsc::channel::<ControlRequest>(1);
let (_clock_disruption_sender, clock_disruption_receiver) =
watch::channel::<ClockDisruptionEvent>(ClockDisruptionEvent {
disruption_marker: None,
});
let (resolver_tx, resolver_rx) = mpsc::channel::<resolver::Message>(8);
let selected_clock = Arc::new(SelectedClockSource::default());
let daemon_info = DaemonInfo {
major_version: 3,
minor_version: 0,
startup_id: 42,
};
let address = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 123));
let source = NtpSource::construct(
"pool.ntp.org".to_string(),
address,
event_sender,
ctrl_receiver,
clock_disruption_receiver,
selected_clock,
daemon_info,
resolver_tx,
);
(source, event_receiver, resolver_rx)
}
fn create_ntp_event() -> NtpEvent {
NtpEvent::builder()
.counter_pre(TscCount::new(1000))
.counter_post(TscCount::new(2000))
.ntp_data(NtpData {
server_recv_time: CbInstant::from_secs(10),
server_send_time: CbInstant::from_secs(11),
root_delay: CbDuration::from_micros(100),
root_dispersion: CbDuration::from_micros(50),
stratum: Stratum::ONE,
})
.build()
.expect("valid NTP event")
}
async fn create_timeout_error() -> NtpIoError {
let elapsed =
tokio::time::timeout(Duration::from_nanos(0), futures::future::pending::<()>())
.await
.unwrap_err();
NtpIoError::Timeout(elapsed)
}
#[tokio::test]
async fn ok_result_sends_event_and_resets_consecutive_timeout_count() {
let (mut source, rx, _resolver_rx) = create_test_ntp_source();
source.consecutive_timeout_count = 3;
let ntp_event = create_ntp_event();
source.handle_ntp_sample_result(Ok(ntp_event.clone())).await;
assert_eq!(source.consecutive_timeout_count, 0);
let sent_event = rx.recv().await.unwrap();
assert_eq!(sent_event, ntp_event);
}
#[tokio::test]
async fn ok_result_does_not_change_marked_unreachable() {
let (mut source, _rx, _resolver_rx) = create_test_ntp_source();
source.sent_unreachable_msg = true;
source.consecutive_timeout_count = MAX_CONSECUTIVE_TIMEOUTS;
let ntp_event = create_ntp_event();
source.handle_ntp_sample_result(Ok(ntp_event)).await;
assert!(source.sent_unreachable_msg);
}
#[tokio::test]
async fn timeout_error_increments_consecutive_timeout_count() {
let (mut source, _rx, _resolver_rx) = create_test_ntp_source();
assert_eq!(source.consecutive_timeout_count, 0);
for expected_count in 1..MAX_CONSECUTIVE_TIMEOUTS {
let err = create_timeout_error().await;
source.handle_ntp_sample_result(Err(err)).await;
assert_eq!(source.consecutive_timeout_count, expected_count);
}
}
#[tokio::test]
async fn non_timeout_error_does_not_increment_consecutive_timeout_count() {
let (mut source, _rx, _resolver_rx) = create_test_ntp_source();
assert_eq!(source.consecutive_timeout_count, 0);
let io_err = NtpIoError::SampleIo(std::io::Error::new(
std::io::ErrorKind::ConnectionRefused,
"connection refused",
));
source.handle_ntp_sample_result(Err(io_err)).await;
assert_eq!(source.consecutive_timeout_count, 0);
assert!(!source.sent_unreachable_msg);
}
#[tokio::test]
async fn notifies_resolver_unreachable_when_consecutive_timeouts_reach_max() {
let (mut source, _rx, mut resolver_rx) = create_test_ntp_source();
for _ in 0..MAX_CONSECUTIVE_TIMEOUTS {
let err = create_timeout_error().await;
source.handle_ntp_sample_result(Err(err)).await;
}
assert_eq!(source.consecutive_timeout_count, MAX_CONSECUTIVE_TIMEOUTS);
assert!(source.sent_unreachable_msg);
let msg = resolver_rx.recv().await.expect("resolver message sent");
let expected_addr = source.ntp.socket_address().ip();
assert_eq!(msg, resolver::Message::new_unreachable_addr(expected_addr));
}
}