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