use std::future::Future;
use std::time::Duration;
use crate::bundle::RuntimeBundle;
use crate::bus::{BusConfig, BusHandle, BusOwner};
use crate::identity::{ExecutionId, ParticipantId, TimelineId};
use crate::participant::api::Participant;
use crate::participant::clock::real::RealClock;
use crate::participant::clock::{ClockMode, ClockReading, ClockSource};
use crate::participant::launch::{Launch, SHUTDOWN_GRACE};
use crate::participant::scheduler::AnyStepScheduler;
use anyhow::Context as _;
use super::ShutdownController;
use super::inputs::{driver_block, open_bundle, participant_config};
use super::lifecycle::{self, BusLease};
pub(crate) struct PreparedRun<R: Participant, C: ClockSource> {
pub(crate) bus: BusHandle,
pub(crate) session: BusLease,
pub(crate) participant_id: ParticipantId,
pub(crate) shutdown_grace: Duration,
pub(crate) bundle: Option<RuntimeBundle>,
pub(crate) config: R::Config,
pub(crate) clock_mode: ClockMode,
pub(crate) clock: Option<C>,
pub(crate) query_reply_delay: Option<Duration>,
}
pub(crate) async fn run_supervised<R, S>(launch: Launch, shutdown: S) -> crate::Result<()>
where
R: Participant,
S: Future<Output = ()>,
{
let mut shutdown = ShutdownController::new(shutdown);
let clock_mode = if launch.simulation {
ClockMode::Simulation
} else {
ClockMode::Real
};
let bundle = open_bundle(&launch.bundle_root)?;
let config = participant_config::<R::Config>(bundle.robot(), &launch.participant_id, R::KIND)?;
validate_declared_connection::<R>(bundle.robot(), &launch.participant_id)?;
tracing::info!(
target: "phoxal.runtime",
endpoints = ?launch.connect_endpoints,
"connecting to the bus"
);
let execution = tokio::select! {
biased;
_ = shutdown.wait() => return Ok(()),
result = learn_execution(&launch.connect_endpoints) => result?,
};
tracing::info!(
target: "phoxal.runtime",
execution = %execution,
"learned the execution identity from the router"
);
let clock = clock_for_mode(clock_mode, execution);
validate_clock_inputs::<R, _>(clock_mode, clock.as_ref())?;
let (owner, bus) = tokio::select! {
biased;
_ = shutdown.wait() => return Ok(()),
result = BusOwner::open(BusConfig::for_participant(
execution,
launch.participant_id.clone(),
launch.connect_endpoints.clone(),
)) => result?,
};
lifecycle::run(
PreparedRun::<R, RealClock> {
bus,
session: BusLease::Owned(owner),
participant_id: launch.participant_id,
shutdown_grace: SHUTDOWN_GRACE,
bundle: Some(bundle),
config,
clock_mode,
clock,
query_reply_delay: None,
},
&mut shutdown,
)
.await
}
async fn learn_execution(endpoints: &[String]) -> crate::Result<ExecutionId> {
let mut observed: Vec<ExecutionId> = Vec::new();
for endpoint in endpoints {
let reported = BusOwner::probe_routers(endpoint)
.await
.with_context(|| format!("failed to reach a Phoxal router on '{endpoint}'"))?;
for execution in reported {
if !observed.contains(&execution) {
observed.push(execution);
}
}
}
match observed.as_slice() {
[execution] => Ok(*execution),
[] => anyhow::bail!(
"no Phoxal router answered on {}; the execution identity is the router's, so there \
is nothing for this participant to join",
rendered(endpoints)
),
many => anyhow::bail!(
"the endpoints {} report {} different executions ({}); a participant joins exactly one",
rendered(endpoints),
many.len(),
many.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
),
}
}
fn rendered(endpoints: &[String]) -> String {
endpoints
.iter()
.map(|endpoint| format!("'{endpoint}'"))
.collect::<Vec<_>>()
.join(", ")
}
fn real_timeline(execution: ExecutionId) -> TimelineId {
let high = (u128::from(execution) >> 64) as u64;
TimelineId::from_raw(high).unwrap_or_else(TimelineId::mint)
}
pub(crate) fn clock_for_mode(clock_mode: ClockMode, execution: ExecutionId) -> Option<RealClock> {
match clock_mode {
ClockMode::Real => Some(RealClock::new(real_timeline(execution))),
ClockMode::Simulation => None,
}
}
fn validate_declared_connection<R: Participant>(
robot: &crate::model::Robot,
participant_id: &ParticipantId,
) -> crate::Result<()> {
let Some(expected) = R::CONNECTION else {
return Ok(());
};
let authored = driver_block(robot, participant_id)?.connection().kind();
anyhow::ensure!(
authored == expected,
"driver '{participant_id}' accepts a {expected} connection, but the component instance \
it is launched for authors a {authored} connection"
);
Ok(())
}
pub(crate) fn validate_clock_inputs<R, C>(
clock_mode: ClockMode,
clock: Option<&C>,
) -> crate::Result<()>
where
R: Participant,
C: ClockSource,
{
let now = match clock_mode {
ClockMode::Real => {
let reading = clock
.context("a real participant is launched with a host clock")?
.read();
match reading {
ClockReading::Synchronized(_) => reading.instant(),
ClockReading::Unsynchronized(reason) => {
return Err(lifecycle::ClockDisciplineLost { reason }.into());
}
}
}
ClockMode::Simulation => None,
};
AnyStepScheduler::validate_clock_mode(clock_mode, R::__step_schedule(), now)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::participant::context::SetupContext;
use phoxal_fixture::staged_bundle;
#[phoxal::driver(id = "front_left_drive", connection = can)]
struct CanDriver;
impl Participant for CanDriver {
async fn setup(
&self,
_ctx: &mut SetupContext<Self>,
_config: Self::Config,
) -> crate::Result<(Self::State, Self::Api)> {
Ok(((), ()))
}
}
#[phoxal::driver(id = "front_left_drive", connection = serial)]
struct SerialDriver;
impl Participant for SerialDriver {
async fn setup(
&self,
_ctx: &mut SetupContext<Self>,
_config: Self::Config,
) -> crate::Result<(Self::State, Self::Api)> {
Ok(((), ()))
}
}
#[phoxal::driver(id = "front_left_drive")]
struct AnyDriver;
impl Participant for AnyDriver {
async fn setup(
&self,
_ctx: &mut SetupContext<Self>,
_config: Self::Config,
) -> crate::Result<(Self::State, Self::Api)> {
Ok(((), ()))
}
}
#[test]
fn a_declared_connection_kind_is_enforced_before_the_bus_opens() {
let staged = staged_bundle();
let bundle = open_bundle(staged.path()).expect("the staged bundle opens");
let robot = bundle.robot();
let id = ParticipantId::new("front_left_drive").expect("a test participant id");
validate_declared_connection::<CanDriver>(robot, &id)
.expect("the authored kind is the declared one");
validate_declared_connection::<AnyDriver>(robot, &id)
.expect("a driver that declared no kind accepts the authored one");
let error = validate_declared_connection::<SerialDriver>(robot, &id)
.expect_err("an authored kind the driver does not accept must be refused");
let message = format!("{error:#}");
for expected in ["front_left_drive", "serial", "can"] {
assert!(message.contains(expected), "{message}");
}
}
#[test]
fn the_real_timeline_is_derived_from_the_execution_and_nothing_else() {
let execution = ExecutionId::mint();
assert_eq!(real_timeline(execution), real_timeline(execution));
assert_ne!(real_timeline(execution), real_timeline(ExecutionId::mint()));
}
#[test]
fn only_a_real_launch_builds_a_host_clock() {
let execution = ExecutionId::mint();
assert!(clock_for_mode(ClockMode::Real, execution).is_some());
assert!(clock_for_mode(ClockMode::Simulation, execution).is_none());
}
}