refactor(#2591): make NodeKind the queue payload — drop JobPayload, agent into variants
This commit is contained in:
parent
2294cd4516
commit
be2dfa8cd3
8 changed files with 233 additions and 177 deletions
|
|
@ -82,24 +82,24 @@ pub(super) async fn run_node(coord: &Arc<Coordinator>, claim: &Claim) -> Result<
|
|||
node_id: claim.node_id,
|
||||
};
|
||||
match &claim.kind {
|
||||
NodeKind::Prebuild { relock } => run_prebuild(coord, claim, &ctx, *relock).await,
|
||||
NodeKind::Swap => run_swap(coord, claim, &ctx).await,
|
||||
NodeKind::PostSwap => run_post_swap(coord, claim, &ctx).await,
|
||||
NodeKind::Provision => run_provision(coord, claim, &ctx).await,
|
||||
NodeKind::Create => run_create(claim, &ctx).await,
|
||||
NodeKind::Prebuild { relock, .. } => run_prebuild(coord, claim, &ctx, *relock).await,
|
||||
NodeKind::Swap { .. } => run_swap(coord, claim, &ctx).await,
|
||||
NodeKind::PostSwap { .. } => run_post_swap(coord, claim, &ctx).await,
|
||||
NodeKind::Provision { .. } => run_provision(coord, claim, &ctx).await,
|
||||
NodeKind::Create { .. } => run_create(claim, &ctx).await,
|
||||
NodeKind::MetaLock { sweep, fanout } => {
|
||||
run_meta_lock(coord, claim, &ctx, *sweep, fanout.clone()).await
|
||||
}
|
||||
NodeKind::Reconcile => run_reconcile(coord, claim).await,
|
||||
NodeKind::Start => run_start(coord, claim, &ctx).await,
|
||||
NodeKind::Stop => run_stop(coord, claim, &ctx).await,
|
||||
NodeKind::StopForUpdate => run_stop_for_update(coord, claim, &ctx).await,
|
||||
NodeKind::Signal => Ok(run_signal(coord, claim, &ctx)),
|
||||
NodeKind::Drain => run_drain(coord, claim, &ctx).await,
|
||||
NodeKind::WriteDropin => run_write_dropin(coord, claim).await,
|
||||
NodeKind::WritePermFile => run_write_perm_file(coord, claim, &ctx).await,
|
||||
NodeKind::ApprovalDeploy => run_approval_deploy(coord, claim).await,
|
||||
NodeKind::SetWanted { up } => run_set_wanted(coord, claim, *up),
|
||||
NodeKind::Reconcile { .. } => run_reconcile(coord, claim).await,
|
||||
NodeKind::Start { .. } => run_start(coord, claim, &ctx).await,
|
||||
NodeKind::Stop { .. } => run_stop(coord, claim, &ctx).await,
|
||||
NodeKind::StopForUpdate { .. } => run_stop_for_update(coord, claim, &ctx).await,
|
||||
NodeKind::Signal { .. } => Ok(run_signal(coord, claim, &ctx)),
|
||||
NodeKind::Drain { .. } => run_drain(coord, claim, &ctx).await,
|
||||
NodeKind::WriteDropin { .. } => run_write_dropin(coord, claim).await,
|
||||
NodeKind::WritePermFile { .. } => run_write_perm_file(coord, claim, &ctx).await,
|
||||
NodeKind::ApprovalDeploy { .. } => run_approval_deploy(coord, claim).await,
|
||||
NodeKind::SetWanted { up, .. } => run_set_wanted(coord, claim, *up),
|
||||
// Pure grouping container — no work; completing it lets it reach
|
||||
// `Finishing` so its child template nodes start. The DAG's terminal
|
||||
// hook fires (inline, via `run_terminal_hook`) when the container itself
|
||||
|
|
@ -398,14 +398,17 @@ async fn run_reconcile(coord: &Arc<Coordinator>, claim: &Claim) -> Result<NodeOu
|
|||
let name = &claim.agent;
|
||||
let running = crate::lifecycle::is_running(name).await;
|
||||
let wanted = coord.power.get_or_seed(name, running)?;
|
||||
// One node targeting this agent, rooted on this reconcile node. The old
|
||||
// `append_node` inherited the emitter's agent implicitly; `append_subgraph`
|
||||
// carries it on the `NodeSpec`, so stamp `claim.agent` explicitly (same
|
||||
// effect, one in-DAG-growth channel instead of two).
|
||||
let sub = |kind| vec![vec![super::templates::node(name, kind, Vec::new())]];
|
||||
// One node targeting this agent, rooted on this reconcile node. `NodeKind`
|
||||
// carries the agent it targets, so stamp `claim.agent` into the fanned-out
|
||||
// Start/Stop kind (one in-DAG-growth channel).
|
||||
let sub = |kind| vec![vec![super::templates::node(kind, Vec::new())]];
|
||||
let append_subgraph = match reconcile_action(wanted, running) {
|
||||
ReconcileAction::Start => sub(NodeKind::Start),
|
||||
ReconcileAction::Stop => sub(NodeKind::Stop),
|
||||
ReconcileAction::Start => sub(NodeKind::Start {
|
||||
agent: name.clone(),
|
||||
}),
|
||||
ReconcileAction::Stop => sub(NodeKind::Stop {
|
||||
agent: name.clone(),
|
||||
}),
|
||||
ReconcileAction::Noop => {
|
||||
tracing::debug!(%name, wanted = wanted.as_str(), running, "reconcile: noop");
|
||||
Vec::new()
|
||||
|
|
|
|||
|
|
@ -4,22 +4,22 @@
|
|||
//! watcher, deferred-start follow-up, meta-update cascade) collapse into DAG
|
||||
//! *shapes* over the shared node primitives ([`model::NodeKind`]).
|
||||
//!
|
||||
//! [`hive_jobq`] owns the graph, the two-class resource pool, and the settle
|
||||
//! loop; this module maps hive-c0re's concepts onto it:
|
||||
//! - `NodeKind` + agent → the crate payload [`resource::JobPayload`];
|
||||
//! - the two resource classes → [`resource::Resource`]
|
||||
//! ([`resource::Resource::BuildSlot`] node-held, [`resource::Resource::Agent`]
|
||||
//! subtree-held), derived per node by [`resource::JobPayload::resource_deps`];
|
||||
//! - a host **DAG id** groups a set of crate nodes; a node declares only its
|
||||
//! `deps` (chain edges + resource deps), and the crate scheduler infers lease
|
||||
//! re-entrancy from the [`Dep::Node`] graph — a node needing an agent lease a
|
||||
//! node it depends on already holds re-enters it, with no parent annotation;
|
||||
//! - per-DAG terminal work is a *focused* graph node per concern — `ResolveApproval`
|
||||
//! (approval DAGs), `EmitRebuilt` (rebuild / perm-change), `RevertIntent`
|
||||
//! (cancelled power-ops) — weak-depending on the DAG's tail nodes (extended onto
|
||||
//! any runtime-appended subgraph's tail), so it runs once everything settles.
|
||||
//! Hook-less DAGs (meta-update / boot / reconcile) get none. No drained event
|
||||
//! stream, and no one node branching on DAG metadata.
|
||||
//! [`hive_jobq`] owns the graph, the two-class resource pool, and the roll-up
|
||||
//! settle loop; this module maps hive-c0re's concepts onto it:
|
||||
//! - [`model::NodeKind`] **is** the crate payload `N` directly — each variant
|
||||
//! carries the agent it targets ([`NodeKind::agent`]); the two resource
|
||||
//! classes are [`resource::Resource`] (`BuildSlot` node-held, `Agent` lease
|
||||
//! subtree-held), derived per node by [`NodeKind::resource_deps`];
|
||||
//! - a **DAG is a single container node** ([`NodeKind::Dag`], `parent = None`)
|
||||
//! carrying the group's metadata, with the template's nodes hung under it as
|
||||
//! its subtree (the **parent axis** groups; `deps` order). So the container's
|
||||
//! `NodeId` is the DAG id, its rolled-up state is the DAG state, and membership
|
||||
//! is a graph walk — there are no host grouping side-tables. The lease is owned
|
||||
//! by a subtree root and borrowed by its descendants (continuity);
|
||||
//! - per-DAG terminal work runs **inline** ([`exec::run_terminal_hook`]) when the
|
||||
//! container rolls up terminal — dispatched off its template
|
||||
//! ([`terminal_hook`]): approval-resolve, `Rebuilt`-emit, or power-intent
|
||||
//! revert. No terminal-hook node, no drained event stream.
|
||||
//!
|
||||
//! The queue is runtime-only (no persistence): an empty graph on boot; desired
|
||||
//! state is re-derived by the reconcile sweep. A single scheduler task
|
||||
|
|
@ -49,7 +49,7 @@ use crate::coordinator::TransientKind;
|
|||
pub use model::{
|
||||
DagSpec, DagView, DepWhen, NodeKind, NodeSpec, PermPayload, Source, State, Template,
|
||||
};
|
||||
use resource::{JobPayload, Resource};
|
||||
use resource::Resource;
|
||||
|
||||
/// How many terminal DAGs (`Done` / `Failed` / `Cancelled`) to retain per
|
||||
/// template in the snapshot, matching the old per-kind history cap.
|
||||
|
|
@ -130,7 +130,7 @@ struct DagMeta {
|
|||
/// are graph queries ([`QueueInner::container`] / [`QueueInner::subtree`] /
|
||||
/// [`QueueInner::dag_meta`]). One shared crate [`Graph`] holds every DAG.
|
||||
struct QueueInner {
|
||||
sched: Scheduler<JobPayload, Resource>,
|
||||
sched: Scheduler<NodeKind, Resource>,
|
||||
/// Per-node runtime metadata (build-log id, step, timestamps, error) —
|
||||
/// mutable after insert, so it can't ride the immutable node payload.
|
||||
node_rt: HashMap<NodeId, NodeRuntime>,
|
||||
|
|
@ -232,10 +232,7 @@ fn insert_group(
|
|||
) -> anyhow::Result<Vec<NodeId>> {
|
||||
let mut ids: Vec<NodeId> = Vec::with_capacity(nodes.len());
|
||||
for ns in nodes {
|
||||
let payload = JobPayload {
|
||||
kind: ns.kind.clone(),
|
||||
agent: ns.agent.clone(),
|
||||
};
|
||||
let payload = ns.kind.clone();
|
||||
let mut deps = payload.resource_deps();
|
||||
for d in &ns.deps {
|
||||
deps.push(Dep::Node {
|
||||
|
|
@ -293,18 +290,15 @@ impl JobQueue {
|
|||
let container = inner
|
||||
.sched
|
||||
.append(
|
||||
JobPayload {
|
||||
kind: NodeKind::Dag {
|
||||
template: spec.template,
|
||||
source: spec.source,
|
||||
reason: spec.reason,
|
||||
transient: spec.transient,
|
||||
approval_id: spec.approval_id,
|
||||
inputs: spec.inputs,
|
||||
perm_payload: spec.perm_payload,
|
||||
created_at: now_unix(),
|
||||
},
|
||||
agent: String::new(),
|
||||
NodeKind::Dag {
|
||||
template: spec.template,
|
||||
source: spec.source,
|
||||
reason: spec.reason,
|
||||
transient: spec.transient,
|
||||
approval_id: spec.approval_id,
|
||||
inputs: spec.inputs,
|
||||
perm_payload: spec.perm_payload,
|
||||
created_at: now_unix(),
|
||||
},
|
||||
Vec::new(),
|
||||
None,
|
||||
|
|
@ -377,8 +371,8 @@ impl JobQueue {
|
|||
let Some(node) = inner.sched.graph().node(id) else {
|
||||
continue;
|
||||
};
|
||||
let kind = node.payload.kind.clone();
|
||||
let agent = node.payload.agent.clone();
|
||||
let kind = node.payload.clone();
|
||||
let agent = node.payload.agent().to_owned();
|
||||
let Some(container) = inner.dag_of(id) else {
|
||||
continue;
|
||||
};
|
||||
|
|
@ -605,7 +599,7 @@ impl QueueInner {
|
|||
self.sched.graph().nodes().find_map(|n| {
|
||||
(n.parent.is_none()
|
||||
&& n.id.get() == dag_id
|
||||
&& matches!(n.payload.kind, NodeKind::Dag { .. }))
|
||||
&& matches!(n.payload, NodeKind::Dag { .. }))
|
||||
.then_some(n.id)
|
||||
})
|
||||
}
|
||||
|
|
@ -646,7 +640,7 @@ impl QueueInner {
|
|||
inputs,
|
||||
perm_payload,
|
||||
created_at,
|
||||
} = &self.sched.graph().node(container)?.payload.kind
|
||||
} = &self.sched.graph().node(container)?.payload
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
|
@ -714,9 +708,9 @@ impl QueueInner {
|
|||
let mut seen: Vec<String> = Vec::new();
|
||||
for id in self.subtree(container) {
|
||||
if let Some(n) = self.sched.graph().node(id) {
|
||||
let agent = &n.payload.agent;
|
||||
let agent = n.payload.agent();
|
||||
if !agent.is_empty() && !seen.iter().any(|s| s == agent) {
|
||||
seen.push(agent.clone());
|
||||
seen.push(agent.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -780,8 +774,8 @@ impl QueueInner {
|
|||
}
|
||||
nodes.push(NodeView {
|
||||
id: id.get(),
|
||||
agent: node.payload.agent.clone(),
|
||||
kind: node.payload.kind.as_str().to_owned(),
|
||||
agent: node.payload.agent().to_owned(),
|
||||
kind: node.payload.as_str().to_owned(),
|
||||
deps,
|
||||
state: to_wire_state(node.state),
|
||||
step: rt.and_then(|r| r.step.clone()),
|
||||
|
|
@ -827,7 +821,7 @@ impl QueueInner {
|
|||
self.sched
|
||||
.graph()
|
||||
.nodes()
|
||||
.filter(|n| n.parent.is_none() && matches!(n.payload.kind, NodeKind::Dag { .. }))
|
||||
.filter(|n| n.parent.is_none() && matches!(n.payload, NodeKind::Dag { .. }))
|
||||
.map(|n| n.id)
|
||||
.collect()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,12 +56,12 @@ pub enum NodeKind {
|
|||
/// skipped when the container is already down — it only exists to
|
||||
/// shrink the swap's downtime, which a stopped agent doesn't need
|
||||
/// (the sync + dir prep still run; `Swap` builds inline).
|
||||
Prebuild { relock: bool },
|
||||
Prebuild { agent: String, relock: bool },
|
||||
/// `nixos-container update` profile-swap (requires the container
|
||||
/// stopped). Re-applies nspawn flags + resource limits first —
|
||||
/// rebuild is the reconcile verb. The post-rebuild bookkeeping tail
|
||||
/// lives in the sibling `PostSwap` node.
|
||||
Swap,
|
||||
Swap { agent: String },
|
||||
/// The post-`Swap` bookkeeping tail as a first-class node: rev marker,
|
||||
/// forge + matrix sync, manager kick, container rescan, meta-inputs
|
||||
/// snapshot. Split out of `Swap` for dashboard visibility + retry
|
||||
|
|
@ -71,16 +71,16 @@ pub enum NodeKind {
|
|||
/// recovery still runs. Store/forge/matrix work only — no nix build, so
|
||||
/// build-slot-exempt; the agent lease taken at `Swap` is held across the
|
||||
/// whole chain until `Reconcile` settles, so it's not re-declared here.
|
||||
PostSwap,
|
||||
PostSwap { agent: String },
|
||||
/// First-spawn pre-create provisioning: proposed/applied repos,
|
||||
/// state subvolume, and meta registration (`sync_agents`). Runs
|
||||
/// ahead of `Create` so the `nixos-container create --flake
|
||||
/// meta#<name>` ref resolves. Store/meta-only — no container yet —
|
||||
/// so it's lease- and build-slot-exempt like `Prebuild`.
|
||||
Provision,
|
||||
Provision { agent: String },
|
||||
/// First-spawn `nixos-container create` proper. Assumes the
|
||||
/// upstream `Provision` node already registered the agent in meta.
|
||||
Create,
|
||||
Create { agent: String },
|
||||
/// Meta flake lock bump. `sweep = false`: `meta::lock_update`
|
||||
/// (commit fused, under `META_LOCK`) with the DAG's `inputs`;
|
||||
/// `sweep = true`: `meta::lock_update_hyperhive`, *non-fatal* (a
|
||||
|
|
@ -97,36 +97,36 @@ pub enum NodeKind {
|
|||
/// `Offline` & up, else noop). The mechanical work is not done in
|
||||
/// this node — it fans a child [`NodeKind::Start`] / [`NodeKind::Stop`]
|
||||
/// DAG out at runtime so the sub-step is a first-class DAG node.
|
||||
Reconcile,
|
||||
Reconcile { agent: String },
|
||||
/// Mechanical container start: the start preamble (runtime dir +
|
||||
/// drop-ins), `start_with_fallback`, MCP listener registration, and
|
||||
/// the manager kick. Fanned out by a [`NodeKind::Reconcile`] that
|
||||
/// observed `wanted = Up` and the container down.
|
||||
Start,
|
||||
Start { agent: String },
|
||||
/// Mechanical container stop: `nixos-container` kill, MCP listener
|
||||
/// unregister, and the `Killed` manager notify. Fanned out by a
|
||||
/// [`NodeKind::Reconcile`] that observed `wanted = Offline` and up.
|
||||
Stop,
|
||||
Stop { agent: String },
|
||||
/// Mechanical `nixos-container stop` for the profile swap. Never
|
||||
/// touches `wanted`. Noop if already stopped.
|
||||
StopForUpdate,
|
||||
StopForUpdate { agent: String },
|
||||
/// Set the graceful-stop fence + kick the harness so it runs one
|
||||
/// stop-checkpoint turn.
|
||||
Signal,
|
||||
Signal { agent: String },
|
||||
/// Await the harness clearing the fence, bounded by
|
||||
/// `GRACEFUL_STOP_TIMEOUT`. Resolves ok either way — the
|
||||
/// downstream `Reconcile` performs the actual stop.
|
||||
Drain,
|
||||
Drain { agent: String },
|
||||
/// `set_nspawn_flags` + `set_resource_limits` + daemon-reload.
|
||||
WriteDropin,
|
||||
WriteDropin { agent: String },
|
||||
/// Commit `tool-groups.json` / `capabilities.json` per the DAG's
|
||||
/// `perm_payload` (commit fused under `META_LOCK`).
|
||||
WritePermFile,
|
||||
WritePermFile { agent: String },
|
||||
/// Opaque approval deploy pipeline (`MergeConfigPr`): the two-phase
|
||||
/// prepare/finalize/abort meta deploy stays inside `actions.rs` in v1 —
|
||||
/// deliberately not
|
||||
/// modeled as scheduler nodes (see the design doc §9).
|
||||
ApprovalDeploy,
|
||||
ApprovalDeploy { agent: String },
|
||||
/// Write the agent's durable power intent (`wanted = Up` when `up`, else
|
||||
/// `Offline`) as a first-class DAG node, at the head of a power-op
|
||||
/// template so the downstream `Reconcile` reads it. Replaces the old
|
||||
|
|
@ -140,7 +140,7 @@ pub enum NodeKind {
|
|||
/// the DAG. (In `stale_start` the lease is thus held across the head
|
||||
/// `Prebuild`, but that's a no-op there — the agent is down, so prebuild
|
||||
/// is skipped.)
|
||||
SetWanted { up: bool },
|
||||
SetWanted { agent: String, up: bool },
|
||||
/// The **DAG container** node: one per submitted DAG, carrying the group's
|
||||
/// domain metadata. Every template node hangs *under* it (its subtree), so
|
||||
/// the container's `NodeId` **is** the DAG id, its rolled-up state **is** the
|
||||
|
|
@ -166,35 +166,60 @@ impl NodeKind {
|
|||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
NodeKind::Prebuild { .. } => "prebuild",
|
||||
NodeKind::Swap => "swap",
|
||||
NodeKind::PostSwap => "post_swap",
|
||||
NodeKind::Provision => "provision",
|
||||
NodeKind::Create => "create",
|
||||
NodeKind::Swap { .. } => "swap",
|
||||
NodeKind::PostSwap { .. } => "post_swap",
|
||||
NodeKind::Provision { .. } => "provision",
|
||||
NodeKind::Create { .. } => "create",
|
||||
NodeKind::MetaLock { .. } => "meta_lock",
|
||||
NodeKind::Reconcile => "reconcile",
|
||||
NodeKind::Start => "start",
|
||||
NodeKind::Stop => "stop",
|
||||
NodeKind::StopForUpdate => "stop_for_update",
|
||||
NodeKind::Signal => "signal",
|
||||
NodeKind::Drain => "drain",
|
||||
NodeKind::WriteDropin => "write_dropin",
|
||||
NodeKind::WritePermFile => "write_perm_file",
|
||||
NodeKind::ApprovalDeploy => "approval_deploy",
|
||||
NodeKind::Reconcile { .. } => "reconcile",
|
||||
NodeKind::Start { .. } => "start",
|
||||
NodeKind::Stop { .. } => "stop",
|
||||
NodeKind::StopForUpdate { .. } => "stop_for_update",
|
||||
NodeKind::Signal { .. } => "signal",
|
||||
NodeKind::Drain { .. } => "drain",
|
||||
NodeKind::WriteDropin { .. } => "write_dropin",
|
||||
NodeKind::WritePermFile { .. } => "write_perm_file",
|
||||
NodeKind::ApprovalDeploy { .. } => "approval_deploy",
|
||||
NodeKind::SetWanted { .. } => "set_wanted",
|
||||
NodeKind::Dag { .. } => "dag",
|
||||
}
|
||||
}
|
||||
|
||||
/// The agent this node targets, or `""` for agentless kinds
|
||||
/// ([`NodeKind::MetaLock`] on the `hyperhive` pseudo-agent, and the
|
||||
/// [`NodeKind::Dag`] container).
|
||||
#[must_use]
|
||||
pub fn agent(&self) -> &str {
|
||||
match self {
|
||||
NodeKind::Prebuild { agent, .. }
|
||||
| NodeKind::Swap { agent }
|
||||
| NodeKind::PostSwap { agent }
|
||||
| NodeKind::Provision { agent }
|
||||
| NodeKind::Create { agent }
|
||||
| NodeKind::Reconcile { agent }
|
||||
| NodeKind::Start { agent }
|
||||
| NodeKind::Stop { agent }
|
||||
| NodeKind::StopForUpdate { agent }
|
||||
| NodeKind::Signal { agent }
|
||||
| NodeKind::Drain { agent }
|
||||
| NodeKind::WriteDropin { agent }
|
||||
| NodeKind::WritePermFile { agent }
|
||||
| NodeKind::ApprovalDeploy { agent }
|
||||
| NodeKind::SetWanted { agent, .. } => agent,
|
||||
NodeKind::MetaLock { .. } | NodeKind::Dag { .. } => "",
|
||||
}
|
||||
}
|
||||
|
||||
/// Nix-heavy kinds hold one of the `buildSlots` semaphore permits
|
||||
/// for the node's duration.
|
||||
pub fn needs_build_slot(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
NodeKind::Prebuild { .. }
|
||||
| NodeKind::Swap
|
||||
| NodeKind::Create
|
||||
| NodeKind::Swap { .. }
|
||||
| NodeKind::Create { .. }
|
||||
| NodeKind::MetaLock { .. }
|
||||
| NodeKind::ApprovalDeploy
|
||||
| NodeKind::ApprovalDeploy { .. }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -209,14 +234,14 @@ impl NodeKind {
|
|||
pub fn needs_lease(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
NodeKind::Swap
|
||||
| NodeKind::Create
|
||||
| NodeKind::Reconcile
|
||||
| NodeKind::StopForUpdate
|
||||
| NodeKind::Signal
|
||||
| NodeKind::Drain
|
||||
| NodeKind::WriteDropin
|
||||
| NodeKind::ApprovalDeploy
|
||||
NodeKind::Swap { .. }
|
||||
| NodeKind::Create { .. }
|
||||
| NodeKind::Reconcile { .. }
|
||||
| NodeKind::StopForUpdate { .. }
|
||||
| NodeKind::Signal { .. }
|
||||
| NodeKind::Drain { .. }
|
||||
| NodeKind::WriteDropin { .. }
|
||||
| NodeKind::ApprovalDeploy { .. }
|
||||
| NodeKind::SetWanted { .. }
|
||||
)
|
||||
}
|
||||
|
|
@ -225,9 +250,9 @@ impl NodeKind {
|
|||
/// Submit-time spec for one node.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NodeSpec {
|
||||
/// The agent this node's work targets. Built by the `templates.rs` `node`
|
||||
/// helper, which stamps the template's agent onto every node.
|
||||
pub agent: String,
|
||||
/// The node's payload — [`NodeKind`] is the queue's payload type directly,
|
||||
/// and each variant carries the agent it targets (a DAG can span agents;
|
||||
/// the queue derives per-agent leasing from [`NodeKind::agent`]).
|
||||
pub kind: NodeKind,
|
||||
pub deps: Vec<Dep>,
|
||||
/// The **structural parent** axis — the spec-local index of this node's
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
//! The concrete resource + payload types the rebuild queue schedules over —
|
||||
//! the bridge from hive-c0re's [`NodeKind`] onto the domain-agnostic
|
||||
//! `hive-jobq` crate. `hive-jobq` is generic over a resource type
|
||||
//! `R: Clone + Eq + Hash` and a node payload `N`; here `R` is [`Resource`] and
|
||||
//! `N` is [`JobPayload`].
|
||||
//! The concrete resource type the rebuild queue schedules over — the bridge
|
||||
//! from hive-c0re's [`NodeKind`] onto the domain-agnostic `hive-jobq` crate.
|
||||
//! `hive-jobq` is generic over a resource type `R: Clone + Eq + Hash` and a node
|
||||
//! payload `N`; here `R` is [`Resource`] and `N` is [`NodeKind`] directly (each
|
||||
//! variant carries the agent it targets).
|
||||
|
||||
use hive_jobq::Dep;
|
||||
|
||||
|
|
@ -24,16 +24,7 @@ pub enum Resource {
|
|||
Agent(String),
|
||||
}
|
||||
|
||||
/// A schedulable node's payload — the crate's generic `N`. Carries the
|
||||
/// primitive operation and the agent it targets. The agent is per-node (a DAG
|
||||
/// spans agents), and the lease [`Resource::Agent`] is keyed on it.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct JobPayload {
|
||||
pub kind: NodeKind,
|
||||
pub agent: String,
|
||||
}
|
||||
|
||||
impl JobPayload {
|
||||
impl NodeKind {
|
||||
/// The [`Dep::Resource`] edges this node must acquire to run, derived from
|
||||
/// its kind + agent: a build slot for nix-heavy kinds
|
||||
/// ([`NodeKind::needs_build_slot`]) and the agent lease for
|
||||
|
|
@ -43,15 +34,15 @@ impl JobPayload {
|
|||
/// `Agent` lock through the crate's recursive re-entrancy.
|
||||
pub fn resource_deps(&self) -> Vec<Dep<Resource>> {
|
||||
let mut deps = Vec::new();
|
||||
if self.kind.needs_build_slot() {
|
||||
if self.needs_build_slot() {
|
||||
deps.push(Dep::Resource {
|
||||
name: Resource::BuildSlot,
|
||||
count: 1,
|
||||
});
|
||||
}
|
||||
if self.kind.needs_lease() {
|
||||
if self.needs_lease() {
|
||||
deps.push(Dep::Resource {
|
||||
name: Resource::Agent(self.agent.clone()),
|
||||
name: Resource::Agent(self.agent().to_owned()),
|
||||
count: 1,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,13 +63,20 @@ fn stop_chain(agent: &str, graceful: bool, running: bool) -> Vec<NodeSpec> {
|
|||
// `SetWanted` is the group root and owns the agent lease; the mechanical
|
||||
// steps are its children (borrow the lease, run once it reaches `Finishing`,
|
||||
// dep-ordered among themselves).
|
||||
let mut n = vec![node(agent, NodeKind::SetWanted { up: false }, Vec::new())];
|
||||
let a = || agent.to_owned();
|
||||
let mut n = vec![node(
|
||||
NodeKind::SetWanted {
|
||||
agent: a(),
|
||||
up: false,
|
||||
},
|
||||
Vec::new(),
|
||||
)];
|
||||
if graceful && running {
|
||||
n.push(child(0, agent, NodeKind::Signal, Vec::new()));
|
||||
n.push(child(0, agent, NodeKind::Drain, after_ok(1)));
|
||||
n.push(child(0, agent, NodeKind::Reconcile, after_ok(2)));
|
||||
n.push(child(0, NodeKind::Signal { agent: a() }, Vec::new()));
|
||||
n.push(child(0, NodeKind::Drain { agent: a() }, after_ok(1)));
|
||||
n.push(child(0, NodeKind::Reconcile { agent: a() }, after_ok(2)));
|
||||
} else {
|
||||
n.push(child(0, agent, NodeKind::Reconcile, Vec::new()));
|
||||
n.push(child(0, NodeKind::Reconcile { agent: a() }, Vec::new()));
|
||||
}
|
||||
n
|
||||
}
|
||||
|
|
@ -79,14 +86,26 @@ fn stop_chain(agent: &str, graceful: bool, running: bool) -> Vec<NodeSpec> {
|
|||
/// current derivations), otherwise a plain `Reconcile` (which starts a down
|
||||
/// agent and noops an already-running one).
|
||||
fn start_chain(agent: &str, running: bool, stale: bool) -> Vec<NodeSpec> {
|
||||
let mut n = vec![node(agent, NodeKind::SetWanted { up: true }, Vec::new())];
|
||||
let mut n = vec![node(
|
||||
NodeKind::SetWanted {
|
||||
agent: agent.to_owned(),
|
||||
up: true,
|
||||
},
|
||||
Vec::new(),
|
||||
)];
|
||||
if !running && stale {
|
||||
// Rebuild subtree after the SetWanted head (base = 1, so the rebuild's
|
||||
// `Prebuild` root deps `after_ok(0)` = the head). `Prebuild` +
|
||||
// `Reconcile` are their own group roots (top-level, per `rebuild_nodes`).
|
||||
n.extend(rebuild_nodes(agent, true, 1));
|
||||
} else {
|
||||
n.push(child(0, agent, NodeKind::Reconcile, Vec::new()));
|
||||
n.push(child(
|
||||
0,
|
||||
NodeKind::Reconcile {
|
||||
agent: agent.to_owned(),
|
||||
},
|
||||
Vec::new(),
|
||||
));
|
||||
}
|
||||
n
|
||||
}
|
||||
|
|
@ -101,22 +120,27 @@ fn start_chain(agent: &str, running: bool, stale: bool) -> Vec<NodeSpec> {
|
|||
/// converges to intent — a stopped (`wanted = Off`) agent stays stopped,
|
||||
/// a crashed (`wanted = Up`) agent comes back up.
|
||||
fn restart_chain(agent: &str, graceful: bool, running: bool) -> Vec<NodeSpec> {
|
||||
let a = || agent.to_owned();
|
||||
if !running {
|
||||
// Nothing to bounce — a lone Reconcile converges to intent.
|
||||
return vec![node(agent, NodeKind::Reconcile, Vec::new())];
|
||||
return vec![node(NodeKind::Reconcile { agent: a() }, Vec::new())];
|
||||
}
|
||||
// Running: mechanical stop then Reconcile. The first stop node is the group
|
||||
// ROOT (no SetWanted head) and owns the agent lease; the rest are its
|
||||
// children (borrow the lease, dep-ordered), so the bounce holds one
|
||||
// continuous lease and `Reconcile` cancel-cascades if a stop step fails.
|
||||
let mut n = vec![if graceful {
|
||||
node(agent, NodeKind::Signal, Vec::new())
|
||||
node(NodeKind::Signal { agent: a() }, Vec::new())
|
||||
} else {
|
||||
node(agent, NodeKind::StopForUpdate, Vec::new())
|
||||
node(NodeKind::StopForUpdate { agent: a() }, Vec::new())
|
||||
}];
|
||||
if graceful {
|
||||
n.push(child(0, agent, NodeKind::Drain, Vec::new()));
|
||||
n.push(child(0, agent, NodeKind::StopForUpdate, after_ok(1)));
|
||||
n.push(child(0, NodeKind::Drain { agent: a() }, Vec::new()));
|
||||
n.push(child(
|
||||
0,
|
||||
NodeKind::StopForUpdate { agent: a() },
|
||||
after_ok(1),
|
||||
));
|
||||
}
|
||||
// `Reconcile` gates on the last mechanical step. When the only step is the
|
||||
// root itself (non-graceful, `StopForUpdate` == index 0), the parent gate
|
||||
|
|
@ -128,7 +152,7 @@ fn restart_chain(agent: &str, graceful: bool, running: bool) -> Vec<NodeSpec> {
|
|||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
n.push(child(0, agent, NodeKind::Reconcile, deps));
|
||||
n.push(child(0, NodeKind::Reconcile { agent: a() }, deps));
|
||||
n
|
||||
}
|
||||
|
||||
|
|
@ -151,7 +175,6 @@ fn concat_subgraphs(chains: Vec<Vec<NodeSpec>>) -> Vec<NodeSpec> {
|
|||
})
|
||||
.collect();
|
||||
out.push(NodeSpec {
|
||||
agent: spec.agent,
|
||||
kind: spec.kind,
|
||||
deps,
|
||||
// Rebase the structural parent by the same offset (a subgraph
|
||||
|
|
|
|||
|
|
@ -42,14 +42,14 @@ pub(crate) fn after_ok(on: u64) -> Vec<Dep> {
|
|||
}]
|
||||
}
|
||||
|
||||
/// Build one **top-level (group-root)** node targeting `agent` — `parent =
|
||||
/// None`. The single place a node's agent is stamped. Shared with `submit.rs`'s
|
||||
/// dynamic power-op builders. A root owns whatever resource it declares for its
|
||||
/// whole subtree; its descendants borrow it (agent-lease / build-slot
|
||||
/// continuity). Ordering vs other nodes is `deps`; grouping is `parent`.
|
||||
pub(crate) fn node(agent: &str, kind: NodeKind, deps: Vec<Dep>) -> NodeSpec {
|
||||
/// Build one **top-level (group-root)** node — `parent = None`. `kind` carries
|
||||
/// the agent it targets ([`NodeKind`] is the payload directly). Shared with
|
||||
/// `submit.rs`'s dynamic power-op builders. A root owns whatever resource it
|
||||
/// declares for its whole subtree; its descendants borrow it (agent-lease /
|
||||
/// build-slot continuity). Ordering vs other nodes is `deps`; grouping is
|
||||
/// `parent`.
|
||||
pub(crate) fn node(kind: NodeKind, deps: Vec<Dep>) -> NodeSpec {
|
||||
NodeSpec {
|
||||
agent: agent.to_owned(),
|
||||
kind,
|
||||
deps,
|
||||
parent: None,
|
||||
|
|
@ -60,9 +60,8 @@ pub(crate) fn node(agent: &str, kind: NodeKind, deps: Vec<Dep>) -> NodeSpec {
|
|||
/// child runs once its parent reaches `Finishing` (the parent gate), so it must
|
||||
/// NOT `deps` on `parent` (dep-scope validation rejects a dep on one's own
|
||||
/// parent). `deps` here order the child against its *siblings* only.
|
||||
pub(crate) fn child(parent: u64, agent: &str, kind: NodeKind, deps: Vec<Dep>) -> NodeSpec {
|
||||
pub(crate) fn child(parent: u64, kind: NodeKind, deps: Vec<Dep>) -> NodeSpec {
|
||||
NodeSpec {
|
||||
agent: agent.to_owned(),
|
||||
kind,
|
||||
deps,
|
||||
parent: Some(parent),
|
||||
|
|
@ -87,22 +86,25 @@ pub(crate) fn child(parent: u64, agent: &str, kind: NodeKind, deps: Vec<Dep>) ->
|
|||
/// (recovery-start invariant). It takes a fresh lease; the tiny gap is
|
||||
/// harmless — `Reconcile` converges to the persisted `wanted` idempotently.
|
||||
pub(crate) fn rebuild_nodes(agent: &str, relock: bool, base: u64) -> Vec<NodeSpec> {
|
||||
let a = || agent.to_owned();
|
||||
vec![
|
||||
node(
|
||||
agent,
|
||||
NodeKind::Prebuild { relock },
|
||||
NodeKind::Prebuild { agent: a(), relock },
|
||||
if base == 0 {
|
||||
Vec::new()
|
||||
} else {
|
||||
after_ok(base - 1)
|
||||
},
|
||||
),
|
||||
child(base, agent, NodeKind::StopForUpdate, Vec::new()),
|
||||
child(base + 1, agent, NodeKind::Swap, Vec::new()),
|
||||
child(base + 1, agent, NodeKind::PostSwap, after_ok(base + 2)),
|
||||
child(base, NodeKind::StopForUpdate { agent: a() }, Vec::new()),
|
||||
child(base + 1, NodeKind::Swap { agent: a() }, Vec::new()),
|
||||
child(
|
||||
base + 1,
|
||||
NodeKind::PostSwap { agent: a() },
|
||||
after_ok(base + 2),
|
||||
),
|
||||
node(
|
||||
agent,
|
||||
NodeKind::Reconcile,
|
||||
NodeKind::Reconcile { agent: a() },
|
||||
vec![Dep {
|
||||
on: base,
|
||||
when: DepWhen::AfterAny,
|
||||
|
|
@ -141,7 +143,12 @@ pub fn approval_deploy(agent: &str, approval_id: i64, reason: String) -> DagSpec
|
|||
inputs: Vec::new(),
|
||||
perm_payload: None,
|
||||
transient: Some(TransientKind::Rebuilding),
|
||||
nodes: vec![node(agent, NodeKind::ApprovalDeploy, Vec::new())],
|
||||
nodes: vec![node(
|
||||
NodeKind::ApprovalDeploy {
|
||||
agent: agent.to_owned(),
|
||||
},
|
||||
Vec::new(),
|
||||
)],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -166,7 +173,12 @@ pub fn reconcile_only(
|
|||
inputs: Vec::new(),
|
||||
perm_payload: None,
|
||||
transient,
|
||||
nodes: vec![node(agent, NodeKind::Reconcile, Vec::new())],
|
||||
nodes: vec![node(
|
||||
NodeKind::Reconcile {
|
||||
agent: agent.to_owned(),
|
||||
},
|
||||
Vec::new(),
|
||||
)],
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -188,12 +200,15 @@ pub fn spawn(agent: &str, approval_id: i64, reason: String) -> DagSpec {
|
|||
inputs: Vec::new(),
|
||||
perm_payload: None,
|
||||
transient: Some(TransientKind::Spawning),
|
||||
nodes: vec![
|
||||
node(agent, NodeKind::Provision, Vec::new()),
|
||||
child(0, agent, NodeKind::Create, Vec::new()),
|
||||
child(1, agent, NodeKind::WriteDropin, Vec::new()),
|
||||
child(1, agent, NodeKind::Reconcile, after_ok(2)),
|
||||
],
|
||||
nodes: {
|
||||
let a = || agent.to_owned();
|
||||
vec![
|
||||
node(NodeKind::Provision { agent: a() }, Vec::new()),
|
||||
child(0, NodeKind::Create { agent: a() }, Vec::new()),
|
||||
child(1, NodeKind::WriteDropin { agent: a() }, Vec::new()),
|
||||
child(1, NodeKind::Reconcile { agent: a() }, after_ok(2)),
|
||||
]
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -201,7 +216,12 @@ pub fn spawn(agent: &str, approval_id: i64, reason: String) -> DagSpec {
|
|||
/// the updated `HIVE_TOOL_GROUPS` / `HIVE_CAPABILITIES` env var takes
|
||||
/// effect in the container.
|
||||
pub fn perm_change(agent: &str, source: Source, reason: String, payload: PermPayload) -> DagSpec {
|
||||
let mut nodes = vec![node(agent, NodeKind::WritePermFile, Vec::new())];
|
||||
let mut nodes = vec![node(
|
||||
NodeKind::WritePermFile {
|
||||
agent: agent.to_owned(),
|
||||
},
|
||||
Vec::new(),
|
||||
)];
|
||||
nodes.extend(rebuild_nodes(agent, true, 1));
|
||||
DagSpec {
|
||||
template: Template::PermChange,
|
||||
|
|
@ -240,7 +260,6 @@ pub fn meta_update(
|
|||
perm_payload: None,
|
||||
transient: Some(TransientKind::Rebuilding),
|
||||
nodes: vec![node(
|
||||
"hyperhive",
|
||||
NodeKind::MetaLock {
|
||||
sweep: false,
|
||||
fanout: None,
|
||||
|
|
|
|||
|
|
@ -109,8 +109,9 @@ fn cyclic_dag_is_rejected_at_submit() {
|
|||
// 0 → 1 → 0 cycle.
|
||||
spec.nodes = vec![
|
||||
NodeSpec {
|
||||
agent: "agent-a".to_owned(),
|
||||
kind: NodeKind::StopForUpdate,
|
||||
kind: NodeKind::StopForUpdate {
|
||||
agent: "agent-a".to_owned(),
|
||||
},
|
||||
deps: vec![Dep {
|
||||
on: 1,
|
||||
when: DepWhen::AfterOk,
|
||||
|
|
@ -118,8 +119,9 @@ fn cyclic_dag_is_rejected_at_submit() {
|
|||
parent: None,
|
||||
},
|
||||
NodeSpec {
|
||||
agent: "agent-a".to_owned(),
|
||||
kind: NodeKind::Reconcile,
|
||||
kind: NodeKind::Reconcile {
|
||||
agent: "agent-a".to_owned(),
|
||||
},
|
||||
deps: vec![Dep {
|
||||
on: 0,
|
||||
when: DepWhen::AfterOk,
|
||||
|
|
@ -136,8 +138,9 @@ fn unknown_dep_is_rejected_at_submit() {
|
|||
let q = JobQueue::new(1);
|
||||
let mut spec = rebuild("agent-a", "bad dep");
|
||||
spec.nodes = vec![NodeSpec {
|
||||
agent: "agent-a".to_owned(),
|
||||
kind: NodeKind::Reconcile,
|
||||
kind: NodeKind::Reconcile {
|
||||
agent: "agent-a".to_owned(),
|
||||
},
|
||||
deps: vec![Dep {
|
||||
on: 9,
|
||||
when: DepWhen::AfterOk,
|
||||
|
|
@ -154,8 +157,9 @@ fn invalid_parent_is_rejected_at_submit() {
|
|||
// A forward/out-of-bounds parent index must be refused at validate, not
|
||||
// panic in `insert_group`.
|
||||
spec.nodes = vec![NodeSpec {
|
||||
agent: "agent-a".to_owned(),
|
||||
kind: NodeKind::Reconcile,
|
||||
kind: NodeKind::Reconcile {
|
||||
agent: "agent-a".to_owned(),
|
||||
},
|
||||
deps: Vec::new(),
|
||||
parent: Some(3),
|
||||
}];
|
||||
|
|
@ -560,7 +564,6 @@ fn append_subgraph_roots_on_emitter_and_rebases_local_deps() {
|
|||
perm_payload: None,
|
||||
transient: None,
|
||||
nodes: vec![NodeSpec {
|
||||
agent: "hyperhive".to_owned(),
|
||||
kind: NodeKind::MetaLock {
|
||||
sweep: true,
|
||||
fanout: None,
|
||||
|
|
|
|||
|
|
@ -325,7 +325,6 @@ fn submit_boot_tree(
|
|||
// MetaLock into `run_meta_lock`, which appends the rebuild subgraphs.
|
||||
if any_stale {
|
||||
nodes.push(NodeSpec {
|
||||
agent: "hyperhive".to_owned(),
|
||||
kind: NodeKind::MetaLock {
|
||||
sweep: true,
|
||||
fanout: Some(fanout),
|
||||
|
|
@ -337,8 +336,7 @@ fn submit_boot_tree(
|
|||
// One boot Reconcile per drifted agent — independent roots.
|
||||
for name in drifted {
|
||||
nodes.push(NodeSpec {
|
||||
agent: name,
|
||||
kind: NodeKind::Reconcile,
|
||||
kind: NodeKind::Reconcile { agent: name },
|
||||
deps: Vec::new(),
|
||||
parent: None,
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue