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

@ -26,7 +26,7 @@
use hive_jobq::TerminalState;
use super::model::{DagSpec, NodeKind, PermPayload, Source};
use super::model::{NodeKind, PermPayload};
use super::resource::Resource;
use super::{Handle, Job};
@ -304,29 +304,17 @@ pub(crate) fn deploy_rebuild_nodes(b: &Job, agent: &str, approval_id: i64) {
/// whole `StopForUpdate`→`Swap`→`PostSwap` subtree, so those three cover every
/// node. Edging `Reconcile` alone would not do: it is `AfterAny` `Prebuild`, so
/// it reaches `Done` even after a failed swap and the tail would report success.
pub fn rebuild(
agent: &str,
source: Source,
reason: String,
relock: bool,
) -> DagSpec<impl FnOnce(&Job) + use<>> {
let agent = agent.to_owned();
DagSpec {
source,
reason,
declare: Box::new(move |b: &Job| {
let roots = rebuild_nodes(
b,
&agent,
RebuildOpts {
relock,
graceful: false,
},
None,
);
emit_rebuilt_tails(b, &agent, &roots.all());
}),
}
pub fn rebuild(b: &Job, agent: &str, relock: bool) {
let roots = rebuild_nodes(
b,
agent,
RebuildOpts {
relock,
graceful: false,
},
None,
);
emit_rebuilt_tails(b, agent, &roots.all());
}
/// Approval-driven deploy (`MergeConfigPr`) as a phase subtree rather than the
@ -354,55 +342,44 @@ pub fn rebuild(
///
/// The window still spans the container build, as it must: `prepare_deploy`
/// leaves `flake.lock` staged-uncommitted for the build's whole duration.
pub fn approval_deploy(
agent: &str,
approval_id: i64,
reason: String,
) -> DagSpec<impl FnOnce(&Job) + use<>> {
let agent = agent.to_owned();
DagSpec {
source: Source::Approval,
reason,
declare: Box::new(move |b: &Job| {
let a = || agent.clone();
// The window is the widest holder in the tree: it brackets a nix
// build (`BuildSlot`), takes the container down across the swap
// (`Agent`), and serialises the meta mutation its subtree performs
// (`MetaWindow`). All three are held for its whole subtree, which
// is what lets the appended rebuild's `MetaSync` and the
// `FinalizeDeploy` re-enter rather than contend.
let window = b
.node(NodeKind::DeployWindow {
agent: a(),
approval_id,
})
.needs(Resource::BuildSlot)
.needs(Resource::Agent(a()))
.needs(Resource::MetaWindow);
let verify = b
.node(NodeKind::MergeVerify {
agent: a(),
approval_id,
})
.part_of(window);
let apply = b
.node(NodeKind::DeployApply {
agent: a(),
approval_id,
})
.part_of(window)
.after_ok(verify);
let _tail = b
.node(NodeKind::DeployTail {
agent: a(),
approval_id,
})
.part_of(window)
.after_any(apply);
pub fn approval_deploy(b: &Job, agent: &str, approval_id: i64) {
let a = || agent.to_owned();
// The window is the widest holder in the tree: it brackets a nix
// build (`BuildSlot`), takes the container down across the swap
// (`Agent`), and serialises the meta mutation its subtree performs
// (`MetaWindow`). All three are held for its whole subtree, which
// is what lets the appended rebuild's `MetaSync` and the
// `FinalizeDeploy` re-enter rather than contend.
let window = b
.node(NodeKind::DeployWindow {
agent: a(),
approval_id,
})
.needs(Resource::BuildSlot)
.needs(Resource::Agent(a()))
.needs(Resource::MetaWindow);
let verify = b
.node(NodeKind::MergeVerify {
agent: a(),
approval_id,
})
.part_of(window);
let apply = b
.node(NodeKind::DeployApply {
agent: a(),
approval_id,
})
.part_of(window)
.after_ok(verify);
let _tail = b
.node(NodeKind::DeployTail {
agent: a(),
approval_id,
})
.part_of(window)
.after_any(apply);
resolve_approval_tails(b, approval_id, window);
}),
}
resolve_approval_tails(b, approval_id, window);
}
/// First-deploy spawn (approval-driven): `Provision` (proposed/applied
@ -416,34 +393,27 @@ pub fn approval_deploy(
/// container was never created). Closed by a `ResolveApproval` tail root edged
/// `AfterAny` onto `Provision` — the DAG's only other group-root, so its roll-up
/// already carries the whole cascade.
pub fn spawn(agent: &str, approval_id: i64, reason: String) -> DagSpec<impl FnOnce(&Job) + use<>> {
let agent = agent.to_owned();
DagSpec {
source: Source::Approval,
reason,
declare: Box::new(move |b: &Job| {
let a = || agent.clone();
let provision = b
.node(NodeKind::Provision { agent: a() })
.needs(Resource::MetaWindow);
let create = b
.node(NodeKind::Create { agent: a() })
.needs(Resource::BuildSlot)
.needs(Resource::Agent(a()))
.part_of(provision);
let dropin = b
.node(NodeKind::WriteDropin { agent: a() })
.needs(Resource::Agent(a()))
.part_of(create);
let _reconcile = b
.node(NodeKind::Reconcile { agent: a() })
.needs(Resource::Agent(a()))
.part_of(create)
.after_ok(dropin);
pub fn spawn(b: &Job, agent: &str, approval_id: i64) {
let a = || agent.to_owned();
let provision = b
.node(NodeKind::Provision { agent: a() })
.needs(Resource::MetaWindow);
let create = b
.node(NodeKind::Create { agent: a() })
.needs(Resource::BuildSlot)
.needs(Resource::Agent(a()))
.part_of(provision);
let dropin = b
.node(NodeKind::WriteDropin { agent: a() })
.needs(Resource::Agent(a()))
.part_of(create);
let _reconcile = b
.node(NodeKind::Reconcile { agent: a() })
.needs(Resource::Agent(a()))
.part_of(create)
.after_ok(dropin);
resolve_approval_tails(b, approval_id, provision);
}),
}
resolve_approval_tails(b, approval_id, provision);
}
/// Perm change: commit the JSON file(s), then the rebuild subgraph so
@ -451,39 +421,27 @@ pub fn spawn(agent: &str, approval_id: i64, reason: String) -> DagSpec<impl FnOn
/// effect in the container. Group-roots are `WritePermFile` plus the rebuild
/// subgraph's `MetaSync` / `Prebuild` / `Reconcile`, so the `EmitRebuilt` tail
/// edges all four.
pub fn perm_change(
agent: &str,
source: Source,
reason: String,
payload: PermPayload,
) -> DagSpec<impl FnOnce(&Job) + use<>> {
let agent = agent.to_owned();
DagSpec {
source,
reason,
declare: Box::new(move |b: &Job| {
let write = b
.node(NodeKind::WritePermFile {
agent: agent.clone(),
payload,
})
.needs(Resource::MetaWindow);
let roots = rebuild_nodes(
b,
&agent,
RebuildOpts {
relock: true,
graceful: false,
},
Some(write),
);
emit_rebuilt_tails(
b,
&agent,
&[write, roots.meta_sync, roots.prebuild, roots.reconcile],
);
}),
}
pub fn perm_change(b: &Job, agent: &str, payload: PermPayload) {
let write = b
.node(NodeKind::WritePermFile {
agent: agent.to_owned(),
payload,
})
.needs(Resource::MetaWindow);
let roots = rebuild_nodes(
b,
agent,
RebuildOpts {
relock: true,
graceful: false,
},
Some(write),
);
emit_rebuilt_tails(
b,
agent,
&[write, roots.meta_sync, roots.prebuild, roots.reconcile],
);
}
/// Meta-input lock bump. The `MetaLock` executor grows one rebuild subgraph
@ -496,33 +454,22 @@ pub fn perm_change(
/// so the "hyperhive" pseudo-agent gets no pill), giving each cascade agent
/// crash-watch suppression during its `Swap` — the property the old child
/// `Rebuild` DAGs carried via their own transient.
pub fn meta_update(
inputs: Vec<String>,
source: Source,
reason: String,
approval_id: Option<i64>,
) -> DagSpec<impl FnOnce(&Job) + use<>> {
DagSpec {
source,
reason,
declare: Box::new(move |b: &Job| {
let lock = b
.node(NodeKind::MetaLock {
sweep: false,
fanout: None,
inputs,
})
.needs(Resource::BuildSlot)
.needs(Resource::MetaWindow);
// The bump itself has no side effect, so an operator-driven one ends
// at the `MetaLock`; an approval-driven one still has its row to
// resolve and gets the per-outcome tails edged onto that single
// group-root — whose roll-up covers the rebuild subgraphs `MetaLock`
// grows into itself.
if let Some(approval_id) = approval_id {
resolve_approval_tails(b, approval_id, lock);
}
}),
pub fn meta_update(b: &Job, inputs: Vec<String>, approval_id: Option<i64>) {
let lock = b
.node(NodeKind::MetaLock {
sweep: false,
fanout: None,
inputs,
})
.needs(Resource::BuildSlot)
.needs(Resource::MetaWindow);
// The bump itself has no side effect, so an operator-driven one ends
// at the `MetaLock`; an approval-driven one still has its row to
// resolve and gets the per-outcome tails edged onto that single
// group-root — whose roll-up covers the rebuild subgraphs `MetaLock`
// grows into itself.
if let Some(approval_id) = approval_id {
resolve_approval_tails(b, approval_id, lock);
}
}
@ -535,20 +482,10 @@ pub fn meta_update(
/// checks), so a parent move needs no container rebuild to take effect.
/// No transient pill either — the node is agentless (no lease to hang one
/// off of) and near-instant. No tail node: the write is the whole effect.
pub fn reparent(
moves: Vec<(hive_types::Ident, Option<hive_types::Ident>)>,
source: Source,
reason: String,
) -> DagSpec<impl FnOnce(&Job) + use<>> {
DagSpec {
source,
reason,
declare: Box::new(move |b: &Job| {
let _reparent = b
.node(NodeKind::Reparent { moves })
.needs(Resource::MetaWindow);
}),
}
pub fn reparent(b: &Job, moves: Vec<(hive_types::Ident, Option<hive_types::Ident>)>) {
let _reparent = b
.node(NodeKind::Reparent { moves })
.needs(Resource::MetaWindow);
}
// The boot is assembled inline in `workers/auto_update.rs::submit_boot_tree`