Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/hive-sh4re/src/jobs.rs
atlas edf9fd036e feat(#2453): remove DAG parent_id now that every op is one DAG
With the meta-update cascade (#2476) and startup sweep (#2450) folded
into single DAGs that grow per-agent subgraphs via append_subgraph,
nothing links parent/child DAGs anymore — parent_id is dead.

hive-c0re: drop parent_id from Dag/DagSpec (+ the DagView copy); delete
append_children and cancel_children (no callers); simplify trim_history
(no more terminal-parent-with-live-children guard — a one-big-DAG is
terminal only when its whole graph settles); drop the rebuild() parent_id
param; QueueDag returns just the polled DAG (no fan-out children to
gather). hive-sh4re: drop the DagView.parent_id wire field.

frontend: a multi-step op is one DAG now, so renderRebuildQueue drops the
childrenOf/orphans cross-DAG grouping and renders each entry flat; its
per-agent subgraphs render as nodes within the one row (split by deps).
Removed the dead rqe-child style + isChild plumbing.

Docs + the child-DAG queue tests updated/removed to match.
2026-07-15 19:50:04 +02:00

205 lines
7.6 KiB
Rust

//! Wire shapes of hive-c0re's job-DAG queue: what a queued job looks
//! like on the dashboard SSE channel (`rebuild_queue_changed`), the
//! `/api/state.rebuild_queue` snapshot, and the host admin socket's
//! `QueueDag` polling surface (`hivectl`'s wait/progress loop). The
//! queue *internals* — node kinds, dependency edges, scheduling state —
//! live in `hive-c0re::job_queue`; these are the serialized views it
//! produces. Semantics: `docs/coordinator.md::Job queue`.
use serde::{Deserialize, Serialize};
/// What a DAG *means* — the request-level shape. Wire strings match
/// the pre-DAG queue's `kind` values so dashboards key off the same
/// tags.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Template {
/// Rebuild one agent's container (prebuild → stop → profile-swap →
/// reconcile).
Rebuild,
/// Bump meta flake locks; child `Rebuild` DAGs fan out on
/// completion for every affected agent.
MetaUpdate,
/// First-deploy spawn (approval-driven).
Spawn,
/// Reserved for a future destroy integration.
Destroy,
/// Boot-time config sweep (hyperhive lock bump + stale-agent
/// rebuild fan-out).
StartupSweep,
/// Mechanical stop + converge to `wanted = Up` (a restart).
Restart,
/// Signal → drain → mechanical stop → converge to `wanted = Up` — a
/// graceful restart as one atomic DAG (drains the harness before the
/// stop, same as `GracefulStop`, but then reconciles back up instead
/// of staying down).
GracefulRestart,
/// Perm-file commit followed by the rebuild subgraph.
PermChange,
/// Quiesce the harness, drain, then stop (`wanted = Offline`).
GracefulStop,
/// Converge to `wanted = Up`.
Start,
/// Converge to `wanted = Offline`.
Stop,
/// Bare converge of observed power state to the persisted intent
/// (boot reconcile).
Reconcile,
/// Boot-time root anchor: a single no-op node that groups this boot's
/// `StartupSweep` + per-agent `Reconcile` child DAGs under one tree, so the
/// dashboard renders the whole boot as one entry instead of N+1 rows.
Boot,
}
impl Template {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Template::Rebuild => "rebuild",
Template::MetaUpdate => "meta_update",
Template::Spawn => "spawn",
Template::Destroy => "destroy",
Template::StartupSweep => "startup_sweep",
Template::Restart => "restart",
Template::GracefulRestart => "graceful_restart",
Template::PermChange => "perm_change",
Template::GracefulStop => "graceful_stop",
Template::Start => "start",
Template::Stop => "stop",
Template::Reconcile => "reconcile",
Template::Boot => "boot",
}
}
}
/// Where the submit request originated — drives the "why" chip on the
/// dashboard.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Source {
/// Operator action (dashboard button, CLI, manager tool).
Manual,
/// Cascade child of a `MetaUpdate` DAG's fan-out.
MetaUpdate,
/// Boot-time submission (sweep parent, boot reconciles).
AutoUpdate,
/// Cascade child of a `StartupSweep` DAG's fan-out.
StartupSweep,
/// Crash recovery path (future use).
CrashRecover,
/// Operator approved a pending `Approval` row; `approval_id` on
/// the DAG points back at the source row.
Approval,
}
impl Source {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Source::Manual => "manual",
Source::MetaUpdate => "meta_update",
Source::AutoUpdate => "auto_update",
Source::StartupSweep => "startup_sweep",
Source::CrashRecover => "crash_recover",
Source::Approval => "approval",
}
}
}
/// Lifecycle state of a node — and, rolled up, of a DAG.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum State {
Queued,
Running,
Done,
Failed,
Cancelled,
}
impl State {
#[must_use]
pub fn is_terminal(self) -> bool {
matches!(self, State::Done | State::Failed | State::Cancelled)
}
}
/// Kind-specific payload for `Template::PermChange` DAGs.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum PermPayload {
/// Set the tool groups for one agent (`tool-groups.json`).
ToolGroups { groups: Vec<String> },
/// Set the capabilities for one agent (`capabilities.json`).
Capabilities { caps: Vec<String> },
/// Set both perm-types in one entry — the batch
/// `POST /api/permissions` path. `None` leaves that file untouched;
/// present fields commit together and rebuild once.
Combined {
groups: Option<Vec<String>>,
caps: Option<Vec<String>>,
},
}
/// Node id, unique within its DAG.
pub type NodeId = u32;
/// One node of a queued DAG, as serialized. Step labels, build-log
/// links, errors, and timestamps are per-node; the DAG-level `state`
/// is a roll-up.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeView {
pub id: NodeId,
/// The agent whose container (or meta repo, for `hyperhive` meta-level
/// nodes) this node operates on. Agent is per-node — a single DAG can
/// span multiple agents (e.g. a hive-wide restart), so there is no
/// DAG-level agent field; consumers group by this.
pub agent: String,
/// Node primitive tag: `"prebuild"`, `"stop_for_update"`,
/// `"swap"`, `"create"`, `"meta_lock"`, `"reconcile"`, `"signal"`,
/// `"drain"`, `"write_dropin"`, `"write_perm_file"`,
/// `"approval_deploy"`.
pub kind: String,
/// Ids of the nodes this one waits for.
#[serde(default)]
pub deps: Vec<NodeId>,
pub state: State,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub step: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub build_log_id: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub started_at: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub finished_at: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
/// A queued/running/recent DAG. `kind` = template string, roll-up
/// `state`; everything per-node appears exactly once, inside `nodes`.
/// There is no DAG-level `agent` — a DAG can span agents, so agent lives
/// on each [`NodeView`]; consumers group nodes by `NodeView::agent`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DagView {
pub id: u64,
/// Template wire string — same values the old `kind` field used.
pub kind: Template,
/// Roll-up: `failed` if any node failed, else `running` /
/// `queued` / `cancelled` / `done`.
pub state: State,
pub source: Source,
pub reason: String,
pub enqueued_at: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub started_at: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub finished_at: Option<i64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub inputs: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub approval_id: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub perm_payload: Option<PermPayload>,
pub nodes: Vec<NodeView>,
}