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