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
|
|
@ -24,28 +24,29 @@
|
|||
use hive_jobq::TerminalState;
|
||||
|
||||
use super::model::{DagSpec, NodeKind, PermPayload, Source};
|
||||
use super::resource::Resource;
|
||||
use super::{Declare, Handle, Job};
|
||||
|
||||
/// Declare one node carrying `kind`, with the resources that kind needs.
|
||||
/// Declare one node carrying `kind`. **Resources are not derived here** — the
|
||||
/// construction site says what the node holds, with `.needs(…)`.
|
||||
///
|
||||
/// The resource declaration is [`NodeKind::resource_deps`] applied at the
|
||||
/// construction site — a build slot for nix-heavy kinds, the agent lease for
|
||||
/// container-affecting ones, the global meta window for meta-mutating ones. A
|
||||
/// node that declares a resource an ancestor already holds re-enters that
|
||||
/// grant rather than taking a fresh unit, so declaring costs nothing.
|
||||
/// That is the point rather than an omission. Deriving `(name, units)` from the
|
||||
/// kind made the declaration a property of the *kind*, so a kind that happened
|
||||
/// to run under an ancestor holding the resource could get away with declaring
|
||||
/// nothing — which is precisely how `Start` / `Stop` / `PostSwap` ended up
|
||||
/// lease-exempt: one construction site fans them out from inside a
|
||||
/// lease-holding `Reconcile`. The requirement is a property of the node, not of
|
||||
/// the one DAG shape it is used in today.
|
||||
///
|
||||
/// Declaring a resource an ancestor already holds is free: a descendant
|
||||
/// re-enters that grant through the crate's recursive lock rather than taking a
|
||||
/// fresh unit.
|
||||
///
|
||||
/// The returned handle is where edges and grouping are declared, and is `Copy`
|
||||
/// — naming a node as a dependency does not consume the ability to name it
|
||||
/// again.
|
||||
pub(crate) fn node(b: &Job, kind: NodeKind) -> Handle<'_> {
|
||||
// Read the resources off the kind before handing it over — the payload is
|
||||
// moved into the node, not cloned for it.
|
||||
let resources = kind.resource_deps();
|
||||
let mut handle = b.node(kind);
|
||||
for (name, count) in resources {
|
||||
handle = handle.needs_units(name, count);
|
||||
}
|
||||
handle
|
||||
b.node(kind)
|
||||
}
|
||||
|
||||
/// The `Rebuilt`-reporting tail pair for a rebuild-shaped DAG: the success node
|
||||
|
|
@ -181,32 +182,52 @@ pub(crate) fn rebuild_nodes<'a>(
|
|||
let a = || agent.to_owned();
|
||||
let RebuildOpts { relock, graceful } = opts;
|
||||
|
||||
let mut meta_sync = node(b, NodeKind::MetaSync { agent: a(), relock });
|
||||
let mut meta_sync =
|
||||
node(b, NodeKind::MetaSync { agent: a(), relock }).needs(Resource::MetaWindow);
|
||||
if let Some(after) = after {
|
||||
meta_sync = meta_sync.after_ok(after);
|
||||
}
|
||||
let prebuild = node(b, NodeKind::Prebuild { agent: a() }).after_ok(meta_sync);
|
||||
let prebuild = node(b, NodeKind::Prebuild { agent: a() })
|
||||
.needs(Resource::BuildSlot)
|
||||
.after_ok(meta_sync);
|
||||
|
||||
// The stop root hangs off `Prebuild` and owns the agent lease for
|
||||
// everything below it. `StopForUpdate` parents the swap pair either way.
|
||||
let stop_for_update = if graceful {
|
||||
let signal = node(b, NodeKind::Signal { agent: a() }).part_of(prebuild);
|
||||
let signal = node(b, NodeKind::Signal { agent: a() })
|
||||
.needs(Resource::Agent(a()))
|
||||
.part_of(prebuild);
|
||||
// `Drain` is a *child* of `Signal`, so the parent gate already orders
|
||||
// it — a child must not dep on its own parent (dep-scope).
|
||||
let drain = node(b, NodeKind::Drain { agent: a() }).part_of(signal);
|
||||
let drain = node(b, NodeKind::Drain { agent: a() })
|
||||
.needs(Resource::Agent(a()))
|
||||
.part_of(signal);
|
||||
node(b, NodeKind::StopForUpdate { agent: a() })
|
||||
.needs(Resource::Agent(a()))
|
||||
.part_of(signal)
|
||||
.after_ok(drain)
|
||||
} else {
|
||||
node(b, NodeKind::StopForUpdate { agent: a() }).part_of(prebuild)
|
||||
node(b, NodeKind::StopForUpdate { agent: a() })
|
||||
.needs(Resource::Agent(a()))
|
||||
.part_of(prebuild)
|
||||
};
|
||||
|
||||
let swap = node(b, NodeKind::Swap { agent: a() }).part_of(stop_for_update);
|
||||
let swap = node(b, NodeKind::Swap { agent: a() })
|
||||
.needs(Resource::BuildSlot)
|
||||
.needs(Resource::Agent(a()))
|
||||
.part_of(stop_for_update);
|
||||
// `PostSwap` declares the lease it actually runs under. It is a child of
|
||||
// `StopForUpdate`, which holds it, so this is a re-entrant borrow — no
|
||||
// second unit, no deadlock. Declaring it is what stops the requirement
|
||||
// being true only of this one DAG shape.
|
||||
let _post_swap = node(b, NodeKind::PostSwap { agent: a() })
|
||||
.needs(Resource::Agent(a()))
|
||||
.part_of(stop_for_update)
|
||||
.after_ok(swap);
|
||||
|
||||
let reconcile = node(b, NodeKind::Reconcile { agent: a() }).after_any(prebuild);
|
||||
let reconcile = node(b, NodeKind::Reconcile { agent: a() })
|
||||
.needs(Resource::Agent(a()))
|
||||
.after_any(prebuild);
|
||||
|
||||
RebuildRoots {
|
||||
meta_sync,
|
||||
|
|
@ -258,6 +279,7 @@ pub(crate) fn deploy_rebuild_nodes(agent: &str, approval_id: i64) -> Declare {
|
|||
approval_id,
|
||||
},
|
||||
)
|
||||
.needs(Resource::MetaWindow)
|
||||
.after_ok(roots.prebuild)
|
||||
.after_ok(roots.reconcile);
|
||||
})
|
||||
|
|
@ -335,13 +357,22 @@ pub fn approval_deploy(
|
|||
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 = node(
|
||||
b,
|
||||
NodeKind::DeployWindow {
|
||||
agent: a(),
|
||||
approval_id,
|
||||
},
|
||||
);
|
||||
)
|
||||
.needs(Resource::BuildSlot)
|
||||
.needs(Resource::Agent(a()))
|
||||
.needs(Resource::MetaWindow);
|
||||
let verify = node(
|
||||
b,
|
||||
NodeKind::MergeVerify {
|
||||
|
|
@ -390,7 +421,9 @@ pub fn reconcile_only(
|
|||
source,
|
||||
reason,
|
||||
declare: Box::new(move |b: &Job| {
|
||||
let _reconcile = node(b, NodeKind::Reconcile { agent });
|
||||
// Name the lease before the agent string is moved into the kind.
|
||||
let lease = Resource::Agent(agent.clone());
|
||||
let _reconcile = node(b, NodeKind::Reconcile { agent }).needs(lease);
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
@ -413,10 +446,16 @@ pub fn spawn(agent: &str, approval_id: i64, reason: String) -> DagSpec<impl FnOn
|
|||
reason,
|
||||
declare: Box::new(move |b: &Job| {
|
||||
let a = || agent.clone();
|
||||
let provision = node(b, NodeKind::Provision { agent: a() });
|
||||
let create = node(b, NodeKind::Create { agent: a() }).part_of(provision);
|
||||
let dropin = node(b, NodeKind::WriteDropin { agent: a() }).part_of(create);
|
||||
let provision = node(b, NodeKind::Provision { agent: a() }).needs(Resource::MetaWindow);
|
||||
let create = node(b, NodeKind::Create { agent: a() })
|
||||
.needs(Resource::BuildSlot)
|
||||
.needs(Resource::Agent(a()))
|
||||
.part_of(provision);
|
||||
let dropin = node(b, NodeKind::WriteDropin { agent: a() })
|
||||
.needs(Resource::Agent(a()))
|
||||
.part_of(create);
|
||||
let _reconcile = node(b, NodeKind::Reconcile { agent: a() })
|
||||
.needs(Resource::Agent(a()))
|
||||
.part_of(create)
|
||||
.after_ok(dropin);
|
||||
|
||||
|
|
@ -447,7 +486,8 @@ pub fn perm_change(
|
|||
agent: agent.clone(),
|
||||
payload,
|
||||
},
|
||||
);
|
||||
)
|
||||
.needs(Resource::MetaWindow);
|
||||
let roots = rebuild_nodes(
|
||||
b,
|
||||
&agent,
|
||||
|
|
@ -493,7 +533,9 @@ pub fn meta_update(
|
|||
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
|
||||
|
|
@ -524,7 +566,7 @@ pub fn reparent(
|
|||
source,
|
||||
reason,
|
||||
declare: Box::new(move |b: &Job| {
|
||||
let _reparent = node(b, NodeKind::Reparent { moves });
|
||||
let _reparent = node(b, NodeKind::Reparent { moves }).needs(Resource::MetaWindow);
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue