Skip to main content

git_internal/internal/object/
run_event.rs

1//! Run lifecycle event.
2//!
3//! `RunEvent` records append-only execution-phase facts for a `Run`.
4//!
5//! # How to use this object
6//!
7//! - Append events as a run moves through creation, patching,
8//!   validation, checkpoints, completion, or failure.
9//! - Attach `error`, `metrics`, and `patchset_id` when they belong to
10//!   that phase transition.
11//! - Do not mutate the `Run` snapshot to reflect phase changes.
12//!
13//! # How it works with other objects
14//!
15//! - `RunEvent.run_id` points at the execution envelope.
16//! - `patchset_id` can associate a run-phase fact with a candidate
17//!   patchset.
18//! - `Decision` normally appears after the terminal run events.
19//!
20//! # How Libra should call it
21//!
22//! Libra should derive the current run phase from the latest events and
23//! scheduler state, while treating `Run` itself as the immutable attempt
24//! record.
25
26use std::fmt;
27
28use serde::{Deserialize, Serialize};
29use uuid::Uuid;
30
31use crate::{
32    errors::GitError,
33    hash::ObjectHash,
34    internal::object::{
35        ObjectTrait,
36        types::{ActorRef, Header, ObjectType},
37    },
38};
39
40#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
41#[serde(rename_all = "snake_case")]
42pub enum RunEventKind {
43    Created,
44    Patching,
45    Validating,
46    Completed,
47    Failed,
48    Checkpointed,
49}
50
51/// Append-only execution-phase fact for one `Run`.
52#[derive(Debug, Clone, Serialize, Deserialize)]
53#[serde(deny_unknown_fields)]
54pub struct RunEvent {
55    /// Common object header carrying the immutable object id, type,
56    /// creator, and timestamps.
57    #[serde(flatten)]
58    header: Header,
59    /// Canonical target run for this execution-phase fact.
60    run_id: Uuid,
61    /// Execution-phase transition kind being recorded.
62    kind: RunEventKind,
63    /// Optional human-readable explanation of the phase change.
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    reason: Option<String>,
66    /// Optional human-readable error summary for failure cases.
67    #[serde(default, skip_serializing_if = "Option::is_none")]
68    error: Option<String>,
69    /// Optional structured metrics captured for this event.
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    metrics: Option<serde_json::Value>,
72    /// Optional patchset associated with this run-phase fact.
73    #[serde(default, skip_serializing_if = "Option::is_none")]
74    patchset_id: Option<Uuid>,
75}
76
77impl RunEvent {
78    /// Create a new execution-phase event for the given run.
79    pub fn new(created_by: ActorRef, run_id: Uuid, kind: RunEventKind) -> Result<Self, String> {
80        Ok(Self {
81            header: Header::new(ObjectType::RunEvent, created_by)?,
82            run_id,
83            kind,
84            reason: None,
85            error: None,
86            metrics: None,
87            patchset_id: None,
88        })
89    }
90
91    /// Return the immutable header for this event.
92    pub fn header(&self) -> &Header {
93        &self.header
94    }
95
96    /// Return the canonical target run id.
97    pub fn run_id(&self) -> Uuid {
98        self.run_id
99    }
100
101    /// Return the execution-phase transition kind.
102    pub fn kind(&self) -> &RunEventKind {
103        &self.kind
104    }
105
106    /// Return the human-readable reason, if present.
107    pub fn reason(&self) -> Option<&str> {
108        self.reason.as_deref()
109    }
110
111    /// Return the human-readable error message, if present.
112    pub fn error(&self) -> Option<&str> {
113        self.error.as_deref()
114    }
115
116    /// Return structured metrics, if present.
117    pub fn metrics(&self) -> Option<&serde_json::Value> {
118        self.metrics.as_ref()
119    }
120
121    /// Return the associated patchset id, if present.
122    pub fn patchset_id(&self) -> Option<Uuid> {
123        self.patchset_id
124    }
125
126    /// Set or clear the human-readable reason.
127    pub fn set_reason(&mut self, reason: Option<String>) {
128        self.reason = reason;
129    }
130
131    /// Set or clear the human-readable error message.
132    pub fn set_error(&mut self, error: Option<String>) {
133        self.error = error;
134    }
135
136    /// Set or clear the structured metrics payload.
137    pub fn set_metrics(&mut self, metrics: Option<serde_json::Value>) {
138        self.metrics = metrics;
139    }
140
141    /// Set or clear the associated patchset id.
142    pub fn set_patchset_id(&mut self, patchset_id: Option<Uuid>) {
143        self.patchset_id = patchset_id;
144    }
145}
146
147impl fmt::Display for RunEvent {
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        write!(f, "RunEvent: {}", self.header.object_id())
150    }
151}
152
153impl ObjectTrait for RunEvent {
154    fn from_bytes(data: &[u8], _hash: ObjectHash) -> Result<Self, GitError>
155    where
156        Self: Sized,
157    {
158        serde_json::from_slice(data).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
159    }
160
161    fn get_type(&self) -> ObjectType {
162        ObjectType::RunEvent
163    }
164
165    fn get_size(&self) -> usize {
166        match serde_json::to_vec(self) {
167            Ok(v) => v.len(),
168            Err(e) => {
169                tracing::warn!("failed to compute RunEvent size: {}", e);
170                0
171            }
172        }
173    }
174
175    fn to_data(&self) -> Result<Vec<u8>, GitError> {
176        serde_json::to_vec(self).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    // Coverage:
185    // - failed run-event creation
186    // - rationale, error message, metrics, and patchset association
187
188    #[test]
189    fn test_run_event_fields() {
190        let actor = ActorRef::agent("planner").expect("actor");
191        let mut event =
192            RunEvent::new(actor, Uuid::from_u128(0x1), RunEventKind::Failed).expect("event");
193        let patchset_id = Uuid::from_u128(0x2);
194        event.set_reason(Some("validation failed".to_string()));
195        event.set_error(Some("cargo test failed".to_string()));
196        event.set_metrics(Some(serde_json::json!({"duration_ms": 1200})));
197        event.set_patchset_id(Some(patchset_id));
198
199        assert_eq!(event.kind(), &RunEventKind::Failed);
200        assert_eq!(event.reason(), Some("validation failed"));
201        assert_eq!(event.error(), Some("cargo test failed"));
202        assert_eq!(event.patchset_id(), Some(patchset_id));
203    }
204}