fix(#2802): a group whose children were all dropped rolls up Cancelled
The roll-up treated a cancelled child the same as a failed one, so a DAG the operator cancelled before it started reported `Failed` — it claimed to have failed at something when nothing under it ever ran. `Failed` still outranks `Cancelled`: a group where one step broke and the rest were dropped in response is a failure, and that is the fact worth surfacing. Only a group with no failed child at all reports the cancel. This also settles a disagreement. `DagView::rollup_state` on the wire has always ranked failed over cancelled over the rest; the graph's own roll-up had no `Cancelled` outcome to rank, so the two described the same DAG differently depending on which one you asked.
This commit is contained in:
parent
6f551334de
commit
6fd91ccf6a
1 changed files with 33 additions and 26 deletions
|
|
@ -233,15 +233,31 @@ impl<N, R: Clone + Eq + Hash> Scheduler<N, R> {
|
||||||
.all(|n| n.parent != Some(id) || n.state.is_terminal())
|
.all(|n| n.parent != Some(id) || n.state.is_terminal())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether any direct child of `id` ended `Failed`/`Cancelled` — the roll-up
|
/// What `id` rolls up to once all its children are terminal: `Failed` if any
|
||||||
/// failure condition for the parent.
|
/// child failed, else `Cancelled` if any was cancelled, else `Done`.
|
||||||
/// `Skipped` children are **not** counted: being ruled out by an edge is the
|
///
|
||||||
/// expected fate of every branch not taken, so counting it would make any
|
/// `Failed` outranks `Cancelled` because the failure is the actionable fact —
|
||||||
/// group that branches on outcome roll up failed no matter how the run went.
|
/// a group where one step broke and the rest were dropped in response is a
|
||||||
fn any_child_failed(&self, id: NodeId) -> bool {
|
/// failure, not a cancellation. A group whose children were *all* dropped
|
||||||
self.graph
|
/// never failed at anything, and says so.
|
||||||
.nodes()
|
///
|
||||||
.any(|n| n.parent == Some(id) && matches!(n.state, State::Failed | State::Cancelled))
|
/// `Skipped` children are ignored: being ruled out by an edge is the expected
|
||||||
|
/// fate of every branch not taken, so counting it would make any group that
|
||||||
|
/// branches on outcome roll up non-`Done` however the run went.
|
||||||
|
fn rolled_up_state(&self, id: NodeId) -> State {
|
||||||
|
let mut any_cancelled = false;
|
||||||
|
for child in self.graph.nodes().filter(|n| n.parent == Some(id)) {
|
||||||
|
match child.state {
|
||||||
|
State::Failed => return State::Failed,
|
||||||
|
State::Cancelled => any_cancelled = true,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if any_cancelled {
|
||||||
|
State::Cancelled
|
||||||
|
} else {
|
||||||
|
State::Done
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Transition a node whose own logic just *succeeded* to its resulting state:
|
/// Transition a node whose own logic just *succeeded* to its resulting state:
|
||||||
|
|
@ -254,12 +270,10 @@ impl<N, R: Clone + Eq + Hash> Scheduler<N, R> {
|
||||||
/// its dependency succeeds, and leaving it `Pending` would wedge the subtree
|
/// its dependency succeeds, and leaving it `Pending` would wedge the subtree
|
||||||
/// non-terminal forever.
|
/// non-terminal forever.
|
||||||
fn settle_terminal(&mut self, id: NodeId) {
|
fn settle_terminal(&mut self, id: NodeId) {
|
||||||
let state = if !self.all_children_terminal(id) {
|
let state = if self.all_children_terminal(id) {
|
||||||
State::Finishing
|
self.rolled_up_state(id)
|
||||||
} else if self.any_child_failed(id) {
|
|
||||||
State::Failed
|
|
||||||
} else {
|
} else {
|
||||||
State::Done
|
State::Finishing
|
||||||
};
|
};
|
||||||
self.graph.set_state(id, state);
|
self.graph.set_state(id, state);
|
||||||
if state.is_terminal() {
|
if state.is_terminal() {
|
||||||
|
|
@ -279,11 +293,7 @@ impl<N, R: Clone + Eq + Hash> Scheduler<N, R> {
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let state = if self.any_child_failed(a) {
|
let state = self.rolled_up_state(a);
|
||||||
State::Failed
|
|
||||||
} else {
|
|
||||||
State::Done
|
|
||||||
};
|
|
||||||
self.graph.set_state(a, state);
|
self.graph.set_state(a, state);
|
||||||
// Any terminal outcome can rule a dependent out — see `settle_terminal`.
|
// Any terminal outcome can rule a dependent out — see `settle_terminal`.
|
||||||
self.cascade_cancel(a);
|
self.cascade_cancel(a);
|
||||||
|
|
@ -982,13 +992,10 @@ mod tests {
|
||||||
assert!(s.cancel_node(root));
|
assert!(s.cancel_node(root));
|
||||||
assert_eq!(s.graph().node(a).unwrap().state, State::Cancelled);
|
assert_eq!(s.graph().node(a).unwrap().state, State::Cancelled);
|
||||||
assert_eq!(s.graph().node(b).unwrap().state, State::Cancelled);
|
assert_eq!(s.graph().node(b).unwrap().state, State::Cancelled);
|
||||||
// With every child terminal the root leaves `Finishing` and rolls up.
|
// With every child terminal the root leaves `Finishing` and rolls up —
|
||||||
// It rolls up `Failed`, not `Cancelled`: the roll-up counts a cancelled
|
// as `Cancelled`, not `Failed`: nothing under it failed at anything, the
|
||||||
// child as a non-success, and it has no separate "the whole group was
|
// work was dropped.
|
||||||
// dropped" outcome. Pre-existing and unchanged here — a caller that
|
assert_eq!(s.graph().node(root).unwrap().state, State::Cancelled);
|
||||||
// wants to show a cancel as a cancel derives that from the node states,
|
|
||||||
// not from the root's.
|
|
||||||
assert_eq!(s.graph().node(root).unwrap().state, State::Failed);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// All-or-nothing: an in-flight node's work is not interruptible, and
|
/// All-or-nothing: an in-flight node's work is not interruptible, and
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue