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