feat(#2591): port hive-c0re job_queue onto the hive-jobq crate

Replace the in-tree scheduler with the domain-agnostic hive-jobq crate
(merged in #2615): parent-axis grouping + borrow/subtree-reservation
resource model + roll-up completion (State::Finishing).

Host adaptation:
- NodeSpec gains an explicit `parent` axis; templates declare grouping +
  sibling ordering directly (deps order execution, parent groups a subtree
  whose resource the descendants borrow).
- Rebuild is a nested two-root subtree: Prebuild (root, owns the build slot
  for the whole subtree, lease-exempt) -> StopForUpdate (child, owns the
  agent lease) -> Swap/PostSwap (children, borrow both); Reconcile is a
  separate top-level root (AfterAny Prebuild) so it survives the cancel-
  cascade of any failed step (recovery-start invariant) and converges to
  the persisted `wanted` on a fresh lease. This is the multi-root
  correction to the single-root-chain sketch: node0=root broke lease-
  exemption (hoisting the lease onto Prebuild) and recovery-reconcile
  (root failure cancels all children).
- Spawn / perm-change / power-ops (stop/start/restart) group-rooted the
  same way; per-agent power-op subgraphs stay independent roots so a
  multi-agent DAG runs them concurrently, each on its own lease.
- insert_group honours the explicit parent axis (no lease hoisting); the
  DAG terminal node deps AfterAny on every group root and runs once the
  whole op rolls up. Drop the old Graph::add_dep terminal wiring.

36/36 job_queue tests, full hive-c0re suite green, clippy --all-targets.
This commit is contained in:
atlas 2026-07-20 21:46:08 +02:00 committed by mara
commit a5c321a1a0
14 changed files with 1111 additions and 893 deletions

View file

@ -0,0 +1,60 @@
//! 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`].
use hive_jobq::Dep;
use super::model::NodeKind;
/// The two resource classes the queue gates concurrency on, as the crate's
/// generic resource type `R`.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Resource {
/// One of the `buildSlots` permits, held by a nix-heavy node for its
/// duration. Capacity is `services.hyperhive.c0re.buildSlots` (default 1),
/// set on the [`hive_jobq::resources::ResourceTable`] at construction.
BuildSlot,
/// The per-agent lifecycle lease — globally exclusive per agent across all
/// DAGs (unconfigured, so the crate's default capacity 1 applies). Held by
/// a DAG's first container-affecting node for that agent and re-entered by
/// the rest of that agent's subtree via the crate's recursive lock, so two
/// DAGs never interleave container ops on one agent.
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 {
/// 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
/// container-affecting kinds ([`NodeKind::needs_lease`]). Lease-exempt
/// container ops (`Start` / `Stop`, fanned out by a lease-holding
/// `Reconcile`) hold no lease of their own — they re-enter the ancestor's
/// `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() {
deps.push(Dep::Resource {
name: Resource::BuildSlot,
count: 1,
});
}
if self.kind.needs_lease() {
deps.push(Dep::Resource {
name: Resource::Agent(self.agent.clone()),
count: 1,
});
}
deps
}
}