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:
parent
1aa88463a0
commit
10dbdb444d
9 changed files with 256 additions and 177 deletions
|
|
@ -102,6 +102,28 @@ fn settle_rebuild_tail(q: &JobQueue, agent: &str, expect_ok: bool) {
|
|||
q.complete_node(tail.node_id, Ok(()));
|
||||
}
|
||||
|
||||
/// The resources a node **declared**, read off its graph edges.
|
||||
///
|
||||
/// The declaration is the thing under test now that construction sites state
|
||||
/// their own holdings: asking the `NodeKind` what it "should" need would just
|
||||
/// re-run the derivation this module removed, and would pass even if the
|
||||
/// construction site declared nothing.
|
||||
fn declared_resources(q: &JobQueue, node_id: hive_jobq::NodeId) -> Vec<Resource> {
|
||||
let inner = q.lock();
|
||||
inner
|
||||
.sched
|
||||
.graph()
|
||||
.node(node_id)
|
||||
.expect("node exists")
|
||||
.deps
|
||||
.iter()
|
||||
.filter_map(|dep| match dep {
|
||||
hive_jobq::Dep::Resource { name, .. } => Some(name.clone()),
|
||||
hive_jobq::Dep::Node { .. } => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn state_of(q: &JobQueue, dag_id: u64) -> State {
|
||||
// A DAG whose nodes have all settled `Done` or `Skipped` drops out of the
|
||||
// snapshot — absence is the completion signal, so map it to `Done`.
|
||||
|
|
@ -708,6 +730,74 @@ fn offline_agents_skip_mechanical_nodes_but_keep_reconcile() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_fanned_out_start_declares_the_lease_and_re_enters_its_reconciles_grant() {
|
||||
// `Start` / `Stop` / `PostSwap` were lease-exempt *as kinds*, which was only
|
||||
// safe because every construction site fans them out from inside a
|
||||
// lease-holding ancestor. Now they declare the lease themselves.
|
||||
//
|
||||
// The contract says that costs nothing — a descendant re-enters the
|
||||
// ancestor's grant instead of taking a fresh unit. That is exactly the sort
|
||||
// of claim that is true until a node is used from a second site, so it is
|
||||
// pinned here rather than argued: the fanned-out `Start` must (a) actually
|
||||
// carry the declaration, (b) still run under its parent's grant, and
|
||||
// (c) not have consumed a second unit of a cap-1 lease.
|
||||
let q = JobQueue::new(4);
|
||||
let id = submit(
|
||||
&q,
|
||||
templates::reconcile_only("agent-a", Source::Manual, "converge".to_owned()),
|
||||
);
|
||||
// A competing DAG on the same agent, to prove the lease is genuinely held
|
||||
// (and held *once*) across the fan-out.
|
||||
let rival = submit(
|
||||
&q,
|
||||
templates::reconcile_only("agent-a", Source::Manual, "rival".to_owned()),
|
||||
);
|
||||
|
||||
let reconcile = claim_one(&q);
|
||||
assert_eq!(reconcile.dag_id, id);
|
||||
assert_eq!(reconcile.kind.as_str(), "reconcile");
|
||||
|
||||
// What `run_reconcile` does on observing a down container with wanted=Up.
|
||||
q.append_subgraph(
|
||||
id,
|
||||
Box::new(|b: &Job| {
|
||||
let kind = NodeKind::Start {
|
||||
agent: "agent-a".to_owned(),
|
||||
};
|
||||
let lease = Resource::Agent(kind.agent().to_owned());
|
||||
let _ = templates::node(b, kind).needs(lease);
|
||||
}),
|
||||
reconcile.node_id,
|
||||
);
|
||||
q.complete_node(reconcile.node_id, Ok(()));
|
||||
|
||||
// (a) + (b): the child runs, under the parent that parked in `Finishing`.
|
||||
let start = claim_one(&q);
|
||||
assert_eq!(start.kind.as_str(), "start");
|
||||
assert_eq!(
|
||||
declared_resources(&q, start.node_id),
|
||||
vec![Resource::Agent("agent-a".to_owned())],
|
||||
"a fanned-out Start declares the lease it runs under"
|
||||
);
|
||||
|
||||
// (c): one unit, not two. `claim_one` above already asserted the rival did
|
||||
// not come back in the same pass; make the reason explicit.
|
||||
assert!(
|
||||
q.claim_ready().is_empty(),
|
||||
"the rival DAG's Reconcile must still be blocked — the appended Start \
|
||||
borrowed the grant rather than acquiring a second unit"
|
||||
);
|
||||
|
||||
q.complete_node(start.node_id, Ok(()));
|
||||
// Subtree terminal → the grant releases and the rival finally runs.
|
||||
let rival_reconcile = claim_one(&q);
|
||||
assert_eq!(rival_reconcile.dag_id, rival);
|
||||
q.complete_node(rival_reconcile.node_id, Ok(()));
|
||||
assert_eq!(state_of(&q, id), State::Done);
|
||||
assert_eq!(state_of(&q, rival), State::Done);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_subgraph_roots_on_emitter_and_rebases_local_deps() {
|
||||
// The startup-sweep mechanism: a `MetaLock` emitter grows one rebuild
|
||||
|
|
@ -1594,12 +1684,12 @@ fn reparent_shape_is_a_lone_agentless_meta_window_node() {
|
|||
let c = claim_one(&q);
|
||||
assert_eq!(c.kind.as_str(), "reparent");
|
||||
assert_eq!(c.agent, "", "Reparent is agentless — no per-agent lease");
|
||||
assert!(
|
||||
c.kind.needs_meta_window(),
|
||||
"a topology commit must hold the same MetaWindow as WritePermFile"
|
||||
assert_eq!(
|
||||
declared_resources(&q, c.node_id),
|
||||
vec![Resource::MetaWindow],
|
||||
"a topology commit must declare the same MetaWindow as WritePermFile, \
|
||||
and nothing else — no lease (agentless), no build slot (no nix work)"
|
||||
);
|
||||
assert!(!c.kind.needs_lease());
|
||||
assert!(!c.kind.needs_build_slot());
|
||||
q.complete_node(c.node_id, Ok(()));
|
||||
assert_eq!(state_of(&q, id), State::Done);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue