diff --git a/hive-jobq-wire/src/lib.rs b/hive-jobq-wire/src/lib.rs index 19ef493b..437782ea 100644 --- a/hive-jobq-wire/src/lib.rs +++ b/hive-jobq-wire/src/lib.rs @@ -152,6 +152,9 @@ pub struct GraphNode { /// What must hold before this node runs. Omitted when empty. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub deps: Vec, + /// 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, /// When the node entered `Running`. `None` until it starts; a node that /// never ran keeps `None`. #[serde(default, skip_serializing_if = "Option::is_none")] @@ -254,6 +257,7 @@ fn wire_node(node: &hive_jobq::Node) -> 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(), @@ -443,6 +447,7 @@ mod tests { parent, state, deps, + created_at: chrono::DateTime::::default(), started_at: None, finished_at: None, error: None, diff --git a/hive-jobq/src/lib.rs b/hive-jobq/src/lib.rs index cccd55a1..0e899dc7 100644 --- a/hive-jobq/src/lib.rs +++ b/hive-jobq/src/lib.rs @@ -255,9 +255,9 @@ impl State { } /// Wall-clock UTC now — the source for node lifecycle timestamps -/// ([`Node::started_at`] / [`Node::finished_at`]). The graph stamps its own -/// timestamps rather than threading a clock through every call, so a node's -/// timing is self-contained. Derived from `SystemTime` (the workspace `chrono` +/// ([`Node::created_at`] / [`Node::started_at`] / [`Node::finished_at`]). The +/// graph stamps its own timestamps rather than threading a clock through every +/// call, so a node's timing is self-contained. Derived from `SystemTime` (the workspace `chrono` /// carries no `clock` feature, matching `hive_sh4re::wire_time`), truncated to /// whole seconds; a pre-epoch or out-of-range clock clamps to the epoch. fn now_utc() -> DateTime { @@ -291,6 +291,14 @@ pub struct Node { pub deps: Vec>, /// Lifecycle state. pub state: State, + /// UTC instant the node was inserted into the graph. Stamped by the graph. + /// + /// Not an `Option`, unlike its two siblings below: starting and finishing + /// are events that may never happen, but a node that exists was created. + /// Together the three are the node's whole lifecycle — a caller asking + /// "how long did this sit before it ran" needs this end of the interval, + /// and previously had to get it from a wrapper the graph knows nothing about. + pub created_at: DateTime, /// UTC instant the node entered [`State::Running`] (`None` until it starts; /// a cancelled node never ran, so it stays `None`). Stamped by the graph. pub started_at: Option>, @@ -481,6 +489,7 @@ impl Graph { payload, deps, state: State::Pending, + created_at: now_utc(), started_at: None, finished_at: None, error: None, @@ -934,6 +943,7 @@ mod tests { when: DepWhen::AFTER_OK, }], state: State::Pending, + created_at: now_utc(), started_at: None, finished_at: None, error: None, @@ -954,6 +964,7 @@ mod tests { payload: "x", deps: vec![], state: State::Pending, + created_at: now_utc(), started_at: None, finished_at: None, error: None,