job_queue: filter jobq graph snapshot by per-node state
graph_snapshot previously filtered which whole roots got projected based on the root node's own state, so a group root that was still Running but had already-Done internal steps couldn't be filtered down to just its live nodes, and a filtered-out root hid its entire subtree even when a descendant still matched. Apply the states filter after GraphWire::wire_snapshot instead, over every node in the flattened tree, not just roots. The jobq-graph client already handles an orphaned node (parent filtered out) by promoting it to a rendered root, so this is safe on the client side with no changes needed there. Fixes hyperhive#3210
This commit is contained in:
parent
0b6b3b755d
commit
861a1f8f26
2 changed files with 98 additions and 37 deletions
|
|
@ -1471,11 +1471,18 @@ fn history_retains_live_dags_and_the_newest_terminals() {
|
|||
);
|
||||
}
|
||||
|
||||
/// `graph_snapshot`'s `states` ask filters by **root** state — a whole group
|
||||
/// is kept or dropped together, never split mid-subtree. `None` is the
|
||||
/// identity filter (every visible root, current default behaviour).
|
||||
/// `graph_snapshot`'s `states` ask, exercised through the real scheduler:
|
||||
/// a queued (never-run) DAG cancels as one unit, so every node in it shares
|
||||
/// one state and this only proves root-level inclusion/exclusion — the
|
||||
/// per-node case (a live group holding a mix of finished and unfinished
|
||||
/// steps) isn't expressible without driving the scheduler, which this test
|
||||
/// module deliberately can't do (see the module doc comment). See
|
||||
/// `filter_nodes_by_state_keeps_matching_nodes_from_a_mixed_state_tree`
|
||||
/// below for that half, exercised directly against hand-built `GraphNode`s.
|
||||
/// `None` is the identity filter (every visible node, current default
|
||||
/// behaviour).
|
||||
#[test]
|
||||
fn graph_snapshot_states_filters_whole_groups_by_root_state() {
|
||||
fn graph_snapshot_states_filters_by_node_state() {
|
||||
let q = JobQueue::new(2);
|
||||
let roots_a = insert_named(&q, |builder| restart_online(builder, &["agent-a"], false));
|
||||
let roots_b = insert_named(&q, |builder| restart_online(builder, &["agent-b"], false));
|
||||
|
|
@ -1520,6 +1527,60 @@ fn graph_snapshot_states_filters_whole_groups_by_root_state() {
|
|||
);
|
||||
}
|
||||
|
||||
/// One hand-built node, bypassing the scheduler entirely — `filter_nodes_by_state`
|
||||
/// is a pure `Vec<GraphNode> -> Vec<GraphNode>` transform, so this exercises it
|
||||
/// directly rather than trying (and failing, per this module's own rule) to
|
||||
/// drive a live group into a genuinely mixed state through the real queue.
|
||||
fn node(id: u64, parent: Option<u64>, state: State) -> GraphNode {
|
||||
GraphNode {
|
||||
id,
|
||||
parent,
|
||||
state,
|
||||
deps: Vec::new(),
|
||||
created_at: Utc::now(),
|
||||
started_at: None,
|
||||
finished_at: None,
|
||||
error: None,
|
||||
payload: hive_jobq_wire::NodePayload {
|
||||
label: id.to_string(),
|
||||
data: serde_json::Value::Null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// The case `graph_snapshot_states_filters_by_node_state` above can't reach:
|
||||
/// a still-live group (root `Running`) holding a mix of already-`Done` and
|
||||
/// still-`Pending` steps. Filtering out `Done` must drop exactly the `Done`
|
||||
/// node and nothing else — the root and the `Pending` sibling both stay,
|
||||
/// even though the root itself isn't in the requested state set.
|
||||
#[test]
|
||||
fn filter_nodes_by_state_keeps_matching_nodes_from_a_mixed_state_tree() {
|
||||
let nodes = vec![
|
||||
node(1, None, State::Running), // root: whole group still live
|
||||
node(2, Some(1), State::Done), // finished step, should be hidden
|
||||
node(3, Some(1), State::Pending), // not-yet-run step, should stay
|
||||
];
|
||||
|
||||
let mut kept: Vec<u64> =
|
||||
filter_nodes_by_state(nodes.clone(), Some(&[State::Running, State::Pending]))
|
||||
.into_iter()
|
||||
.map(|n| n.id)
|
||||
.collect();
|
||||
kept.sort_unstable();
|
||||
assert_eq!(
|
||||
kept,
|
||||
vec![1, 3],
|
||||
"Done is filtered out even though its still-live parent (root) isn't"
|
||||
);
|
||||
|
||||
let mut unfiltered: Vec<u64> = filter_nodes_by_state(nodes, None)
|
||||
.into_iter()
|
||||
.map(|n| n.id)
|
||||
.collect();
|
||||
unfiltered.sort_unstable();
|
||||
assert_eq!(unfiltered, vec![1, 2, 3], "None is the identity filter");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_truncation_cuts_on_a_char_boundary() {
|
||||
// `truncate_error` is a pure `&str -> String`. This used to submit a DAG,
|
||||
|
|
|
|||
Loading…
Reference in a new issue