use std::collections::BTreeMap;
use crate::identity::{ParticipantId, ProducerId};
use crate::model::Robot;
use crate::participant::metadata::ParticipantKind;
use crate::supervisor::api::execution::{Lifecycle, Process, ProcessState};
#[derive(Clone, Debug)]
pub(crate) struct Presence {
rows: BTreeMap<ParticipantId, Row>,
completed: bool,
}
#[derive(Clone, Debug)]
struct Row {
kind: ParticipantKind,
producers: Vec<ProducerId>,
}
impl Row {
fn incumbent(&self) -> Option<ProducerId> {
self.producers.last().copied()
}
fn project(&self, participant: &ParticipantId) -> Process {
let producer = self.incumbent();
Process {
participant: participant.clone(),
kind: self.kind,
state: if producer.is_some() {
ProcessState::Present
} else {
ProcessState::Absent
},
producer,
}
}
}
impl Presence {
#[expect(
clippy::expect_used,
reason = "a service and component-instance id are validated `is_topology_token` values and \
`brain` is a literal in that same alphabet, which is exactly what a participant \
id accepts, so no robot the bundle could have parsed reaches the failure arm"
)]
pub(crate) fn for_robot(robot: &Robot) -> Self {
let mut rows = BTreeMap::new();
let mut insert = |id: &str, kind: ParticipantKind| {
let participant = ParticipantId::new(id).expect("a model id is a participant id");
rows.insert(
participant,
Row {
kind,
producers: Vec::new(),
},
);
};
insert(BRAIN, ParticipantKind::Brain);
for (service, _) in robot.services() {
insert(service.as_str(), ParticipantKind::Service);
}
for component in robot.components() {
if component.instance().driver().is_some() {
insert(component.id().as_str(), ParticipantKind::Driver);
}
}
Self {
rows,
completed: false,
}
}
pub(crate) fn record(
&mut self,
participant: &ParticipantId,
producer: ProducerId,
ready: bool,
) {
let Some(row) = self.rows.get_mut(participant) else {
tracing::debug!(
%participant,
%producer,
ready,
"ignoring a Ready lease for a participant this robot does not expect"
);
return;
};
row.producers.retain(|held| *held != producer);
if ready {
row.producers.push(producer);
}
if self.rows.values().all(|row| row.incumbent().is_some()) {
self.completed = true;
}
}
pub(crate) fn lifecycle(&self) -> Lifecycle {
if !self.completed {
return Lifecycle::Starting;
}
if self.rows.values().all(|row| row.incumbent().is_some()) {
Lifecycle::Ready
} else {
Lifecycle::Degraded
}
}
pub(crate) fn processes(&self) -> Vec<Process> {
self.rows
.iter()
.map(|(participant, row)| row.project(participant))
.collect()
}
}
const BRAIN: &str = "brain";
#[cfg(test)]
mod tests {
use crate::model::RobotBuilder;
use super::*;
fn producer(seed: u128) -> ProducerId {
ProducerId::try_from((1_u128 << 124) | seed).expect("a canonical producer id")
}
fn participant(id: &str) -> ParticipantId {
ParticipantId::new(id).expect("a valid participant id")
}
fn presence() -> Presence {
let robot = RobotBuilder::new("rover")
.service("drive", None)
.component_type("motor", |motor| motor.motor("spin", "axle"))
.component_with("left", "motor", |mounted| {
mounted.driver(
crate::model::connection::Connection::Can(
crate::model::connection::Can { bus: 0, node_id: 1 },
),
None,
)
})
.component("simulated_only", "motor")
.build()
.expect("a valid robot");
Presence::for_robot(&robot)
}
#[test]
fn the_expected_set_is_the_brain_plus_the_services_and_the_driven_components() {
let rows = presence().processes();
assert_eq!(
rows.iter()
.map(|row| (row.participant.as_str().to_owned(), row.kind))
.collect::<Vec<_>>(),
vec![
("brain".to_owned(), ParticipantKind::Brain),
("drive".to_owned(), ParticipantKind::Service),
("left".to_owned(), ParticipantKind::Driver),
]
);
assert!(
rows.iter()
.all(|row| row.state == ProcessState::Absent && row.producer.is_none())
);
}
#[test]
fn the_lifecycle_follows_presence_and_never_fails() {
let mut presence = presence();
assert_eq!(presence.lifecycle(), Lifecycle::Starting);
presence.record(&participant("brain"), producer(1), true);
presence.record(&participant("drive"), producer(2), true);
assert_eq!(
presence.lifecycle(),
Lifecycle::Starting,
"one expected runtime has still never been seen"
);
presence.record(&participant("left"), producer(3), true);
assert_eq!(
presence.lifecycle(),
Lifecycle::Ready,
"the driverless instance launches no process, so nothing waits for it"
);
presence.record(&participant("left"), producer(3), false);
assert_eq!(presence.lifecycle(), Lifecycle::Degraded);
let row = presence
.processes()
.into_iter()
.find(|row| row.participant.as_str() == "left")
.expect("the component row");
assert_eq!(row.state, ProcessState::Absent);
assert_eq!(row.producer, None);
presence.record(&participant("left"), producer(4), true);
assert_eq!(presence.lifecycle(), Lifecycle::Ready);
}
#[test]
fn a_second_producer_takes_the_row_and_losing_the_older_one_is_a_no_op() {
let mut presence = presence();
let drive = participant("drive");
presence.record(&drive, producer(1), true);
presence.record(&drive, producer(2), true);
let row = |presence: &Presence| {
presence
.processes()
.into_iter()
.find(|row| row.participant == drive)
.expect("the service row")
};
assert_eq!(row(&presence).producer, Some(producer(2)));
presence.record(&drive, producer(1), false);
let row = row(&presence);
assert_eq!(row.producer, Some(producer(2)));
assert_eq!(row.state, ProcessState::Present);
}
#[test]
fn an_unexpected_participant_has_no_row_and_no_effect() {
let mut presence = presence();
presence.record(&participant("webots"), producer(9), true);
assert_eq!(presence.processes().len(), 3);
assert_eq!(presence.lifecycle(), Lifecycle::Starting);
}
#[test]
fn the_projection_is_always_a_publishable_snapshot() {
use crate::supervisor::api::execution::Snapshot;
let mut presence = presence();
presence.record(&participant("brain"), producer(1), true);
let snapshot = Snapshot {
revision: 1,
lifecycle: presence.lifecycle(),
processes: presence.processes(),
};
snapshot.validate().expect("the projection is publishable");
}
}