feat(#2484): unify in-DAG growth on append_subgraph (drop append_node)
append_subgraph is the multi-node/multi-agent generalisation of the single-node append_node, so the two in-DAG-growth channels collapse to one: the Reconcile planner now emits its mechanical Start/Stop as a single-node append_subgraph rooted on the reconcile node (stamping claim.agent on the NodeSpec, which append_node inherited implicitly). Removes NodeOutput.append_nodes + its scheduler drain loop and JobQueue::append_node. No behaviour change — a channel unification.
This commit is contained in:
parent
6c4ef5f798
commit
b87eac0a61
3 changed files with 48 additions and 104 deletions
|
|
@ -27,24 +27,19 @@ pub const GRACEFUL_STOP_TIMEOUT: std::time::Duration = std::time::Duration::from
|
||||||
/// success.
|
/// success.
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct NodeOutput {
|
pub struct NodeOutput {
|
||||||
/// Mechanical sub-step nodes to append into *this same* DAG at
|
|
||||||
/// runtime, each depending `AfterOk` on the emitting node — e.g. a
|
|
||||||
/// `Reconcile` planner emitting a `Start` / `Stop`. Keeps the sub-step a
|
|
||||||
/// first-class node in the same DAG so the lease-window transient is held
|
|
||||||
/// across it. The scheduler applies these *before* the emitting node's
|
|
||||||
/// completion so the DAG never rolls terminal with the appended work
|
|
||||||
/// still pending.
|
|
||||||
pub append_nodes: Vec<NodeKind>,
|
|
||||||
/// Whole per-agent *subgraphs* to append into *this same* DAG at
|
/// Whole per-agent *subgraphs* to append into *this same* DAG at
|
||||||
/// runtime — the multi-node generalisation of `append_nodes`. Each
|
/// runtime — the single in-DAG-growth channel. Each inner
|
||||||
/// inner `Vec<NodeSpec>` is one independent subgraph whose `deps` are
|
/// `Vec<NodeSpec>` is one independent subgraph whose `deps` are local
|
||||||
/// local (0-based within that subgraph); the scheduler appends each via
|
/// (0-based within that subgraph); the scheduler appends each via
|
||||||
/// [`JobQueue::append_subgraph`], which rebases the deps onto the DAG's
|
/// [`JobQueue::append_subgraph`], which rebases the deps onto the DAG's
|
||||||
/// node-id space and roots the subgraph on the emitting node. Both
|
/// node-id space and roots the subgraph on the emitting node. Used both
|
||||||
/// `MetaLock` flavours use this to grow one rebuild subgraph per agent
|
/// for the multi-node case (`MetaLock` growing one rebuild subgraph per
|
||||||
/// into their own DAG (the startup sweep's stale agents; the meta-update
|
/// agent — the startup sweep's stale agents, the meta-update cascade's
|
||||||
/// cascade's affected agents) instead of fanning out child DAGs. Same
|
/// affected agents) and the single-node case (a `Reconcile` planner
|
||||||
/// before-completion ordering as `append_nodes`.
|
/// emitting its mechanical `Start` / `Stop` as a one-node subgraph). The
|
||||||
|
/// scheduler applies these *before* the emitting node's completion so the
|
||||||
|
/// DAG never rolls terminal with the appended work still pending — keeping
|
||||||
|
/// the lease-window transient held across the sub-step.
|
||||||
pub append_subgraph: Vec<Vec<NodeSpec>>,
|
pub append_subgraph: Vec<Vec<NodeSpec>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -293,10 +288,7 @@ async fn run_meta_lock(
|
||||||
.iter()
|
.iter()
|
||||||
.map(|agent| super::templates::rebuild_nodes(agent, true, 0))
|
.map(|agent| super::templates::rebuild_nodes(agent, true, 0))
|
||||||
.collect();
|
.collect();
|
||||||
return Ok(NodeOutput {
|
return Ok(NodeOutput { append_subgraph });
|
||||||
append_subgraph,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
let _progress = coord.meta_update_guard();
|
let _progress = coord.meta_update_guard();
|
||||||
ctx.step("nix flake update");
|
ctx.step("nix flake update");
|
||||||
|
|
@ -320,35 +312,34 @@ async fn run_meta_lock(
|
||||||
.iter()
|
.iter()
|
||||||
.map(|agent| super::templates::rebuild_nodes(agent, false, 0))
|
.map(|agent| super::templates::rebuild_nodes(agent, false, 0))
|
||||||
.collect();
|
.collect();
|
||||||
Ok(NodeOutput {
|
Ok(NodeOutput { append_subgraph })
|
||||||
append_subgraph,
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Idempotent power-converge *planner*: compare `wanted` (durable
|
/// Idempotent power-converge *planner*: compare `wanted` (durable
|
||||||
/// intent) against observed state and, when they diverge, fan the
|
/// intent) against observed state and, when they diverge, fan the
|
||||||
/// mechanical `Start` / `Stop` out as a first-class node appended to
|
/// mechanical `Start` / `Stop` out as a first-class node appended to
|
||||||
/// *this* DAG (`NodeOutput::append_nodes`). Does no container work
|
/// *this* DAG (a single-node `NodeOutput::append_subgraph` rooted on
|
||||||
/// itself — the sub-step becomes visible in the DAG and the
|
/// this node). Does no container work itself — the sub-step becomes
|
||||||
/// lease-window transient (or the sub-step's own node-local guard)
|
/// visible in the DAG and the lease-window transient (or the sub-step's
|
||||||
/// rides across it.
|
/// own node-local guard) rides across it.
|
||||||
async fn run_reconcile(coord: &Arc<Coordinator>, claim: &Claim) -> Result<NodeOutput> {
|
async fn run_reconcile(coord: &Arc<Coordinator>, claim: &Claim) -> Result<NodeOutput> {
|
||||||
let name = &claim.agent;
|
let name = &claim.agent;
|
||||||
let running = crate::lifecycle::is_running(name).await;
|
let running = crate::lifecycle::is_running(name).await;
|
||||||
let wanted = coord.power.get_or_seed(name, running)?;
|
let wanted = coord.power.get_or_seed(name, running)?;
|
||||||
let append_nodes = match reconcile_action(wanted, running) {
|
// One node targeting this agent, rooted on this reconcile node. The old
|
||||||
ReconcileAction::Start => vec![NodeKind::Start],
|
// `append_node` inherited the emitter's agent implicitly; `append_subgraph`
|
||||||
ReconcileAction::Stop => vec![NodeKind::Stop],
|
// carries it on the `NodeSpec`, so stamp `claim.agent` explicitly (same
|
||||||
|
// effect, one in-DAG-growth channel instead of two).
|
||||||
|
let sub = |kind| vec![vec![super::templates::node(name, kind, Vec::new())]];
|
||||||
|
let append_subgraph = match reconcile_action(wanted, running) {
|
||||||
|
ReconcileAction::Start => sub(NodeKind::Start),
|
||||||
|
ReconcileAction::Stop => sub(NodeKind::Stop),
|
||||||
ReconcileAction::Noop => {
|
ReconcileAction::Noop => {
|
||||||
tracing::debug!(%name, wanted = wanted.as_str(), running, "reconcile: noop");
|
tracing::debug!(%name, wanted = wanted.as_str(), running, "reconcile: noop");
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Ok(NodeOutput {
|
Ok(NodeOutput { append_subgraph })
|
||||||
append_nodes,
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mechanical container start — the sub-step a `Reconcile` planner fans
|
/// Mechanical container start — the sub-step a `Reconcile` planner fans
|
||||||
|
|
|
||||||
|
|
@ -154,57 +154,18 @@ impl JobQueue {
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Append a node into a *live* (non-terminal) DAG at runtime,
|
/// Append a whole *subgraph* into a live (non-terminal) DAG at runtime —
|
||||||
/// depending `AfterOk` on `dep_on` (the node that emitted it). Lets a
|
/// the single in-DAG-growth primitive. Each [`NodeSpec`] carries its own
|
||||||
/// planner node — e.g. [`NodeKind::Reconcile`] — fan a mechanical
|
/// `agent` and subgraph-relative `deps` (indices into `nodes`); this
|
||||||
/// sub-step ([`NodeKind::Start`] / [`NodeKind::Stop`]) out as a
|
/// rebases those onto the DAG's node-id space (`id == index`) and attaches
|
||||||
/// first-class node in the *same* DAG.
|
/// every subgraph *root* — a node with no internal deps — to `dep_on` with
|
||||||
///
|
/// an `AfterOk` edge. Used both for multi-node growth (the `MetaLock`
|
||||||
/// Must be called *before* the emitting node's [`Self::complete_node`]
|
/// growing per-agent rebuild subgraphs into the same boot / meta-update
|
||||||
/// so the DAG doesn't roll terminal with the new node still pending —
|
/// DAG instead of fanning out child DAGs) and the single-node case (a
|
||||||
/// that keeps the lease-window transient held across the sub-step and
|
/// `Reconcile` planner's `Start` / `Stop` as a one-node subgraph). Must be
|
||||||
/// lets the appended node's `AfterOk` dep resolve as soon as the
|
/// called *before* the emitting node's [`Self::complete_node`] so the DAG
|
||||||
/// emitter settles `Done`. No-op (returns `None`) if the DAG is gone.
|
/// can't roll terminal with the appended work still pending. Returns the
|
||||||
pub fn append_node(&self, dag_id: u64, kind: NodeKind, dep_on: NodeId) -> Option<NodeId> {
|
/// new node ids; empty if the DAG is gone or `nodes` is empty.
|
||||||
let mut inner = self.inner.lock().expect("job_queue mutex poisoned");
|
|
||||||
let dag = inner.dags.iter_mut().find(|d| d.id == dag_id)?;
|
|
||||||
// The appended sub-step targets the same agent as the node that
|
|
||||||
// emitted it (a `Reconcile` fanning out its `Start`/`Stop` acts on
|
|
||||||
// the same container), so inherit `dep_on`'s agent.
|
|
||||||
let agent = dag.node(dep_on)?.agent.clone();
|
|
||||||
let new_id: NodeId = u32::try_from(dag.nodes.len()).unwrap_or(u32::MAX);
|
|
||||||
dag.nodes.push(Node {
|
|
||||||
id: new_id,
|
|
||||||
agent,
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Append a whole *subgraph* into a live (non-terminal) DAG at runtime
|
|
||||||
/// — the multi-node, multi-agent generalisation of [`Self::append_node`].
|
|
||||||
/// Each [`NodeSpec`] carries its own `agent` and subgraph-relative `deps`
|
|
||||||
/// (indices into `nodes`); this rebases those onto the DAG's node-id
|
|
||||||
/// space (`id == index`, an invariant `append_node` also maintains) and
|
|
||||||
/// attaches every subgraph *root* — a node with no internal deps — to
|
|
||||||
/// `dep_on` with an `AfterOk` edge. Used by the startup sweep's
|
|
||||||
/// `MetaLock` to grow per-agent rebuild subgraphs into the same boot DAG
|
|
||||||
/// instead of fanning out child DAGs. Same call-*before*-`complete_node`
|
|
||||||
/// contract as `append_node` (so the DAG can't roll terminal with the
|
|
||||||
/// appended work still pending). Returns the new node ids; empty if the
|
|
||||||
/// DAG is gone or `nodes` is empty.
|
|
||||||
pub fn append_subgraph(
|
pub fn append_subgraph(
|
||||||
&self,
|
&self,
|
||||||
dag_id: u64,
|
dag_id: u64,
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@
|
||||||
//!
|
//!
|
||||||
//! In-DAG growth (a `MetaLock` growing rebuild subgraphs after the lock
|
//! In-DAG growth (a `MetaLock` growing rebuild subgraphs after the lock
|
||||||
//! bump, a `Reconcile` fanning its `Start`/`Stop`) flows through
|
//! bump, a `Reconcile` fanning its `Start`/`Stop`) flows through
|
||||||
//! `NodeOutput.append_subgraph` / `append_nodes`, applied before the
|
//! `NodeOutput.append_subgraph`, applied before the emitting node
|
||||||
//! emitting node completes — see `handle_completion`.
|
//! completes — see `handle_completion`.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -103,22 +103,14 @@ async fn handle_completion(
|
||||||
node = claim.node_id,
|
node = claim.node_id,
|
||||||
"job_queue: node done"
|
"job_queue: node done"
|
||||||
);
|
);
|
||||||
// Append any in-DAG sub-step nodes (e.g. a `Reconcile`
|
// Append any in-DAG subgraphs BEFORE completing this node, so
|
||||||
// planner's `Start` / `Stop`) BEFORE completing this node, so
|
// completing it doesn't roll the DAG terminal while the appended
|
||||||
// completing it doesn't roll the DAG terminal while the
|
// work is still pending — that keeps the lease-window transient
|
||||||
// appended work is still pending — that keeps the lease-window
|
// held across it. Each subgraph is independent, rooted on this
|
||||||
// transient held across the sub-step. Each depends `AfterOk`
|
// node (`AfterOk`), so it becomes ready the instant this one
|
||||||
// on this node, so it becomes ready the instant this one
|
// settles `Done` just below. Covers both the multi-node case (a
|
||||||
// settles `Done` just below.
|
// `MetaLock` growing per-agent rebuild subgraphs) and the
|
||||||
for kind in output.append_nodes {
|
// single-node case (a `Reconcile` planner's `Start` / `Stop`).
|
||||||
coord
|
|
||||||
.job_queue
|
|
||||||
.append_node(claim.dag_id, kind, claim.node_id);
|
|
||||||
}
|
|
||||||
// Same before-completion ordering as `append_nodes`, but for
|
|
||||||
// whole per-agent subgraphs (the startup sweep's rebuild
|
|
||||||
// subgraphs growing into the boot DAG) — each an independent
|
|
||||||
// subgraph rooted on this node.
|
|
||||||
for subgraph in output.append_subgraph {
|
for subgraph in output.append_subgraph {
|
||||||
coord
|
coord
|
||||||
.job_queue
|
.job_queue
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue