Skip to main content

dpm_model/run/
lifecycle.rs

1//! Durable run lifecycle: the transitions that are facts rather than telemetry.
2
3use crate::{ActorId, RunEventId, RunId};
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use std::str::FromStr;
7
8/// Where a run stands according to its last recorded transition.
9///
10/// This is lifecycle truth only. Whether anything has been heard from the run lately is a
11/// separate, derived question (see `ObservedStatus`), so silence never edits this state.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum RunState {
15    /// The executor is working.
16    Working,
17    /// The executor waits for input, approval or another party.
18    Waiting,
19    /// The run ended without completing its work.
20    Failed,
21    /// The run was stopped before it finished.
22    Interrupted,
23    /// The executor finished. The work is unchanged: submission and verification are separate.
24    Completed,
25}
26
27impl RunState {
28    /// Whether no transition may follow.
29    #[must_use]
30    pub fn is_terminal(self) -> bool {
31        matches!(self, Self::Failed | Self::Interrupted | Self::Completed)
32    }
33
34    /// The lowercase word every adapter spells the state with.
35    #[must_use]
36    pub fn word(self) -> &'static str {
37        match self {
38            Self::Working => "working",
39            Self::Waiting => "waiting",
40            Self::Failed => "failed",
41            Self::Interrupted => "interrupted",
42            Self::Completed => "completed",
43        }
44    }
45}
46
47impl std::fmt::Display for RunState {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        f.write_str(self.word())
50    }
51}
52
53/// A state word no run has.
54#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
55#[error("unknown run state {0:?}; expected working, waiting, failed, interrupted or completed")]
56pub struct RunStateParseError(String);
57
58impl FromStr for RunState {
59    type Err = RunStateParseError;
60
61    fn from_str(word: &str) -> Result<Self, Self::Err> {
62        [
63            Self::Working,
64            Self::Waiting,
65            Self::Failed,
66            Self::Interrupted,
67            Self::Completed,
68        ]
69        .into_iter()
70        .find(|state| state.word() == word)
71        .ok_or_else(|| RunStateParseError(word.into()))
72    }
73}
74
75/// What a caller asks to record as a run's next lifecycle transition.
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
77#[serde(deny_unknown_fields)]
78pub struct RunTransition {
79    /// Transition identity and idempotency key, a version 7 UUID.
80    pub id: RunEventId,
81    /// The run that changes state.
82    pub run: RunId,
83    /// The state it moves to.
84    pub to: RunState,
85    /// Why, in the executor's words; a failure or interruption should say what happened.
86    #[serde(default, skip_serializing_if = "Option::is_none")]
87    pub detail: Option<String>,
88    /// When the executor says it happened. It is a claim, never used to judge freshness.
89    #[serde(default, skip_serializing_if = "Option::is_none")]
90    pub observed_at: Option<DateTime<Utc>>,
91}
92
93/// One recorded lifecycle fact. A run's start is its first one, with the run's identity as its own.
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
95#[serde(deny_unknown_fields)]
96pub struct LifecycleEvent {
97    /// Transition identity.
98    pub id: RunEventId,
99    /// The run it belongs to.
100    pub run: RunId,
101    /// The state the run entered.
102    pub state: RunState,
103    /// Why, in the reporter's words.
104    #[serde(default, skip_serializing_if = "Option::is_none")]
105    pub detail: Option<String>,
106    /// When the reporter says it happened.
107    #[serde(default, skip_serializing_if = "Option::is_none")]
108    pub observed_at: Option<DateTime<Utc>>,
109    /// The principal that reported it.
110    pub recorded_by: ActorId,
111    /// When DPM recorded it; the receipt time, from DPM's own clock.
112    pub recorded_at: DateTime<Utc>,
113}
114
115/// A lifecycle fact addressed by its position in the lifecycle feed.
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117pub struct LifecycleEntry {
118    /// Feed cursor value; lifecycle facts are never removed, so the feed has no gaps.
119    pub sequence: u64,
120    /// The fact.
121    #[serde(flatten)]
122    pub event: LifecycleEvent,
123}
124
125/// A bounded page of the lifecycle feed.
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
127pub struct LifecyclePage {
128    /// Facts in ascending feed order.
129    pub entries: Vec<LifecycleEntry>,
130    /// Cursor for the next request; unchanged for an empty page.
131    pub next_after_sequence: u64,
132    /// Newest sequence in the whole feed, so a client can tell it is caught up or was reset.
133    pub head_sequence: u64,
134}