drop hive and agent response fields the frontend never reads
This commit is contained in:
parent
1875181d59
commit
a437f9ccd5
1 changed files with 347 additions and 598 deletions
|
|
@ -27,9 +27,7 @@
|
|||
//!
|
||||
//! [`Node`]: hive_jobq::Node
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use enumflags2::BitFlags;
|
||||
use hive_jobq::{Dep, Graph, NodeId, State, TerminalState};
|
||||
use hive_jobq::{Dep, Graph, NodeId, State};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
|
|
@ -73,30 +71,6 @@ impl StateSchema {
|
|||
}
|
||||
}
|
||||
|
||||
/// `OpenAPI` mirror of [`hive_jobq::TerminalState`] — same rationale and same
|
||||
/// exhaustive-match guard as [`StateSchema`].
|
||||
#[derive(Debug, Clone, Copy, ToSchema)]
|
||||
pub enum TerminalStateSchema {
|
||||
Done,
|
||||
Failed,
|
||||
Cancelled,
|
||||
Skipped,
|
||||
}
|
||||
|
||||
impl TerminalStateSchema {
|
||||
/// The mirror variant for `outcome`. Public for the same reason as
|
||||
/// [`StateSchema::of`].
|
||||
#[must_use]
|
||||
pub fn of(outcome: TerminalState) -> Self {
|
||||
match outcome {
|
||||
TerminalState::Done => Self::Done,
|
||||
TerminalState::Failed => Self::Failed,
|
||||
TerminalState::Cancelled => Self::Cancelled,
|
||||
TerminalState::Skipped => Self::Skipped,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Node id, carried verbatim from [`hive_jobq::NodeId`]: globally unique across
|
||||
/// the whole graph, not per group. Opaque to consumers — they group by
|
||||
/// [`GraphNode::parent`] and match dependency edges, nothing more.
|
||||
|
|
@ -152,16 +126,6 @@ pub struct GraphNode {
|
|||
/// What must hold before this node runs. Omitted when empty.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub deps: Vec<GraphDep>,
|
||||
/// When the node was inserted into the graph. Always present — a node that
|
||||
/// exists was created, so unlike the two below this is not an `Option`.
|
||||
pub created_at: DateTime<Utc>,
|
||||
/// When the node entered `Running`. `None` until it starts; a node that
|
||||
/// never ran keeps `None`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub started_at: Option<DateTime<Utc>>,
|
||||
/// When the node reached a terminal state. `None` while non-terminal.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub finished_at: Option<DateTime<Utc>>,
|
||||
/// Failure reason, set only when this node's *own* logic failed — a node
|
||||
/// that rolled up `Failed` from a child carries none of its own.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
|
|
@ -237,18 +201,6 @@ pub enum GraphDep {
|
|||
/// The node depended on. May name a node the consumer has not been sent
|
||||
/// (a filtered view); treat an absent target as satisfied.
|
||||
id: WireId,
|
||||
/// **The outcomes that satisfy this edge, as a set** — not a
|
||||
/// strong/weak flag.
|
||||
///
|
||||
/// A host routinely emits several tails edged on the *same* upstream
|
||||
/// node, distinguished only by which outcomes each accepts (one for
|
||||
/// `Done`, one for `Failed`/`Cancelled`, …). Collapsing this to a
|
||||
/// boolean renders those as identical nodes.
|
||||
///
|
||||
/// Every accepted outcome is **named**, so a client never shifts bits
|
||||
/// to read an edge.
|
||||
#[schema(value_type = Vec<TerminalStateSchema>)]
|
||||
accepts: Vec<TerminalState>,
|
||||
},
|
||||
/// Need `count` units of a named resource, per [`WireResource::name`].
|
||||
Resource { name: String, count: u32 },
|
||||
|
|
@ -401,9 +353,6 @@ fn wire_node<N: WireNode, R: WireResource>(node: &hive_jobq::Node<N, R>) -> Grap
|
|||
parent: node.parent.map(NodeId::get),
|
||||
state: node.state,
|
||||
deps: node.deps.iter().map(wire_dep).collect(),
|
||||
created_at: node.created_at,
|
||||
started_at: node.started_at,
|
||||
finished_at: node.finished_at,
|
||||
error: node.error.clone(),
|
||||
payload: NodePayload {
|
||||
label: node.payload.label(),
|
||||
|
|
@ -414,17 +363,7 @@ fn wire_node<N: WireNode, R: WireResource>(node: &hive_jobq::Node<N, R>) -> Grap
|
|||
|
||||
fn wire_dep<R: WireResource>(dep: &Dep<R>) -> GraphDep {
|
||||
match dep {
|
||||
Dep::Node { id, when } => GraphDep::Node {
|
||||
id: id.get(),
|
||||
// Every outcome the type knows about, not a hand-listed set: a
|
||||
// variant added to `TerminalState` is then named on the wire
|
||||
// automatically, where a literal array would have silently
|
||||
// dropped it from every edge that accepts it.
|
||||
accepts: BitFlags::<TerminalState>::ALL
|
||||
.iter()
|
||||
.filter(|outcome| when.accepts(*outcome))
|
||||
.collect(),
|
||||
},
|
||||
Dep::Node { id, .. } => GraphDep::Node { id: id.get() },
|
||||
Dep::Resource { name, count } => GraphDep::Resource {
|
||||
name: name.name(),
|
||||
count: *count,
|
||||
|
|
@ -439,9 +378,8 @@ mod tests {
|
|||
use hive_jobq::scheduler::Scheduler;
|
||||
|
||||
use super::{
|
||||
ALL_STATES, BitFlags, GraphDep, GraphNode, GraphWire, NodePayload, State, StateSchema,
|
||||
TerminalState, TerminalStateSchema, WireId, WireNode, filter_nodes_by_state, parse_states,
|
||||
state_rollup,
|
||||
ALL_STATES, GraphDep, GraphNode, GraphWire, NodePayload, State, StateSchema, WireId,
|
||||
WireNode, filter_nodes_by_state, parse_states, state_rollup,
|
||||
};
|
||||
|
||||
/// The mirrors document what the wire actually says — a mirror that
|
||||
|
|
@ -473,16 +411,6 @@ mod tests {
|
|||
"{state:?} is documented as {documented} but serialises as {on_the_wire}"
|
||||
);
|
||||
}
|
||||
|
||||
for outcome in BitFlags::<TerminalState>::ALL {
|
||||
let on_the_wire = serde_json::to_string(&outcome).expect("serialises");
|
||||
let documented = format!("{:?}", TerminalStateSchema::of(outcome));
|
||||
assert_eq!(
|
||||
on_the_wire,
|
||||
format!("\"{documented}\""),
|
||||
"{outcome:?} is documented as {documented} but serialises as {on_the_wire}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A scheduler over the test payloads, built through the same public API a
|
||||
|
|
@ -573,12 +501,8 @@ mod tests {
|
|||
assert_eq!(wire[0].payload.label, "reconcile");
|
||||
assert_eq!(wire[0].payload.data["seen_id"], first.get());
|
||||
match &wire[1].deps[..] {
|
||||
[
|
||||
GraphDep::Node { id, accepts },
|
||||
GraphDep::Resource { name, count },
|
||||
] => {
|
||||
[GraphDep::Node { id }, GraphDep::Resource { name, count }] => {
|
||||
assert_eq!(*id, first.get());
|
||||
assert_eq!(accepts, &vec![TerminalState::Done]);
|
||||
assert_eq!(name, "build-slot", "the host's WireResource::name");
|
||||
assert_eq!(*count, 2);
|
||||
}
|
||||
|
|
@ -592,9 +516,6 @@ mod tests {
|
|||
parent,
|
||||
state,
|
||||
deps,
|
||||
created_at: chrono::DateTime::<chrono::Utc>::default(),
|
||||
started_at: None,
|
||||
finished_at: None,
|
||||
error: None,
|
||||
payload: NodePayload {
|
||||
label: "reconcile".to_owned(),
|
||||
|
|
@ -610,10 +531,7 @@ mod tests {
|
|||
Some(3),
|
||||
State::Running,
|
||||
vec![
|
||||
GraphDep::Node {
|
||||
id: 3,
|
||||
accepts: vec![TerminalState::Done],
|
||||
},
|
||||
GraphDep::Node { id: 3 },
|
||||
GraphDep::Resource {
|
||||
name: "build-slot".to_owned(),
|
||||
count: 1,
|
||||
|
|
@ -625,34 +543,27 @@ mod tests {
|
|||
assert_eq!(format!("{after:?}"), format!("{before:?}"));
|
||||
}
|
||||
|
||||
/// The serialised node carries exactly these keys (`parent`, `deps` and
|
||||
/// `error` are omitted when empty, `payload.data` when null).
|
||||
#[test]
|
||||
fn two_tails_on_one_upstream_are_distinguishable_by_their_accepted_set() {
|
||||
// The reason `accepts` is a set and not a strong/weak bool: these two
|
||||
// are the same shape and the same upstream, and *only* the outcome
|
||||
// set tells them apart.
|
||||
let ok_tail = node(
|
||||
10,
|
||||
None,
|
||||
State::Pending,
|
||||
vec![GraphDep::Node {
|
||||
id: 4,
|
||||
accepts: vec![TerminalState::Done],
|
||||
}],
|
||||
);
|
||||
let fail_tail = node(
|
||||
11,
|
||||
None,
|
||||
State::Pending,
|
||||
vec![GraphDep::Node {
|
||||
id: 4,
|
||||
accepts: vec![TerminalState::Failed, TerminalState::Cancelled],
|
||||
}],
|
||||
);
|
||||
let render = |n: &GraphNode| match n.deps.first() {
|
||||
Some(GraphDep::Node { accepts, .. }) => format!("{accepts:?}"),
|
||||
_ => "none".to_owned(),
|
||||
fn a_node_serialises_exactly_its_read_fields() {
|
||||
let full = GraphNode {
|
||||
error: Some("boom".to_owned()),
|
||||
..node(7, Some(3), State::Failed, vec![GraphDep::Node { id: 3 }])
|
||||
};
|
||||
assert_ne!(render(&ok_tail), render(&fail_tail));
|
||||
let value = serde_json::to_value(&full).expect("serialises");
|
||||
let mut keys: Vec<&str> = value
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.keys()
|
||||
.map(String::as_str)
|
||||
.collect();
|
||||
keys.sort_unstable();
|
||||
assert_eq!(keys, ["deps", "error", "id", "parent", "payload", "state"]);
|
||||
assert_eq!(
|
||||
value["deps"][0],
|
||||
serde_json::json!({ "kind": "Node", "id": 3 })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in a new issue