refactor(#2908): cancel a node, not a DAG

`JobQueue::cancel(dag_id)` resolved the id to a `NodeKind::Dag` container
and cancelled that. But the container lookup was the only DAG-specific
part — everything that makes cancel work already lives in the scheduler:
`cancel_node` marks the node `Cancelled` and cascades to its pending
descendants, sparing any node whose edge accepts `Cancelled` (which is
what keeps a dropped approval DAG from dangling its row).

So `cancel` now takes any node id. A group root cancels the whole group,
which is what the dashboard's button does today and why nothing about
its behaviour changes: a DAG id *is* its root node's id. An interior
node cancels just that branch — a capability the DAG-scoped version
could not express, covered by the new test (a hive-wide restart drops
one agent's subgraph while the other keeps running).

`QueueInner::node_by_id` replaces `container()` here: same search, same
cost, without asserting the node is a DAG container. `container()` stays
for `first_error` and the append-subgraph guard, which are genuinely
DAG-scoped.

No wire change. The route is `POST /api/rebuild-queue/{id}/cancel` with
a `u64` path param — same type, same route, and the client keeps sending
the same number. Only the param's documented meaning moves from "DAG id"
to "node id".

Checked with clippy (`--all-targets -D warnings`), `cargo test -p
hive-c0re` (322 passed) and `nix fmt`. No option surface touched, so no
nix-eval gate.
This commit is contained in:
atlas 2026-08-01 15:09:05 +02:00 committed by mara
commit eb557ee3c1
3 changed files with 60 additions and 6 deletions

View file

@ -345,12 +345,16 @@ impl JobQueue {
/// say) is cancelled along with everything else — there is nothing to converge
/// when no node ever ran. Only a node that named `Cancelled` survives, and it
/// survives because it asked to.
pub fn cancel(&self, dag_id: u64) -> bool {
/// `id` names **any node**, not specifically a DAG. Cancelling a group root
/// drops that whole group (the cascade is the scheduler's), which is what
/// the dashboard's whole-DAG cancel does; cancelling an interior node drops
/// just that branch. Nothing here knows about DAGs.
pub fn cancel(&self, id: u64) -> bool {
let mut inner = self.lock();
let Some(container) = inner.container(dag_id) else {
let Some(node) = inner.node_by_id(id) else {
return false;
};
if !inner.sched.cancel_node(container) {
if !inner.sched.cancel_node(node) {
return false;
}
drop(inner);
@ -460,6 +464,17 @@ impl QueueInner {
/// The container node of `dag_id` — the `NodeKind::Dag` root whose id equals
/// `dag_id`. `NodeId` is un-fabricable from a raw `u64`, so this is a search.
/// Resolve a raw wire `u64` to a graph [`NodeId`], whatever kind of node it
/// names. `NodeId` is un-fabricable from a `u64`, so this is a search —
/// same cost as [`QueueInner::container`], without asserting the node is a
/// DAG container.
fn node_by_id(&self, id: u64) -> Option<NodeId> {
self.sched
.graph()
.nodes()
.find_map(|n| (n.id.get() == id).then_some(n.id))
}
fn container(&self, dag_id: u64) -> Option<NodeId> {
self.sched.graph().nodes().find_map(|n| {
(n.parent.is_none()