1use serde::{Deserialize, Serialize};
4
5use crate::{Event, RunId};
6
7#[derive(Serialize, Deserialize, ts_rs::TS, Clone, Copy, Debug, PartialEq, Eq)]
13pub enum WorkflowStatus {
14 Running,
16 Completed,
18 Failed,
20 Cancelled,
22 TimedOut,
24 ContinuedAsNew,
26 Paused,
30}
31
32impl WorkflowStatus {
33 #[must_use]
35 pub const fn is_terminal(self) -> bool {
36 match self {
37 Self::Running | Self::Paused => false,
40 Self::Completed
41 | Self::Failed
42 | Self::Cancelled
43 | Self::TimedOut
44 | Self::ContinuedAsNew => true,
45 }
46 }
47}
48
49#[must_use]
57pub fn status_from_events(events: &[Event]) -> WorkflowStatus {
58 events
59 .iter()
60 .rev()
61 .find_map(|event| match event {
62 Event::WorkflowStarted { .. }
69 | Event::WorkflowReopened { .. }
70 | Event::WorkflowResumed { .. } => Some(WorkflowStatus::Running),
71 Event::WorkflowCompleted { .. } => Some(WorkflowStatus::Completed),
72 Event::WorkflowFailed { .. } => Some(WorkflowStatus::Failed),
73 Event::WorkflowCancelled { .. } => Some(WorkflowStatus::Cancelled),
74 Event::WorkflowTimedOut { .. } => Some(WorkflowStatus::TimedOut),
75 Event::WorkflowContinuedAsNew { .. } => Some(WorkflowStatus::ContinuedAsNew),
76 Event::WorkflowPaused { .. } => Some(WorkflowStatus::Paused),
79 Event::SearchAttributesUpdated { .. }
80 | Event::ActivityScheduled { .. }
81 | Event::ActivityStarted { .. }
82 | Event::ActivityCompleted { .. }
83 | Event::ActivityFailed { .. }
84 | Event::ActivityCancelled { .. }
85 | Event::TimerStarted { .. }
86 | Event::TimerFired { .. }
87 | Event::TimerCancelled { .. }
88 | Event::WithTimeoutCompleted { .. }
89 | Event::SignalReceived { .. }
90 | Event::SignalSent { .. }
91 | Event::ChildWorkflowStarted { .. }
92 | Event::ChildWorkflowCompleted { .. }
93 | Event::ChildWorkflowFailed { .. }
94 | Event::ChildWorkflowCancelled { .. }
95 | Event::ScheduleCreated { .. }
96 | Event::ScheduleUpdated { .. }
97 | Event::SchedulePaused { .. }
98 | Event::ScheduleResumed { .. }
99 | Event::ScheduleDeleted { .. }
100 | Event::ScheduleTriggered { .. } => None,
101 })
102 .unwrap_or(WorkflowStatus::Running)
103}
104
105#[must_use]
116pub fn current_lease_terminal(events: &[Event]) -> Option<&Event> {
117 events
118 .iter()
119 .rev()
120 .find_map(|event| match event {
121 Event::WorkflowCompleted { .. }
122 | Event::WorkflowFailed { .. }
123 | Event::WorkflowCancelled { .. }
124 | Event::WorkflowTimedOut { .. }
125 | Event::WorkflowContinuedAsNew { .. } => Some(Some(event)),
126 Event::WorkflowStarted { .. } | Event::WorkflowReopened { .. } => Some(None),
128 Event::WorkflowPaused { .. }
133 | Event::WorkflowResumed { .. }
134 | Event::SearchAttributesUpdated { .. }
135 | Event::ActivityScheduled { .. }
136 | Event::ActivityStarted { .. }
137 | Event::ActivityCompleted { .. }
138 | Event::ActivityFailed { .. }
139 | Event::ActivityCancelled { .. }
140 | Event::TimerStarted { .. }
141 | Event::TimerFired { .. }
142 | Event::TimerCancelled { .. }
143 | Event::WithTimeoutCompleted { .. }
144 | Event::SignalReceived { .. }
145 | Event::SignalSent { .. }
146 | Event::ChildWorkflowStarted { .. }
147 | Event::ChildWorkflowCompleted { .. }
148 | Event::ChildWorkflowFailed { .. }
149 | Event::ChildWorkflowCancelled { .. }
150 | Event::ScheduleCreated { .. }
151 | Event::ScheduleUpdated { .. }
152 | Event::SchedulePaused { .. }
153 | Event::ScheduleResumed { .. }
154 | Event::ScheduleDeleted { .. }
155 | Event::ScheduleTriggered { .. } => None,
156 })
157 .flatten()
158}
159
160#[must_use]
168pub fn run_segment<'a>(events: &'a [Event], run_id: &RunId) -> &'a [Event] {
169 let Some(start) = events.iter().position(
170 |event| matches!(event, Event::WorkflowStarted { run_id: id, .. } if id == run_id),
171 ) else {
172 return &[];
173 };
174 let end = events[start + 1..]
175 .iter()
176 .position(|event| matches!(event, Event::WorkflowStarted { .. }))
177 .map_or(events.len(), |offset| start + 1 + offset);
178 &events[start..end]
179}
180
181#[cfg(test)]
182mod tests {
183 use std::collections::HashMap;
184
185 use chrono::{DateTime, Utc};
186 use serde_json::json;
187
188 use super::{WorkflowStatus, current_lease_terminal, run_segment, status_from_events};
189 use crate::{
190 ActivityId, Event, EventEnvelope, Payload, RunId, ScheduleId, SearchAttributeValue,
191 WorkflowError, WorkflowId,
192 };
193
194 fn recorded_at(offset: i64) -> DateTime<Utc> {
195 DateTime::from_timestamp(1_700_000_000 + offset, 0).unwrap_or_default()
196 }
197
198 fn envelope(seq: u64) -> EventEnvelope {
199 EventEnvelope {
200 seq,
201 recorded_at: recorded_at(i64::try_from(seq).unwrap_or(0)),
202 workflow_id: WorkflowId::new(uuid::Uuid::nil()),
203 }
204 }
205
206 fn payload(label: &str) -> Result<Payload, crate::PayloadError> {
207 Payload::from_json(&json!({ "label": label }))
208 }
209
210 fn workflow_started(seq: u64) -> Result<Event, crate::PayloadError> {
211 Ok(Event::WorkflowStarted {
212 envelope: envelope(seq),
213 workflow_type: String::from("checkout"),
214 input: payload("input")?,
215 run_id: RunId::new(uuid::Uuid::from_u128(1)),
216 parent_run_id: None,
217 package_version: crate::PackageVersion::new("a".repeat(64)),
218 })
219 }
220
221 fn workflow_error(message: &str) -> WorkflowError {
222 WorkflowError {
223 message: String::from(message),
224 details: None,
225 }
226 }
227
228 #[test]
229 fn empty_history_projects_to_running() {
230 assert_eq!(status_from_events(&[]), WorkflowStatus::Running);
231 }
232
233 #[test]
234 fn replacement_start_projects_continue_as_new_chain_running() -> Result<(), crate::PayloadError>
235 {
236 let parent_run_id = RunId::new(uuid::Uuid::from_u128(7));
237 let events = vec![
238 workflow_started(1)?,
239 Event::WorkflowContinuedAsNew {
240 envelope: envelope(2),
241 input: payload("replacement")?,
242 workflow_type: None,
243 parent_run_id: parent_run_id.clone(),
244 },
245 Event::WorkflowStarted {
246 envelope: envelope(3),
247 workflow_type: String::from("checkout"),
248 input: payload("replacement")?,
249 run_id: RunId::new(uuid::Uuid::from_u128(1)),
250 parent_run_id: Some(parent_run_id),
251 package_version: crate::PackageVersion::new("a".repeat(64)),
252 },
253 ];
254
255 assert_eq!(status_from_events(&events), WorkflowStatus::Running);
256 Ok(())
257 }
258
259 #[test]
260 fn completed_terminal_event_projects_to_completed() -> Result<(), Box<dyn std::error::Error>> {
261 let events = vec![
262 workflow_started(1)?,
263 Event::WorkflowCompleted {
264 envelope: envelope(2),
265 result: payload("result")?,
266 },
267 ];
268
269 assert_eq!(status_from_events(&events), WorkflowStatus::Completed);
270 Ok(())
271 }
272
273 #[test]
274 fn failed_terminal_event_projects_to_failed() -> Result<(), Box<dyn std::error::Error>> {
275 let events = vec![
276 workflow_started(1)?,
277 Event::WorkflowFailed {
278 envelope: envelope(2),
279 error: workflow_error("failed"),
280 },
281 ];
282
283 assert_eq!(status_from_events(&events), WorkflowStatus::Failed);
284 Ok(())
285 }
286
287 #[test]
288 fn cancelled_terminal_event_projects_to_cancelled() -> Result<(), Box<dyn std::error::Error>> {
289 let events = vec![
290 workflow_started(1)?,
291 Event::WorkflowCancelled {
292 envelope: envelope(2),
293 reason: String::from("caller requested cancellation"),
294 },
295 ];
296
297 assert_eq!(status_from_events(&events), WorkflowStatus::Cancelled);
298 Ok(())
299 }
300
301 #[test]
302 fn timed_out_terminal_event_projects_to_timed_out() -> Result<(), Box<dyn std::error::Error>> {
303 let events = vec![
304 workflow_started(1)?,
305 Event::WorkflowTimedOut {
306 envelope: envelope(2),
307 timeout: String::from("execution"),
308 },
309 ];
310
311 assert_eq!(status_from_events(&events), WorkflowStatus::TimedOut);
312 Ok(())
313 }
314
315 #[test]
316 fn continued_as_new_projects_status() -> Result<(), Box<dyn std::error::Error>> {
317 let events = vec![
318 workflow_started(1)?,
319 Event::WorkflowContinuedAsNew {
320 envelope: envelope(2),
321 input: payload("continued-input")?,
322 workflow_type: Some(String::from("checkout-v2")),
323 parent_run_id: RunId::new(uuid::Uuid::from_u128(2)),
324 },
325 ];
326
327 assert_eq!(status_from_events(&events), WorkflowStatus::ContinuedAsNew);
328 Ok(())
329 }
330
331 #[test]
332 fn workflow_status_terminality_classifies_running_and_terminal_statuses() {
333 assert!(!WorkflowStatus::Running.is_terminal());
334 assert!(WorkflowStatus::Completed.is_terminal());
335 assert!(WorkflowStatus::Failed.is_terminal());
336 assert!(WorkflowStatus::Cancelled.is_terminal());
337 assert!(WorkflowStatus::TimedOut.is_terminal());
338 assert!(WorkflowStatus::ContinuedAsNew.is_terminal());
339 }
340
341 #[test]
342 fn started_then_continued_as_new_projects_status() -> Result<(), Box<dyn std::error::Error>> {
343 let events = vec![
344 workflow_started(1)?,
345 Event::WorkflowContinuedAsNew {
346 envelope: envelope(2),
347 input: payload("continued-input")?,
348 workflow_type: None,
349 parent_run_id: RunId::new(uuid::Uuid::from_u128(3)),
350 },
351 ];
352
353 assert_eq!(status_from_events(&events), WorkflowStatus::ContinuedAsNew);
354 Ok(())
355 }
356
357 #[test]
358 fn non_terminal_history_projects_to_running() -> Result<(), Box<dyn std::error::Error>> {
359 let events = vec![
360 workflow_started(1)?,
361 Event::SearchAttributesUpdated {
362 envelope: envelope(2),
363 workflow_id: WorkflowId::new(uuid::Uuid::nil()),
364 attributes: HashMap::from([(
365 String::from("customer_id"),
366 SearchAttributeValue::String(String::from("customer-123")),
367 )]),
368 },
369 Event::ActivityScheduled {
370 envelope: envelope(3),
371 activity_id: ActivityId::from_sequence_position(3),
372 activity_type: String::from("charge-card"),
373 input: payload("activity-input")?,
374 task_queue: String::from("default"),
375 node: None,
376 },
377 ];
378
379 assert_eq!(status_from_events(&events), WorkflowStatus::Running);
380 Ok(())
381 }
382
383 #[test]
384 fn schedule_events_do_not_change_workflow_status() -> Result<(), Box<dyn std::error::Error>> {
385 let events = vec![
386 workflow_started(1)?,
387 Event::SchedulePaused {
388 envelope: envelope(2),
389 schedule_id: ScheduleId::new(uuid::Uuid::from_u128(2)),
390 },
391 ];
392
393 assert_eq!(status_from_events(&events), WorkflowStatus::Running);
394 Ok(())
395 }
396
397 #[test]
398 fn projection_is_deterministic() -> Result<(), Box<dyn std::error::Error>> {
399 let events = vec![
400 workflow_started(1)?,
401 Event::WorkflowCompleted {
402 envelope: envelope(2),
403 result: payload("result")?,
404 },
405 ];
406
407 let first = status_from_events(&events);
408 let second = status_from_events(&events);
409
410 assert_eq!(first, second);
411 Ok(())
412 }
413
414 #[test]
415 fn last_terminal_lifecycle_event_determines_status() -> Result<(), Box<dyn std::error::Error>> {
416 let events = vec![
417 workflow_started(1)?,
418 Event::WorkflowCompleted {
419 envelope: envelope(2),
420 result: payload("result")?,
421 },
422 Event::WorkflowTimedOut {
423 envelope: envelope(3),
424 timeout: String::from("execution"),
425 },
426 ];
427
428 assert_eq!(status_from_events(&events), WorkflowStatus::TimedOut);
429 Ok(())
430 }
431
432 fn run_id() -> RunId {
433 RunId::new(uuid::Uuid::from_u128(1))
434 }
435
436 fn workflow_reopened(seq: u64, reopened: Vec<ActivityId>) -> Event {
437 Event::WorkflowReopened {
438 envelope: envelope(seq),
439 run_id: run_id(),
440 reopened,
441 }
442 }
443
444 #[test]
445 fn reopen_after_failure_projects_running() -> Result<(), Box<dyn std::error::Error>> {
446 let events = vec![
447 workflow_started(1)?,
448 Event::WorkflowFailed {
449 envelope: envelope(2),
450 error: workflow_error("transient"),
451 },
452 workflow_reopened(3, vec![ActivityId::from_sequence_position(2)]),
453 ];
454
455 assert_eq!(status_from_events(&events), WorkflowStatus::Running);
456 Ok(())
457 }
458
459 #[test]
460 fn reopen_then_new_terminal_projects_that_terminal() -> Result<(), Box<dyn std::error::Error>> {
461 let events = vec![
462 workflow_started(1)?,
463 Event::WorkflowFailed {
464 envelope: envelope(2),
465 error: workflow_error("transient"),
466 },
467 workflow_reopened(3, vec![ActivityId::from_sequence_position(2)]),
468 Event::WorkflowCompleted {
469 envelope: envelope(4),
470 result: payload("result")?,
471 },
472 ];
473
474 assert_eq!(status_from_events(&events), WorkflowStatus::Completed);
475 Ok(())
476 }
477
478 #[test]
479 fn current_lease_terminal_is_none_for_running_and_reopened()
480 -> Result<(), Box<dyn std::error::Error>> {
481 let running = vec![workflow_started(1)?];
482 assert!(current_lease_terminal(&running).is_none());
483
484 let failed = vec![
485 workflow_started(1)?,
486 Event::WorkflowFailed {
487 envelope: envelope(2),
488 error: workflow_error("boom"),
489 },
490 ];
491 assert!(matches!(
492 current_lease_terminal(&failed),
493 Some(Event::WorkflowFailed { .. })
494 ));
495
496 let mut reopened = failed.clone();
497 reopened.push(workflow_reopened(
498 3,
499 vec![ActivityId::from_sequence_position(2)],
500 ));
501 assert!(
502 current_lease_terminal(&reopened).is_none(),
503 "a reopened run has no current-lease terminal"
504 );
505 Ok(())
506 }
507
508 #[test]
509 fn current_lease_terminal_returns_terminal_after_reopen_and_retermination()
510 -> Result<(), Box<dyn std::error::Error>> {
511 let events = vec![
512 workflow_started(1)?,
513 Event::WorkflowFailed {
514 envelope: envelope(2),
515 error: workflow_error("boom"),
516 },
517 workflow_reopened(3, vec![ActivityId::from_sequence_position(2)]),
518 Event::WorkflowCompleted {
519 envelope: envelope(4),
520 result: payload("result")?,
521 },
522 ];
523
524 assert!(matches!(
525 current_lease_terminal(&events),
526 Some(Event::WorkflowCompleted { .. })
527 ));
528 Ok(())
529 }
530
531 fn workflow_paused(seq: u64) -> Event {
532 Event::WorkflowPaused {
533 envelope: envelope(seq),
534 run_id: run_id(),
535 reason: None,
536 operator: None,
537 }
538 }
539
540 fn workflow_resumed(seq: u64) -> Event {
541 Event::WorkflowResumed {
542 envelope: envelope(seq),
543 run_id: run_id(),
544 operator: None,
545 }
546 }
547
548 #[test]
549 fn pause_projects_paused_and_is_non_terminal() -> Result<(), Box<dyn std::error::Error>> {
550 let events = vec![workflow_started(1)?, workflow_paused(2)];
551 assert_eq!(status_from_events(&events), WorkflowStatus::Paused);
552 assert!(!WorkflowStatus::Paused.is_terminal());
553 assert!(current_lease_terminal(&events).is_none());
556 Ok(())
557 }
558
559 #[test]
560 fn resume_after_pause_projects_running() -> Result<(), Box<dyn std::error::Error>> {
561 let events = vec![
562 workflow_started(1)?,
563 workflow_paused(2),
564 workflow_resumed(3),
565 ];
566 assert_eq!(status_from_events(&events), WorkflowStatus::Running);
567 assert!(current_lease_terminal(&events).is_none());
568 Ok(())
569 }
570
571 #[test]
572 fn complete_after_resume_projects_completed() -> Result<(), Box<dyn std::error::Error>> {
573 let events = vec![
574 workflow_started(1)?,
575 workflow_paused(2),
576 workflow_resumed(3),
577 Event::WorkflowCompleted {
578 envelope: envelope(4),
579 result: payload("result")?,
580 },
581 ];
582 assert_eq!(status_from_events(&events), WorkflowStatus::Completed);
583 assert!(matches!(
584 current_lease_terminal(&events),
585 Some(Event::WorkflowCompleted { .. })
586 ));
587 Ok(())
588 }
589
590 #[test]
591 fn complete_while_paused_still_projects_completed() -> Result<(), Box<dyn std::error::Error>> {
592 let events = vec![
595 workflow_started(1)?,
596 workflow_paused(2),
597 Event::WorkflowCompleted {
598 envelope: envelope(3),
599 result: payload("result")?,
600 },
601 ];
602 assert_eq!(status_from_events(&events), WorkflowStatus::Completed);
603 Ok(())
604 }
605
606 #[test]
607 fn run_segment_scopes_to_the_named_run() -> Result<(), Box<dyn std::error::Error>> {
608 let first_run = RunId::new(uuid::Uuid::from_u128(10));
609 let second_run = RunId::new(uuid::Uuid::from_u128(20));
610 let events = vec![
611 Event::WorkflowStarted {
612 envelope: envelope(1),
613 workflow_type: String::from("checkout"),
614 input: payload("input")?,
615 run_id: first_run.clone(),
616 parent_run_id: None,
617 package_version: crate::PackageVersion::new("a".repeat(64)),
618 },
619 Event::WorkflowContinuedAsNew {
620 envelope: envelope(2),
621 input: payload("again")?,
622 workflow_type: None,
623 parent_run_id: first_run.clone(),
624 },
625 Event::WorkflowStarted {
626 envelope: envelope(3),
627 workflow_type: String::from("checkout"),
628 input: payload("input")?,
629 run_id: second_run.clone(),
630 parent_run_id: Some(first_run.clone()),
631 package_version: crate::PackageVersion::new("a".repeat(64)),
632 },
633 Event::WorkflowCompleted {
634 envelope: envelope(4),
635 result: payload("result")?,
636 },
637 ];
638
639 let first = run_segment(&events, &first_run);
640 assert_eq!(first.len(), 2, "first run spans its start through its CAN");
641 assert!(matches!(
642 current_lease_terminal(first),
643 Some(Event::WorkflowContinuedAsNew { .. })
644 ));
645
646 let second = run_segment(&events, &second_run);
647 assert_eq!(
648 second.len(),
649 2,
650 "second run spans its start through completion"
651 );
652 assert!(matches!(
653 current_lease_terminal(second),
654 Some(Event::WorkflowCompleted { .. })
655 ));
656
657 assert!(
658 run_segment(&events, &RunId::new(uuid::Uuid::from_u128(99))).is_empty(),
659 "absent run yields an empty segment"
660 );
661 Ok(())
662 }
663}