use std::future::Future;
use std::time::Duration;
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 phoxal_bundle::RuntimeBundle;
use phoxal_bus::{BusConfig, BusHandle, BusOwner};
use phoxal_runtime_contract::identity::{ExecutionId, ParticipantId, TimelineId};
use super::ShutdownController;
use super::inputs::{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)?;
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,
}
}
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::*;
#[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());
}
}