use std::sync::{Arc, Mutex};
use thiserror::Error;
use tokio::{
fs, io,
sync::{mpsc, watch},
time::{Duration, Interval, MissedTickBehavior, interval},
};
use tracing::{debug, error, info};
use crate::shm::ShmError;
use crate::vmclock::{shm::VMClockShmBody, shm_reader::VMClockShmReader};
use super::{ClockDisruptionEvent, ControlRequest};
const VMCLOCK_TIMEOUT: Duration = Duration::from_millis(100);
const VMCLOCK_FAILED_LOG_INTERVAL: Duration = Duration::from_secs(10);
pub struct VMClock {
path: String,
shared_state: Arc<Mutex<State>>,
internal_state: InternalState,
interval: Interval,
ctrl_receiver: mpsc::Receiver<ControlRequest>,
clock_disruption_sender: watch::Sender<ClockDisruptionEvent>,
}
impl VMClock {
pub fn construct(
vmclock_shm_path: &str,
ctrl_receiver: mpsc::Receiver<ControlRequest>,
clock_disruption_sender: watch::Sender<ClockDisruptionEvent>,
) -> VMClock {
VMClock {
path: vmclock_shm_path.into(),
shared_state: Arc::new(Mutex::new(State::Failed)),
internal_state: InternalState::Failed,
interval: interval(VMCLOCK_FAILED_LOG_INTERVAL),
ctrl_receiver,
clock_disruption_sender,
}
}
pub async fn initialize(&mut self) -> Result<(), Error> {
if !fs::try_exists(&self.path).await? {
return Err(Error::FileNonexistent(self.path.clone()));
}
let mut reader = Reader::new(&self.path)?;
let previous_shm_body = *reader.snapshot()?;
self.transition_to_running(Running {
reader,
previous_shm_body,
});
Ok(())
}
pub fn shared_state(&self) -> Arc<Mutex<State>> {
self.shared_state.clone()
}
pub fn last_disruption_marker(&self) -> u64 {
match &self.internal_state {
InternalState::Running(running) => running.previous_shm_body.disruption_marker,
InternalState::Failed => 0,
}
}
pub async fn run(&mut self) {
debug!("Starting VMClock runner.");
loop {
tokio::select! {
_ = self.interval.tick() => {
self.handle_tick();
}
ctrl_req = self.ctrl_receiver.recv() => {
match ctrl_req {
None => break,
Some(ControlRequest::Shutdown) => {
debug!("Received shutdown signal. Exiting.");
break;
}
}
}
}
}
debug!("VMClock runner exiting.");
}
fn handle_tick(&mut self) {
match &mut self.internal_state {
InternalState::Running(running) => {
let res = running.sample();
self.handle_sample_result(res);
}
InternalState::Failed => {
error!("VMClock expected but not found. ClockStatus UNKNOWN");
}
}
}
fn handle_sample_result(&mut self, res: Result<ClockDisruptionStatus, ShmError>) {
match res {
Ok(ClockDisruptionStatus::Disrupted(disruption_marker)) => {
self.clock_disruption_sender
.send(ClockDisruptionEvent {
disruption_marker: Some(disruption_marker),
})
.unwrap();
info!(
disruption_marker,
"A clock disruption event occurred and a disruption event was sent."
);
}
Ok(ClockDisruptionStatus::Normal) => {}
Err(e) => {
error!(
?e,
"Failed to sample the VMClock. Transitioning to Failed state."
);
self.transition_to_failed();
}
}
}
fn transition_to_running(&mut self, running: Running) {
self.internal_state = InternalState::Running(Box::new(running));
self.set_shared_state(State::Running);
self.interval = interval(VMCLOCK_TIMEOUT);
}
fn transition_to_failed(&mut self) {
self.internal_state = InternalState::Failed;
self.set_shared_state(State::Failed);
let mut failed_interval = interval(VMCLOCK_FAILED_LOG_INTERVAL);
failed_interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
self.interval = failed_interval;
}
fn set_shared_state(&self, state: State) {
*self.shared_state.lock().unwrap() = state;
}
}
impl std::fmt::Debug for VMClock {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
let state = self.shared_state.lock().map(|s| *s).ok();
f.debug_struct("VMClock")
.field("path", &self.path)
.field("shared_state", &state)
.field("interval", &self.interval)
.finish_non_exhaustive()
}
}
struct Reader(VMClockShmReader);
impl Reader {
fn new(path: &str) -> Result<Self, ShmError> {
Ok(Reader(VMClockShmReader::new(path)?))
}
fn snapshot(&mut self) -> Result<&VMClockShmBody, ShmError> {
self.0.snapshot()
}
}
unsafe impl Send for Reader {}
#[derive(Debug, PartialEq)]
pub enum ClockDisruptionStatus {
Normal,
Disrupted(u64),
}
#[derive(Debug, Error)]
pub enum Error {
#[error("IO failure.")]
Io(#[from] io::Error),
#[error("Error with shared memory file.")]
ShmError(#[from] ShmError),
#[error("File does not exist")]
FileNonexistent(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum State {
Failed,
Running,
}
enum InternalState {
Failed,
Running(Box<Running>),
}
struct Running {
reader: Reader,
previous_shm_body: VMClockShmBody,
}
impl Running {
fn sample(&mut self) -> Result<ClockDisruptionStatus, ShmError> {
let vmclock_snapshot = self.reader.snapshot()?;
if self.previous_shm_body.disruption_marker != vmclock_snapshot.disruption_marker {
self.previous_shm_body.disruption_marker = vmclock_snapshot.disruption_marker;
return Ok(ClockDisruptionStatus::Disrupted(
vmclock_snapshot.disruption_marker,
));
}
Ok(ClockDisruptionStatus::Normal)
}
}
#[derive(Debug, Clone)]
pub struct VMClockParams {
pub shared_state: Arc<Mutex<State>>,
pub disruption_marker: u64,
}
#[cfg(test)]
mod test {
use super::*;
use crate::vmclock::shm::VMClockClockStatus;
use std::fs::{File, OpenOptions};
use std::io::{Seek, Write};
use tempfile::NamedTempFile;
use tokio::time::timeout;
#[repr(C)]
#[derive(Debug, Copy, Clone, PartialEq)]
struct VMClockContent {
magic: u32,
size: u32,
version: u16,
counter_id: u8,
time_type: u8,
seq_count: u32,
disruption_marker: u64,
flags: u64,
_padding: [u8; 2],
clock_status: VMClockClockStatus,
leap_second_smearing_hint: u8,
tai_offset_sec: i16,
leap_indicator: u8,
counter_period_shift: u8,
counter_value: u64,
counter_period_frac_sec: u64,
counter_period_esterror_rate_frac_sec: u64,
counter_period_maxerror_rate_frac_sec: u64,
time_sec: u64,
time_frac_sec: u64,
time_esterror_nanosec: u64,
time_maxerror_nanosec: u64,
}
impl Default for VMClockContent {
fn default() -> Self {
VMClockContent {
magic: 0x4B4C4356,
size: 104_u32,
version: 1_u16,
counter_id: 1_u8,
time_type: 0_u8,
seq_count: 10_u32,
disruption_marker: 888888_u64,
flags: 0_u64,
_padding: [0x00, 0x00],
clock_status: VMClockClockStatus::Synchronized,
leap_second_smearing_hint: 0_u8,
tai_offset_sec: 0_i16,
leap_indicator: 0_u8,
counter_period_shift: 0_u8,
counter_value: 123456_u64,
counter_period_frac_sec: 0_u64,
counter_period_esterror_rate_frac_sec: 0_u64,
counter_period_maxerror_rate_frac_sec: 0_u64,
time_sec: 0_u64,
time_frac_sec: 0_u64,
time_esterror_nanosec: 0_u64,
time_maxerror_nanosec: 0_u64,
}
}
}
fn write_vmclock_content(file: &mut File, vmclock_content: &VMClockContent) {
let slice = unsafe {
::core::slice::from_raw_parts(
(vmclock_content as *const VMClockContent) as *const u8,
::core::mem::size_of::<VMClockContent>(),
)
};
file.write_all(slice).expect("Write failed VMClockContent");
file.sync_all().expect("Sync to disk failed");
}
fn write_valid_vmclock() -> (NamedTempFile, File, String) {
let vmclock_shm_tempfile = NamedTempFile::new().expect("create vmclock file failed");
let vmclock_shm_path = vmclock_shm_tempfile
.path()
.to_str()
.expect("path is valid utf-8")
.to_owned();
let mut vmclock_shm_file = OpenOptions::new()
.write(true)
.open(&vmclock_shm_path)
.expect("open vmclock file failed");
let vmclock_content = VMClockContent::default();
write_vmclock_content(&mut vmclock_shm_file, &vmclock_content);
(vmclock_shm_tempfile, vmclock_shm_file, vmclock_shm_path)
}
fn channels() -> (
mpsc::Receiver<ControlRequest>,
watch::Sender<ClockDisruptionEvent>,
) {
let (_, ctrl_receiver) = mpsc::channel::<ControlRequest>(1);
let (clock_disruption_sender, _) = watch::channel(ClockDisruptionEvent::default());
(ctrl_receiver, clock_disruption_sender)
}
#[tokio::test]
async fn construct_does_no_io_and_starts_failed() {
let (ctrl_receiver, clock_disruption_sender) = channels();
let vmclock = VMClock::construct(
"name/of/file/that/shouldnt_exist",
ctrl_receiver,
clock_disruption_sender,
);
assert!(matches!(vmclock.internal_state, InternalState::Failed));
assert_eq!(*vmclock.shared_state().lock().unwrap(), State::Failed);
assert_eq!(vmclock.last_disruption_marker(), 0);
}
#[tokio::test]
async fn initialize_success_transitions_to_running() {
let (_tempfile, _file, path) = write_valid_vmclock();
let (ctrl_receiver, clock_disruption_sender) = channels();
let mut vmclock = VMClock::construct(&path, ctrl_receiver, clock_disruption_sender);
vmclock.initialize().await.unwrap();
assert!(matches!(
vmclock.internal_state,
InternalState::Running { .. }
));
assert_eq!(*vmclock.shared_state().lock().unwrap(), State::Running);
assert_eq!(vmclock.last_disruption_marker(), 888888);
}
#[tokio::test]
async fn initialize_failure_stays_failed() {
let (ctrl_receiver, clock_disruption_sender) = channels();
let mut vmclock = VMClock::construct(
"name/of/file/that/shouldnt_exist",
ctrl_receiver,
clock_disruption_sender,
);
let result = vmclock.initialize().await;
assert!(matches!(result, Err(Error::FileNonexistent(_))));
assert!(matches!(vmclock.internal_state, InternalState::Failed));
assert_eq!(*vmclock.shared_state().lock().unwrap(), State::Failed);
}
#[tokio::test]
async fn shared_state_getter_reflects_transitions() {
let (_tempfile, _file, path) = write_valid_vmclock();
let (ctrl_receiver, clock_disruption_sender) = channels();
let mut vmclock = VMClock::construct(&path, ctrl_receiver, clock_disruption_sender);
let shared = vmclock.shared_state();
assert_eq!(*shared.lock().unwrap(), State::Failed);
vmclock.initialize().await.unwrap();
assert_eq!(*shared.lock().unwrap(), State::Running);
}
#[tokio::test]
async fn sample_no_clock_disruption() {
let (_tempfile, _file, path) = write_valid_vmclock();
let (ctrl_receiver, clock_disruption_sender) = channels();
let mut vmclock = VMClock::construct(&path, ctrl_receiver, clock_disruption_sender);
vmclock.initialize().await.unwrap();
let InternalState::Running(running) = &mut vmclock.internal_state else {
panic!("expected Running state");
};
let clock_status = running.sample().unwrap();
assert_eq!(clock_status, ClockDisruptionStatus::Normal);
}
#[tokio::test]
async fn sample_clock_disruption() {
let (_tempfile, mut file, path) = write_valid_vmclock();
let (ctrl_receiver, clock_disruption_sender) = channels();
let mut vmclock = VMClock::construct(&path, ctrl_receiver, clock_disruption_sender);
vmclock.initialize().await.unwrap();
let mut vmclock_content = VMClockContent::default();
vmclock_content.seq_count += 10;
vmclock_content.disruption_marker += 1;
file.rewind().unwrap();
write_vmclock_content(&mut file, &vmclock_content);
let InternalState::Running(running) = &mut vmclock.internal_state else {
panic!("expected Running state");
};
let clock_status = running.sample().unwrap();
assert_eq!(
clock_status,
ClockDisruptionStatus::Disrupted(vmclock_content.disruption_marker)
);
}
#[tokio::test]
async fn interval_period_matches_running_state() {
let (_tempfile, _file, path) = write_valid_vmclock();
let (ctrl_receiver, clock_disruption_sender) = channels();
let mut vmclock = VMClock::construct(&path, ctrl_receiver, clock_disruption_sender);
vmclock.initialize().await.unwrap();
assert!(matches!(vmclock.internal_state, InternalState::Running(_)));
assert_eq!(*vmclock.shared_state().lock().unwrap(), State::Running);
assert_eq!(vmclock.interval.period(), VMCLOCK_TIMEOUT);
}
#[tokio::test]
async fn handle_sample_result_error_transitions_to_failed() {
let (_tempfile, _file, path) = write_valid_vmclock();
let (ctrl_receiver, clock_disruption_sender) = channels();
let mut vmclock = VMClock::construct(&path, ctrl_receiver, clock_disruption_sender);
vmclock.initialize().await.unwrap();
assert_eq!(vmclock.interval.period(), VMCLOCK_TIMEOUT);
vmclock.handle_sample_result(Err(ShmError::SegmentNotInitialized(
"test-induced error".into(),
)));
assert!(matches!(vmclock.internal_state, InternalState::Failed));
assert_eq!(*vmclock.shared_state().lock().unwrap(), State::Failed);
assert_eq!(vmclock.interval.period(), VMCLOCK_FAILED_LOG_INTERVAL);
}
#[tokio::test]
async fn handle_sample_result_normal_stays_running() {
let (_tempfile, _file, path) = write_valid_vmclock();
let (_, ctrl_receiver) = mpsc::channel::<ControlRequest>(1);
let (clock_disruption_sender, clock_disruption_receiver) =
watch::channel(ClockDisruptionEvent::default());
let mut vmclock = VMClock::construct(&path, ctrl_receiver, clock_disruption_sender);
vmclock.initialize().await.unwrap();
vmclock.handle_sample_result(Ok(ClockDisruptionStatus::Normal));
assert!(matches!(vmclock.internal_state, InternalState::Running(_)));
assert_eq!(*vmclock.shared_state().lock().unwrap(), State::Running);
assert_eq!(vmclock.interval.period(), VMCLOCK_TIMEOUT);
assert_eq!(clock_disruption_receiver.borrow().disruption_marker, None);
}
#[tokio::test]
async fn handle_sample_result_disrupted_sends_event() {
let (_tempfile, _file, path) = write_valid_vmclock();
let (_, ctrl_receiver) = mpsc::channel::<ControlRequest>(1);
let (clock_disruption_sender, clock_disruption_receiver) =
watch::channel(ClockDisruptionEvent::default());
let mut vmclock = VMClock::construct(&path, ctrl_receiver, clock_disruption_sender);
vmclock.initialize().await.unwrap();
vmclock.handle_sample_result(Ok(ClockDisruptionStatus::Disrupted(42)));
assert!(matches!(vmclock.internal_state, InternalState::Running(_)));
assert_eq!(*vmclock.shared_state().lock().unwrap(), State::Running);
assert_eq!(
clock_disruption_receiver.borrow().disruption_marker,
Some(42)
);
}
#[tokio::test]
async fn run_running_honors_shutdown() {
let (_tempfile, _file, path) = write_valid_vmclock();
let (ctrl_sender, ctrl_receiver) = mpsc::channel::<ControlRequest>(1);
let (clock_disruption_sender, _) = watch::channel(ClockDisruptionEvent::default());
let mut vmclock = VMClock::construct(&path, ctrl_receiver, clock_disruption_sender);
vmclock.initialize().await.unwrap();
ctrl_sender.send(ControlRequest::Shutdown).await.unwrap();
timeout(Duration::from_secs(1), vmclock.run())
.await
.unwrap();
}
#[tokio::test]
async fn run_failed_honors_shutdown() {
let (ctrl_sender, ctrl_receiver) = mpsc::channel::<ControlRequest>(1);
let (clock_disruption_sender, _) = watch::channel(ClockDisruptionEvent::default());
let mut vmclock = VMClock::construct(
"name/of/file/that/shouldnt_exist",
ctrl_receiver,
clock_disruption_sender,
);
ctrl_sender.send(ControlRequest::Shutdown).await.unwrap();
vmclock.run().await;
assert_eq!(*vmclock.shared_state().lock().unwrap(), State::Failed);
}
}