hyperhive/hive-c0re/src/job_queue/resource.rs
atlas 7966d5eb66 jobq-wire: move the generic graph projection into its own crate
The wire types were in hive-host-sock, which is the host *socket* crate — so
anything living there is core-shaped by construction, and the projection had
quietly grown two core dependencies to match: it selected roots by matching
NodeKind::Dag, and rendered payloads through free functions in hive-c0re that
nothing obliged a second host to write.

hive-jobq is the wrong home too. That crate is the scheduler — logic — and
folding presentation in means every consumer of it carries a JSON vocabulary
it may never serve.

So: a new hive-jobq-wire. A host implements WireNode for its payload N and
WireResource for its resource name R; GraphWire::wire_snapshot is
blanket-implemented for Graph<N, R> when both hold, and for nothing else. A
payload that has never said how it displays has no way onto the wire.

wire_snapshot takes the roots to serve rather than reading Graph::roots
itself. Nothing is ever removed from a Graph, so retention is a policy only
the host can hold; hive-c0re passes visible_roots(), which is the existing
MAX_HISTORY_DAGS bound selected structurally (a root is a node with no
parent) instead of by node kind.
2026-08-03 00:39:14 +02:00

75 lines
4.1 KiB
Rust

//! 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).
//!
//! **A node's resources are declared where the node is constructed** with
//! `.needs(…)`, not derived from its kind. Deriving them made the requirement a
//! property of the *kind*, which let a kind that happened to run under a
//! holding ancestor declare nothing at all.
//!
//! All of a node's resource edges are acquired **atomically**
//! (`try_acquire_all`) — a node never holds one resource while waiting on
//! another, so the multi-resource nodes (a `MetaLock` wants a build slot *and*
//! the meta window) cannot deadlock against each other.
/// 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.
///
/// Nodes that touch the *store or meta repo* rather than the running
/// container do not declare it — `MetaSync`, `Prebuild`, `Provision`,
/// `MetaLock`, `WritePermFile`, `Reparent`. That exemption is what lets a
/// `Prebuild` overlap another DAG's work on the same agent. `Provision`
/// precedes the container's existence entirely, so the lease is first taken
/// at the `Create` it feeds.
Agent(String),
/// The meta-repo mutation window — a global singleton (default capacity 1)
/// held by any node that mutates the meta repo, so two meta mutations never
/// interleave. That exclusion is load-bearing: a commit landing inside
/// another node's staged `prepare_deploy`→`finalize_deploy` window would
/// sweep the staged `flake.lock` into its own commit and neuter
/// `abort_deploy`. Replaces the former runtime `meta::exclusive()` mutex: a
/// `MutexGuard` cannot span scheduler nodes, but a resource held by a
/// subtree root *can* — which is what lets the two-phase deploy
/// (`prepare_deploy` stages `flake.lock` uncommitted across the whole
/// container build, `finalize_deploy`/`abort_deploy` resolve it) be
/// decomposed into sub-nodes instead of one opaque node. Descendants of a
/// holder re-enter it through the crate's recursive lock, exactly like
/// [`Resource::Agent`].
///
/// Deliberately **not** declared by `Prebuild`: the window must stay off the
/// multi-minute toplevel build, which only *reads* the store. Holding a
/// hive-global cap-1 across it would serialize every agent's rebuild behind
/// every other's — which is why the meta preamble is its own `MetaSync`
/// node, and a sibling of `Prebuild` rather than its parent (a resource is
/// held across the holder's whole subtree).
MetaWindow,
}
/// This resource's name on the generic graph wire.
///
/// `hive_jobq` is generic over the resource type, so a viewer that can render
/// any graph gets a string here rather than this enum. The `agent:` prefix
/// keeps the per-agent leases from colliding with a hypothetical global
/// resource that happens to share an agent's name.
impl hive_jobq_wire::WireResource for Resource {
fn name(&self) -> String {
match self {
Resource::BuildSlot => "build-slot".to_owned(),
Resource::Agent(agent) => format!("agent:{agent}"),
Resource::MetaWindow => "meta-window".to_owned(),
}
}
}