jobq: templates swallow the DagSpec layer

DagSpec described the graph the templates were about to build, one layer
below the templates themselves. Per #2972 the templates should be that
unit, so the spec type is gone and every declarer writes onto the job
builder directly.

- delete DagSpec<F> and its hand-written Debug impl
- submit(source, reason, declare: impl FnOnce(&Job)) replaces the
  pre-built-spec signature; submit_and_emit follows
- all six templates take &Job; the Source is now the caller's to pass,
  which spawn and approval_deploy previously hardcoded while the other
  four did not
- power_dag dissolves into stop_nodes/start_nodes/restart_nodes, which
  borrow their targets instead of owning them

315 tests pass unchanged.
This commit is contained in:
atlas 2026-08-03 01:23:40 +02:00 committed by mara
commit 6899f574f6
7 changed files with 347 additions and 550 deletions

View file

@ -24,18 +24,23 @@
use std::sync::Arc;
use super::model::{DagSpec, NodeKind};
use super::model::NodeKind;
use super::resource::Resource;
use super::templates::{RebuildOpts, rebuild_nodes};
use super::{Job, Source, templates};
use crate::coordinator::Coordinator;
use crate::lifecycle;
fn submit_and_emit<F: FnOnce(&Job)>(coord: &Arc<Coordinator>, spec: super::DagSpec<F>) -> u64 {
fn submit_and_emit(
coord: &Arc<Coordinator>,
source: Source,
reason: String,
declare: impl FnOnce(&Job),
) -> u64 {
let id = coord
.job_queue
.submit(spec)
.expect("template-built dag specs are acyclic");
.submit(source, reason, declare)
.expect("template-declared shapes are acyclic");
coord.emit_rebuild_queue_snapshot();
id
}
@ -44,7 +49,9 @@ fn submit_and_emit<F: FnOnce(&Job)>(coord: &Arc<Coordinator>, spec: super::DagSp
/// meta input — the meta-update cascade grows its own rebuild subgraphs
/// in-DAG instead of going through this surface).
pub fn rebuild(coord: &Arc<Coordinator>, agent: &str, source: Source, reason: String) -> u64 {
submit_and_emit(coord, templates::rebuild(agent, source, reason, true))
submit_and_emit(coord, source, reason, |b| {
templates::rebuild(b, agent, true);
})
}
// ---- dynamic power-op DAG assembly ----------------------------------------
@ -185,44 +192,24 @@ fn restart_chain(b: &Job, agent: &str, graceful: bool, running: bool) {
}
}
/// Wrap the per-agent subgraphs in a `DagSpec`. No tail node: a power op's
/// effect is its nodes (`SetWanted` + `Reconcile`), with nothing left to do once
/// they settle.
///
/// There is no concatenation step: every chain declares into the same builder
/// and each keeps its own root, so the per-agent subgraphs are independent and
/// run concurrently, each on its own lease. Rebasing one subgraph's indices
/// onto another's used to be a function.
fn power_dag<F: FnOnce(&Job)>(source: Source, reason: String, declare: F) -> DagSpec<F> {
DagSpec {
source,
reason,
declare,
}
}
// The `*_spec` builders below are the PURE core the async `*_many` fns call
// The `*_nodes` declarers below are the PURE core the async `*_many` fns call
// after reading live state — they take the per-agent running (and stale)
// flags explicitly, so unit tests exercise the online/offline shapes without
// a live container. `*_many` = gather state + call `*_spec` + submit.
// a live container. `*_many` = gather state + declare + submit.
//
// A power op has no tail node: its effect is its nodes (`SetWanted` +
// `Reconcile`), with nothing left to do once they settle.
//
// There is no concatenation step either: every chain declares into the same
// builder and each keeps its own root, so the per-agent subgraphs are
// independent and run concurrently, each on its own lease. Rebasing one
// subgraph's indices onto another's used to be a function.
/// Assemble the stop DAG from explicit `(agent, running)` targets.
pub(crate) fn stop_spec(
targets: &[(String, bool)],
graceful: bool,
source: Source,
reason: String,
) -> DagSpec<impl FnOnce(&Job) + use<>> {
let targets = targets.to_vec();
power_dag(
source,
reason,
Box::new(move |b: &Job| {
for (agent, running) in targets {
stop_chain(b, &agent, graceful, running);
}
}),
)
/// Declare the stop DAG from explicit `(agent, running)` targets.
pub(crate) fn stop_nodes(b: &Job, targets: &[(String, bool)], graceful: bool) {
for (agent, running) in targets {
stop_chain(b, agent, graceful, *running);
}
}
/// Assemble the start DAG from explicit `(agent, running, stale)` targets.
@ -231,40 +218,17 @@ pub(crate) fn stop_spec(
/// running under its lease, so a down+stale agent that grew a rebuild subgraph
/// reports `rebuilding` during its swap and `starting` at its reconcile,
/// without the DAG having to guess one label covering every target.
pub(crate) fn start_spec(
targets: &[(String, bool, bool)],
source: Source,
reason: String,
) -> DagSpec<impl FnOnce(&Job) + use<>> {
let targets = targets.to_vec();
power_dag(
source,
reason,
Box::new(move |b: &Job| {
for (agent, running, stale) in targets {
start_chain(b, &agent, running, stale);
}
}),
)
pub(crate) fn start_nodes(b: &Job, targets: &[(String, bool, bool)]) {
for (agent, running, stale) in targets {
start_chain(b, agent, *running, *stale);
}
}
/// Assemble the restart DAG from explicit `(agent, running)` targets.
pub(crate) fn restart_spec(
targets: &[(String, bool)],
graceful: bool,
source: Source,
reason: String,
) -> DagSpec<impl FnOnce(&Job) + use<>> {
let targets = targets.to_vec();
power_dag(
source,
reason,
Box::new(move |b: &Job| {
for (agent, running) in targets {
restart_chain(b, &agent, graceful, running);
}
}),
)
/// Declare the restart DAG from explicit `(agent, running)` targets.
pub(crate) fn restart_nodes(b: &Job, targets: &[(String, bool)], graceful: bool) {
for (agent, running) in targets {
restart_chain(b, agent, graceful, *running);
}
}
/// Restart a single agent. Thin wrapper over [`restart_many`].
@ -302,7 +266,9 @@ pub async fn restart_many(
for agent in agents {
targets.push((agent.clone(), lifecycle::is_running(agent).await));
}
submit_and_emit(coord, restart_spec(&targets, graceful, source, reason))
submit_and_emit(coord, source, reason, |b| {
restart_nodes(b, &targets, graceful);
})
}
/// Start a single agent. Thin wrapper over [`start_many`].
@ -335,7 +301,9 @@ pub async fn start_many(
}
targets.push((agent.clone(), running, stale));
}
submit_and_emit(coord, start_spec(&targets, source, reason))
submit_and_emit(coord, source, reason, |b| {
start_nodes(b, &targets);
})
}
/// Hard stop a single agent. Thin wrapper over [`stop_many`].
@ -371,7 +339,9 @@ pub async fn stop_many(
for agent in agents {
targets.push((agent.clone(), lifecycle::is_running(agent).await));
}
submit_and_emit(coord, stop_spec(&targets, graceful, source, reason))
submit_and_emit(coord, source, reason, |b| {
stop_nodes(b, &targets, graceful);
})
}
/// Perm change: commit the JSON file(s) then rebuild.
@ -382,10 +352,9 @@ pub fn perm_change(
reason: String,
payload: super::PermPayload,
) -> u64 {
submit_and_emit(
coord,
templates::perm_change(agent, source, reason, payload),
)
submit_and_emit(coord, source, reason, |b| {
templates::perm_change(b, agent, payload);
})
}
/// Meta-input lock bump; cascade rebuilds fan out on completion.
@ -395,7 +364,9 @@ pub fn meta_update(
source: Source,
reason: String,
) -> u64 {
submit_and_emit(coord, templates::meta_update(inputs, source, reason, None))
submit_and_emit(coord, source, reason, |b| {
templates::meta_update(b, inputs, None);
})
}
/// Topology move(s) as a queue DAG. `moves` is `(child, new_parent)` pairs —
@ -411,5 +382,7 @@ pub fn reparent(
source: Source,
reason: String,
) -> u64 {
submit_and_emit(coord, templates::reparent(moves, source, reason))
submit_and_emit(coord, source, reason, |b| {
templates::reparent(b, moves);
})
}