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
|
|
@ -14,6 +14,7 @@ use super::{Claim, Declare};
|
|||
use hive_jobq::TerminalState;
|
||||
|
||||
use super::model::NodeKind;
|
||||
use super::resource::Resource;
|
||||
use crate::coordinator::Coordinator;
|
||||
use crate::power::{ReconcileAction, reconcile_action};
|
||||
|
||||
|
|
@ -168,8 +169,8 @@ fn run_emit_rebuilt(coord: &Arc<Coordinator>, claim: &Claim, ok: bool) -> NodeOu
|
|||
|
||||
/// Write the agent's durable power intent — the DAG-node form of the old
|
||||
/// pre-submit `set_wanted` side effect. Store-only (no container touch), so
|
||||
/// build-slot-exempt; but it takes the agent's lifecycle lease (see
|
||||
/// `NodeKind::needs_lease`) so the whole power-op DAG is atomic per-agent.
|
||||
/// build-slot-exempt; but it declares the agent's lifecycle lease
|
||||
/// (`Resource::Agent`) so the whole power-op DAG is atomic per-agent.
|
||||
/// The downstream `Reconcile` reads the intent this writes. Unlike the old
|
||||
/// warn-and-continue write, a failed write fails the node (cancel-downstream
|
||||
/// cancels the `Reconcile`) rather than letting it converge to a stale
|
||||
|
|
@ -189,7 +190,7 @@ fn run_set_wanted(coord: &Arc<Coordinator>, claim: &Claim, up: bool) -> Result<N
|
|||
|
||||
/// The rebuild's meta preamble: runtime-dir prep, an idempotent meta
|
||||
/// `sync_agents`, and the optional per-agent relock. Runs under the deploy
|
||||
/// window (`NodeKind::needs_meta_window`, held by the scheduler for this
|
||||
/// window (`Resource::MetaWindow`, held by the scheduler for this
|
||||
/// node) so its commits can never land inside another node's staged
|
||||
/// prepare→finalize window.
|
||||
///
|
||||
|
|
@ -302,7 +303,7 @@ async fn run_post_swap(coord: &Arc<Coordinator>, claim: &Claim) -> Result<NodeOu
|
|||
|
||||
/// First-spawn pre-create provisioning: proposed/applied repos, state
|
||||
/// subvolume, and the meta `sync_agents` registration. Runs under the
|
||||
/// deploy window (`NodeKind::needs_meta_window`) so its commit can't
|
||||
/// deploy window (it declares `Resource::MetaWindow`) so its commit can't
|
||||
/// land inside another node's staged deploy window.
|
||||
async fn run_provision(coord: &Arc<Coordinator>, claim: &Claim) -> Result<NodeOutput> {
|
||||
let name = &claim.agent;
|
||||
|
|
@ -417,8 +418,13 @@ async fn run_reconcile(coord: &Arc<Coordinator>, claim: &Claim) -> Result<NodeOu
|
|||
// carries the agent it targets, so stamp `claim.agent` into the fanned-out
|
||||
// Start/Stop kind (one in-DAG-growth channel).
|
||||
let sub = |kind: NodeKind| {
|
||||
// `Start` / `Stop` declare the lease they run under. This node is their
|
||||
// parent and holds it, so the declaration is a re-entrant borrow — no
|
||||
// second unit, no deadlock. It exists so the requirement belongs to the
|
||||
// node rather than to the fact that a `Reconcile` happens to fan it out.
|
||||
let lease = Resource::Agent(kind.agent().to_owned());
|
||||
vec![Box::new(move |b: &super::Job| {
|
||||
let _ = super::templates::node(b, kind);
|
||||
let _ = super::templates::node(b, kind).needs(lease);
|
||||
}) as Declare]
|
||||
};
|
||||
let append_subgraph = match reconcile_action(wanted, running) {
|
||||
|
|
@ -556,7 +562,7 @@ async fn run_write_perm_file(coord: &Arc<Coordinator>, claim: &Claim) -> Result<
|
|||
let NodeKind::WritePermFile { payload, .. } = &claim.kind else {
|
||||
anyhow::bail!("run_write_perm_file on a non-WritePermFile node");
|
||||
};
|
||||
// Runs under the deploy window (`NodeKind::needs_meta_window`): a
|
||||
// Runs under the deploy window (it declares `Resource::MetaWindow`): a
|
||||
// perm commit landing inside another node's staged prepare→finalize
|
||||
// window would sweep the staged deploy lock into its commit (the
|
||||
// commits are also path-limited in meta.rs — belt and braces).
|
||||
|
|
@ -592,7 +598,7 @@ async fn run_write_perm_file(coord: &Arc<Coordinator>, claim: &Claim) -> Result<
|
|||
/// commit (`Coordinator::reparent_bulk_with_notify`, which already handles
|
||||
/// both the single- and bulk-move case, sends the per-agent move
|
||||
/// notifications, and rescans + diff-emits the container tree). Runs under
|
||||
/// the deploy window (`NodeKind::needs_meta_window`), same reasoning as
|
||||
/// the deploy window (it declares `Resource::MetaWindow`), same reasoning as
|
||||
/// `run_write_perm_file`: a topology commit landing inside another node's
|
||||
/// staged deploy window would sweep the staged lock into its commit.
|
||||
async fn run_reparent(coord: &Arc<Coordinator>, claim: &Claim) -> Result<NodeOutput> {
|
||||
|
|
|
|||
Loading…
Reference in a new issue