feat(#2349): fan reconcile's start/stop out as first-class dag nodes

This commit is contained in:
damocles 2026-07-11 23:33:45 +02:00 committed by mara
commit 79d4c345bb
4 changed files with 156 additions and 63 deletions

View file

@ -169,6 +169,40 @@ impl JobQueue {
ids
}
/// Append a node into a *live* (non-terminal) DAG at runtime,
/// depending `AfterOk` on `dep_on` (the node that emitted it). Lets a
/// planner node — e.g. [`NodeKind::Reconcile`] — fan a mechanical
/// sub-step ([`NodeKind::Start`] / [`NodeKind::Stop`]) out as a
/// first-class node in the *same* DAG.
///
/// Must be called *before* the emitting node's [`Self::complete_node`]
/// so the DAG doesn't roll terminal with the new node still pending —
/// that keeps the lease-window transient held across the sub-step and
/// lets the appended node's `AfterOk` dep resolve as soon as the
/// emitter settles `Done`. No-op (returns `None`) if the DAG is gone.
pub fn append_node(&self, dag_id: u64, kind: NodeKind, dep_on: NodeId) -> Option<NodeId> {
let mut inner = self.inner.lock().expect("job_queue mutex poisoned");
let dag = inner.dags.iter_mut().find(|d| d.id == dag_id)?;
let new_id: NodeId = u32::try_from(dag.nodes.len()).unwrap_or(u32::MAX);
dag.nodes.push(Node {
id: new_id,
kind,
deps: vec![model::Dep {
on: dep_on,
when: DepWhen::AfterOk,
}],
state: State::Queued,
step: None,
build_log_id: None,
started_at: None,
finished_at: None,
error: None,
});
drop(inner);
self.notify.notify_one();
Some(new_id)
}
fn dedup_target<'a>(inner: &'a mut Inner, spec: &DagSpec) -> Option<&'a mut Dag> {
inner.dags.iter_mut().find(|d| {
d.rollup() == State::Queued