use std::sync::Arc;
use std::sync::Mutex;
use std::time::Duration;
use tokio::sync::watch;
use crate::bus::{LocalInstant, RobotInstant, TimelineId};
pub use phoxal_runtime_contract::{BootId, ExecutionOrigin};
#[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;
}
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);
};
let started_at = LocalInstant::from_boot_ns(origin.boot_ns());
if now < started_at {
return ClockReading::Unsynchronized(TimeUnsynchronized::ClockFault);
}
let ticks =
u64::try_from(now.saturating_duration_since(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))
}
}
#[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 execution_origins_and_local_instants_share_the_same_boot_clock_scale() {
let before = LocalInstant::try_now().expect("test host clock");
let origin = ExecutionOrigin::mint();
let after = LocalInstant::try_now().expect("test host clock");
assert!(
before.boot_ns() <= origin.boot_ns() && origin.boot_ns() <= after.boot_ns(),
"runtime-contract origin {} escaped the bus clock interval {}..={}",
origin.boot_ns(),
before.boot_ns(),
after.boot_ns()
);
}
#[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::from_raw(BootId::current().get() ^ 0xffff),
LocalInstant::try_now().expect("test host clock").boot_ns(),
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))
);
}
}