job_queue: fix the boot sweep's lost declarations, drop the node wrapper

Two review findings on the resources-at-construction change.

argus: `workers::auto_update`'s boot sweep constructs nodes through
`templates::node` too, and it was not converted. With the kind-derived
declaration gone, its sweep `MetaLock` and its per-agent `Reconcile`
silently declared no resources at all — so a boot reconcile no longer
held the agent lease and could race another DAG's container ops, and the
sweep's meta commit could land inside another node's staged deploy
window. Nothing failed to compile: removing an implicit behaviour from a
helper is invisible at every call site that relied on it.

The declarations now live in a pure `boot_nodes`, split out of
`submit_boot_tree` so they can be exercised without a `Coordinator`.
That path is the only place job nodes are built outside `job_queue/`,
which is exactly why it had no coverage; `boot_sweep_nodes_declare_
their_own_resources` closes that, asserting against declared graph edges
rather than against the kind.

mara: `templates::node` is a redundant redirect now that it no longer
derives resources — deleted, and its 43 call sites use `Job::node`
directly. The reasoning it documented moved to the module docs of
`templates.rs` and `resource.rs`, which is where it stays true.
This commit is contained in:
atlas 2026-08-02 16:22:56 +02:00 committed by mara
commit 58a9f218f2
6 changed files with 231 additions and 180 deletions

View file

@ -6,7 +6,7 @@
//! state, which needs an async `lifecycle::is_running` read that a pure/sync
//! template can't do. So these fns are async — they read each agent's state,
//! assemble a per-agent subgraph out of the shared pure primitives
//! (`templates::{node, rebuild_nodes}`), all declaring into ONE job
//! (`Job::node` + `templates::rebuild_nodes`), all declaring into ONE job
//! (independent per-agent roots, concurrent on their own leases).
//!
//! Dynamic shape rule: `stop`/`start` carry a head `SetWanted(w)` (durable
@ -26,7 +26,7 @@ use std::sync::Arc;
use super::model::{DagSpec, NodeKind};
use super::resource::Resource;
use super::templates::{RebuildOpts, node, rebuild_nodes};
use super::templates::{RebuildOpts, rebuild_nodes};
use super::{Job, Source, templates};
use crate::coordinator::Coordinator;
use crate::lifecycle;
@ -65,30 +65,32 @@ fn stop_chain(b: &Job, agent: &str, graceful: bool, running: bool) {
// steps are its children (borrow the lease, run once it reaches `Finishing`,
// dep-ordered among themselves).
let a = || agent.to_owned();
let wanted = node(
b,
NodeKind::SetWanted {
let wanted = b
.node(NodeKind::SetWanted {
agent: a(),
up: false,
},
)
.needs(Resource::Agent(a()));
})
.needs(Resource::Agent(a()));
// Declaration order is dependency order: the quiesce steps come first so
// the `Reconcile` that waits on them can name them.
if graceful && running {
let signal = node(b, NodeKind::Signal { agent: a() })
let signal = b
.node(NodeKind::Signal { agent: a() })
.needs(Resource::Agent(a()))
.part_of(wanted);
let drain = node(b, NodeKind::Drain { agent: a() })
let drain = b
.node(NodeKind::Drain { agent: a() })
.needs(Resource::Agent(a()))
.part_of(wanted)
.after_ok(signal);
let _ = node(b, NodeKind::Reconcile { agent: a() })
let _ = b
.node(NodeKind::Reconcile { agent: a() })
.needs(Resource::Agent(a()))
.part_of(wanted)
.after_ok(drain);
} else {
let _ = node(b, NodeKind::Reconcile { agent: a() })
let _ = b
.node(NodeKind::Reconcile { agent: a() })
.needs(Resource::Agent(a()))
.part_of(wanted);
}
@ -99,14 +101,12 @@ fn stop_chain(b: &Job, agent: &str, graceful: bool, running: bool) {
/// current derivations), otherwise a plain `Reconcile` (which starts a down
/// agent and noops an already-running one).
fn start_chain(b: &Job, agent: &str, running: bool, stale: bool) {
let wanted = node(
b,
NodeKind::SetWanted {
let wanted = b
.node(NodeKind::SetWanted {
agent: agent.to_owned(),
up: true,
},
)
.needs(Resource::Agent(agent.to_owned()));
})
.needs(Resource::Agent(agent.to_owned()));
if !running && stale {
// Rebuild subtree chained behind the `SetWanted` head. `MetaSync`,
// `Prebuild` + `Reconcile` are their own group roots (top-level, per
@ -121,14 +121,12 @@ fn start_chain(b: &Job, agent: &str, running: bool, stale: bool) {
Some(wanted),
);
} else {
let _ = node(
b,
NodeKind::Reconcile {
let _ = b
.node(NodeKind::Reconcile {
agent: agent.to_owned(),
},
)
.needs(Resource::Agent(agent.to_owned()))
.part_of(wanted);
})
.needs(Resource::Agent(agent.to_owned()))
.part_of(wanted);
}
}
@ -145,7 +143,9 @@ fn restart_chain(b: &Job, agent: &str, graceful: bool, running: bool) {
let a = || agent.to_owned();
if !running {
// Nothing to bounce — a lone Reconcile converges to intent.
let _ = node(b, NodeKind::Reconcile { agent: a() }).needs(Resource::Agent(a()));
let _ = b
.node(NodeKind::Reconcile { agent: a() })
.needs(Resource::Agent(a()));
return;
}
// Running: mechanical stop then Reconcile. The first stop node is the group
@ -157,21 +157,29 @@ fn restart_chain(b: &Job, agent: &str, graceful: bool, running: bool) {
// that step *is* the root, and the parent gate already orders it — a child
// must NOT dep on its own parent (dep-scope), so it takes no sibling edge.
if graceful {
let signal = node(b, NodeKind::Signal { agent: a() }).needs(Resource::Agent(a()));
let drain = node(b, NodeKind::Drain { agent: a() })
let signal = b
.node(NodeKind::Signal { agent: a() })
.needs(Resource::Agent(a()));
let drain = b
.node(NodeKind::Drain { agent: a() })
.needs(Resource::Agent(a()))
.part_of(signal);
let stop = node(b, NodeKind::StopForUpdate { agent: a() })
let stop = b
.node(NodeKind::StopForUpdate { agent: a() })
.needs(Resource::Agent(a()))
.part_of(signal)
.after_ok(drain);
let _ = node(b, NodeKind::Reconcile { agent: a() })
let _ = b
.node(NodeKind::Reconcile { agent: a() })
.needs(Resource::Agent(a()))
.part_of(signal)
.after_ok(stop);
} else {
let stop = node(b, NodeKind::StopForUpdate { agent: a() }).needs(Resource::Agent(a()));
let _ = node(b, NodeKind::Reconcile { agent: a() })
let stop = b
.node(NodeKind::StopForUpdate { agent: a() })
.needs(Resource::Agent(a()));
let _ = b
.node(NodeKind::Reconcile { agent: a() })
.needs(Resource::Agent(a()))
.part_of(stop);
}