//! 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(), } } }