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, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
142#[serde(rename_all = "snake_case")]
143pub enum ScheduledWakeupKind {
144 Wait,
145 Retry,
146}
147
148impl ScheduledWakeupKind {
149 #[cfg(any(feature = "postgres", feature = "sqlite"))]
150 pub(crate) fn from_database_code(code: i64) -> Result<Self> {
151 match code {
152 0 => Ok(Self::Wait),
153 2 => Ok(Self::Retry),
154 _ => Err(FlowError::Store(format!(
155 "invalid scheduled wakeup kind code {code}"
156 ))),
157 }
158 }
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
163pub struct ScheduledWakeup {
164 pub run_id: String,
165 pub kind: ScheduledWakeupKind,
166 pub subject_id: String,
167 pub scheduled_at: DateTime<Utc>,
168}
169
170#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
172#[serde(default)]
173pub struct WorkflowRunSummary {
174 pub total_runs: usize,
175 pub pending_runs: usize,
176 pub running_runs: usize,
177 pub suspended_runs: usize,
178 pub cancelling_runs: usize,
179 pub completed_runs: usize,
180 pub failed_runs: usize,
181 pub cancelled_runs: usize,
182 pub terminal_runs: usize,
183 pub non_terminal_runs: usize,
184 pub open_waits: usize,
185 pub active_hooks: usize,
186 pub pending_retries: usize,
187}
188
189impl WorkflowRunSummary {
190 pub fn from_snapshots(snapshots: &[WorkflowRunSnapshot]) -> Self {
191 let mut summary = Self::default();
192 for snapshot in snapshots {
193 summary.record(snapshot);
194 }
195 summary
196 }
197
198 pub fn record(&mut self, snapshot: &WorkflowRunSnapshot) {
199 self.total_runs += 1;
200 match snapshot.status {
201 WorkflowRunStatus::Pending => self.pending_runs += 1,
202 WorkflowRunStatus::Running => self.running_runs += 1,
203 WorkflowRunStatus::Suspended => self.suspended_runs += 1,
204 WorkflowRunStatus::Cancelling => self.cancelling_runs += 1,
205 WorkflowRunStatus::Completed => self.completed_runs += 1,
206 WorkflowRunStatus::Failed => self.failed_runs += 1,
207 WorkflowRunStatus::Cancelled => self.cancelled_runs += 1,
208 }
209
210 if snapshot.status.is_terminal() {
211 self.terminal_runs += 1;
212 return;
213 }
214
215 self.non_terminal_runs += 1;
216 self.open_waits += snapshot
217 .waits
218 .values()
219 .filter(|wait| wait.status == WaitStatus::Waiting)
220 .count();
221 self.active_hooks += snapshot
222 .hooks
223 .values()
224 .filter(|hook| hook.status == HookStatus::Active)
225 .count();
226 self.pending_retries += snapshot
227 .steps
228 .values()
229 .filter(|step| step.status == StepStatus::Pending && step.retry_after.is_some())
230 .count();
231 }
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
236#[serde(tag = "kind", rename_all = "snake_case")]
237pub enum WorkflowRunSuspension {
238 Wait {
239 run_id: String,
240 wait: WaitSnapshot,
241 due: bool,
242 },
243 Hook {
244 run_id: String,
245 hook: HookSnapshot,
246 },
247 Retry {
248 run_id: String,
249 step: StepSnapshot,
250 due: bool,
251 },
252}
253
254impl WorkflowRunSuspension {
255 pub fn run_id(&self) -> &str {
256 match self {
257 Self::Wait { run_id, .. } | Self::Hook { run_id, .. } | Self::Retry { run_id, .. } => {
258 run_id
259 }
260 }
261 }
262
263 pub fn subject_id(&self) -> &str {
264 match self {
265 Self::Wait { wait, .. } => &wait.wait_id,
266 Self::Hook { hook, .. } => &hook.hook_id,
267 Self::Retry { step, .. } => &step.step_id,
268 }
269 }
270
271 pub(crate) fn kind_order(&self) -> u8 {
272 match self {
273 Self::Wait { .. } => 0,
274 Self::Hook { .. } => 1,
275 Self::Retry { .. } => 2,
276 }
277 }
278
279 pub fn is_due(&self) -> bool {
280 match self {
281 Self::Wait { due, .. } | Self::Retry { due, .. } => *due,
282 Self::Hook { .. } => false,
283 }
284 }
285
286 pub fn scheduled_at(&self) -> Option<DateTime<Utc>> {
288 match self {
289 Self::Wait { wait, .. } => Some(wait.resume_at),
290 Self::Retry { step, .. } => step.retry_after,
291 Self::Hook { .. } => None,
292 }
293 }
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
298pub struct WorkflowRunSnapshot {
299 pub run_id: String,
300 pub spec: WorkflowSpec,
301 pub input: JsonValue,
302 pub status: WorkflowRunStatus,
303 pub steps: BTreeMap<String, StepSnapshot>,
304 pub waits: BTreeMap<String, WaitSnapshot>,
305 pub hooks: BTreeMap<String, HookSnapshot>,
306 #[serde(default)]
307 pub cancellation: Option<CancellationRequestSnapshot>,
308 #[serde(default)]
309 pub progress: Vec<WorkflowProgress>,
310 #[serde(default)]
311 pub child_operations: BTreeMap<String, ChildOperationReference>,
312 pub output: Option<JsonValue>,
313 pub error: Option<String>,
314 #[serde(default)]
315 pub terminal_outcome: Option<WorkflowTerminalOutcome>,
316 pub last_sequence: u64,
317}
318
319impl WorkflowRunSnapshot {
320 pub fn input_as<T>(&self) -> Result<T>
322 where
323 T: DeserializeOwned,
324 {
325 serde_json::from_value(self.input.clone()).map_err(FlowError::from)
326 }
327
328 pub fn output_as<T>(&self) -> Result<Option<T>>
330 where
331 T: DeserializeOwned,
332 {
333 self.output
334 .clone()
335 .map(serde_json::from_value)
336 .transpose()
337 .map_err(FlowError::from)
338 }
339
340 pub fn step_output(&self, step_id: &str) -> Option<&JsonValue> {
341 self.steps
342 .get(step_id)
343 .and_then(|step| step.output.as_ref())
344 }
345
346 pub fn step_output_as<T>(&self, step_id: &str) -> Result<Option<T>>
348 where
349 T: DeserializeOwned,
350 {
351 match self.steps.get(step_id) {
352 Some(step) => step.output_as(),
353 None => Ok(None),
354 }
355 }
356
357 pub fn hook_payload(&self, hook_id: &str) -> Option<&JsonValue> {
358 self.hooks
359 .get(hook_id)
360 .and_then(|hook| hook.payload.as_ref())
361 }
362
363 pub fn progress(&self, progress_id: &str) -> Option<&WorkflowProgress> {
365 self.progress
366 .iter()
367 .find(|progress| progress.progress_id == progress_id)
368 }
369
370 pub fn latest_progress(&self) -> Option<&WorkflowProgress> {
372 self.progress.last()
373 }
374
375 pub fn child_operation(&self, reference_id: &str) -> Option<&ChildOperationReference> {
377 self.child_operations.get(reference_id)
378 }
379
380 pub fn hook_metadata_as<T>(&self, hook_id: &str) -> Result<Option<T>>
382 where
383 T: DeserializeOwned,
384 {
385 match self.hooks.get(hook_id) {
386 Some(hook) => hook.metadata_as().map(Some),
387 None => Ok(None),
388 }
389 }
390
391 pub fn hook_payload_as<T>(&self, hook_id: &str) -> Result<Option<T>>
393 where
394 T: DeserializeOwned,
395 {
396 match self.hooks.get(hook_id) {
397 Some(hook) => hook.payload_as(),
398 None => Ok(None),
399 }
400 }
401
402 pub fn has_open_suspension(&self) -> bool {
403 self.waits
404 .values()
405 .any(|wait| wait.status == WaitStatus::Waiting)
406 || self
407 .hooks
408 .values()
409 .any(|hook| hook.status == HookStatus::Active)
410 || self.steps.values().any(|step| step.retry_after.is_some())
411 }
412
413 pub fn due_retries(&self, now: DateTime<Utc>) -> Vec<(String, DateTime<Utc>)> {
414 self.steps
415 .values()
416 .filter_map(|step| match step.retry_after {
417 Some(retry_after) if step.status == StepStatus::Pending && retry_after <= now => {
418 Some((step.step_id.clone(), retry_after))
419 }
420 _ => None,
421 })
422 .collect()
423 }
424
425 pub fn has_future_retry(&self, now: DateTime<Utc>) -> bool {
426 self.steps.values().any(|step| {
427 step.status == StepStatus::Pending
428 && step
429 .retry_after
430 .map(|retry_after| retry_after > now)
431 .unwrap_or(false)
432 })
433 }
434}