use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::sync::watch;
use super::{SchedulerTick, StepScheduler};
use crate::bus::RobotInstant;
use crate::participant::clock::simulation::SimulationClock;
use crate::participant::{duration_nanos, lock};
use phoxal_bus::RetiredTimelines;
pub(crate) struct SimulationScheduler {
period: Option<Duration>,
_tx_keepalive: watch::Sender<Option<RobotInstant>>,
rx: watch::Receiver<Option<RobotInstant>>,
}
struct SimulationClockState {
current: Option<RobotInstant>,
retired_timelines: RetiredTimelines,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum SimulationClockAdvance {
Advanced,
DuplicateOrBackward,
RetiredTimeline,
}
#[derive(Clone)]
pub(crate) struct SimulationClockHandle {
tx: watch::Sender<Option<RobotInstant>>,
state: Arc<Mutex<SimulationClockState>>,
}
impl SimulationClockHandle {
pub(crate) fn advance(&self, at: RobotInstant) -> SimulationClockAdvance {
let mut state = lock(&self.state);
match state.current {
Some(current) if current.timeline() == at.timeline() => {
if at.ticks() <= current.ticks() {
return SimulationClockAdvance::DuplicateOrBackward;
}
}
current => {
if state.retired_timelines.contains(at.timeline()) {
return SimulationClockAdvance::RetiredTimeline;
}
if let Some(previous) = current {
state.retired_timelines.retire(previous.timeline());
}
state.retired_timelines.activate(at.timeline());
}
}
state.current = Some(at);
self.tx.send_replace(Some(at));
SimulationClockAdvance::Advanced
}
}
impl SimulationScheduler {
pub(crate) fn new(period: Option<Duration>) -> (Self, SimulationClockHandle) {
let (tx, rx) = watch::channel(None);
let scheduler = SimulationScheduler {
period,
_tx_keepalive: tx.clone(),
rx,
};
let handle = SimulationClockHandle {
tx,
state: Arc::new(Mutex::new(SimulationClockState {
current: None,
retired_timelines: RetiredTimelines::default(),
})),
};
(scheduler, handle)
}
pub(crate) fn simulation_clock(&self) -> SimulationClock {
SimulationClock::from_receiver(self.rx.clone())
}
pub(crate) fn time_receiver(&self) -> watch::Receiver<Option<RobotInstant>> {
self.rx.clone()
}
fn missed_ticks(&self, target: RobotInstant, current: RobotInstant) -> u32 {
let Some(period) = self.period else {
return 0;
};
let Ok(overrun) = current.duration_since(target) else {
return 0;
};
let period_ns = duration_nanos(period);
if period_ns == 0 {
return 0;
}
u32::try_from(duration_nanos(overrun) / period_ns).unwrap_or(u32::MAX)
}
}
impl StepScheduler for SimulationScheduler {
async fn wait_until(&self, target: RobotInstant) -> SchedulerTick {
let mut rx = self.rx.clone();
loop {
if let Some(current) = *rx.borrow_and_update() {
let reached = current.timeline() != target.timeline()
|| current
.checked_cmp(target)
.is_ok_and(|order| order != std::cmp::Ordering::Less);
if reached {
return SchedulerTick {
fired_at: current,
missed_ticks: self.missed_ticks(target, current),
};
}
}
if rx.changed().await.is_err() {
return std::future::pending().await;
}
}
}
fn now(&self) -> Option<RobotInstant> {
*self.rx.borrow()
}
}
#[cfg(test)]
mod tests {
use std::future::Future;
use super::*;
use crate::bus::TimelineId;
fn lt(ticks: u64) -> RobotInstant {
RobotInstant::new(
TimelineId::from_raw(1).expect("test timeline must be nonzero"),
ticks,
)
}
fn other(ticks: u64) -> RobotInstant {
RobotInstant::new(
TimelineId::from_raw(2).expect("test timeline must be nonzero"),
ticks,
)
}
const SIM_PERIOD: Duration = Duration::from_nanos(10);
#[tokio::test]
async fn simulation_scheduler_releases_ticks_in_order_deterministically() {
let (scheduler, handle) = SimulationScheduler::new(Some(SIM_PERIOD));
let mut fired = Vec::new();
for step in 1..=5u64 {
let target = lt(step * 10);
let handle = handle.clone();
let advancer = tokio::spawn(async move { handle.advance(target) });
let tick = scheduler.wait_until(target).await;
advancer.await.unwrap();
fired.push(tick.fired_at.ticks());
}
assert_eq!(fired, vec![10, 20, 30, 40, 50]);
}
#[test]
fn a_scheduler_with_no_world_history_yet_reports_no_time_rather_than_zero() {
let (scheduler, handle) = SimulationScheduler::new(Some(SIM_PERIOD));
assert_eq!(
scheduler.now(),
None,
"before the authority publishes, there is no world history at all"
);
handle.advance(lt(10));
assert_eq!(scheduler.now(), Some(lt(10)));
}
#[test]
fn timeline_replacement_is_equality_only_with_no_generation_order() {
let (scheduler, handle) = SimulationScheduler::new(Some(SIM_PERIOD));
assert_eq!(handle.advance(lt(100)), SimulationClockAdvance::Advanced);
assert_eq!(handle.advance(other(0)), SimulationClockAdvance::Advanced);
assert_eq!(scheduler.now(), Some(other(0)));
assert_eq!(
handle.advance(lt(101)),
SimulationClockAdvance::RetiredTimeline,
"a late clock from the retired world must not reactivate it"
);
assert_eq!(
handle.advance(other(0)),
SimulationClockAdvance::DuplicateOrBackward
);
assert_eq!(
scheduler.now(),
Some(other(0)),
"duplicate and same-timeline non-forward samples are ignored"
);
}
#[tokio::test]
async fn simulation_scheduler_never_sleeps_on_a_wall_clock_timer() {
let (scheduler, handle) = SimulationScheduler::new(Some(SIM_PERIOD));
handle.advance(lt(100));
let started = std::time::Instant::now();
let tick = scheduler.wait_until(lt(100)).await;
let elapsed = started.elapsed();
assert_eq!(tick.fired_at, lt(100));
assert!(
elapsed < Duration::from_millis(50),
"simulation scheduler should resolve immediately once time is already due, took {elapsed:?}"
);
}
#[tokio::test]
async fn simulation_scheduler_releases_when_advance_happened_before_wait() {
let (scheduler, handle) = SimulationScheduler::new(Some(SIM_PERIOD));
handle.advance(lt(30));
let tick = tokio::time::timeout(Duration::from_millis(50), scheduler.wait_until(lt(20)))
.await
.expect("advance-before-wait should release without waiting for another change");
assert_eq!(tick.fired_at, lt(30));
assert_eq!(tick.missed_ticks, 1);
}
#[tokio::test]
async fn simulation_scheduler_does_not_miss_racing_advance_after_pending_poll() {
let (scheduler, handle) = SimulationScheduler::new(Some(SIM_PERIOD));
let wait = scheduler.wait_until(lt(10));
tokio::pin!(wait);
assert!(
poll_once(wait.as_mut()).is_none(),
"wait should pend before robot time reaches the target"
);
handle.advance(lt(10));
let tick = tokio::time::timeout(Duration::from_secs(1), &mut wait)
.await
.expect("advance after a pending poll should wake the waiter");
assert_eq!(tick.fired_at, lt(10));
assert_eq!(tick.missed_ticks, 0);
}
#[tokio::test]
async fn simulation_scheduler_keeps_waiting_if_external_handle_is_dropped() {
let (scheduler, handle) = SimulationScheduler::new(Some(SIM_PERIOD));
drop(handle);
let wait = scheduler.wait_until(lt(10));
tokio::pin!(wait);
assert!(
poll_once(wait.as_mut()).is_none(),
"dropping the external handle must not close the scheduler feed and release a stale tick"
);
}
#[tokio::test]
async fn simulation_scheduler_collapses_a_multi_period_jump() {
let (scheduler, handle) = SimulationScheduler::new(Some(SIM_PERIOD));
handle.advance(lt(40));
let tick = scheduler.wait_until(lt(10)).await;
assert_eq!(
tick.missed_ticks, 3,
"a jump past the target collapses to one released tick, reporting all 3 skipped periods"
);
assert_eq!(tick.fired_at, lt(40));
}
fn poll_once<F: Future>(fut: std::pin::Pin<&mut F>) -> Option<F::Output> {
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
fn noop(_: *const ()) {}
fn clone(_: *const ()) -> RawWaker {
RawWaker::new(std::ptr::null(), &VTABLE)
}
static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, noop, noop, noop);
let waker = unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &VTABLE)) };
let mut cx = Context::from_waker(&waker);
match fut.poll(&mut cx) {
Poll::Ready(output) => Some(output),
Poll::Pending => None,
}
}
}