use chrono::{DateTime, TimeDelta, Utc};
use dpm_model::{
LineageId, LineageStatus, ObservedStatus, Orphan, Plan, RunSnapshot, RunState, RunView,
WorkStatus,
};
pub const STALE_AFTER: TimeDelta = TimeDelta::seconds(300);
#[must_use]
pub fn project(
snapshot: RunSnapshot,
plan: Option<&Plan>,
lineage: Option<LineageId>,
now: DateTime<Utc>,
) -> RunView {
let RunSnapshot {
record,
last,
activity,
operations,
} = snapshot;
let current = super::same_history(record.contract.lineage_id, lineage);
let last_receipt_at = activity.latest.as_ref().map_or(last.recorded_at, |latest| {
latest.recorded_at.max(last.recorded_at)
});
let silent = now.signed_duration_since(last_receipt_at) >= STALE_AFTER;
let status = match last.state {
RunState::Failed => ObservedStatus::Failed,
RunState::Interrupted => ObservedStatus::Interrupted,
RunState::Completed => ObservedStatus::Completed,
_ if !current => ObservedStatus::Unknown,
_ if silent => ObservedStatus::Stale,
RunState::Working => ObservedStatus::Working,
RunState::Waiting => ObservedStatus::Waiting,
};
let stale_at = matches!(status, ObservedStatus::Working | ObservedStatus::Waiting)
.then(|| super::stale_after(last_receipt_at))
.flatten();
let orphan = (!last.state.is_terminal() && current)
.then(|| plan.and_then(|plan| orphan(&record, plan)))
.flatten();
RunView {
state: last.state,
state_since: last.recorded_at,
state_detail: last.detail,
status,
last_receipt_at,
stale_at,
activity,
lineage: if current {
LineageStatus::Current
} else {
LineageStatus::Foreign
},
orphan,
operations,
evaluated_at: now,
run: record,
}
}
fn orphan(record: &dpm_model::RunRecord, plan: &Plan) -> Option<Orphan> {
let Some(work) = plan.work_items.get(&record.work) else {
return Some(Orphan::WorkMissing);
};
if work.execution.owner.as_ref() != Some(&record.executor) {
return Some(Orphan::OwnerChanged {
owner: work.execution.owner.clone(),
});
}
match work.execution.status {
WorkStatus::Claimed | WorkStatus::InProgress => None,
status => Some(Orphan::NotExecuting { status }),
}
}