use std::sync::Arc;
use std::sync::Mutex;
use std::time::Duration;
use tokio::sync::watch;
use crate::bus::{LocalInstant, RobotInstant, TimelineId};
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
pub enum TimeUnsynchronized {
#[error("the launch contract carried no valid execution origin")]
MissingOrigin,
#[error("the execution origin belongs to a different host boot")]
ForeignBoot,
#[error("the host boot clock read failed or regressed")]
ClockFault,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ClockReading {
Synchronized(RobotInstant),
Unsynchronized(TimeUnsynchronized),
}
impl ClockReading {
pub const fn instant(self) -> Option<RobotInstant> {
match self {
ClockReading::Synchronized(instant) => Some(instant),
ClockReading::Unsynchronized(_) => None,
}
}
}
pub trait ClockSource: Send + Sync + 'static {
fn read(&self) -> ClockReading;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ExecutionOrigin {
boot: BootId,
at: LocalInstant,
timeline: TimelineId,
}
impl ExecutionOrigin {
pub fn try_mint() -> Option<Self> {
Some(ExecutionOrigin {
boot: BootId::current(),
at: LocalInstant::try_now()?,
timeline: TimelineId::mint(),
})
}
pub fn mint() -> Self {
Self::try_mint().expect("the host boot clock must be readable to start an execution")
}
pub const fn new(boot: BootId, at: LocalInstant, timeline: TimelineId) -> Self {
ExecutionOrigin { boot, at, timeline }
}
pub const fn boot(self) -> BootId {
self.boot
}
pub const fn started_at(self) -> LocalInstant {
self.at
}
pub const fn timeline(self) -> TimelineId {
self.timeline
}
pub fn encode(self) -> String {
format!(
"{}:{}:{}",
self.boot.0,
self.at.boot_ns(),
self.timeline.get()
)
}
pub fn decode(value: &str) -> Option<Self> {
let mut parts = value.split(':');
let boot = BootId(parts.next()?.parse().ok()?);
let at = LocalInstant::from_boot_ns(parts.next()?.parse().ok()?);
let timeline = TimelineId::from_raw(parts.next()?.parse().ok()?)?;
if parts.next().is_some() {
return None;
}
Some(ExecutionOrigin { boot, at, timeline })
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BootId(u64);
impl BootId {
pub fn current() -> Self {
BootId(host_boot_identity())
}
}
pub struct RealClock {
origin: Result<ExecutionOrigin, TimeUnsynchronized>,
last_ticks: Mutex<u64>,
}
impl RealClock {
pub fn new(origin: ExecutionOrigin) -> Self {
let origin = if origin.boot() == BootId::current() {
Ok(origin)
} else {
Err(TimeUnsynchronized::ForeignBoot)
};
RealClock {
origin,
last_ticks: Mutex::new(0),
}
}
pub fn without_origin() -> Self {
RealClock {
origin: Err(TimeUnsynchronized::MissingOrigin),
last_ticks: Mutex::new(0),
}
}
}
impl ClockSource for RealClock {
fn read(&self) -> ClockReading {
if LocalInstant::clock_faulted() {
return ClockReading::Unsynchronized(TimeUnsynchronized::ClockFault);
}
let origin = match self.origin {
Ok(origin) => origin,
Err(reason) => return ClockReading::Unsynchronized(reason),
};
let Some(now) = LocalInstant::try_now() else {
return ClockReading::Unsynchronized(TimeUnsynchronized::ClockFault);
};
if now < origin.started_at() {
return ClockReading::Unsynchronized(TimeUnsynchronized::ClockFault);
}
let ticks = u64::try_from(
now.saturating_duration_since(origin.started_at())
.as_nanos(),
)
.unwrap_or(u64::MAX);
let mut last = self.last_ticks.lock().expect("real clock poisoned");
if ticks < *last {
return ClockReading::Unsynchronized(TimeUnsynchronized::ClockFault);
}
*last = ticks;
ClockReading::Synchronized(RobotInstant::new(origin.timeline(), ticks))
}
}
#[derive(Clone)]
pub struct SimulationClock {
rx: watch::Receiver<Option<RobotInstant>>,
}
impl SimulationClock {
pub(crate) fn from_receiver(rx: watch::Receiver<Option<RobotInstant>>) -> Self {
Self { rx }
}
}
impl ClockSource for SimulationClock {
fn read(&self) -> ClockReading {
match *self.rx.borrow() {
Some(instant) => ClockReading::Synchronized(instant),
None => ClockReading::Unsynchronized(TimeUnsynchronized::MissingOrigin),
}
}
}
#[derive(Clone)]
pub struct TestClock {
state: Arc<Mutex<(TimelineId, u64)>>,
unsynchronized: Arc<Mutex<Option<TimeUnsynchronized>>>,
}
impl TestClock {
pub fn new() -> Self {
TestClock {
state: Arc::new(Mutex::new((TimelineId::mint(), 0))),
unsynchronized: Arc::new(Mutex::new(None)),
}
}
pub fn timeline(&self) -> TimelineId {
self.state.lock().expect("test clock poisoned").0
}
pub fn set_unsynchronized(&self, reason: TimeUnsynchronized) {
*self.unsynchronized.lock().expect("test clock poisoned") = Some(reason);
}
pub fn advance(&self, delta: Duration) {
let mut state = self.state.lock().expect("test clock poisoned");
let ticks = u64::try_from(delta.as_nanos()).unwrap_or(u64::MAX);
state.1 = state.1.saturating_add(ticks);
}
pub fn replace_timeline(&self) -> TimelineId {
let mut state = self.state.lock().expect("test clock poisoned");
state.0 = TimelineId::mint();
state.1 = 0;
state.0
}
}
impl Default for TestClock {
fn default() -> Self {
TestClock::new()
}
}
impl ClockSource for TestClock {
fn read(&self) -> ClockReading {
if let Some(reason) = *self.unsynchronized.lock().expect("test clock poisoned") {
return ClockReading::Unsynchronized(reason);
}
let state = self.state.lock().expect("test clock poisoned");
ClockReading::Synchronized(RobotInstant::new(state.0, state.1))
}
}
fn host_boot_identity() -> u64 {
#[cfg(target_os = "linux")]
{
if let Ok(boot_id) = std::fs::read_to_string("/proc/sys/kernel/random/boot_id") {
return fnv1a(boot_id.trim().as_bytes());
}
}
#[cfg(target_os = "macos")]
{
let mut boottime = libc::timeval {
tv_sec: 0,
tv_usec: 0,
};
let mut size = std::mem::size_of::<libc::timeval>();
let outcome = unsafe {
libc::sysctlbyname(
c"kern.boottime".as_ptr(),
(&raw mut boottime).cast(),
&raw mut size,
std::ptr::null_mut(),
0,
)
};
if outcome == 0 {
return fnv1a(&boottime.tv_sec.to_le_bytes());
}
}
let wall = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|since| since.as_secs())
.unwrap_or(0);
let uptime = LocalInstant::try_now()
.map(|now| Duration::from_nanos(now.boot_ns()).as_secs())
.unwrap_or(0);
fnv1a(&wall.saturating_sub(uptime).to_le_bytes())
}
fn fnv1a(bytes: &[u8]) -> u64 {
let mut hash = 0xcbf2_9ce4_8422_2325_u64;
for byte in bytes {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
hash
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_real_clock_shares_one_host_wide_domain_across_processes() {
let origin = ExecutionOrigin::mint();
let a = RealClock::new(origin);
let b = RealClock::new(origin);
let ta = a.read().instant().expect("clock a must be synchronized");
let tb = b.read().instant().expect("clock b must be synchronized");
assert_eq!(ta.timeline(), tb.timeline());
let gap = tb
.duration_since(ta)
.expect("same timeline must be comparable");
assert!(
gap < Duration::from_secs(1),
"two host clocks disagree by {gap:?}"
);
}
#[test]
fn the_real_clock_never_regresses_within_a_timeline() {
let clock = RealClock::new(ExecutionOrigin::mint());
let mut last = clock.read().instant().unwrap();
for _ in 0..1000 {
let next = clock.read().instant().unwrap();
assert!(
next.checked_cmp(last).unwrap() != std::cmp::Ordering::Less,
"robot time regressed: {next} < {last}"
);
last = next;
}
}
#[test]
fn a_missing_or_foreign_boot_origin_is_reported_not_papered_over() {
assert_eq!(
RealClock::without_origin().read(),
ClockReading::Unsynchronized(TimeUnsynchronized::MissingOrigin)
);
let foreign = ExecutionOrigin::new(
BootId(BootId::current().0 ^ 0xffff),
LocalInstant::try_now().expect("test host clock"),
TimelineId::mint(),
);
assert_eq!(
RealClock::new(foreign).read(),
ClockReading::Unsynchronized(TimeUnsynchronized::ForeignBoot)
);
}
#[test]
fn an_execution_origin_round_trips_through_the_launch_contract() {
let origin = ExecutionOrigin::mint();
assert_eq!(ExecutionOrigin::decode(&origin.encode()), Some(origin));
assert_eq!(ExecutionOrigin::decode("garbage"), None);
assert_eq!(
ExecutionOrigin::decode("1:2:0"),
None,
"timeline zero is not a timeline"
);
assert_eq!(ExecutionOrigin::decode("1:2:3:4"), None);
}
#[test]
fn the_boot_identity_is_stable_within_one_boot() {
assert_eq!(BootId::current(), BootId::current());
}
#[test]
fn the_test_clock_is_deterministic_and_resets_onto_a_new_timeline() {
let clock = TestClock::new();
let first = clock.timeline();
assert_eq!(
clock.read(),
ClockReading::Synchronized(RobotInstant::new(first, 0))
);
clock.advance(Duration::from_nanos(5));
clock.advance(Duration::from_nanos(7));
assert_eq!(
clock.read(),
ClockReading::Synchronized(RobotInstant::new(first, 12))
);
let second = clock.replace_timeline();
assert_ne!(second, first);
assert_eq!(
clock.read(),
ClockReading::Synchronized(RobotInstant::new(second, 0))
);
}
}