jobq: a generic per-state roll-up, served beside the graph
A consumer that wants "how much is in flight" — a summary line, a badge, a health check — had to fetch the whole graph and tally it client-side, on every poll, in every consumer. `hive_jobq_wire::state_rollup` counts `roots` and their subtrees by state, straight off a `Graph<N, R>` with **no bound on either parameter**. A node's state is a scheduler concept, so counting by state needs to know nothing about what the payload or the resource are; bounding it like the projection does would make a host implement two display traits to be allowed to count, which is a requirement about rendering imposed on arithmetic. It takes the roots for the same reason `wire_snapshot` does — which groups are in view is the host's policy, and nothing is ever removed from a graph — so passing the same set makes the roll-up describe exactly the graph beside it. Each entry carries BOTH counts: `nodes` (the whole subtree) and `roots` (just the group tops). One rebuild is ~7 nodes and 1 root, so a summary meaning *operations* and one meaning *steps* are different numbers over the same queue, and picking one here would make this crate decide what counts as a job — the domain question it exists not to answer. It reports both structural facts; the viewer chooses. A pair, not a map: JSON object keys are strings, so a map would spell the state twice and give the wire no ordering. Every state rides with its zeros in a fixed order, so a consumer can index positionally and never handles a missing bucket. Tallying positionally against `ALL_STATES` means a new upstream `State` fails the exhaustive match in `state_index` rather than silently landing in an existing bucket. hive-c0re serves it at `GET /api/jobq/rollup`. The queue-side method is a call site, not an implementation: it supplies the lock and the same `visible_roots` as `graph_snapshot`, so the summary cannot describe a different visible set than the graph it summarises.
This commit is contained in:
parent
b04e7d985d
commit
9e7a2002d1
4 changed files with 195 additions and 3 deletions
|
|
@ -206,6 +206,7 @@ pub async fn serve(
|
|||
.routes(routes!(state_snapshot::dashboard_stream))
|
||||
.routes(routes!(state_snapshot::dashboard_history))
|
||||
.routes(routes!(state_snapshot::jobq_graph))
|
||||
.routes(routes!(state_snapshot::jobq_rollup))
|
||||
.split_for_parts();
|
||||
// Just the JSON, not the UI — Swagger UI itself is nginx-hosted from
|
||||
// the nix store (see the module doc comment above `ApiDoc`). `api`
|
||||
|
|
@ -450,6 +451,7 @@ mod router_build_probe {
|
|||
.routes(routes!(build_logs::get_build_log_stream))
|
||||
.routes(routes!(state_snapshot::dashboard_stream))
|
||||
.routes(routes!(state_snapshot::dashboard_history))
|
||||
.routes(routes!(state_snapshot::jobq_graph));
|
||||
.routes(routes!(state_snapshot::jobq_graph))
|
||||
.routes(routes!(state_snapshot::jobq_rollup));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -678,6 +678,28 @@ pub(super) async fn jobq_graph(
|
|||
axum::Json(state.coord.job_queue.graph_snapshot())
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/jobq/rollup",
|
||||
responses(
|
||||
(status = 200, description = "counts by lifecycle state over the same \
|
||||
groups `/api/jobq/graph` serves, as `(state, nodes, roots)` \
|
||||
triples. Every state is present, zero counts included, in a fixed \
|
||||
order — a consumer renders a summary (\"3 running · 2 queued\") \
|
||||
without fetching the graph and without re-deriving the tally. \
|
||||
`roots` counts groups, `nodes` counts every step at any depth: one \
|
||||
rebuild is 1 root and ~7 nodes, so a summary meaning *operations* \
|
||||
reads `roots` and one meaning *steps* reads `nodes`.",
|
||||
body = Vec<hive_jobq_wire::StateCount>),
|
||||
),
|
||||
tag = "state_snapshot"
|
||||
)]
|
||||
pub(super) async fn jobq_rollup(
|
||||
State(state): State<AppState>,
|
||||
) -> axum::Json<Vec<hive_jobq_wire::StateCount>> {
|
||||
axum::Json(state.coord.job_queue.state_rollup())
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/dashboard/history",
|
||||
|
|
|
|||
|
|
@ -368,6 +368,18 @@ impl JobQueue {
|
|||
inner.graph().wire_snapshot(visible_roots(&inner))
|
||||
}
|
||||
|
||||
/// Per-state counts over the **same** groups [`Queue::graph_snapshot`]
|
||||
/// serves.
|
||||
///
|
||||
/// Supplies the same two things and nothing else: the lock, and
|
||||
/// [`visible_roots`]. The counting is [`hive_jobq_wire::state_rollup`]'s and
|
||||
/// is generic over the payload — this is a call site, not an implementation.
|
||||
#[must_use]
|
||||
pub fn state_rollup(&self) -> Vec<hive_jobq_wire::StateCount> {
|
||||
let inner = self.lock();
|
||||
hive_jobq_wire::state_rollup(inner.graph(), visible_roots(&inner))
|
||||
}
|
||||
|
||||
/// Snapshot every live + retained DAG for `/api/state` + `RebuildQueueChanged`.
|
||||
#[must_use]
|
||||
pub fn snapshot(&self) -> Vec<DagView> {
|
||||
|
|
|
|||
Loading…
Reference in a new issue