refactor(#2808): the wire state enum is the scheduler's own
`hive_host_sock::jobs::State` was a hand-maintained copy of `hive_jobq::State` — five variants spelled the same in both, kept in sync by whoever remembered. Adding `Skipped` last week meant adding it twice. The wire crate now re-exports the scheduler's enum and `to_wire_state` is gone. Two states that were hidden now reach clients. `to_wire_state` renamed `Pending` to `Queued` and folded `Finishing` into `Running`, so the dashboard could not distinguish a node waiting on its dependencies from one whose own work is done while its sub-nodes still run. Both are now visible, and consumers say which they mean. Every consumer had to move with it, and only the Rust ones said so: the exhaustive matches in `hivectl` and `DagView::rollup_state` failed to compile, while the dashboard's fourteen string comparisons would have gone quietly wrong — a `finishing` node no longer counting as running, a `pending` node no longer as queued. The frontend also builds CSS class names out of the state string (`rqe-` + state, `rqe-node-` + state) and keys its glyph map on it, all lowercase. Those go through a `stateSlug` helper now; comparisons use the wire spelling, presentation lowercases. Without that split every queue entry and node chip would have silently lost its styling. Dropping the `State as JobState` alias in hive-c0re falls out of this: the alias only existed to tell two `State` types apart, and there is one now.
This commit is contained in:
parent
6fd91ccf6a
commit
b9aab7e923
8 changed files with 72 additions and 93 deletions
|
|
@ -25,20 +25,28 @@ let rebuildQueueState = [];
|
|||
// DagView no longer carries top-level `kind`/`state`/`started_at`/`finished_at`
|
||||
// — these are all derived from the NodeView array by the client.
|
||||
|
||||
// Rollup state from nodes (failed > cancelled > running > queued).
|
||||
// Node/DAG states arrive in the wire spelling of `hive_jobq::State` — the
|
||||
// scheduler's own enum, serialised verbatim, so the names are PascalCase and
|
||||
// there is no separate display-shaped wire type. Compare against those names;
|
||||
// lowercase only where a CSS class or human-facing label needs it.
|
||||
const stateSlug = (s) => String(s || '').toLowerCase();
|
||||
|
||||
// Rollup state from nodes (Failed > Cancelled > Running > Pending).
|
||||
// `Done` nodes are excluded from the payload, so a fully-done DAG is absent;
|
||||
// an empty nodes array should not arise in practice — return 'done' defensively.
|
||||
// an empty nodes array should not arise in practice — return 'Done' defensively.
|
||||
function rollupState(nodes) {
|
||||
const ns = nodes || [];
|
||||
if (!ns.length) return 'done';
|
||||
if (ns.some((n) => n.state === 'failed')) return 'failed';
|
||||
if (ns.some((n) => n.state === 'cancelled')) return 'cancelled';
|
||||
if (ns.some((n) => n.state === 'running')) return 'running';
|
||||
if (!ns.length) return 'Done';
|
||||
if (ns.some((n) => n.state === 'Failed')) return 'Failed';
|
||||
if (ns.some((n) => n.state === 'Cancelled')) return 'Cancelled';
|
||||
// `Finishing` is a node whose own work is done while its sub-nodes still
|
||||
// run — in flight, so it counts as running.
|
||||
if (ns.some((n) => n.state === 'Running' || n.state === 'Finishing')) return 'Running';
|
||||
// A skipped node is a branch the run ruled out, which is expected on a
|
||||
// healthy DAG — it must not make the roll-up read as still-pending. This
|
||||
// mirrors `DagView::rollup_state` in hive-host-sock; edit the two together.
|
||||
if (ns.every((n) => n.state === 'skipped' || n.state === 'done')) return 'done';
|
||||
return 'queued';
|
||||
if (ns.every((n) => n.state === 'Skipped' || n.state === 'Done')) return 'Done';
|
||||
return 'Pending';
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -168,7 +176,7 @@ const QUEUE_STATE_GLYPH = {
|
|||
};
|
||||
|
||||
function firstFailedNode(entry) {
|
||||
return (entry.nodes || []).find((n) => n.state === 'failed') || null;
|
||||
return (entry.nodes || []).find((n) => n.state === 'Failed') || null;
|
||||
}
|
||||
|
||||
// Topo-sort a flat node list using `deps` edges. Nodes whose deps are all
|
||||
|
|
@ -299,22 +307,23 @@ function renderQueueEntry(entry) {
|
|||
const finishedAt = isoToSecs(entry.finished_at);
|
||||
const createdAt = isoToSecs(entry.created_at);
|
||||
|
||||
const slug = stateSlug(state);
|
||||
const li = el('li', {
|
||||
class: 'rebuild-queue-entry rqe-' + state,
|
||||
class: 'rebuild-queue-entry rqe-' + slug,
|
||||
'data-id': String(entry.id),
|
||||
});
|
||||
li.append(
|
||||
el('span', { class: 'rqe-state', title: state }, QUEUE_STATE_GLYPH[state] || '?'),
|
||||
el('span', { class: 'rqe-state', title: slug }, QUEUE_STATE_GLYPH[slug] || '?'),
|
||||
' ',
|
||||
el('span', { class: 'rqe-kind' }, entry.source),
|
||||
);
|
||||
li.append(' ', el('span', { class: 'rqe-source rqe-source-' + entry.source }, entry.source));
|
||||
if (state === 'queued') {
|
||||
if (state === 'Pending') {
|
||||
li.append(' ', el('span', {
|
||||
class: 'rqe-when',
|
||||
'data-rqe-enqueued': String(createdAt ?? ''),
|
||||
}, '· queued ' + (createdAt ? fmtAgo(createdAt) : '')));
|
||||
} else if (state === 'running' && startedAt) {
|
||||
} else if (state === 'Running' && startedAt) {
|
||||
const elapsed = Math.max(0, Math.floor(Date.now() / 1000) - startedAt);
|
||||
li.append(' ', el('span', {
|
||||
class: 'rqe-when',
|
||||
|
|
@ -324,8 +333,8 @@ function renderQueueEntry(entry) {
|
|||
li.append(' ', el('span', {
|
||||
class: 'rqe-when',
|
||||
'data-rqe-finished': String(finishedAt),
|
||||
'data-rqe-state': state,
|
||||
}, '· ' + state + ' ' + fmtAgo(finishedAt)));
|
||||
'data-rqe-state': slug,
|
||||
}, '· ' + slug + ' ' + fmtAgo(finishedAt)));
|
||||
}
|
||||
if (entry.reason) {
|
||||
const r = entry.reason.split('\n')[0];
|
||||
|
|
@ -359,7 +368,7 @@ function renderQueueEntry(entry) {
|
|||
}));
|
||||
}
|
||||
const chip = el('span', {
|
||||
class: 'rqe-node rqe-node-' + n.state,
|
||||
class: 'rqe-node rqe-node-' + stateSlug(n.state),
|
||||
title: (n.agent ? n.agent + ' · ' : '') + n.kind + ' · ' + n.state
|
||||
+ (n.error ? ' — ' + n.error : ''),
|
||||
}, (QUEUE_STATE_GLYPH[n.state] || '?') + ' ' + n.kind);
|
||||
|
|
@ -390,7 +399,7 @@ function renderQueueEntry(entry) {
|
|||
if (failed && failed.error) {
|
||||
li.append(el('pre', { class: 'rqe-error', title: failed.error }, truncate(failed.error, 200)));
|
||||
}
|
||||
if (state === 'queued') {
|
||||
if (state === 'Pending') {
|
||||
const cancelForm = el('form', {
|
||||
method: 'POST',
|
||||
action: '/api/rebuild-queue/' + entry.id + '/cancel',
|
||||
|
|
@ -421,7 +430,7 @@ function renderQueueEntry(entry) {
|
|||
// rebuild_queue_changed tick; a terminal node's log is static (one last fetch
|
||||
// on transition, then done).
|
||||
let liveLogId = null; // current node id being shown
|
||||
let liveLogDone = false; // true once the node left 'running'
|
||||
let liveLogDone = false; // true once the node left 'Running'
|
||||
let liveLogCollapsed = false;
|
||||
let liveLogPollTimer = null;
|
||||
|
||||
|
|
@ -433,8 +442,8 @@ function clearLiveLogPoll() {
|
|||
// Gate on has_log so lock/noop/store-only nodes don't open a blank panel.
|
||||
function findLiveBuild(queue) {
|
||||
for (const e of queue || []) {
|
||||
if (rollupState(e.nodes || []) !== 'running') continue;
|
||||
const node = (e.nodes || []).find((n) => n.state === 'running' && n.has_log);
|
||||
if (rollupState(e.nodes || []) !== 'Running') continue;
|
||||
const node = (e.nodes || []).find((n) => n.state === 'Running' && n.has_log);
|
||||
if (node) return { entry: e, node };
|
||||
}
|
||||
return null;
|
||||
|
|
@ -471,14 +480,14 @@ function renderRebuildLiveLog(queue) {
|
|||
// Same node, already polling — just let the timer tick (or do a final
|
||||
// fetch if the node just went non-running and we haven't marked done yet).
|
||||
if (liveNode.id === liveLogId) {
|
||||
if (!liveLogDone && liveNode.state !== 'running') {
|
||||
if (!liveLogDone && liveNode.state !== 'Running') {
|
||||
clearLiveLogPoll();
|
||||
liveLogDone = true;
|
||||
const pre = root.querySelector('.rebuild-live-log-output');
|
||||
const badge = root.querySelector('.rebuild-live-log-badge');
|
||||
if (pre) fetchAndRenderLiveLog(liveNode.id, pre);
|
||||
if (badge) {
|
||||
const ok = liveNode.state !== 'failed';
|
||||
const ok = liveNode.state !== 'Failed';
|
||||
badge.className = 'rebuild-live-log-badge ' + (ok ? 'rll-ok' : 'rll-fail');
|
||||
badge.textContent = liveNode.state;
|
||||
}
|
||||
|
|
@ -536,7 +545,7 @@ function updateRebuildCount() {
|
|||
let n = 0;
|
||||
for (const e of rebuildQueueState) {
|
||||
const s = rollupState(e.nodes || []);
|
||||
if (s === 'queued' || s === 'running') n++;
|
||||
if (s === 'Pending' || s === 'Running') n++;
|
||||
}
|
||||
if (n > 0) { pill.textContent = String(n); pill.hidden = false; }
|
||||
else { pill.hidden = true; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue