refactor(#2591): DagView carries host-computed timestamps + NodeView.has_log
Reconcile with mara + argus's review on the frontend PR (#2660): - DagView regains started_at/finished_at (DateTime<Utc>), computed host-side as min/max over ALL subtree nodes (including the Done ones filtered off the wire). The client can't derive these — the earliest/only-started node is often Done and absent — so the backend sets them, per mara's call. - NodeView gains has_log: bool = build_log_id.is_some(), the precise old 'node has a captured build log' guard so the dashboard only shows a log link for nodes that actually produce one.
This commit is contained in:
parent
f3207ce9f8
commit
02e2bf895e
2 changed files with 34 additions and 0 deletions
|
|
@ -38,6 +38,7 @@ mod tests;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
use hive_jobq::resources::ResourceTable;
|
use hive_jobq::resources::ResourceTable;
|
||||||
use hive_jobq::scheduler::{Outcome, Scheduler};
|
use hive_jobq::scheduler::{Outcome, Scheduler};
|
||||||
use hive_jobq::{Dep, DepWhen as JobDepWhen, Graph, NodeId, State as JobState};
|
use hive_jobq::{Dep, DepWhen as JobDepWhen, Graph, NodeId, State as JobState};
|
||||||
|
|
@ -758,10 +759,21 @@ impl QueueInner {
|
||||||
fn dag_view(&self, container: NodeId) -> Option<DagView> {
|
fn dag_view(&self, container: NodeId) -> Option<DagView> {
|
||||||
let meta = self.dag_meta(container)?;
|
let meta = self.dag_meta(container)?;
|
||||||
let mut nodes = Vec::new();
|
let mut nodes = Vec::new();
|
||||||
|
// DAG-level timestamps are taken over *all* subtree nodes (including the
|
||||||
|
// `Done` ones excluded from the wire) — the client can't derive them
|
||||||
|
// from a `Done`-filtered node set, so the host computes them here.
|
||||||
|
let mut started: Vec<DateTime<Utc>> = Vec::new();
|
||||||
|
let mut finished: Vec<DateTime<Utc>> = Vec::new();
|
||||||
for id in self.subtree(container) {
|
for id in self.subtree(container) {
|
||||||
let Some(node) = self.sched.graph().node(id) else {
|
let Some(node) = self.sched.graph().node(id) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
if let Some(s) = node.started_at {
|
||||||
|
started.push(s);
|
||||||
|
}
|
||||||
|
if let Some(f) = node.finished_at {
|
||||||
|
finished.push(f);
|
||||||
|
}
|
||||||
if node.state == JobState::Done {
|
if node.state == JobState::Done {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -782,6 +794,7 @@ impl QueueInner {
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
|
let has_log = self.node_rt.get(&id).and_then(|r| r.build_log_id).is_some();
|
||||||
nodes.push(NodeView {
|
nodes.push(NodeView {
|
||||||
id: id.get(),
|
id: id.get(),
|
||||||
agent: node.payload.agent().to_owned(),
|
agent: node.payload.agent().to_owned(),
|
||||||
|
|
@ -793,16 +806,20 @@ impl QueueInner {
|
||||||
error: node.error.clone(),
|
error: node.error.clone(),
|
||||||
approval_id,
|
approval_id,
|
||||||
inputs,
|
inputs,
|
||||||
|
has_log,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if nodes.is_empty() {
|
if nodes.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
let is_terminal = self.dag_is_terminal(container);
|
||||||
Some(DagView {
|
Some(DagView {
|
||||||
id: container.get(),
|
id: container.get(),
|
||||||
source: meta.source,
|
source: meta.source,
|
||||||
reason: meta.reason.clone(),
|
reason: meta.reason.clone(),
|
||||||
created_at: hive_sh4re::wire_time::from_secs(meta.created_at),
|
created_at: hive_sh4re::wire_time::from_secs(meta.created_at),
|
||||||
|
started_at: started.into_iter().min(),
|
||||||
|
finished_at: is_terminal.then(|| finished.into_iter().max()).flatten(),
|
||||||
nodes,
|
nodes,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,12 @@ pub struct NodeView {
|
||||||
/// node. Display-only payload, not derivable from the graph.
|
/// node. Display-only payload, not derivable from the graph.
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
pub inputs: Vec<String>,
|
pub inputs: Vec<String>,
|
||||||
|
/// Whether this node has a captured build log fetchable at
|
||||||
|
/// `GET /api/build-log/<id>`. Only the nix-heavy nodes that stream build
|
||||||
|
/// output set one; the client gates its log link on this so lock / noop /
|
||||||
|
/// store-only nodes don't render a link that 404s.
|
||||||
|
#[serde(default)]
|
||||||
|
pub has_log: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A queued / running / failed DAG — a thin projection of one container
|
/// A queued / running / failed DAG — a thin projection of one container
|
||||||
|
|
@ -137,6 +143,17 @@ pub struct DagView {
|
||||||
pub reason: String,
|
pub reason: String,
|
||||||
/// When the DAG was enqueued.
|
/// When the DAG was enqueued.
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
|
/// When the DAG's first node started (min over *all* its nodes) — computed
|
||||||
|
/// host-side, **not** derived on the client: `Done` nodes are excluded from
|
||||||
|
/// `nodes` below, so the earliest-started node is usually absent from the
|
||||||
|
/// wire and the client can't take the min itself. `None` until a node runs.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub started_at: Option<DateTime<Utc>>,
|
||||||
|
/// When the DAG finished (max `finished_at` over all its nodes), set only
|
||||||
|
/// once the DAG has settled terminal. Host-computed for the same reason as
|
||||||
|
/// `started_at`. `None` while the DAG is still live.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub finished_at: Option<DateTime<Utc>>,
|
||||||
/// Nodes of this DAG with `Done` ones excluded. A DAG whose nodes are
|
/// Nodes of this DAG with `Done` ones excluded. A DAG whose nodes are
|
||||||
/// all `Done` is omitted from the snapshot entirely; a `Failed` DAG
|
/// all `Done` is omitted from the snapshot entirely; a `Failed` DAG
|
||||||
/// lingers until aged out by the history cap.
|
/// lingers until aged out by the history cap.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue