Skip to main content

git_internal/internal/object/
task_event.rs

1//! Task lifecycle event.
2//!
3//! `TaskEvent` records append-only lifecycle changes for a `Task`.
4//!
5//! # How to use this object
6//!
7//! - Append events as the scheduler creates, starts, blocks, finishes,
8//!   fails, or cancels task execution.
9//! - Set `run_id` when the event is associated with a concrete `Run`.
10//! - Keep the `Task` snapshot itself stable.
11//!
12//! # How it works with other objects
13//!
14//! - `TaskEvent.task_id` points at the durable task definition.
15//! - `run_id` optionally links the state change to a specific execution
16//!   attempt.
17//!
18//! # How Libra should call it
19//!
20//! Libra should reconstruct current task status from event history and
21//! scheduler state instead of storing mutable task status inside
22//! `Task`.
23
24use std::fmt;
25
26use serde::{Deserialize, Serialize};
27use uuid::Uuid;
28
29use crate::{
30    errors::GitError,
31    hash::ObjectHash,
32    internal::object::{
33        ObjectTrait,
34        types::{ActorRef, Header, ObjectType},
35    },
36};
37
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
39#[serde(rename_all = "snake_case")]
40pub enum TaskEventKind {
41    Created,
42    Running,
43    Blocked,
44    Done,
45    Failed,
46    Cancelled,
47}
48
49/// Append-only lifecycle fact for one `Task`.
50#[derive(Debug, Clone, Serialize, Deserialize)]
51#[serde(deny_unknown_fields)]
52pub struct TaskEvent {
53    /// Common object header carrying the immutable object id, type,
54    /// creator, and timestamps.
55    #[serde(flatten)]
56    header: Header,
57    /// Canonical target task for this lifecycle fact.
58    task_id: Uuid,
59    /// Lifecycle transition kind being recorded.
60    kind: TaskEventKind,
61    /// Optional human-readable explanation for the transition.
62    #[serde(default, skip_serializing_if = "Option::is_none")]
63    reason: Option<String>,
64    /// Optional run associated with the transition.
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    run_id: Option<Uuid>,
67}
68
69impl TaskEvent {
70    /// Create a new lifecycle event for the given task.
71    pub fn new(created_by: ActorRef, task_id: Uuid, kind: TaskEventKind) -> Result<Self, String> {
72        Ok(Self {
73            header: Header::new(ObjectType::TaskEvent, created_by)?,
74            task_id,
75            kind,
76            reason: None,
77            run_id: None,
78        })
79    }
80
81    /// Return the immutable header for this event.
82    pub fn header(&self) -> &Header {
83        &self.header
84    }
85
86    /// Return the canonical target task id.
87    pub fn task_id(&self) -> Uuid {
88        self.task_id
89    }
90
91    /// Return the lifecycle transition kind.
92    pub fn kind(&self) -> &TaskEventKind {
93        &self.kind
94    }
95
96    /// Return the human-readable explanation, if present.
97    pub fn reason(&self) -> Option<&str> {
98        self.reason.as_deref()
99    }
100
101    /// Return the associated run id, if present.
102    pub fn run_id(&self) -> Option<Uuid> {
103        self.run_id
104    }
105
106    /// Set or clear the human-readable explanation.
107    pub fn set_reason(&mut self, reason: Option<String>) {
108        self.reason = reason;
109    }
110
111    /// Set or clear the associated run id.
112    pub fn set_run_id(&mut self, run_id: Option<Uuid>) {
113        self.run_id = run_id;
114    }
115}
116
117impl fmt::Display for TaskEvent {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        write!(f, "TaskEvent: {}", self.header.object_id())
120    }
121}
122
123impl ObjectTrait for TaskEvent {
124    fn from_bytes(data: &[u8], _hash: ObjectHash) -> Result<Self, GitError>
125    where
126        Self: Sized,
127    {
128        serde_json::from_slice(data).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
129    }
130
131    fn get_type(&self) -> ObjectType {
132        ObjectType::TaskEvent
133    }
134
135    fn get_size(&self) -> usize {
136        match serde_json::to_vec(self) {
137            Ok(v) => v.len(),
138            Err(e) => {
139                tracing::warn!("failed to compute TaskEvent size: {}", e);
140                0
141            }
142        }
143    }
144
145    fn to_data(&self) -> Result<Vec<u8>, GitError> {
146        serde_json::to_vec(self).map_err(|e| GitError::InvalidObjectInfo(e.to_string()))
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    // Coverage:
155    // - task-event creation for running state
156    // - optional rationale and associated run linking
157
158    #[test]
159    fn test_task_event_fields() {
160        let actor = ActorRef::agent("planner").expect("actor");
161        let mut event =
162            TaskEvent::new(actor, Uuid::from_u128(0x1), TaskEventKind::Running).expect("event");
163        let run_id = Uuid::from_u128(0x2);
164        event.set_reason(Some("agent started".to_string()));
165        event.set_run_id(Some(run_id));
166
167        assert_eq!(event.kind(), &TaskEventKind::Running);
168        assert_eq!(event.reason(), Some("agent started"));
169        assert_eq!(event.run_id(), Some(run_id));
170    }
171}