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.
75 lines
4.1 KiB
Rust
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(),
|
|
}
|
|
}
|
|
}
|