job_queue: declare a node's resources where the node is constructed

Resources were derived from the node's kind: `templates::node` called
`NodeKind::resource_deps()`, which fanned out to `needs_build_slot` /
`needs_lease` / `needs_meta_window`. That made the requirement a property
of the *kind*, so a kind that happened to run under an ancestor already
holding the resource could get away with declaring nothing.

Three did. `Start`, `Stop` and `PostSwap` appear in none of the three
predicates, and that was only safe because one construction site fans
them out from inside a lease-holding `Reconcile` — a fact about today's
DAG shape, not about the nodes.

Each of the 41 construction sites now says what it holds. `Start` /
`Stop` / `PostSwap` declare the agent lease; per the contract that is a
re-entrant borrow, which a new test pins rather than argues.

`running_transients` reads the node's declared deps instead of
re-deriving from the kind. That closes the blank-pill gap: the pill went
blank during container start, stop and the post-swap tail because the
declaration was missing, not because the filter was wrong.

The deleted predicates carried the only written record of three design
decisions; each moved to the `Resource` variant it constrains rather than
dying with its function.
This commit is contained in:
atlas 2026-08-02 15:57:51 +02:00 committed by mara
commit 10dbdb444d
9 changed files with 256 additions and 177 deletions

View file

@ -25,6 +25,7 @@
use std::sync::Arc;
use super::model::{DagSpec, NodeKind};
use super::resource::Resource;
use super::templates::{RebuildOpts, node, rebuild_nodes};
use super::{Job, Source, templates};
use crate::coordinator::Coordinator;
@ -70,19 +71,26 @@ fn stop_chain(b: &Job, agent: &str, graceful: bool, running: bool) {
agent: a(),
up: false,
},
);
)
.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() }).part_of(wanted);
let signal = node(b, NodeKind::Signal { agent: a() })
.needs(Resource::Agent(a()))
.part_of(wanted);
let drain = node(b, NodeKind::Drain { agent: a() })
.needs(Resource::Agent(a()))
.part_of(wanted)
.after_ok(signal);
let _ = node(b, NodeKind::Reconcile { agent: a() })
.needs(Resource::Agent(a()))
.part_of(wanted)
.after_ok(drain);
} else {
let _ = node(b, NodeKind::Reconcile { agent: a() }).part_of(wanted);
let _ = node(b, NodeKind::Reconcile { agent: a() })
.needs(Resource::Agent(a()))
.part_of(wanted);
}
}
@ -97,7 +105,8 @@ fn start_chain(b: &Job, agent: &str, running: bool, stale: bool) {
agent: agent.to_owned(),
up: true,
},
);
)
.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
@ -118,6 +127,7 @@ fn start_chain(b: &Job, agent: &str, running: bool, stale: bool) {
agent: agent.to_owned(),
},
)
.needs(Resource::Agent(agent.to_owned()))
.part_of(wanted);
}
}
@ -135,7 +145,7 @@ 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() });
let _ = node(b, NodeKind::Reconcile { agent: a() }).needs(Resource::Agent(a()));
return;
}
// Running: mechanical stop then Reconcile. The first stop node is the group
@ -147,17 +157,23 @@ 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() });
let drain = node(b, NodeKind::Drain { agent: a() }).part_of(signal);
let signal = node(b, NodeKind::Signal { agent: a() }).needs(Resource::Agent(a()));
let drain = node(b, NodeKind::Drain { agent: a() })
.needs(Resource::Agent(a()))
.part_of(signal);
let stop = node(b, NodeKind::StopForUpdate { agent: a() })
.needs(Resource::Agent(a()))
.part_of(signal)
.after_ok(drain);
let _ = node(b, NodeKind::Reconcile { agent: a() })
.needs(Resource::Agent(a()))
.part_of(signal)
.after_ok(stop);
} else {
let stop = node(b, NodeKind::StopForUpdate { agent: a() });
let _ = node(b, NodeKind::Reconcile { agent: a() }).part_of(stop);
let stop = node(b, NodeKind::StopForUpdate { agent: a() }).needs(Resource::Agent(a()));
let _ = node(b, NodeKind::Reconcile { agent: a() })
.needs(Resource::Agent(a()))
.part_of(stop);
}
}