1use chrono::{DateTime, Utc};
2use serde::de::DeserializeOwned;
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5
6use crate::error::{FlowError, Result};
7
8use super::{
9 CancellationRequestSnapshot, ChildOperationReference, JsonValue, RetryPolicy, WorkflowProgress,
10 WorkflowSpec, WorkflowTerminalOutcome,
11};
12
13#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
14#[serde(rename_all = "snake_case")]
15pub enum WorkflowRunStatus {
16 Pending,
17 Running,
18 Suspended,
19 Cancelling,
20 Completed,
21 Failed,
22 Cancelled,
23}
24
25impl WorkflowRunStatus {
26 pub fn is_terminal(self) -> bool {
27 matches!(self, Self::Completed | Self::Failed | Self::Cancelled)
28 }
29}
30
31#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
32#[serde(rename_all = "snake_case")]
33pub enum StepStatus {
34 Pending,
35 Running,
36 Completed,
37 Failed,
38 Cancelled,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
42pub struct StepSnapshot {
43 pub step_id: String,
44 pub step_name: String,
45 pub status: StepStatus,
46 pub input: JsonValue,
47 pub retry: RetryPolicy,
48 pub output: Option<JsonValue>,
49 pub error: Option<String>,
50 pub attempt: u32,
51 pub retry_after: Option<DateTime<Utc>>,
52}
53
54impl StepSnapshot {
55 pub fn output_as<T>(&self) -> Result<Option<T>>
57 where
58 T: DeserializeOwned,
59 {
60 self.output
61 .clone()
62 .map(serde_json::from_value)
63 .transpose()
64 .map_err(FlowError::from)
65 }
66}
67
68#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
69#[serde(rename_all = "snake_case")]
70pub enum WaitStatus {
71 Waiting,
72 Completed,
73 Cancelled,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
77pub struct WaitSnapshot {
78 pub wait_id: String,
79 pub status: WaitStatus,
80 pub resume_at: DateTime<Utc>,
81}
82
83#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
84#[serde(rename_all = "snake_case")]
85pub enum HookStatus {
86 Active,
87 Received,
88 Disposed,
89 Cancelled,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
93pub struct HookSnapshot {
94 pub hook_id: String,
95 pub token: String,
96 pub status: HookStatus,
97 pub metadata: JsonValue,
98 pub payload: Option<JsonValue>,
99}
100
101impl HookSnapshot {
102 pub fn metadata_as<T>(&self) -> Result<T>
104 where
105 T: DeserializeOwned,
106 {
107 serde_json::from_value(self.metadata.clone()).map_err(FlowError::from)
108 }
109
110 pub fn payload_as<T>(&self) -> Result<Option<T>>
112 where
113 T: DeserializeOwned,
114 {
115 self.payload
116 .clone()
117 .map(serde_json::from_value)
118 .transpose()
119 .map_err(FlowError::from)
120 }
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
125pub struct ActiveHookSnapshot {
126 pub run_id: String,
127 pub hook: HookSnapshot,
128}
129
130impl ActiveHookSnapshot {
131 pub fn metadata_as<T>(&self) -> Result<T>
133 where
134 T: DeserializeOwned,
135 {
136 self.hook.metadata_as()
137 }
138}
139
140#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
142#[serde(default)]
143pub struct WorkflowRunSummary {
144 pub total_runs: usize,
145 pub pending_runs: usize,
146 pub running_runs: usize,
147 pub suspended_runs: usize,
148 pub cancelling_runs: usize,
149 pub completed_runs: usize,
150 pub failed_runs: usize,
151 pub cancelled_runs: usize,
152 pub terminal_runs: usize,
153 pub non_terminal_runs: usize,
154 pub open_waits: usize,
155 pub active_hooks: usize,
156 pub pending_retries: usize,
157}
158
159impl WorkflowRunSummary {
160 pub fn from_snapshots(snapshots: &[WorkflowRunSnapshot]) -> Self {
161 let mut summary = Self::default();
162 for snapshot in snapshots {
163 summary.record(snapshot);
164 }
165 summary
166 }
167
168 pub fn record(&mut self, snapshot: &WorkflowRunSnapshot) {
169 self.total_runs += 1;
170 match snapshot.status {
171 WorkflowRunStatus::Pending => self.pending_runs += 1,
172 WorkflowRunStatus::Running => self.running_runs += 1,
173 WorkflowRunStatus::Suspended => self.suspended_runs += 1,
174 WorkflowRunStatus::Cancelling => self.cancelling_runs += 1,
175 WorkflowRunStatus::Completed => self.completed_runs += 1,
176 WorkflowRunStatus::Failed => self.failed_runs += 1,
177 WorkflowRunStatus::Cancelled => self.cancelled_runs += 1,
178 }
179
180 if snapshot.status.is_terminal() {
181 self.terminal_runs += 1;
182 return;
183 }
184
185 self.non_terminal_runs += 1;
186 self.open_waits += snapshot
187 .waits
188 .values()
189 .filter(|wait| wait.status == WaitStatus::Waiting)
190 .count();
191 self.active_hooks += snapshot
192 .hooks
193 .values()
194 .filter(|hook| hook.status == HookStatus::Active)
195 .count();
196 self.pending_retries += snapshot
197 .steps
198 .values()
199 .filter(|step| step.status == StepStatus::Pending && step.retry_after.is_some())
200 .count();
201 }
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
206#[serde(tag = "kind", rename_all = "snake_case")]
207pub enum WorkflowRunSuspension {
208 Wait {
209 run_id: String,
210 wait: WaitSnapshot,
211 due: bool,
212 },
213 Hook {
214 run_id: String,
215 hook: HookSnapshot,
216 },
217 Retry {
218 run_id: String,
219 step: StepSnapshot,
220 due: bool,
221 },
222}
223
224impl WorkflowRunSuspension {
225 pub fn run_id(&self) -> &str {
226 match self {
227 Self::Wait { run_id, .. } | Self::Hook { run_id, .. } | Self::Retry { run_id, .. } => {
228 run_id
229 }
230 }
231 }
232
233 pub fn subject_id(&self) -> &str {
234 match self {
235 Self::Wait { wait, .. } => &wait.wait_id,
236 Self::Hook { hook, .. } => &hook.hook_id,
237 Self::Retry { step, .. } => &step.step_id,
238 }
239 }
240
241 pub(crate) fn kind_order(&self) -> u8 {
242 match self {
243 Self::Wait { .. } => 0,
244 Self::Hook { .. } => 1,
245 Self::Retry { .. } => 2,
246 }
247 }
248
249 pub fn is_due(&self) -> bool {
250 match self {
251 Self::Wait { due, .. } | Self::Retry { due, .. } => *due,
252 Self::Hook { .. } => false,
253 }
254 }
255
256 pub fn scheduled_at(&self) -> Option<DateTime<Utc>> {
258 match self {
259 Self::Wait { wait, .. } => Some(wait.resume_at),
260 Self::Retry { step, .. } => step.retry_after,
261 Self::Hook { .. } => None,
262 }
263 }
264}
265
266#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
268pub struct WorkflowRunSnapshot {
269 pub run_id: String,
270 pub spec: WorkflowSpec,
271 pub input: JsonValue,
272 pub status: WorkflowRunStatus,
273 pub steps: BTreeMap<String, StepSnapshot>,
274 pub waits: BTreeMap<String, WaitSnapshot>,
275 pub hooks: BTreeMap<String, HookSnapshot>,
276 #[serde(default)]
277 pub cancellation: Option<CancellationRequestSnapshot>,
278 #[serde(default)]
279 pub progress: Vec<WorkflowProgress>,
280 #[serde(default)]
281 pub child_operations: BTreeMap<String, ChildOperationReference>,
282 pub output: Option<JsonValue>,
283 pub error: Option<String>,
284 #[serde(default)]
285 pub terminal_outcome: Option<WorkflowTerminalOutcome>,
286 pub last_sequence: u64,
287}
288
289impl WorkflowRunSnapshot {
290 pub fn input_as<T>(&self) -> Result<T>
292 where
293 T: DeserializeOwned,
294 {
295 serde_json::from_value(self.input.clone()).map_err(FlowError::from)
296 }
297
298 pub fn output_as<T>(&self) -> Result<Option<T>>
300 where
301 T: DeserializeOwned,
302 {
303 self.output
304 .clone()
305 .map(serde_json::from_value)
306 .transpose()
307 .map_err(FlowError::from)
308 }
309
310 pub fn step_output(&self, step_id: &str) -> Option<&JsonValue> {
311 self.steps
312 .get(step_id)
313 .and_then(|step| step.output.as_ref())
314 }
315
316 pub fn step_output_as<T>(&self, step_id: &str) -> Result<Option<T>>
318 where
319 T: DeserializeOwned,
320 {
321 match self.steps.get(step_id) {
322 Some(step) => step.output_as(),
323 None => Ok(None),
324 }
325 }
326
327 pub fn hook_payload(&self, hook_id: &str) -> Option<&JsonValue> {
328 self.hooks
329 .get(hook_id)
330 .and_then(|hook| hook.payload.as_ref())
331 }
332
333 pub fn progress(&self, progress_id: &str) -> Option<&WorkflowProgress> {
335 self.progress
336 .iter()
337 .find(|progress| progress.progress_id == progress_id)
338 }
339
340 pub fn latest_progress(&self) -> Option<&WorkflowProgress> {
342 self.progress.last()
343 }
344
345 pub fn child_operation(&self, reference_id: &str) -> Option<&ChildOperationReference> {
347 self.child_operations.get(reference_id)
348 }
349
350 pub fn hook_metadata_as<T>(&self, hook_id: &str) -> Result<Option<T>>
352 where
353 T: DeserializeOwned,
354 {
355 match self.hooks.get(hook_id) {
356 Some(hook) => hook.metadata_as().map(Some),
357 None => Ok(None),
358 }
359 }
360
361 pub fn hook_payload_as<T>(&self, hook_id: &str) -> Result<Option<T>>
363 where
364 T: DeserializeOwned,
365 {
366 match self.hooks.get(hook_id) {
367 Some(hook) => hook.payload_as(),
368 None => Ok(None),
369 }
370 }
371
372 pub fn has_open_suspension(&self) -> bool {
373 self.waits
374 .values()
375 .any(|wait| wait.status == WaitStatus::Waiting)
376 || self
377 .hooks
378 .values()
379 .any(|hook| hook.status == HookStatus::Active)
380 || self.steps.values().any(|step| step.retry_after.is_some())
381 }
382
383 pub fn due_retries(&self, now: DateTime<Utc>) -> Vec<(String, DateTime<Utc>)> {
384 self.steps
385 .values()
386 .filter_map(|step| match step.retry_after {
387 Some(retry_after) if step.status == StepStatus::Pending && retry_after <= now => {
388 Some((step.step_id.clone(), retry_after))
389 }
390 _ => None,
391 })
392 .collect()
393 }
394
395 pub fn has_future_retry(&self, now: DateTime<Utc>) -> bool {
396 self.steps.values().any(|step| {
397 step.status == StepStatus::Pending
398 && step
399 .retry_after
400 .map(|retry_after| retry_after > now)
401 .unwrap_or(false)
402 })
403 }
404}