use std::time::Duration;
use anyhow::Context as _;
use tokio::sync::watch;
use crate::bus::RobotInstant;
use phoxal_bundle::ParticipantClock;
pub(crate) mod real;
pub(crate) mod simulation;
use real::RealScheduler;
use simulation::{SimulationClockHandle, SimulationScheduler};
#[derive(Clone, Copy, Debug)]
pub struct StepSchedule {
hz: f64,
}
impl StepSchedule {
pub const fn hz(hz: f64) -> Self {
StepSchedule { hz }
}
pub(crate) fn period(&self) -> Duration {
std::cmp::max(
Duration::from_secs_f64(1.0 / self.hz),
Duration::from_nanos(1),
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct SchedulerTick {
pub(crate) fired_at: RobotInstant,
pub(crate) missed_ticks: u32,
}
pub(crate) trait StepScheduler: Send + Sync + 'static {
async fn wait_until(&self, target: RobotInstant) -> SchedulerTick;
fn now(&self) -> Option<RobotInstant>;
}
pub(crate) enum AnyStepScheduler {
Real(RealScheduler),
Simulation(SimulationScheduler),
Disabled,
}
impl AnyStepScheduler {
pub(crate) fn validate_clock_mode(
clock_mode: ParticipantClock,
schedule: Option<StepSchedule>,
now: Option<RobotInstant>,
) -> crate::Result<()> {
let _period = schedule.map(|schedule| schedule.period());
match clock_mode {
ParticipantClock::Real if schedule.is_none() => Ok(()),
ParticipantClock::Real => {
now.context(
"a real participant cannot anchor its cadence without a synchronized clock",
)?;
Ok(())
}
ParticipantClock::Simulation => Ok(()),
ParticipantClock::Clockless if schedule.is_some() => {
anyhow::bail!("a participant with a step schedule cannot use clockless mode")
}
ParticipantClock::Clockless => Ok(()),
}
}
pub(crate) fn for_clock_mode(
clock_mode: ParticipantClock,
schedule: Option<StepSchedule>,
now: Option<RobotInstant>,
) -> crate::Result<(Self, Option<SimulationClockHandle>)> {
Self::validate_clock_mode(clock_mode, schedule, now)?;
let period = schedule.map(|schedule| schedule.period());
Ok(match clock_mode {
ParticipantClock::Real if schedule.is_none() => (AnyStepScheduler::Disabled, None),
ParticipantClock::Real => {
let now = now.ok_or_else(|| {
anyhow::anyhow!(
"a real participant cannot anchor its cadence without a synchronized clock"
)
})?;
(
AnyStepScheduler::Real(
RealScheduler::new(period, now)
.context("the host boot clock could not be read to anchor cadence")?,
),
None,
)
}
ParticipantClock::Simulation => {
let (scheduler, handle) = SimulationScheduler::new(period);
(AnyStepScheduler::Simulation(scheduler), Some(handle))
}
ParticipantClock::Clockless => (AnyStepScheduler::Disabled, None),
})
}
pub(crate) fn simulation_time_receiver(&self) -> Option<watch::Receiver<Option<RobotInstant>>> {
match self {
AnyStepScheduler::Real(_) | AnyStepScheduler::Disabled => None,
AnyStepScheduler::Simulation(scheduler) => Some(scheduler.time_receiver()),
}
}
pub(crate) async fn wait_until_due(&self, target: Option<RobotInstant>) -> SchedulerTick {
match target {
Some(target) => self.wait_until(target).await,
None => std::future::pending().await,
}
}
}
impl StepScheduler for AnyStepScheduler {
async fn wait_until(&self, target: RobotInstant) -> SchedulerTick {
match self {
AnyStepScheduler::Real(scheduler) => scheduler.wait_until(target).await,
AnyStepScheduler::Simulation(scheduler) => scheduler.wait_until(target).await,
AnyStepScheduler::Disabled => std::future::pending().await,
}
}
fn now(&self) -> Option<RobotInstant> {
match self {
AnyStepScheduler::Real(scheduler) => scheduler.now(),
AnyStepScheduler::Simulation(scheduler) => scheduler.now(),
AnyStepScheduler::Disabled => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bus::TimelineId;
use crate::participant::duration_nanos;
fn at(timeline: u64, ticks: u64) -> RobotInstant {
RobotInstant::new(
TimelineId::from_raw(timeline).expect("test timeline must be nonzero"),
ticks,
)
}
#[test]
fn step_schedule_period_never_rounds_to_zero() {
assert_eq!(StepSchedule::hz(f64::MAX).period(), Duration::from_nanos(1));
}
#[test]
fn the_clock_mode_selects_the_real_or_simulation_scheduler() {
let schedule = Some(StepSchedule::hz(100.0));
let (real, real_handle) =
AnyStepScheduler::for_clock_mode(ParticipantClock::Real, schedule, Some(at(1, 0)))
.expect("real scheduler");
assert!(matches!(real, AnyStepScheduler::Real(_)));
assert!(
real_handle.is_none(),
"real mode has no simulation clock handle to drive"
);
let (simulation, simulation_handle) =
AnyStepScheduler::for_clock_mode(ParticipantClock::Simulation, schedule, None)
.expect("simulation scheduler");
assert!(matches!(simulation, AnyStepScheduler::Simulation(_)));
assert!(
simulation_handle.is_some(),
"simulation mode must hand back the driving handle so the caller can wire the live feed"
);
assert_eq!(
simulation.now(),
None,
"simulation mode starts with no world history, not with an invented zero"
);
}
#[test]
fn a_real_participant_without_a_step_schedule_allocates_no_step_scheduler() {
let (scheduler, handle) =
AnyStepScheduler::for_clock_mode(ParticipantClock::Real, None, Some(at(1, 0)))
.expect("runner clock");
assert!(matches!(scheduler, AnyStepScheduler::Disabled));
assert!(handle.is_none());
}
#[test]
fn a_clockless_participant_cannot_silently_disable_its_declared_step() {
let error = AnyStepScheduler::for_clock_mode(
ParticipantClock::Clockless,
Some(StepSchedule::hz(50.0)),
None,
)
.err()
.expect("clockless mode cannot drive a scheduled transition");
assert!(error.to_string().contains("step schedule"));
}
#[test]
fn a_real_participant_with_no_trustworthy_clock_does_not_get_a_cadence_at_all() {
assert!(
AnyStepScheduler::for_clock_mode(
ParticipantClock::Real,
Some(StepSchedule::hz(50.0)),
None
)
.is_err()
);
}
#[test]
fn preflight_scheduler_validation_does_not_construct_a_live_scheduler() {
AnyStepScheduler::validate_clock_mode(
ParticipantClock::Simulation,
Some(StepSchedule::hz(20.0)),
None,
)
.expect("simulation facts do not need a seed instant or a live channel");
AnyStepScheduler::validate_clock_mode(ParticipantClock::Clockless, None, None)
.expect("clockless facts are valid without a scheduler");
assert!(
AnyStepScheduler::validate_clock_mode(
ParticipantClock::Clockless,
Some(StepSchedule::hz(20.0)),
None,
)
.is_err()
);
assert!(
AnyStepScheduler::validate_clock_mode(
ParticipantClock::Real,
Some(StepSchedule::hz(20.0)),
None,
)
.is_err()
);
}
#[tokio::test]
async fn the_simulation_scheduler_the_runner_selects_schedules_deterministically() {
let schedule = StepSchedule::hz(10.0); let period_ns = duration_nanos(schedule.period());
let (scheduler, handle) =
AnyStepScheduler::for_clock_mode(ParticipantClock::Simulation, Some(schedule), None)
.expect("simulation scheduler");
let handle = handle.expect("simulation mode must hand back a driving handle");
let mut fired = Vec::new();
let mut target = at(1, period_ns);
for _ in 0..3 {
handle.advance(target);
let tick = scheduler.wait_until(target).await;
fired.push(tick.fired_at.ticks());
target = at(1, target.ticks() + period_ns);
}
assert_eq!(
fired,
vec![100_000_000, 200_000_000, 300_000_000],
"ticks fire in order at the instants the handle advanced to, with no real sleeping"
);
}
}