use std::path::Path;
use std::process::ExitCode;
use std::thread::sleep;
use std::time::{Duration, Instant};
use clap::Args;
use clock_bound::client::{
CLOCKBOUND_SHM_CLIENT_DEFAULT_PATH, ClockBoundClient, ClockBoundError, ClockStatus,
VMCLOCK_SHM_DEFAULT_PATH,
};
use clock_bound::daemon::autodetect::Autodetect;
const EXIT_CODE_SYNCHRONIZED: u8 = 0;
const EXIT_CODE_TIMEOUT: u8 = 1;
const EXIT_CODE_FATAL: u8 = 2;
const POLL_INTERVAL: Duration = Duration::from_secs(1);
#[derive(Debug, Args)]
pub struct WaitsyncArgs {
#[arg(short = 't', long, default_value_t = 60, value_name = "SECONDS")]
pub timeout_seconds: u64,
#[arg(short, long)]
pub verbose: bool,
}
fn wait_for_sync(
max_tries: u64,
interval: Duration,
verbose: bool,
mut observe: impl FnMut() -> Result<ClockStatus, String>,
) -> ExitCode {
let mut tries: u64 = 0;
let mut next_attempt = Instant::now();
loop {
tries += 1;
match observe() {
Ok(ClockStatus::Synchronized) => {
if verbose {
println!("try: {tries}, status: Synchronized");
}
return ExitCode::from(EXIT_CODE_SYNCHRONIZED);
}
Ok(status) => {
if verbose {
println!("try: {tries}, status: {status:?}");
}
}
Err(reason) => {
if verbose {
println!("try: {tries}, error: {reason}");
}
}
}
if max_tries != 0 && tries >= max_tries {
eprintln!(
"cbctl waitsync: clock not synchronized after {max_tries} seconds, timing out"
);
return ExitCode::from(EXIT_CODE_TIMEOUT);
}
next_attempt += interval;
sleep(next_attempt.saturating_duration_since(Instant::now()));
}
}
fn describe_error(error: &ClockBoundError) -> String {
if error.detail.is_empty() {
format!("{:?}", error.kind)
} else {
format!("{:?}: {}", error.kind, error.detail)
}
}
struct ShmObserver {
clockbound_shm_path: String,
vmclock_shm_path: String,
client: Option<ClockBoundClient>,
}
impl ShmObserver {
fn new(clockbound_shm_path: &str, vmclock_shm_path: &str) -> Self {
ShmObserver {
clockbound_shm_path: String::from(clockbound_shm_path),
vmclock_shm_path: String::from(vmclock_shm_path),
client: None,
}
}
fn observe(&mut self) -> Result<ClockStatus, String> {
if self.client.is_none() {
match ClockBoundClient::new_with_paths(
&self.clockbound_shm_path,
&self.vmclock_shm_path,
) {
Ok(client) => self.client = Some(client),
Err(error) => return Err(describe_error(&error)),
}
}
let client = self.client.as_mut().expect("client was just created above");
match client.now() {
Ok(result) => Ok(result.clock_status),
Err(error) => {
self.client = None;
Err(describe_error(&error))
}
}
}
}
pub fn run(args: &WaitsyncArgs) -> ExitCode {
let autodetect = match Autodetect::detect() {
Ok(result) => result,
Err(error) => {
if args.verbose {
eprintln!(
"cbctl waitsync: warning: could not determine the platform ({error}); \
skipping the VMClock environment check"
);
}
Autodetect::Other
}
};
let vmclock_required = match &autodetect {
Autodetect::Amazon(amazon) => !amazon.is_metal(),
Autodetect::Other => false,
};
if vmclock_required && !Path::new(VMCLOCK_SHM_DEFAULT_PATH).exists() {
eprintln!(
"cbctl waitsync: the VMClock device is expected for time synchronization in \
virtualized EC2 environments, but was not found."
);
return ExitCode::from(EXIT_CODE_FATAL);
}
let mut observer =
ShmObserver::new(CLOCKBOUND_SHM_CLIENT_DEFAULT_PATH, VMCLOCK_SHM_DEFAULT_PATH);
wait_for_sync(args.timeout_seconds, POLL_INTERVAL, args.verbose, || {
observer.observe()
})
}
#[cfg(test)]
mod test {
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering::Relaxed;
use clock_bound::client::ClockBoundErrorKind;
use errno::Errno;
use super::*;
fn sequence(
results: Vec<Result<ClockStatus, String>>,
calls: &AtomicUsize,
) -> impl FnMut() -> Result<ClockStatus, String> + '_ {
move || {
let index = calls.fetch_add(1, Relaxed).min(results.len() - 1);
results[index].clone()
}
}
#[test]
fn describe_error_prepends_kind_to_detail() {
let error = ClockBoundError {
kind: ClockBoundErrorKind::SegmentNotInitialized,
errno: Errno(0),
detail: String::from("waiting for the daemon"),
};
assert_eq!(
describe_error(&error),
"SegmentNotInitialized: waiting for the daemon"
);
}
#[test]
fn describe_error_without_detail_is_kind_only() {
let error = ClockBoundError {
kind: ClockBoundErrorKind::Syscall,
errno: Errno(0),
detail: String::new(),
};
assert_eq!(describe_error(&error), "Syscall");
}
#[test]
fn wait_for_sync_synchronized_immediately() {
let calls = AtomicUsize::new(0);
let observe = sequence(vec![Ok(ClockStatus::Synchronized)], &calls);
let exit_code = wait_for_sync(5, Duration::ZERO, false, observe);
assert_eq!(exit_code, ExitCode::from(EXIT_CODE_SYNCHRONIZED));
assert_eq!(calls.load(Relaxed), 1);
}
#[test]
fn wait_for_sync_synchronized_after_retries() {
let calls = AtomicUsize::new(0);
let observe = sequence(
vec![
Err(String::from("failed to open shared memory segment")),
Ok(ClockStatus::Unknown),
Ok(ClockStatus::FreeRunning),
Ok(ClockStatus::Synchronized),
],
&calls,
);
let exit_code = wait_for_sync(10, Duration::ZERO, false, observe);
assert_eq!(exit_code, ExitCode::from(EXIT_CODE_SYNCHRONIZED));
assert_eq!(calls.load(Relaxed), 4);
}
#[test]
fn wait_for_sync_timeout() {
let calls = AtomicUsize::new(0);
let observe = sequence(vec![Ok(ClockStatus::FreeRunning)], &calls);
let exit_code = wait_for_sync(3, Duration::ZERO, false, observe);
assert_eq!(exit_code, ExitCode::from(EXIT_CODE_TIMEOUT));
assert_eq!(calls.load(Relaxed), 3);
}
#[test]
fn wait_for_sync_disrupted_times_out() {
let calls = AtomicUsize::new(0);
let observe = sequence(vec![Ok(ClockStatus::Disrupted)], &calls);
let exit_code = wait_for_sync(2, Duration::ZERO, false, observe);
assert_eq!(exit_code, ExitCode::from(EXIT_CODE_TIMEOUT));
assert_eq!(calls.load(Relaxed), 2);
}
#[test]
fn wait_for_sync_zero_waits_indefinitely() {
let mut results = vec![Ok(ClockStatus::FreeRunning); 100];
results.push(Ok(ClockStatus::Synchronized));
let calls = AtomicUsize::new(0);
let observe = sequence(results, &calls);
let exit_code = wait_for_sync(0, Duration::ZERO, false, observe);
assert_eq!(exit_code, ExitCode::from(EXIT_CODE_SYNCHRONIZED));
assert_eq!(calls.load(Relaxed), 101);
}
#[test]
fn wait_for_sync_persistent_error_times_out() {
let calls = AtomicUsize::new(0);
let observe = sequence(vec![Err(String::from("daemon not running"))], &calls);
let exit_code = wait_for_sync(4, Duration::ZERO, false, observe);
assert_eq!(exit_code, ExitCode::from(EXIT_CODE_TIMEOUT));
assert_eq!(calls.load(Relaxed), 4);
}
}