1use crate::context::WorkflowContext;
16use crate::task::{RetryPolicy, UntypedCoreTask};
17use sha2::{Digest, Sha256};
18use std::collections::{HashMap, HashSet};
19use std::marker::PhantomData;
20use std::ops::Deref;
21use std::sync::Arc;
22
23#[derive(
25 Debug,
26 Clone,
27 Copy,
28 PartialEq,
29 Eq,
30 serde::Serialize,
31 serde::Deserialize,
32 strum::EnumString,
33 strum::Display,
34)]
35pub enum MaxIterationsPolicy {
36 #[strum(serialize = "fail")]
38 Fail,
39 #[strum(serialize = "exit_with_last")]
41 ExitWithLast,
42}
43
44macro_rules! impl_find_duplicate_id {
47 ($name:ident, task_fields: { $($task_extra:tt)* }, delay_extra: { $($delay_extra:tt)* }, deref_branch: $deref:expr, deref_branch_map: $deref_map:expr) => {
48 impl $name {
49 pub(crate) fn find_duplicate_id(&self) -> Option<String> {
50 fn collect(cont: &$name, seen: &mut HashSet<String>) -> Option<String> {
51 match cont {
52 $name::Task { id, next, $($task_extra)* } => {
53 if !seen.insert(id.clone()) {
54 return Some(id.clone());
55 }
56 next.as_ref().and_then(|n| collect(n, seen))
57 }
58 $name::Fork { id, branches, join } => {
59 if !seen.insert(id.clone()) {
60 return Some(id.clone());
61 }
62 let deref_fn: fn(&_) -> &$name = $deref;
63 branches
64 .iter()
65 .find_map(|b| collect(deref_fn(b), seen))
66 .or_else(|| join.as_ref().and_then(|j| collect(j, seen)))
67 }
68 $name::Branch { id, branches, default, next, .. } => {
69 if !seen.insert(id.clone()) {
70 return Some(id.clone());
71 }
72 let deref_map_fn: fn(&_) -> &$name = $deref_map;
73 branches
74 .values()
75 .find_map(|b| collect(deref_map_fn(b), seen))
76 .or_else(|| default.as_ref().and_then(|d| collect(d, seen)))
77 .or_else(|| next.as_ref().and_then(|n| collect(n, seen)))
78 }
79 $name::Delay { id, next, $($delay_extra)* }
80 | $name::AwaitSignal { id, next, $($delay_extra)* } => {
81 if !seen.insert(id.clone()) {
82 return Some(id.clone());
83 }
84 next.as_ref().and_then(|n| collect(n, seen))
85 }
86 $name::Loop { id, body, next, .. } => {
87 if !seen.insert(id.clone()) {
88 return Some(id.clone());
89 }
90 collect(body, seen)
91 .or_else(|| next.as_ref().and_then(|n| collect(n, seen)))
92 }
93 $name::ChildWorkflow { id, child, next } => {
94 if !seen.insert(id.clone()) {
95 return Some(id.clone());
96 }
97 collect(child, seen)
98 .or_else(|| next.as_ref().and_then(|n| collect(n, seen)))
99 }
100 }
101 }
102 collect(self, &mut HashSet::new())
103 }
104 }
105 };
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::AsRefStr, strum::Display, strum::EnumString)]
110#[strum(serialize_all = "snake_case")]
111pub enum NodeKind {
112 Task,
114 Fork,
116 Delay,
118 AwaitSignal,
120 Branch,
122 Loop,
124 ChildWorkflow,
126}
127
128#[derive(Debug, Clone)]
131pub struct NodeInfo<'a> {
132 pub id: &'a str,
134 pub kind: NodeKind,
136 pub predecessor_id: Option<&'a str>,
139 pub timeout: Option<std::time::Duration>,
141 pub retry_policy: Option<&'a RetryPolicy>,
143 pub priority: Option<u8>,
145 pub tags: &'a [String],
147 pub version: Option<&'a str>,
149}
150
151pub struct NodeIter<'a> {
155 stack: Vec<(&'a WorkflowContinuation, Option<&'a str>)>,
156}
157
158const EMPTY_TAGS: &[String] = &[];
159
160impl<'a> Iterator for NodeIter<'a> {
161 type Item = NodeInfo<'a>;
162
163 #[allow(clippy::too_many_lines)]
164 fn next(&mut self) -> Option<Self::Item> {
165 let (cont, predecessor) = self.stack.pop()?;
166
167 let (id, kind, timeout, retry_policy, priority, tags, version) = match cont {
168 WorkflowContinuation::Task {
169 id,
170 timeout,
171 retry_policy,
172 priority,
173 tags,
174 version,
175 ..
176 } => (
177 id.as_str(),
178 NodeKind::Task,
179 *timeout,
180 retry_policy.as_ref(),
181 *priority,
182 tags.as_slice(),
183 version.as_deref(),
184 ),
185 WorkflowContinuation::Fork { id, .. } => (
186 id.as_str(),
187 NodeKind::Fork,
188 None,
189 None,
190 None,
191 EMPTY_TAGS,
192 None,
193 ),
194 WorkflowContinuation::Delay { id, duration, .. } => (
195 id.as_str(),
196 NodeKind::Delay,
197 Some(*duration),
198 None,
199 None,
200 EMPTY_TAGS,
201 None,
202 ),
203 WorkflowContinuation::AwaitSignal { id, timeout, .. } => (
204 id.as_str(),
205 NodeKind::AwaitSignal,
206 *timeout,
207 None,
208 None,
209 EMPTY_TAGS,
210 None,
211 ),
212 WorkflowContinuation::Branch { id, .. } => (
213 id.as_str(),
214 NodeKind::Branch,
215 None,
216 None,
217 None,
218 EMPTY_TAGS,
219 None,
220 ),
221 WorkflowContinuation::Loop { id, .. } => (
222 id.as_str(),
223 NodeKind::Loop,
224 None,
225 None,
226 None,
227 EMPTY_TAGS,
228 None,
229 ),
230 WorkflowContinuation::ChildWorkflow { id, .. } => (
231 id.as_str(),
232 NodeKind::ChildWorkflow,
233 None,
234 None,
235 None,
236 EMPTY_TAGS,
237 None,
238 ),
239 };
240
241 match cont {
243 WorkflowContinuation::Task { id, next, .. }
244 | WorkflowContinuation::Delay { id, next, .. }
245 | WorkflowContinuation::AwaitSignal { id, next, .. } => {
246 if let Some(n) = next {
247 self.stack.push((n, Some(id)));
248 }
249 }
250 WorkflowContinuation::Fork { id, branches, join } => {
251 if let Some(j) = join {
252 self.stack.push((j, Some(id)));
253 }
254 for b in branches.iter().rev() {
255 self.stack.push((b, Some(id)));
256 }
257 }
258 WorkflowContinuation::Branch {
259 id,
260 branches,
261 default,
262 next,
263 ..
264 } => {
265 if let Some(n) = next {
266 self.stack.push((n, Some(id)));
267 }
268 if let Some(d) = default {
269 self.stack.push((d, Some(id)));
270 }
271 let mut keys: Vec<&String> = branches.keys().collect();
273 keys.sort();
274 for k in keys.into_iter().rev() {
275 self.stack.push((&branches[k], Some(id)));
276 }
277 }
278 WorkflowContinuation::Loop { id, body, next, .. } => {
279 if let Some(n) = next {
280 self.stack.push((n, Some(id)));
281 }
282 self.stack.push((body, Some(id)));
283 }
284 WorkflowContinuation::ChildWorkflow {
285 id, child, next, ..
286 } => {
287 if let Some(n) = next {
288 self.stack.push((n, Some(id)));
289 }
290 self.stack.push((child, Some(id)));
291 }
292 }
293
294 Some(NodeInfo {
295 id,
296 kind,
297 predecessor_id: predecessor,
298 timeout,
299 retry_policy,
300 priority,
301 tags,
302 version,
303 })
304 }
305}
306
307pub enum WorkflowContinuation {
309 Task {
311 id: String,
313 func: Option<UntypedCoreTask>,
316 timeout: Option<std::time::Duration>,
318 retry_policy: Option<RetryPolicy>,
320 version: Option<String>,
322 priority: Option<u8>,
324 tags: Vec<String>,
326 next: Option<Box<WorkflowContinuation>>,
328 },
329 Fork {
331 id: String,
333 branches: Box<[Arc<WorkflowContinuation>]>,
335 join: Option<Box<WorkflowContinuation>>,
337 },
338 Delay {
340 id: String,
342 duration: std::time::Duration,
344 next: Option<Box<WorkflowContinuation>>,
346 },
347 AwaitSignal {
351 id: String,
353 signal_name: String,
355 timeout: Option<std::time::Duration>,
357 next: Option<Box<WorkflowContinuation>>,
359 },
360 Branch {
364 id: String,
366 key_fn: Option<UntypedCoreTask>,
369 branches: HashMap<String, Box<WorkflowContinuation>>,
371 default: Option<Box<WorkflowContinuation>>,
373 next: Option<Box<WorkflowContinuation>>,
375 },
376 Loop {
379 id: String,
381 body: Box<WorkflowContinuation>,
383 max_iterations: u32,
385 on_max: MaxIterationsPolicy,
387 next: Option<Box<WorkflowContinuation>>,
389 },
390 ChildWorkflow {
392 id: String,
394 child: Arc<WorkflowContinuation>,
396 next: Option<Box<WorkflowContinuation>>,
398 },
399}
400
401impl_find_duplicate_id!(
402 WorkflowContinuation,
403 task_fields: { .. },
404 delay_extra: { .. },
405 deref_branch: |b: &Arc<WorkflowContinuation>| -> &WorkflowContinuation { b },
406 deref_branch_map: |b: &WorkflowContinuation| -> &WorkflowContinuation { b }
407);
408
409#[must_use]
414pub fn key_fn_id(branch_id: &str) -> String {
415 format!("{branch_id}::key_fn")
416}
417
418#[must_use]
423pub fn loop_node_id(counter: usize) -> String {
424 format!("loop_{counter}")
425}
426
427impl WorkflowContinuation {
428 #[must_use]
432 pub fn derive_fork_id(branch_ids: &[&str]) -> String {
433 branch_ids.join("||")
434 }
435
436 #[must_use]
438 pub fn id(&self) -> &str {
439 match self {
440 WorkflowContinuation::Task { id, .. }
441 | WorkflowContinuation::Fork { id, .. }
442 | WorkflowContinuation::Delay { id, .. }
443 | WorkflowContinuation::AwaitSignal { id, .. }
444 | WorkflowContinuation::Branch { id, .. }
445 | WorkflowContinuation::Loop { id, .. }
446 | WorkflowContinuation::ChildWorkflow { id, .. } => id,
447 }
448 }
449
450 #[must_use]
453 pub fn get_next(&self) -> Option<&WorkflowContinuation> {
454 match self {
455 Self::Task { next, .. }
456 | Self::Delay { next, .. }
457 | Self::AwaitSignal { next, .. }
458 | Self::Branch { next, .. }
459 | Self::Loop { next, .. }
460 | Self::ChildWorkflow { next, .. } => next.as_deref(),
461 Self::Fork { join, .. } => join.as_deref(),
462 }
463 }
464
465 #[must_use]
470 pub fn first_task_id(&self) -> &str {
471 match self {
472 WorkflowContinuation::Task { id, .. }
473 | WorkflowContinuation::Delay { id, .. }
474 | WorkflowContinuation::AwaitSignal { id, .. }
475 | WorkflowContinuation::Branch { id, .. } => id,
476 WorkflowContinuation::Fork { branches, .. } => {
477 if let Some(first_branch) = branches.first() {
478 first_branch.first_task_id()
479 } else {
480 "unknown"
481 }
482 }
483 WorkflowContinuation::Loop { body, .. } => body.first_task_id(),
484 WorkflowContinuation::ChildWorkflow { child, .. } => child.first_task_id(),
485 }
486 }
487
488 #[must_use]
493 pub fn first_task_priority(&self) -> Option<u8> {
494 match self {
495 WorkflowContinuation::Task { priority, .. } => *priority,
496 WorkflowContinuation::Delay { .. }
497 | WorkflowContinuation::AwaitSignal { .. }
498 | WorkflowContinuation::Branch { .. } => None,
499 WorkflowContinuation::Fork { branches, .. } => {
500 branches.first().and_then(|b| b.first_task_priority())
501 }
502 WorkflowContinuation::Loop { body, .. } => body.first_task_priority(),
503 WorkflowContinuation::ChildWorkflow { child, .. } => child.first_task_priority(),
504 }
505 }
506
507 #[must_use]
512 pub fn first_task_tags(&self) -> Vec<String> {
513 match self {
514 WorkflowContinuation::Task { tags, .. } => tags.clone(),
515 WorkflowContinuation::Delay { .. }
516 | WorkflowContinuation::AwaitSignal { .. }
517 | WorkflowContinuation::Branch { .. } => vec![],
518 WorkflowContinuation::Fork { branches, .. } => branches
519 .first()
520 .map(|b| b.first_task_tags())
521 .unwrap_or_default(),
522 WorkflowContinuation::Loop { body, .. } => body.first_task_tags(),
523 WorkflowContinuation::ChildWorkflow { child, .. } => child.first_task_tags(),
524 }
525 }
526
527 #[must_use]
533 pub fn first_task_hint(&self) -> crate::snapshot::TaskHint {
534 crate::snapshot::TaskHint::new(
535 self.first_task_id(),
536 self.first_task_priority(),
537 &self.first_task_tags(),
538 )
539 }
540
541 #[must_use]
547 pub fn terminal_task_id(&self) -> &str {
548 let mut current = self;
549 while let Some(next) = current.get_next() {
550 current = next;
551 }
552 current.first_task_id()
553 }
554
555 #[must_use]
562 pub fn find_task_name(&self, target_id: &crate::TaskId) -> Option<&str> {
563 self.find_task(target_id).and_then(|n| match n {
564 WorkflowContinuation::Task { id, .. } => Some(id.as_str()),
565 _ => None,
566 })
567 }
568
569 fn find_task(&self, target_id: &crate::TaskId) -> Option<&Self> {
570 match self {
571 WorkflowContinuation::Task { id, next, .. } => {
572 if crate::TaskId::from(id.as_str()) == *target_id {
573 return Some(self);
574 }
575 next.as_ref().and_then(|n| n.find_task(target_id))
576 }
577 WorkflowContinuation::Delay { next, .. }
578 | WorkflowContinuation::AwaitSignal { next, .. } => {
579 next.as_ref().and_then(|n| n.find_task(target_id))
580 }
581 WorkflowContinuation::Fork { branches, join, .. } => {
582 for branch in branches {
583 if let Some(found) = branch.find_task(target_id) {
584 return Some(found);
585 }
586 }
587 join.as_ref().and_then(|j| j.find_task(target_id))
588 }
589 WorkflowContinuation::Branch {
590 branches,
591 default,
592 next,
593 ..
594 } => {
595 for branch in branches.values() {
596 if let Some(found) = branch.find_task(target_id) {
597 return Some(found);
598 }
599 }
600 if let Some(d) = default
601 && let Some(found) = d.find_task(target_id)
602 {
603 return Some(found);
604 }
605 next.as_ref().and_then(|n| n.find_task(target_id))
606 }
607 WorkflowContinuation::Loop { body, next, .. } => body
608 .find_task(target_id)
609 .or_else(|| next.as_ref().and_then(|n| n.find_task(target_id))),
610 WorkflowContinuation::ChildWorkflow { child, next, .. } => child
611 .find_task(target_id)
612 .or_else(|| next.as_ref().and_then(|n| n.find_task(target_id))),
613 }
614 }
615
616 fn find_task_mut(&mut self, target_id: &crate::TaskId) -> Option<&mut Self> {
622 match self {
623 WorkflowContinuation::Task { id, .. }
624 if crate::TaskId::from(id.as_str()) == *target_id =>
625 {
626 Some(self)
627 }
628 WorkflowContinuation::Task { next, .. } => {
629 next.as_mut().and_then(|n| n.find_task_mut(target_id))
630 }
631 WorkflowContinuation::Delay { next, .. }
632 | WorkflowContinuation::AwaitSignal { next, .. } => {
633 next.as_mut().and_then(|n| n.find_task_mut(target_id))
634 }
635 WorkflowContinuation::Fork { join, .. } => {
636 join.as_mut().and_then(|j| j.find_task_mut(target_id))
637 }
638 WorkflowContinuation::Branch {
639 branches,
640 default,
641 next,
642 ..
643 } => {
644 for branch in branches.values_mut() {
645 if let Some(found) = branch.find_task_mut(target_id) {
646 return Some(found);
647 }
648 }
649 if let Some(d) = default
650 && let Some(found) = d.find_task_mut(target_id)
651 {
652 return Some(found);
653 }
654 next.as_mut().and_then(|n| n.find_task_mut(target_id))
655 }
656 WorkflowContinuation::Loop { body, next, .. } => {
657 if let Some(found) = body.find_task_mut(target_id) {
658 return Some(found);
659 }
660 next.as_mut().and_then(|n| n.find_task_mut(target_id))
661 }
662 WorkflowContinuation::ChildWorkflow { next, .. } => {
663 next.as_mut().and_then(|n| n.find_task_mut(target_id))
665 }
666 }
667 }
668
669 pub fn set_task_timeout(
671 &mut self,
672 target_id: &crate::TaskId,
673 timeout: Option<std::time::Duration>,
674 ) {
675 if let Some(WorkflowContinuation::Task { timeout: t, .. }) = self.find_task_mut(target_id) {
676 *t = timeout;
677 }
678 }
679
680 pub fn set_task_retry_policy(
682 &mut self,
683 target_id: &crate::TaskId,
684 policy: Option<RetryPolicy>,
685 ) {
686 if let Some(WorkflowContinuation::Task { retry_policy, .. }) = self.find_task_mut(target_id)
687 {
688 *retry_policy = policy;
689 }
690 }
691
692 pub fn set_task_version(&mut self, target_id: &crate::TaskId, ver: Option<String>) {
694 if let Some(WorkflowContinuation::Task { version, .. }) = self.find_task_mut(target_id) {
695 *version = ver;
696 }
697 }
698
699 #[must_use]
701 pub fn get_task_retry_policy(&self, task_id: &crate::TaskId) -> Option<&RetryPolicy> {
702 match self.find_task(task_id)? {
703 WorkflowContinuation::Task { retry_policy, .. } => retry_policy.as_ref(),
704 _ => None,
705 }
706 }
707
708 #[must_use]
710 pub fn get_task_timeout(&self, task_id: &crate::TaskId) -> Option<std::time::Duration> {
711 match self.find_task(task_id)? {
712 WorkflowContinuation::Task { timeout, .. } => *timeout,
713 _ => None,
714 }
715 }
716
717 #[must_use]
719 pub fn get_task_priority(&self, task_id: &crate::TaskId) -> Option<u8> {
720 match self.find_task(task_id)? {
721 WorkflowContinuation::Task { priority, .. } => *priority,
722 _ => None,
723 }
724 }
725
726 #[must_use]
728 pub fn get_task_tags(&self, task_id: &crate::TaskId) -> Vec<String> {
729 match self.find_task(task_id) {
730 Some(WorkflowContinuation::Task { tags, .. }) => tags.clone(),
731 _ => vec![],
732 }
733 }
734
735 pub fn set_task_tags(&mut self, target_id: &crate::TaskId, new_tags: Vec<String>) {
737 if let Some(WorkflowContinuation::Task { tags, .. }) = self.find_task_mut(target_id) {
738 *tags = new_tags;
739 }
740 }
741
742 #[must_use]
749 pub fn build_task_metadata(&self, task_id: &crate::TaskId) -> crate::task::TaskMetadata {
750 match self.find_task(task_id) {
751 Some(WorkflowContinuation::Task {
752 timeout,
753 retry_policy,
754 version,
755 priority,
756 tags,
757 ..
758 }) => crate::task::TaskMetadata::from_node_fields(
759 *timeout,
760 retry_policy.clone(),
761 version.clone(),
762 *priority,
763 tags.clone(),
764 ),
765 _ => crate::task::TaskMetadata::default(),
766 }
767 }
768
769 #[must_use]
778 pub fn iter_nodes(&self) -> NodeIter<'_> {
779 NodeIter {
780 stack: vec![(self, None)],
781 }
782 }
783
784 #[must_use]
786 pub fn to_serializable(&self) -> SerializableContinuation {
787 match self {
788 #[allow(clippy::cast_possible_truncation)] WorkflowContinuation::Task {
790 id,
791 timeout,
792 retry_policy,
793 version,
794 priority,
795 tags,
796 next,
797 ..
798 } => SerializableContinuation::Task {
799 id: id.clone(),
800 timeout_ms: timeout.map(|d| d.as_millis() as u64),
801 retry_policy: retry_policy.clone(),
802 version: version.clone(),
803 priority: *priority,
804 tags: tags.clone(),
805 next: next.as_ref().map(|n| Box::new(n.to_serializable())),
806 },
807 WorkflowContinuation::Fork { id, branches, join } => SerializableContinuation::Fork {
808 id: id.clone(),
809 branches: branches.iter().map(|b| b.to_serializable()).collect(),
810 join: join.as_ref().map(|j| Box::new(j.to_serializable())),
811 },
812 #[allow(clippy::cast_possible_truncation)] WorkflowContinuation::Delay { id, duration, next } => SerializableContinuation::Delay {
814 id: id.clone(),
815 duration_ms: duration.as_millis() as u64,
816 next: next.as_ref().map(|n| Box::new(n.to_serializable())),
817 },
818 #[allow(clippy::cast_possible_truncation)]
819 WorkflowContinuation::AwaitSignal {
820 id,
821 signal_name,
822 timeout,
823 next,
824 } => SerializableContinuation::AwaitSignal {
825 id: id.clone(),
826 signal_name: signal_name.clone(),
827 timeout_ms: timeout.map(|d| d.as_millis() as u64),
828 next: next.as_ref().map(|n| Box::new(n.to_serializable())),
829 },
830 WorkflowContinuation::Branch {
831 id,
832 branches,
833 default,
834 next,
835 ..
836 } => SerializableContinuation::Branch {
837 id: id.clone(),
838 branches: branches
839 .iter()
840 .map(|(k, v)| (k.clone(), Box::new(v.to_serializable())))
841 .collect(),
842 default: default.as_ref().map(|d| Box::new(d.to_serializable())),
843 next: next.as_ref().map(|n| Box::new(n.to_serializable())),
844 },
845 WorkflowContinuation::ChildWorkflow { id, child, next } => {
846 SerializableContinuation::ChildWorkflow {
847 id: id.clone(),
848 child: Box::new(child.to_serializable()),
849 next: next.as_ref().map(|n| Box::new(n.to_serializable())),
850 }
851 }
852 WorkflowContinuation::Loop {
853 id,
854 body,
855 max_iterations,
856 on_max,
857 next,
858 } => SerializableContinuation::Loop {
859 id: id.clone(),
860 body: Box::new(body.to_serializable()),
861 max_iterations: *max_iterations,
862 on_max: *on_max,
863 next: next.as_ref().map(|n| Box::new(n.to_serializable())),
864 },
865 }
866 }
867
868 pub fn append_to_chain(&mut self, new_node: WorkflowContinuation) {
872 match self {
873 WorkflowContinuation::Task { next, .. }
874 | WorkflowContinuation::Delay { next, .. }
875 | WorkflowContinuation::AwaitSignal { next, .. }
876 | WorkflowContinuation::Branch { next, .. }
877 | WorkflowContinuation::Loop { next, .. }
878 | WorkflowContinuation::ChildWorkflow { next, .. } => match next {
879 Some(next_box) => next_box.append_to_chain(new_node),
880 None => *next = Some(Box::new(new_node)),
881 },
882 WorkflowContinuation::Fork { join, .. } => match join {
883 Some(join_box) => join_box.append_to_chain(new_node),
884 None => *join = Some(Box::new(new_node)),
885 },
886 }
887 }
888}
889
890#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
932pub enum SerializableContinuation {
933 Task {
935 id: String,
937 #[serde(default, skip_serializing_if = "Option::is_none")]
939 timeout_ms: Option<u64>,
940 #[serde(default, skip_serializing_if = "Option::is_none")]
942 retry_policy: Option<RetryPolicy>,
943 #[serde(default, skip_serializing_if = "Option::is_none")]
945 version: Option<String>,
946 #[serde(default, skip_serializing_if = "Option::is_none")]
948 priority: Option<u8>,
949 #[serde(default, skip_serializing_if = "Vec::is_empty")]
951 tags: Vec<String>,
952 next: Option<Box<SerializableContinuation>>,
954 },
955 Fork {
957 id: String,
959 branches: Vec<SerializableContinuation>,
961 join: Option<Box<SerializableContinuation>>,
963 },
964 Delay {
966 id: String,
968 duration_ms: u64,
970 next: Option<Box<SerializableContinuation>>,
972 },
973 AwaitSignal {
975 id: String,
977 signal_name: String,
979 #[serde(default, skip_serializing_if = "Option::is_none")]
981 timeout_ms: Option<u64>,
982 next: Option<Box<SerializableContinuation>>,
984 },
985 Branch {
987 id: String,
989 branches: HashMap<String, Box<SerializableContinuation>>,
991 #[serde(default, skip_serializing_if = "Option::is_none")]
993 default: Option<Box<SerializableContinuation>>,
994 next: Option<Box<SerializableContinuation>>,
996 },
997 Loop {
999 id: String,
1001 body: Box<SerializableContinuation>,
1003 max_iterations: u32,
1005 on_max: MaxIterationsPolicy,
1007 next: Option<Box<SerializableContinuation>>,
1009 },
1010 ChildWorkflow {
1012 id: String,
1014 child: Box<SerializableContinuation>,
1016 next: Option<Box<SerializableContinuation>>,
1018 },
1019}
1020
1021impl_find_duplicate_id!(
1022 SerializableContinuation,
1023 task_fields: { .. },
1024 delay_extra: { .. },
1025 deref_branch: |b: &SerializableContinuation| -> &SerializableContinuation { b },
1026 deref_branch_map: |b: &SerializableContinuation| -> &SerializableContinuation { b }
1027);
1028
1029impl SerializableContinuation {
1030 pub fn to_runnable(
1038 &self,
1039 registry: &crate::registry::TaskRegistry,
1040 ) -> Result<WorkflowContinuation, crate::error::BuildError> {
1041 if let Some(dup) = self.find_duplicate_id() {
1042 return Err(crate::error::BuildError::DuplicateTaskId(dup));
1043 }
1044
1045 self.to_runnable_unchecked(registry)
1046 }
1047
1048 #[allow(clippy::too_many_lines)]
1050 fn to_runnable_unchecked(
1051 &self,
1052 registry: &crate::registry::TaskRegistry,
1053 ) -> Result<WorkflowContinuation, crate::error::BuildError> {
1054 match self {
1055 SerializableContinuation::Task {
1056 id,
1057 timeout_ms,
1058 retry_policy,
1059 version,
1060 priority,
1061 tags,
1062 next,
1063 } => {
1064 let func = registry
1065 .get(id)
1066 .ok_or_else(|| crate::error::BuildError::TaskNotFound(id.clone()))?;
1067 let next = next
1068 .as_ref()
1069 .map(|n| n.to_runnable_unchecked(registry).map(Box::new))
1070 .transpose()?;
1071 Ok(WorkflowContinuation::Task {
1072 id: id.clone(),
1073 func: Some(func),
1074 timeout: timeout_ms.map(std::time::Duration::from_millis),
1075 retry_policy: retry_policy.clone(),
1076 version: version.clone(),
1077 priority: *priority,
1078 tags: tags.clone(),
1079 next,
1080 })
1081 }
1082 SerializableContinuation::Fork { id, branches, join } => {
1083 let branches: Result<Vec<_>, _> = branches
1084 .iter()
1085 .map(|b| b.to_runnable_unchecked(registry).map(Arc::new))
1086 .collect();
1087 let join = join
1088 .as_ref()
1089 .map(|j| j.to_runnable_unchecked(registry).map(Box::new))
1090 .transpose()?;
1091 Ok(WorkflowContinuation::Fork {
1092 id: id.clone(),
1093 branches: branches?.into_boxed_slice(),
1094 join,
1095 })
1096 }
1097 SerializableContinuation::Delay {
1098 id,
1099 duration_ms,
1100 next,
1101 } => {
1102 let next = next
1103 .as_ref()
1104 .map(|n| n.to_runnable_unchecked(registry).map(Box::new))
1105 .transpose()?;
1106 Ok(WorkflowContinuation::Delay {
1107 id: id.clone(),
1108 duration: std::time::Duration::from_millis(*duration_ms),
1109 next,
1110 })
1111 }
1112 SerializableContinuation::AwaitSignal {
1113 id,
1114 signal_name,
1115 timeout_ms,
1116 next,
1117 } => {
1118 let next = next
1119 .as_ref()
1120 .map(|n| n.to_runnable_unchecked(registry).map(Box::new))
1121 .transpose()?;
1122 Ok(WorkflowContinuation::AwaitSignal {
1123 id: id.clone(),
1124 signal_name: signal_name.clone(),
1125 timeout: timeout_ms.map(std::time::Duration::from_millis),
1126 next,
1127 })
1128 }
1129 SerializableContinuation::Branch {
1130 id,
1131 branches,
1132 default,
1133 next,
1134 } => {
1135 let kf_id = key_fn_id(id);
1136 let key_fn = registry
1137 .get(&kf_id)
1138 .ok_or(crate::error::BuildError::TaskNotFound(kf_id))?;
1139 let branches: Result<HashMap<_, _>, _> = branches
1140 .iter()
1141 .map(|(k, v)| {
1142 v.to_runnable_unchecked(registry)
1143 .map(|c| (k.clone(), Box::new(c)))
1144 })
1145 .collect();
1146 let default = default
1147 .as_ref()
1148 .map(|d| d.to_runnable_unchecked(registry).map(Box::new))
1149 .transpose()?;
1150 let next = next
1151 .as_ref()
1152 .map(|n| n.to_runnable_unchecked(registry).map(Box::new))
1153 .transpose()?;
1154 Ok(WorkflowContinuation::Branch {
1155 id: id.clone(),
1156 key_fn: Some(key_fn),
1157 branches: branches?,
1158 default,
1159 next,
1160 })
1161 }
1162 SerializableContinuation::Loop {
1163 id,
1164 body,
1165 max_iterations,
1166 on_max,
1167 next,
1168 } => {
1169 let body = body.to_runnable_unchecked(registry)?;
1170 let next = next
1171 .as_ref()
1172 .map(|n| n.to_runnable_unchecked(registry).map(Box::new))
1173 .transpose()?;
1174 Ok(WorkflowContinuation::Loop {
1175 id: id.clone(),
1176 body: Box::new(body),
1177 max_iterations: *max_iterations,
1178 on_max: *on_max,
1179 next,
1180 })
1181 }
1182 SerializableContinuation::ChildWorkflow { id, child, next } => {
1183 let child = child.to_runnable_unchecked(registry)?;
1184 let next = next
1185 .as_ref()
1186 .map(|n| n.to_runnable_unchecked(registry).map(Box::new))
1187 .transpose()?;
1188 Ok(WorkflowContinuation::ChildWorkflow {
1189 id: id.clone(),
1190 child: Arc::new(child),
1191 next,
1192 })
1193 }
1194 }
1195 }
1196
1197 #[must_use]
1199 pub fn task_ids(&self) -> Vec<&str> {
1200 fn collect<'a>(cont: &'a SerializableContinuation, ids: &mut Vec<&'a str>) {
1201 match cont {
1202 SerializableContinuation::Task { id, next, .. }
1203 | SerializableContinuation::Delay { id, next, .. }
1204 | SerializableContinuation::AwaitSignal { id, next, .. } => {
1205 ids.push(id.as_str());
1206 if let Some(n) = next {
1207 collect(n, ids);
1208 }
1209 }
1210 SerializableContinuation::Fork { id, branches, join } => {
1211 ids.push(id.as_str());
1212 for b in branches {
1213 collect(b, ids);
1214 }
1215 if let Some(j) = join {
1216 collect(j, ids);
1217 }
1218 }
1219 SerializableContinuation::Branch {
1220 id,
1221 branches,
1222 default,
1223 next,
1224 } => {
1225 ids.push(id.as_str());
1226 for b in branches.values() {
1227 collect(b, ids);
1228 }
1229 if let Some(d) = default {
1230 collect(d, ids);
1231 }
1232 if let Some(n) = next {
1233 collect(n, ids);
1234 }
1235 }
1236 SerializableContinuation::Loop { id, body, next, .. } => {
1237 ids.push(id.as_str());
1238 collect(body, ids);
1239 if let Some(n) = next {
1240 collect(n, ids);
1241 }
1242 }
1243 SerializableContinuation::ChildWorkflow { id, child, next } => {
1244 ids.push(id.as_str());
1245 collect(child, ids);
1246 if let Some(n) = next {
1247 collect(n, ids);
1248 }
1249 }
1250 }
1251 }
1252 let mut ids = vec![];
1253 collect(self, &mut ids);
1254 ids
1255 }
1256
1257 #[must_use]
1266 #[allow(clippy::too_many_lines)]
1267 pub fn compute_definition_hash(&self) -> crate::DefinitionHash {
1268 #[allow(clippy::too_many_lines)]
1269 fn hash_continuation(cont: &SerializableContinuation, hasher: &mut Sha256) {
1270 match cont {
1271 SerializableContinuation::Task {
1272 id,
1273 timeout_ms,
1274 retry_policy,
1275 version,
1276 next,
1277 ..
1278 } => {
1279 hasher.update(b"T:"); hasher.update(id.as_bytes());
1281 if let Some(ms) = timeout_ms {
1282 hasher.update(b":t:");
1283 hasher.update(ms.to_string().as_bytes());
1284 }
1285 if let Some(rp) = retry_policy {
1286 hasher.update(b":r:");
1287 hasher.update(rp.max_retries.to_string().as_bytes());
1288 hasher.update(b":");
1289 hasher.update(rp.initial_delay.as_millis().to_string().as_bytes());
1290 hasher.update(b":");
1291 hasher.update(rp.backoff_multiplier.to_string().as_bytes());
1292 }
1293 if let Some(v) = version {
1294 hasher.update(b":v:");
1295 hasher.update(v.as_bytes());
1296 }
1297 hasher.update(b";");
1298 if let Some(n) = next {
1299 hash_continuation(n, hasher);
1300 }
1301 }
1302 SerializableContinuation::Fork { id, branches, join } => {
1303 hasher.update(b"F:");
1304 hasher.update(id.as_bytes());
1305 hasher.update(b"[");
1306 for branch in branches {
1307 hash_continuation(branch, hasher);
1308 hasher.update(b",");
1309 }
1310 hasher.update(b"]");
1311 if let Some(j) = join {
1312 hasher.update(b"J:");
1313 hash_continuation(j, hasher);
1314 }
1315 }
1316 SerializableContinuation::Delay {
1317 id,
1318 duration_ms,
1319 next,
1320 } => {
1321 hasher.update(b"D:");
1322 hasher.update(id.as_bytes());
1323 hasher.update(b":");
1324 hasher.update(duration_ms.to_string().as_bytes());
1325 hasher.update(b";");
1326 if let Some(n) = next {
1327 hash_continuation(n, hasher);
1328 }
1329 }
1330 SerializableContinuation::AwaitSignal {
1331 id,
1332 signal_name,
1333 timeout_ms,
1334 next,
1335 } => {
1336 hasher.update(b"S:");
1337 hasher.update(id.as_bytes());
1338 hasher.update(b":");
1339 hasher.update(signal_name.as_bytes());
1340 if let Some(ms) = timeout_ms {
1341 hasher.update(b":t:");
1342 hasher.update(ms.to_string().as_bytes());
1343 }
1344 hasher.update(b";");
1345 if let Some(n) = next {
1346 hash_continuation(n, hasher);
1347 }
1348 }
1349 SerializableContinuation::Branch {
1350 id,
1351 branches,
1352 default,
1353 next,
1354 } => {
1355 hasher.update(b"B:");
1356 hasher.update(id.as_bytes());
1357 hasher.update(b"{");
1358 let mut keys: Vec<&String> = branches.keys().collect();
1360 keys.sort();
1361 for key in keys {
1362 hasher.update(key.as_bytes());
1363 hasher.update(b"=>");
1364 if let Some(branch) = branches.get(key) {
1365 hash_continuation(branch, hasher);
1366 }
1367 hasher.update(b",");
1368 }
1369 hasher.update(b"}");
1370 if let Some(d) = default {
1371 hasher.update(b"_=>");
1372 hash_continuation(d, hasher);
1373 }
1374 hasher.update(b";");
1375 if let Some(n) = next {
1376 hash_continuation(n, hasher);
1377 }
1378 }
1379 SerializableContinuation::Loop {
1380 id,
1381 body,
1382 max_iterations,
1383 on_max,
1384 next,
1385 } => {
1386 hasher.update(b"L:");
1387 hasher.update(id.as_bytes());
1388 hasher.update(b":");
1389 hasher.update(max_iterations.to_string().as_bytes());
1390 hasher.update(b":");
1391 hasher.update(on_max.to_string().as_bytes());
1392 hasher.update(b"{");
1393 hash_continuation(body, hasher);
1394 hasher.update(b"}");
1395 hasher.update(b";");
1396 if let Some(n) = next {
1397 hash_continuation(n, hasher);
1398 }
1399 }
1400 SerializableContinuation::ChildWorkflow { id, child, next } => {
1401 hasher.update(b"CW:");
1402 hasher.update(id.as_bytes());
1403 hasher.update(b"{");
1404 hash_continuation(child, hasher);
1405 hasher.update(b"}");
1406 hasher.update(b";");
1407 if let Some(n) = next {
1408 hash_continuation(n, hasher);
1409 }
1410 }
1411 }
1412 }
1413
1414 let mut hasher = Sha256::new();
1415 hash_continuation(self, &mut hasher);
1416 crate::DefinitionHash::from_hash(crate::Hash32::from_digest(hasher))
1417 }
1418}
1419
1420#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1427pub struct SerializedWorkflowState {
1428 pub workflow_id: String,
1430 pub definition_hash: crate::DefinitionHash,
1433 pub continuation: SerializableContinuation,
1435}
1436
1437#[derive(
1440 Debug,
1441 Clone,
1442 Copy,
1443 Default,
1444 PartialEq,
1445 Eq,
1446 strum::EnumString,
1447 strum::Display,
1448 strum::VariantNames,
1449)]
1450#[strum(serialize_all = "snake_case")]
1451pub enum ConflictPolicy {
1452 #[default]
1454 Fail,
1455 #[strum(serialize = "use_existing", serialize = "useExisting")]
1457 UseExisting,
1458 #[strum(serialize = "terminate_existing", serialize = "terminateExisting")]
1460 TerminateExisting,
1461}
1462
1463impl ConflictPolicy {
1464 pub fn parse_optional(s: Option<&str>) -> Result<Self, &str> {
1473 match s {
1474 None => Ok(Self::default()),
1475 Some(val) => val.parse::<Self>().map_err(|_| val),
1476 }
1477 }
1478
1479 #[must_use]
1486 pub fn valid_names() -> &'static [&'static str] {
1487 <Self as strum::VariantNames>::VARIANTS
1488 }
1489}
1490
1491#[derive(Debug, strum::AsRefStr, strum::EnumDiscriminants)]
1493#[strum_discriminants(name(WorkflowStatusKind))]
1494#[strum_discriminants(derive(strum::AsRefStr))]
1495#[strum_discriminants(strum(serialize_all = "snake_case"))]
1496#[strum_discriminants(doc = "Fieldless discriminant of [`WorkflowStatus`] for string comparisons.")]
1497pub enum WorkflowStatus {
1498 #[strum(serialize = "in_progress")]
1500 InProgress,
1501 #[strum(serialize = "completed")]
1503 Completed,
1504 #[strum(serialize = "failed")]
1506 Failed(String),
1507 #[strum(serialize = "cancelled")]
1509 Cancelled {
1510 reason: Option<String>,
1512 cancelled_by: Option<String>,
1514 },
1515 #[strum(serialize = "paused")]
1517 Paused {
1518 reason: Option<String>,
1520 paused_by: Option<String>,
1522 },
1523 #[strum(serialize = "waiting")]
1525 Waiting {
1526 wake_at: chrono::DateTime<chrono::Utc>,
1528 delay_id: crate::TaskId,
1530 },
1531 #[strum(serialize = "awaiting_signal")]
1533 AwaitingSignal {
1534 signal_id: crate::TaskId,
1536 signal_name: String,
1538 wake_at: Option<chrono::DateTime<chrono::Utc>>,
1540 },
1541}
1542
1543#[derive(Debug, Default)]
1549pub struct FlatWorkflowStatus {
1550 pub status: String,
1553 pub error: Option<String>,
1555 pub reason: Option<String>,
1557 pub cancelled_by: Option<String>,
1559 pub paused_by: Option<String>,
1561 pub wake_at: Option<String>,
1563 pub delay_id: Option<String>,
1565 pub signal_id: Option<String>,
1567 pub signal_name: Option<String>,
1569}
1570
1571impl From<WorkflowStatus> for FlatWorkflowStatus {
1572 fn from(status: WorkflowStatus) -> Self {
1573 let mut flat = Self {
1574 status: status.as_ref().to_string(),
1575 ..Self::default()
1576 };
1577 match status {
1578 WorkflowStatus::Completed | WorkflowStatus::InProgress => {}
1579 WorkflowStatus::Failed(e) => flat.error = Some(e),
1580 WorkflowStatus::Cancelled {
1581 reason,
1582 cancelled_by,
1583 } => {
1584 flat.reason = reason;
1585 flat.cancelled_by = cancelled_by;
1586 }
1587 WorkflowStatus::Paused { reason, paused_by } => {
1588 flat.reason = reason;
1589 flat.paused_by = paused_by;
1590 }
1591 WorkflowStatus::Waiting { wake_at, delay_id } => {
1592 flat.wake_at = Some(wake_at.to_rfc3339());
1593 flat.delay_id = Some(delay_id.to_hex());
1595 }
1596 WorkflowStatus::AwaitingSignal {
1597 signal_id,
1598 signal_name,
1599 wake_at,
1600 } => {
1601 flat.signal_id = Some(signal_id.to_hex());
1602 flat.signal_name = Some(signal_name);
1603 flat.wake_at = wake_at.map(|t| t.to_rfc3339());
1604 }
1605 }
1606 flat
1607 }
1608}
1609
1610pub use crate::builder::{
1612 BranchCollector, ContinuationState, ForkBuilder, NoContinuation, NoRegistry, RegistryBehavior,
1613 RouteBuilder, SubBuilder, WorkflowBuilder,
1614};
1615
1616use crate::registry::TaskRegistry;
1617
1618pub struct Workflow<C, Input, M = ()> {
1620 pub(crate) definition_hash: crate::DefinitionHash,
1621 pub(crate) context: WorkflowContext<C, M>,
1622 pub(crate) continuation: WorkflowContinuation,
1623 pub(crate) task_index: Arc<crate::task_index::TaskIndex>,
1628 pub(crate) _phantom: PhantomData<Input>,
1629}
1630
1631impl<C, Input, M> Workflow<C, Input, M> {
1632 #[must_use]
1634 pub fn workflow_id(&self) -> &str {
1635 &self.context.workflow_name
1636 }
1637
1638 #[must_use]
1644 pub fn definition_hash(&self) -> &crate::DefinitionHash {
1645 &self.definition_hash
1646 }
1647
1648 #[must_use]
1650 pub fn context(&self) -> &WorkflowContext<C, M> {
1651 &self.context
1652 }
1653
1654 #[must_use]
1656 pub fn codec(&self) -> &Arc<C> {
1657 &self.context.codec
1658 }
1659
1660 #[must_use]
1662 pub fn continuation(&self) -> &WorkflowContinuation {
1663 &self.continuation
1664 }
1665
1666 #[must_use]
1668 pub fn task_index(&self) -> &crate::task_index::TaskIndex {
1669 &self.task_index
1670 }
1671
1672 #[must_use]
1675 pub fn task_index_arc(&self) -> Arc<crate::task_index::TaskIndex> {
1676 Arc::clone(&self.task_index)
1677 }
1678
1679 #[must_use]
1681 pub fn metadata(&self) -> &Arc<M> {
1682 &self.context.metadata
1683 }
1684
1685 #[must_use]
1689 pub fn iter_nodes(&self) -> NodeIter<'_> {
1690 self.continuation.iter_nodes()
1691 }
1692
1693 #[must_use]
1697 pub fn into_continuation(self) -> WorkflowContinuation {
1698 self.continuation
1699 }
1700}
1701
1702pub struct SerializableWorkflow<C, Input, M = ()> {
1750 pub(crate) inner: Workflow<C, Input, M>,
1751 pub(crate) registry: TaskRegistry,
1752}
1753
1754impl<C, Input, M> SerializableWorkflow<C, Input, M> {
1755 #[must_use]
1757 pub fn workflow_id(&self) -> &str {
1758 self.inner.workflow_id()
1759 }
1760
1761 #[must_use]
1763 pub fn definition_hash(&self) -> &crate::DefinitionHash {
1764 self.inner.definition_hash()
1765 }
1766
1767 #[must_use]
1769 pub fn workflow(&self) -> &Workflow<C, Input, M> {
1770 &self.inner
1771 }
1772
1773 #[must_use]
1775 pub fn context(&self) -> &WorkflowContext<C, M> {
1776 self.inner.context()
1777 }
1778
1779 #[must_use]
1781 pub fn codec(&self) -> &Arc<C> {
1782 self.inner.codec()
1783 }
1784
1785 #[must_use]
1787 pub fn continuation(&self) -> &WorkflowContinuation {
1788 self.inner.continuation()
1789 }
1790
1791 #[must_use]
1793 pub fn metadata(&self) -> &Arc<M> {
1794 self.inner.metadata()
1795 }
1796
1797 #[must_use]
1799 pub fn registry(&self) -> &TaskRegistry {
1800 &self.registry
1801 }
1802
1803 #[must_use]
1808 pub fn into_parts(self) -> (WorkflowContinuation, TaskRegistry) {
1809 (self.inner.continuation, self.registry)
1810 }
1811
1812 #[must_use]
1818 pub fn to_serializable(&self) -> SerializedWorkflowState {
1819 SerializedWorkflowState {
1820 workflow_id: self.inner.workflow_id().to_string(),
1821 definition_hash: self.inner.definition_hash,
1822 continuation: self.inner.continuation().to_serializable(),
1823 }
1824 }
1825
1826 pub fn to_runnable(
1836 &self,
1837 state: &SerializedWorkflowState,
1838 ) -> Result<WorkflowContinuation, crate::error::BuildError> {
1839 if state.definition_hash != self.inner.definition_hash {
1840 return Err(crate::error::BuildError::DefinitionMismatch {
1841 expected: self.inner.definition_hash,
1842 found: state.definition_hash,
1843 });
1844 }
1845 state.continuation.to_runnable(&self.registry)
1846 }
1847}
1848
1849impl<C, Input, M> Deref for SerializableWorkflow<C, Input, M> {
1850 type Target = Workflow<C, Input, M>;
1851
1852 fn deref(&self) -> &Self::Target {
1853 &self.inner
1854 }
1855}
1856
1857#[cfg(test)]
1858#[allow(
1859 clippy::unwrap_used,
1860 clippy::panic,
1861 clippy::cast_lossless,
1862 clippy::cast_possible_truncation,
1863 clippy::uninlined_format_args,
1864 clippy::manual_let_else,
1865 clippy::too_many_lines,
1866 clippy::items_after_statements,
1867 clippy::indexing_slicing
1868)]
1869mod tests {
1870 use crate::codec::{Decoder, Encoder, sealed};
1871 use crate::error::BoxError;
1872 use crate::workflow::WorkflowBuilder;
1873 use bytes::Bytes;
1874
1875 struct DummyCodec;
1876
1877 impl Encoder for DummyCodec {}
1878 impl Decoder for DummyCodec {}
1879
1880 impl<Input> sealed::EncodeValue<Input> for DummyCodec {
1881 fn encode_value(&self, _value: &Input) -> Result<Bytes, BoxError> {
1882 Ok(Bytes::new())
1883 }
1884 }
1885 impl<Output> sealed::DecodeValue<Output> for DummyCodec {
1886 fn decode_value(&self, _bytes: Bytes) -> Result<Output, BoxError> {
1887 Err("Not implemented".into())
1888 }
1889 }
1890
1891 #[test]
1892 fn test_workflow_build() {
1893 use crate::context::WorkflowContext;
1894 use crate::workflow::Workflow;
1895 use std::sync::Arc;
1896
1897 let ctx = WorkflowContext::new("test-workflow", Arc::new(DummyCodec), Arc::new(()));
1898 let workflow: Workflow<DummyCodec, u32> = WorkflowBuilder::new(ctx)
1899 .then("test", |i: u32| async move { Ok(i + 1) })
1900 .build()
1901 .unwrap();
1902
1903 let _workflow_ref = &workflow;
1906 }
1907
1908 #[test]
1909 fn test_workflow_with_metadata() {
1910 use crate::context::WorkflowContext;
1911 use crate::workflow::Workflow;
1912 use std::sync::Arc;
1913
1914 let ctx = WorkflowContext::new(
1915 "test-workflow",
1916 Arc::new(DummyCodec),
1917 Arc::new("test_metadata"),
1918 );
1919 let workflow: Workflow<DummyCodec, u32, &str> = WorkflowBuilder::new(ctx)
1920 .then("test", |i: u32| async move { Ok(i + 1) })
1921 .build()
1922 .unwrap();
1923
1924 assert_eq!(**workflow.metadata(), "test_metadata");
1925 }
1926
1927 #[test]
1928 fn test_task_order() {
1929 use crate::context::WorkflowContext;
1930 use crate::workflow::Workflow;
1931 use std::sync::Arc;
1932
1933 let ctx = WorkflowContext::new("test-workflow", Arc::new(DummyCodec), Arc::new(()));
1934 let workflow: Workflow<DummyCodec, u32> = WorkflowBuilder::new(ctx)
1935 .then("first", |i: u32| async move { Ok(i + 1) })
1936 .then("second", |i: u32| async move { Ok(i + 2) })
1937 .then("third", |i: u32| async move { Ok(i + 3) })
1938 .build()
1939 .unwrap();
1940
1941 let mut current = workflow.continuation();
1944 let mut task_ids = vec![];
1945
1946 while let crate::workflow::WorkflowContinuation::Task { id, next, .. } = current {
1947 task_ids.push(id.clone());
1948 match next {
1949 Some(next_box) => current = next_box.as_ref(),
1950 None => break,
1951 }
1952 }
1953
1954 assert_eq!(
1955 task_ids,
1956 vec!["first", "second", "third"],
1957 "Tasks should execute in the order they were added"
1958 );
1959 }
1960
1961 #[test]
1962 fn test_heterogeneous_fork_join_compiles() {
1963 use crate::context::WorkflowContext;
1964 use crate::task::BranchOutputs;
1965 use crate::workflow::Workflow;
1966 use std::sync::Arc;
1967
1968 let ctx = WorkflowContext::new("test-workflow", Arc::new(DummyCodec), Arc::new(()));
1969 let workflow: Workflow<DummyCodec, u32> = WorkflowBuilder::new(ctx)
1972 .then("prepare", |i: u32| async move { Ok(i) })
1973 .branches(|b| {
1974 b.add("count", |i: u32| async move { Ok(i * 2) });
1976 b.add("name", |i: u32| async move { Ok(format!("item_{}", i)) });
1978 b.add("ratio", |i: u32| async move { Ok(i as f64 / 100.0) });
1980 })
1981 .join("combine", |outputs: BranchOutputs<DummyCodec>| async move {
1982 let _ = outputs.len();
1988 Ok(format!("combined {} branches", outputs.len()))
1989 })
1990 .then("final", |s: String| async move { Ok(s.len() as u32) })
1991 .build()
1992 .unwrap();
1993
1994 let _workflow_ref = &workflow;
1995 }
1996
1997 #[test]
1998 fn test_duplicate_branch_id_returns_error() {
1999 use crate::context::WorkflowContext;
2000 use crate::error::BuildError;
2001 use std::sync::Arc;
2002
2003 let ctx = WorkflowContext::new("test-workflow", Arc::new(DummyCodec), Arc::new(()));
2004 let result = WorkflowBuilder::<_, u32, _>::new(ctx)
2005 .then("prepare", |i: u32| async move { Ok(i) })
2006 .branches(|b| {
2007 b.add("count", |i: u32| async move { Ok(i * 2) });
2008 b.add("count", |i: u32| async move { Ok(i * 3) }); })
2010 .join("combine", |_outputs| async move { Ok(0u32) })
2011 .build();
2012
2013 let err = match result {
2014 Err(e) => e,
2015 Ok(_) => panic!("expected build error"),
2016 };
2017 assert!(
2018 err.iter()
2019 .any(|e| matches!(e, BuildError::DuplicateTaskId(id) if id == "count"))
2020 );
2021 }
2022
2023 #[test]
2024 fn test_serializable_continuation() {
2025 use crate::context::WorkflowContext;
2026 use crate::error::BuildError;
2027 use crate::registry::TaskRegistry;
2028 use std::sync::Arc;
2029
2030 let codec = Arc::new(DummyCodec);
2032 let ctx = WorkflowContext::new("test-workflow", codec.clone(), Arc::new(()));
2033 let workflow = WorkflowBuilder::new(ctx)
2034 .then("step1", |i: u32| async move { Ok(i + 1) })
2035 .then("step2", |i: u32| async move { Ok(i * 2) })
2036 .build()
2037 .unwrap();
2038
2039 let serializable = workflow.continuation().to_serializable();
2041
2042 let task_ids = serializable.task_ids();
2044 assert_eq!(task_ids, vec!["step1", "step2"]);
2045
2046 let empty_registry = TaskRegistry::new();
2048 let result = serializable.to_runnable(&empty_registry);
2049 assert!(matches!(result, Err(BuildError::TaskNotFound(id)) if id == "step1"));
2050
2051 let mut registry = TaskRegistry::new();
2053 registry.register_fn("step1", codec.clone(), |i: u32| async move { Ok(i + 1) });
2054 registry.register_fn("step2", codec.clone(), |i: u32| async move { Ok(i * 2) });
2055
2056 let hydrated = serializable.to_runnable(®istry);
2057 assert!(hydrated.is_ok());
2058 }
2059
2060 #[test]
2061 fn test_serializable_fork_join() {
2062 use crate::context::WorkflowContext;
2063 use crate::task::BranchOutputs;
2064 use std::sync::Arc;
2065
2066 let ctx = WorkflowContext::new("test-workflow", Arc::new(DummyCodec), Arc::new(()));
2067 let workflow = WorkflowBuilder::new(ctx)
2068 .then("prepare", |i: u32| async move { Ok(i) })
2069 .branches(|b| {
2070 b.add("branch_a", |i: u32| async move { Ok(i * 2) });
2071 b.add("branch_b", |i: u32| async move { Ok(i + 10) });
2072 })
2073 .join(
2074 "merge",
2075 |_: BranchOutputs<DummyCodec>| async move { Ok(0u32) },
2076 )
2077 .build()
2078 .unwrap();
2079
2080 let serializable = workflow.continuation().to_serializable();
2081 let task_ids = serializable.task_ids();
2082
2083 assert!(task_ids.contains(&"prepare"));
2085 assert!(task_ids.contains(&"branch_a||branch_b"));
2086 assert!(task_ids.contains(&"branch_a"));
2087 assert!(task_ids.contains(&"branch_b"));
2088 assert!(task_ids.contains(&"merge"));
2089 assert_eq!(task_ids.len(), 5);
2090 }
2091
2092 #[test]
2093 fn test_serializable_workflow_builder() {
2094 use crate::context::WorkflowContext;
2095 use std::sync::Arc;
2096
2097 let codec = Arc::new(DummyCodec);
2098 let ctx = WorkflowContext::new("test-workflow", codec, Arc::new(()));
2099
2100 let workflow = WorkflowBuilder::new(ctx)
2102 .with_registry()
2103 .then("step1", |i: u32| async move { Ok(i + 1) })
2104 .then("step2", |i: u32| async move { Ok(i * 2) })
2105 .build()
2106 .unwrap();
2107
2108 assert!(workflow.registry().contains("step1"));
2110 assert!(workflow.registry().contains("step2"));
2111 assert_eq!(workflow.registry().len(), 2);
2112
2113 let serializable = workflow.to_serializable();
2115 assert_eq!(serializable.continuation.task_ids(), vec!["step1", "step2"]);
2116
2117 let hydrated = workflow.to_runnable(&serializable);
2119 assert!(hydrated.is_ok());
2120 }
2121
2122 #[test]
2123 fn test_with_existing_registry_and_then_registered() {
2124 use crate::context::WorkflowContext;
2125 use crate::registry::TaskRegistry;
2126 use crate::workflow::SerializableWorkflow;
2127 use std::sync::Arc;
2128
2129 let codec = Arc::new(DummyCodec);
2130
2131 let mut registry = TaskRegistry::new();
2133 registry.register_fn("double", codec.clone(), |i: u32| async move { Ok(i * 2) });
2134 registry.register_fn("add_ten", codec.clone(), |i: u32| async move { Ok(i + 10) });
2135
2136 let ctx = WorkflowContext::new("test-workflow", codec.clone(), Arc::new(()));
2138 let workflow: SerializableWorkflow<_, u32> = WorkflowBuilder::new(ctx)
2139 .with_existing_registry(registry)
2140 .then_registered::<u32>("double")
2141 .then_registered::<u32>("add_ten")
2142 .build()
2143 .unwrap();
2144
2145 assert!(workflow.registry().contains("double"));
2147 assert!(workflow.registry().contains("add_ten"));
2148
2149 let serializable = workflow.to_serializable();
2151 assert_eq!(
2152 serializable.continuation.task_ids(),
2153 vec!["double", "add_ten"]
2154 );
2155
2156 let hydrated = workflow.to_runnable(&serializable);
2158 assert!(hydrated.is_ok());
2159 }
2160
2161 #[test]
2162 fn test_mixed_inline_and_registered_tasks() {
2163 use crate::context::WorkflowContext;
2164 use crate::registry::TaskRegistry;
2165 use crate::workflow::SerializableWorkflow;
2166 use std::sync::Arc;
2167
2168 let codec = Arc::new(DummyCodec);
2169
2170 let mut registry = TaskRegistry::new();
2172 registry.register_fn(
2173 "preregistered",
2174 codec.clone(),
2175 |i: u32| async move { Ok(i * 2) },
2176 );
2177
2178 let ctx = WorkflowContext::new("test-workflow", codec.clone(), Arc::new(()));
2180 let workflow: SerializableWorkflow<_, u32> = WorkflowBuilder::new(ctx)
2181 .with_existing_registry(registry)
2182 .then_registered::<u32>("preregistered") .then("inline", |i: u32| async move { Ok(i + 5) }) .build()
2185 .unwrap();
2186
2187 assert!(workflow.registry().contains("preregistered"));
2189 assert!(workflow.registry().contains("inline"));
2190 assert_eq!(workflow.registry().len(), 2);
2191 }
2192
2193 #[test]
2194 fn test_workflow_id_and_definition_hash() {
2195 use crate::context::WorkflowContext;
2196 use std::sync::Arc;
2197
2198 let ctx = WorkflowContext::new("my-workflow-id", Arc::new(DummyCodec), Arc::new(()));
2199 let workflow = WorkflowBuilder::new(ctx)
2200 .with_registry()
2201 .then("step1", |i: u32| async move { Ok(i + 1) })
2202 .then("step2", |i: u32| async move { Ok(i * 2) })
2203 .build()
2204 .unwrap();
2205
2206 assert_eq!(workflow.workflow_id(), "my-workflow-id");
2208
2209 assert_ne!(
2211 *workflow.definition_hash(),
2212 crate::DefinitionHash::from_bytes([0u8; 32])
2213 );
2214
2215 let state = workflow.to_serializable();
2217 assert_eq!(state.workflow_id, "my-workflow-id");
2218 assert_eq!(&state.definition_hash, workflow.definition_hash());
2219 }
2220
2221 #[test]
2222 fn test_definition_hash_changes_with_structure() {
2223 use crate::context::WorkflowContext;
2224 use std::sync::Arc;
2225
2226 let ctx1 = WorkflowContext::new("workflow", Arc::new(DummyCodec), Arc::new(()));
2228 let workflow1 = WorkflowBuilder::new(ctx1)
2229 .with_registry()
2230 .then("step1", |i: u32| async move { Ok(i + 1) })
2231 .build()
2232 .unwrap();
2233
2234 let ctx2 = WorkflowContext::new("workflow", Arc::new(DummyCodec), Arc::new(()));
2235 let workflow2 = WorkflowBuilder::new(ctx2)
2236 .with_registry()
2237 .then("step1", |i: u32| async move { Ok(i + 1) })
2238 .then("step2", |i: u32| async move { Ok(i * 2) })
2239 .build()
2240 .unwrap();
2241
2242 assert_ne!(workflow1.definition_hash(), workflow2.definition_hash());
2243 }
2244
2245 #[test]
2246 fn test_definition_mismatch_error() {
2247 use crate::context::WorkflowContext;
2248 use crate::error::BuildError;
2249 use std::sync::Arc;
2250
2251 let ctx = WorkflowContext::new("test-workflow", Arc::new(DummyCodec), Arc::new(()));
2252 let workflow = WorkflowBuilder::new(ctx)
2253 .with_registry()
2254 .then("step1", |i: u32| async move { Ok(i + 1) })
2255 .build()
2256 .unwrap();
2257
2258 let mut state = workflow.to_serializable();
2260 state.definition_hash = crate::DefinitionHash::sha256(b"wrong-hash");
2261
2262 let result = workflow.to_runnable(&state);
2264 assert!(matches!(result, Err(BuildError::DefinitionMismatch { .. })));
2265 }
2266
2267 #[test]
2268 fn test_duplicate_id_tampering_detection() {
2269 use crate::error::BuildError;
2270 use crate::registry::TaskRegistry;
2271 use crate::workflow::SerializableContinuation;
2272 use std::sync::Arc;
2273
2274 let codec = Arc::new(DummyCodec);
2275
2276 let mut registry = TaskRegistry::new();
2278 registry.register_fn("step1", codec.clone(), |i: u32| async move { Ok(i + 1) });
2279 registry.register_fn("step2", codec.clone(), |i: u32| async move { Ok(i * 2) });
2280
2281 let tampered = SerializableContinuation::Task {
2283 id: "step1".to_string(),
2284 timeout_ms: None,
2285 retry_policy: None,
2286 version: None,
2287 priority: None,
2288
2289 tags: vec![],
2290 next: Some(Box::new(SerializableContinuation::Task {
2291 id: "step1".to_string(), timeout_ms: None,
2293 retry_policy: None,
2294 version: None,
2295 priority: None,
2296
2297 tags: vec![],
2298 next: None,
2299 })),
2300 };
2301
2302 let result = tampered.to_runnable(®istry);
2304 assert!(matches!(
2305 result,
2306 Err(BuildError::DuplicateTaskId(id)) if id == "step1"
2307 ));
2308 }
2309
2310 #[test]
2315 fn test_delay_builder() {
2316 use crate::context::WorkflowContext;
2317 use crate::workflow::{Workflow, WorkflowContinuation};
2318 use std::sync::Arc;
2319 use std::time::Duration;
2320
2321 let ctx = WorkflowContext::new("test-workflow", Arc::new(DummyCodec), Arc::new(()));
2322 let workflow: Workflow<DummyCodec, u32> = WorkflowBuilder::new(ctx)
2323 .then("step1", |i: u32| async move { Ok(i + 1) })
2324 .delay("wait_1s", Duration::from_secs(1))
2325 .then("step2", |i: u32| async move { Ok(i * 2) })
2326 .build()
2327 .unwrap();
2328
2329 let mut ids = vec![];
2331 let mut current = workflow.continuation();
2332 loop {
2333 match current {
2334 WorkflowContinuation::Task { id, next, .. } => {
2335 ids.push(format!("task:{id}"));
2336 match next {
2337 Some(n) => current = n,
2338 None => break,
2339 }
2340 }
2341 WorkflowContinuation::Delay {
2342 id, duration, next, ..
2343 } => {
2344 ids.push(format!("delay:{id}:{}ms", duration.as_millis()));
2345 match next {
2346 Some(n) => current = n,
2347 None => break,
2348 }
2349 }
2350 _ => break,
2351 }
2352 }
2353
2354 assert_eq!(
2355 ids,
2356 vec!["task:step1", "delay:wait_1s:1000ms", "task:step2"]
2357 );
2358 }
2359
2360 #[test]
2361 fn test_delay_serialization_roundtrip() {
2362 use crate::context::WorkflowContext;
2363 use crate::workflow::SerializableContinuation;
2364 use std::sync::Arc;
2365 use std::time::Duration;
2366
2367 let ctx = WorkflowContext::new("test-workflow", Arc::new(DummyCodec), Arc::new(()));
2368 let workflow = WorkflowBuilder::new(ctx)
2369 .with_registry()
2370 .then("step1", |i: u32| async move { Ok(i + 1) })
2371 .delay("wait_5s", Duration::from_secs(5))
2372 .then("step2", |i: u32| async move { Ok(i * 2) })
2373 .build()
2374 .unwrap();
2375
2376 let serializable = workflow.to_serializable();
2378
2379 let task_ids = serializable.continuation.task_ids();
2381 assert_eq!(task_ids, vec!["step1", "wait_5s", "step2"]);
2382
2383 match &serializable.continuation {
2385 SerializableContinuation::Task { next, .. } => {
2386 let next = next.as_ref().unwrap();
2387 match next.as_ref() {
2388 SerializableContinuation::Delay {
2389 id, duration_ms, ..
2390 } => {
2391 assert_eq!(id, "wait_5s");
2392 assert_eq!(*duration_ms, 5000);
2393 }
2394 other => panic!("Expected Delay, got {other:?}"),
2395 }
2396 }
2397 other => panic!("Expected Task, got {other:?}"),
2398 }
2399
2400 let hydrated = workflow.to_runnable(&serializable);
2402 assert!(hydrated.is_ok());
2403 }
2404
2405 #[test]
2406 fn test_delay_first_task_id() {
2407 use crate::context::WorkflowContext;
2408 use std::sync::Arc;
2409 use std::time::Duration;
2410
2411 let ctx = WorkflowContext::new("test-workflow", Arc::new(DummyCodec), Arc::new(()));
2412 let workflow = WorkflowBuilder::new(ctx)
2413 .delay("initial_delay", Duration::from_secs(10))
2414 .then("step1", |i: u32| async move { Ok(i + 1) })
2415 .build()
2416 .unwrap();
2417
2418 assert_eq!(workflow.continuation().first_task_id(), "initial_delay");
2419 }
2420
2421 #[test]
2422 fn test_delay_duplicate_id_detection() {
2423 use crate::context::WorkflowContext;
2424 use crate::error::BuildError;
2425 use std::sync::Arc;
2426 use std::time::Duration;
2427
2428 let ctx = WorkflowContext::new("test-workflow", Arc::new(DummyCodec), Arc::new(()));
2429 let result = WorkflowBuilder::<_, u32, _>::new(ctx)
2430 .then("dup", |i: u32| async move { Ok(i + 1) })
2431 .delay("dup", Duration::from_secs(1))
2432 .build();
2433
2434 let err = match result {
2435 Err(e) => e,
2436 Ok(_) => panic!("expected build error"),
2437 };
2438 assert!(
2439 err.iter()
2440 .any(|e| matches!(e, BuildError::DuplicateTaskId(id) if id == "dup"))
2441 );
2442 }
2443
2444 #[test]
2445 fn test_delay_definition_hash_includes_duration() {
2446 use crate::context::WorkflowContext;
2447 use crate::workflow::SerializableWorkflow;
2448 use std::sync::Arc;
2449 use std::time::Duration;
2450
2451 let ctx1 = WorkflowContext::new("workflow", Arc::new(DummyCodec), Arc::new(()));
2453 let wf1: SerializableWorkflow<_, u32> = WorkflowBuilder::new(ctx1)
2454 .with_registry()
2455 .then("step1", |i: u32| async move { Ok(i + 1) })
2456 .delay("wait", Duration::from_secs(1))
2457 .build()
2458 .unwrap();
2459
2460 let ctx2 = WorkflowContext::new("workflow", Arc::new(DummyCodec), Arc::new(()));
2462 let wf2: SerializableWorkflow<_, u32> = WorkflowBuilder::new(ctx2)
2463 .with_registry()
2464 .then("step1", |i: u32| async move { Ok(i + 1) })
2465 .delay("wait", Duration::from_mins(1))
2466 .build()
2467 .unwrap();
2468
2469 assert_ne!(wf1.definition_hash(), wf2.definition_hash());
2471 }
2472
2473 #[test]
2474 fn test_delay_definition_hash_differs_from_task() {
2475 use crate::context::WorkflowContext;
2476 use crate::workflow::SerializableWorkflow;
2477 use std::sync::Arc;
2478 use std::time::Duration;
2479
2480 let ctx1 = WorkflowContext::new("workflow", Arc::new(DummyCodec), Arc::new(()));
2482 let wf1: SerializableWorkflow<_, u32> = WorkflowBuilder::new(ctx1)
2483 .with_registry()
2484 .then("step1", |i: u32| async move { Ok(i + 1) })
2485 .build()
2486 .unwrap();
2487
2488 let ctx2 = WorkflowContext::new("workflow", Arc::new(DummyCodec), Arc::new(()));
2490 let wf2: SerializableWorkflow<_, u32> = WorkflowBuilder::new(ctx2)
2491 .with_registry()
2492 .delay("step1", Duration::from_secs(1))
2493 .build()
2494 .unwrap();
2495
2496 assert_ne!(wf1.definition_hash(), wf2.definition_hash());
2498 }
2499
2500 #[test]
2501 fn test_delay_task_ids() {
2502 use crate::context::WorkflowContext;
2503 use std::sync::Arc;
2504 use std::time::Duration;
2505
2506 let ctx = WorkflowContext::new("test-workflow", Arc::new(DummyCodec), Arc::new(()));
2507 let workflow = WorkflowBuilder::new(ctx)
2508 .then("fetch", |i: u32| async move { Ok(i) })
2509 .delay("wait_24h", Duration::from_hours(24))
2510 .then("process", |i: u32| async move { Ok(i + 1) })
2511 .build()
2512 .unwrap();
2513
2514 let serializable = workflow.continuation().to_serializable();
2515 let ids = serializable.task_ids();
2516 assert_eq!(ids, vec!["fetch", "wait_24h", "process"]);
2517 }
2518
2519 #[test]
2520 fn test_delay_only_workflow() {
2521 use crate::context::WorkflowContext;
2522 use std::sync::Arc;
2523 use std::time::Duration;
2524
2525 use crate::workflow::Workflow;
2526
2527 let ctx = WorkflowContext::new("test-workflow", Arc::new(DummyCodec), Arc::new(()));
2528 let workflow: Workflow<DummyCodec, u32> = WorkflowBuilder::new(ctx)
2529 .delay("just_wait", Duration::from_millis(10))
2530 .build()
2531 .unwrap();
2532
2533 assert_eq!(workflow.continuation().first_task_id(), "just_wait");
2534
2535 let serializable = workflow.continuation().to_serializable();
2536 assert_eq!(serializable.task_ids(), vec!["just_wait"]);
2537 }
2538
2539 #[test]
2540 fn test_delay_to_runnable_no_registry_needed() {
2541 use crate::registry::TaskRegistry;
2542 use crate::workflow::SerializableContinuation;
2543
2544 let delay = SerializableContinuation::Delay {
2546 id: "wait".to_string(),
2547 duration_ms: 5000,
2548 next: None,
2549 };
2550
2551 let empty_registry = TaskRegistry::new();
2552 let result = delay.to_runnable(&empty_registry);
2553 assert!(result.is_ok());
2554
2555 let runnable = result.unwrap();
2556 match runnable {
2557 crate::workflow::WorkflowContinuation::Delay {
2558 id, duration, next, ..
2559 } => {
2560 assert_eq!(id, "wait");
2561 assert_eq!(duration, std::time::Duration::from_secs(5));
2562 assert!(next.is_none());
2563 }
2564 _ => panic!("Expected Delay variant"),
2565 }
2566 }
2567
2568 #[test]
2573 fn test_timeout_serialization_roundtrip() {
2574 use crate::context::WorkflowContext;
2575 use crate::task::TaskMetadata;
2576 use crate::workflow::SerializableContinuation;
2577 use std::sync::Arc;
2578 use std::time::Duration;
2579
2580 let ctx = WorkflowContext::new("test-workflow", Arc::new(DummyCodec), Arc::new(()));
2581 let workflow = WorkflowBuilder::new(ctx)
2582 .with_registry()
2583 .then("step1", |i: u32| async move { Ok(i + 1) })
2584 .with_metadata(TaskMetadata {
2585 timeout: Some(Duration::from_secs(30)),
2586 ..Default::default()
2587 })
2588 .then("step2", |i: u32| async move { Ok(i * 2) })
2589 .build()
2590 .unwrap();
2591
2592 let serializable = workflow.to_serializable();
2594
2595 match &serializable.continuation {
2597 SerializableContinuation::Task { id, timeout_ms, .. } => {
2598 assert_eq!(id, "step1");
2599 assert_eq!(*timeout_ms, Some(30_000));
2600 }
2601 other => panic!("Expected Task, got {other:?}"),
2602 }
2603
2604 let hydrated = workflow.to_runnable(&serializable).unwrap();
2606 match &hydrated {
2607 crate::workflow::WorkflowContinuation::Task { id, timeout, .. } => {
2608 assert_eq!(id, "step1");
2609 assert_eq!(*timeout, Some(Duration::from_secs(30)));
2610 }
2611 _ => panic!("Expected Task variant"),
2612 }
2613 }
2614
2615 #[test]
2616 fn test_timeout_changes_definition_hash() {
2617 use crate::context::WorkflowContext;
2618 use crate::task::TaskMetadata;
2619 use crate::workflow::SerializableWorkflow;
2620 use std::sync::Arc;
2621 use std::time::Duration;
2622
2623 let ctx1 = WorkflowContext::new("workflow", Arc::new(DummyCodec), Arc::new(()));
2625 let wf1: SerializableWorkflow<_, u32> = WorkflowBuilder::new(ctx1)
2626 .with_registry()
2627 .then("step1", |i: u32| async move { Ok(i + 1) })
2628 .build()
2629 .unwrap();
2630
2631 let ctx2 = WorkflowContext::new("workflow", Arc::new(DummyCodec), Arc::new(()));
2633 let wf2: SerializableWorkflow<_, u32> = WorkflowBuilder::new(ctx2)
2634 .with_registry()
2635 .then("step1", |i: u32| async move { Ok(i + 1) })
2636 .with_metadata(TaskMetadata {
2637 timeout: Some(Duration::from_secs(30)),
2638 ..Default::default()
2639 })
2640 .build()
2641 .unwrap();
2642
2643 assert_ne!(wf1.definition_hash(), wf2.definition_hash());
2645 }
2646
2647 #[test]
2648 fn test_no_timeout_field_absent_in_serialization() {
2649 use crate::context::WorkflowContext;
2650 use std::sync::Arc;
2651
2652 let ctx = WorkflowContext::new("test-workflow", Arc::new(DummyCodec), Arc::new(()));
2653 let workflow = WorkflowBuilder::new(ctx)
2654 .with_registry()
2655 .then("step1", |i: u32| async move { Ok(i + 1) })
2656 .build()
2657 .unwrap();
2658
2659 let serializable = workflow.to_serializable();
2660 let json = serde_json::to_string(&serializable.continuation).unwrap();
2662 assert!(
2663 !json.contains("timeout_ms"),
2664 "timeout_ms should be absent when None: {json}"
2665 );
2666 }
2667
2668 #[test]
2669 fn test_task_version_changes_definition_hash() {
2670 use crate::context::WorkflowContext;
2671 use crate::task::TaskMetadata;
2672 use crate::workflow::SerializableWorkflow;
2673 use std::sync::Arc;
2674
2675 let ctx1 = WorkflowContext::new("workflow", Arc::new(DummyCodec), Arc::new(()));
2677 let wf_no_version: SerializableWorkflow<_, u32> = WorkflowBuilder::new(ctx1)
2678 .with_registry()
2679 .then("step1", |i: u32| async move { Ok(i + 1) })
2680 .build()
2681 .unwrap();
2682
2683 let ctx2 = WorkflowContext::new("workflow", Arc::new(DummyCodec), Arc::new(()));
2685 let wf_v1: SerializableWorkflow<_, u32> = WorkflowBuilder::new(ctx2)
2686 .with_registry()
2687 .then("step1", |i: u32| async move { Ok(i + 1) })
2688 .with_metadata(TaskMetadata {
2689 version: Some("1.0".into()),
2690 ..Default::default()
2691 })
2692 .build()
2693 .unwrap();
2694
2695 let ctx3 = WorkflowContext::new("workflow", Arc::new(DummyCodec), Arc::new(()));
2697 let wf_v2: SerializableWorkflow<_, u32> = WorkflowBuilder::new(ctx3)
2698 .with_registry()
2699 .then("step1", |i: u32| async move { Ok(i + 1) })
2700 .with_metadata(TaskMetadata {
2701 version: Some("2.0".into()),
2702 ..Default::default()
2703 })
2704 .build()
2705 .unwrap();
2706
2707 let ctx4 = WorkflowContext::new("workflow", Arc::new(DummyCodec), Arc::new(()));
2709 let wf_v1_again: SerializableWorkflow<_, u32> = WorkflowBuilder::new(ctx4)
2710 .with_registry()
2711 .then("step1", |i: u32| async move { Ok(i + 1) })
2712 .with_metadata(TaskMetadata {
2713 version: Some("1.0".into()),
2714 ..Default::default()
2715 })
2716 .build()
2717 .unwrap();
2718
2719 assert_ne!(
2720 wf_no_version.definition_hash(),
2721 wf_v1.definition_hash(),
2722 "Adding version should change hash"
2723 );
2724 assert_ne!(
2725 wf_v1.definition_hash(),
2726 wf_v2.definition_hash(),
2727 "Different versions should produce different hashes"
2728 );
2729 assert_eq!(
2730 wf_v1.definition_hash(),
2731 wf_v1_again.definition_hash(),
2732 "Same version should produce same hash"
2733 );
2734 }
2735
2736 #[test]
2737 fn test_version_absent_in_serialization_when_none() {
2738 use crate::context::WorkflowContext;
2739 use std::sync::Arc;
2740
2741 let ctx = WorkflowContext::new("test-workflow", Arc::new(DummyCodec), Arc::new(()));
2742 let workflow = WorkflowBuilder::new(ctx)
2743 .with_registry()
2744 .then("step1", |i: u32| async move { Ok(i + 1) })
2745 .build()
2746 .unwrap();
2747
2748 let serializable = workflow.to_serializable();
2749 let json = serde_json::to_string(&serializable.continuation).unwrap();
2750 assert!(
2751 !json.contains("version"),
2752 "version should be absent when None: {json}"
2753 );
2754 }
2755
2756 #[test]
2757 fn test_version_present_in_serialization_when_set() {
2758 use crate::context::WorkflowContext;
2759 use crate::task::TaskMetadata;
2760 use std::sync::Arc;
2761
2762 let ctx = WorkflowContext::new("test-workflow", Arc::new(DummyCodec), Arc::new(()));
2763 let workflow = WorkflowBuilder::new(ctx)
2764 .with_registry()
2765 .then("step1", |i: u32| async move { Ok(i + 1) })
2766 .with_metadata(TaskMetadata {
2767 version: Some("3.0".into()),
2768 ..Default::default()
2769 })
2770 .build()
2771 .unwrap();
2772
2773 let serializable = workflow.to_serializable();
2774 let json = serde_json::to_string(&serializable.continuation).unwrap();
2775 assert!(
2776 json.contains(r#""version":"3.0""#),
2777 "version should be present in JSON: {json}"
2778 );
2779 }
2780
2781 #[test]
2786 fn test_nodes_single_task() {
2787 use crate::context::WorkflowContext;
2788 use crate::workflow::{NodeKind, Workflow};
2789 use std::sync::Arc;
2790
2791 let ctx = WorkflowContext::new("test-workflow", Arc::new(DummyCodec), Arc::new(()));
2792 let workflow: Workflow<DummyCodec, u32> = WorkflowBuilder::new(ctx)
2793 .then("only", |i: u32| async move { Ok(i + 1) })
2794 .build()
2795 .unwrap();
2796
2797 let nodes: Vec<_> = workflow.iter_nodes().collect();
2798 assert_eq!(nodes.len(), 1);
2799 assert_eq!(nodes[0].id, "only");
2800 assert_eq!(nodes[0].kind, NodeKind::Task);
2801 assert!(nodes[0].predecessor_id.is_none());
2802 }
2803
2804 #[test]
2805 fn test_nodes_chain_order() {
2806 use crate::context::WorkflowContext;
2807 use crate::workflow::{NodeKind, Workflow};
2808 use std::sync::Arc;
2809
2810 let ctx = WorkflowContext::new("test-workflow", Arc::new(DummyCodec), Arc::new(()));
2811 let workflow: Workflow<DummyCodec, u32> = WorkflowBuilder::new(ctx)
2812 .then("a", |i: u32| async move { Ok(i + 1) })
2813 .then("b", |i: u32| async move { Ok(i + 2) })
2814 .then("c", |i: u32| async move { Ok(i + 3) })
2815 .build()
2816 .unwrap();
2817
2818 let nodes: Vec<_> = workflow.iter_nodes().collect();
2819 let ids: Vec<&str> = nodes.iter().map(|n| n.id).collect();
2820 assert_eq!(ids, vec!["a", "b", "c"]);
2821 assert!(nodes.iter().all(|n| n.kind == NodeKind::Task));
2822
2823 assert_eq!(nodes[0].predecessor_id, None);
2825 assert_eq!(nodes[1].predecessor_id, Some("a"));
2826 assert_eq!(nodes[2].predecessor_id, Some("b"));
2827 }
2828
2829 #[test]
2830 fn test_nodes_fork_with_join() {
2831 use crate::context::WorkflowContext;
2832 use crate::task::BranchOutputs;
2833 use crate::workflow::{NodeKind, Workflow};
2834 use std::sync::Arc;
2835
2836 let ctx = WorkflowContext::new("test-workflow", Arc::new(DummyCodec), Arc::new(()));
2837 let workflow: Workflow<DummyCodec, u32> = WorkflowBuilder::new(ctx)
2838 .then("prepare", |i: u32| async move { Ok(i) })
2839 .branches(|b| {
2840 b.add("left", |i: u32| async move { Ok(i * 2) });
2841 b.add("right", |i: u32| async move { Ok(i + 10) });
2842 })
2843 .join(
2844 "merge",
2845 |_: BranchOutputs<DummyCodec>| async move { Ok(0u32) },
2846 )
2847 .build()
2848 .unwrap();
2849
2850 let nodes: Vec<_> = workflow.iter_nodes().collect();
2851 let ids: Vec<&str> = nodes.iter().map(|n| n.id).collect();
2852
2853 assert_eq!(ids[0], "prepare");
2855 assert_eq!(nodes[1].kind, NodeKind::Fork);
2856 assert!(ids.contains(&"left"));
2857 assert!(ids.contains(&"right"));
2858 assert_eq!(*ids.last().unwrap(), "merge");
2859
2860 assert_eq!(nodes[1].predecessor_id, Some("prepare"));
2862
2863 let fork_id = nodes[1].id;
2865 let left_node = nodes.iter().find(|n| n.id == "left").unwrap();
2866 let right_node = nodes.iter().find(|n| n.id == "right").unwrap();
2867 assert_eq!(left_node.predecessor_id, Some(fork_id));
2868 assert_eq!(right_node.predecessor_id, Some(fork_id));
2869
2870 let merge_node = nodes.iter().find(|n| n.id == "merge").unwrap();
2872 assert_eq!(merge_node.predecessor_id, Some(fork_id));
2873 }
2874
2875 #[test]
2876 fn test_nodes_loop() {
2877 use crate::context::WorkflowContext;
2878 use crate::loop_result::LoopResult;
2879 use crate::workflow::{NodeKind, Workflow};
2880 use std::sync::Arc;
2881
2882 let ctx = WorkflowContext::new("test-workflow", Arc::new(DummyCodec), Arc::new(()));
2883 let workflow: Workflow<DummyCodec, u32> = WorkflowBuilder::new(ctx)
2884 .loop_task(
2885 "iterate",
2886 |i: u32| async move { Ok(LoopResult::Done(i)) },
2887 5,
2888 )
2889 .then("after", |i: u32| async move { Ok(i) })
2890 .build()
2891 .unwrap();
2892
2893 let nodes: Vec<_> = workflow.iter_nodes().collect();
2894
2895 assert_eq!(nodes[0].kind, NodeKind::Loop);
2897 assert_eq!(nodes[1].id, "iterate");
2898 assert_eq!(nodes[1].kind, NodeKind::Task);
2899 assert_eq!(nodes[2].id, "after");
2900 assert_eq!(nodes[2].kind, NodeKind::Task);
2901
2902 assert_eq!(nodes[0].predecessor_id, None);
2904 assert_eq!(nodes[1].predecessor_id, Some(nodes[0].id)); assert_eq!(nodes[2].predecessor_id, Some(nodes[0].id)); }
2907
2908 #[test]
2909 fn test_nodes_delay_reports_duration_as_timeout() {
2910 use crate::context::WorkflowContext;
2911 use crate::workflow::{NodeKind, Workflow};
2912 use std::sync::Arc;
2913 use std::time::Duration;
2914
2915 let ctx = WorkflowContext::new("test-workflow", Arc::new(DummyCodec), Arc::new(()));
2916 let workflow: Workflow<DummyCodec, u32> = WorkflowBuilder::new(ctx)
2917 .delay("wait_5s", Duration::from_secs(5))
2918 .then("after", |i: u32| async move { Ok(i) })
2919 .build()
2920 .unwrap();
2921
2922 let nodes: Vec<_> = workflow.iter_nodes().collect();
2923 assert_eq!(nodes[0].id, "wait_5s");
2924 assert_eq!(nodes[0].kind, NodeKind::Delay);
2925 assert_eq!(nodes[0].timeout, Some(Duration::from_secs(5)));
2926 assert_eq!(nodes[0].predecessor_id, None);
2927
2928 assert_eq!(nodes[1].id, "after");
2929 assert_eq!(nodes[1].predecessor_id, Some("wait_5s"));
2930 }
2931
2932 #[test]
2933 fn test_nodes_metadata_extraction() {
2934 use crate::context::WorkflowContext;
2935 use crate::task::{RetryPolicy, TaskMetadata};
2936 use crate::workflow::NodeKind;
2937 use std::sync::Arc;
2938 use std::time::Duration;
2939
2940 let retry = RetryPolicy {
2941 max_retries: 3,
2942 initial_delay: Duration::from_millis(100),
2943 backoff_multiplier: 2.0,
2944 max_delay: Some(Duration::from_secs(10)),
2945 };
2946
2947 let ctx = WorkflowContext::new("test-workflow", Arc::new(DummyCodec), Arc::new(()));
2948 let workflow = WorkflowBuilder::new(ctx)
2949 .with_registry()
2950 .then("step", |i: u32| async move { Ok(i) })
2951 .with_metadata(TaskMetadata {
2952 timeout: Some(Duration::from_secs(30)),
2953 retries: Some(retry.clone()),
2954 version: Some("2.0".into()),
2955 ..Default::default()
2956 })
2957 .build()
2958 .unwrap();
2959
2960 let nodes: Vec<_> = workflow.iter_nodes().collect();
2961 assert_eq!(nodes.len(), 1);
2962 let node = &nodes[0];
2963
2964 assert_eq!(node.id, "step");
2965 assert_eq!(node.kind, NodeKind::Task);
2966 assert_eq!(node.timeout, Some(Duration::from_secs(30)));
2967 assert_eq!(node.retry_policy.unwrap().max_retries, 3);
2968 }
2969}
2970
2971#[cfg(test)]
2972#[allow(
2973 clippy::unwrap_used,
2974 clippy::expect_used,
2975 clippy::panic,
2976 clippy::indexing_slicing,
2977 clippy::too_many_lines,
2978 clippy::items_after_statements
2979)]
2980mod proptests {
2981 use super::{MaxIterationsPolicy, SerializableContinuation};
2982 use proptest::prelude::*;
2983
2984 fn arb_id() -> impl Strategy<Value = String> {
2986 "[a-z0-9]{1,8}"
2987 }
2988
2989 fn arb_continuation(depth: usize) -> BoxedStrategy<SerializableContinuation> {
2991 let leaf = arb_id().prop_map(|id| SerializableContinuation::Task {
2992 id,
2993 timeout_ms: None,
2994 retry_policy: None,
2995 version: None,
2996 priority: None,
2997
2998 tags: vec![],
2999 next: None,
3000 });
3001
3002 if depth == 0 {
3003 return leaf.boxed();
3004 }
3005
3006 prop_oneof![
3007 (
3009 arb_id(),
3010 prop::option::of(any::<u64>()),
3011 prop::option::of(arb_continuation(depth - 1).prop_map(Box::new)),
3012 )
3013 .prop_map(|(id, timeout_ms, next)| SerializableContinuation::Task {
3014 id,
3015 timeout_ms,
3016 retry_policy: None,
3017 version: None,
3018 priority: None,
3019
3020 tags: vec![],
3021 next,
3022 }),
3023 (
3025 arb_id(),
3026 prop::collection::vec(arb_continuation(depth - 1), 0..3),
3027 prop::option::of(arb_continuation(depth - 1).prop_map(Box::new)),
3028 )
3029 .prop_map(|(id, branches, join)| SerializableContinuation::Fork {
3030 id,
3031 branches,
3032 join,
3033 }),
3034 (
3036 arb_id(),
3037 any::<u64>(),
3038 prop::option::of(arb_continuation(depth - 1).prop_map(Box::new)),
3039 )
3040 .prop_map(|(id, duration_ms, next)| SerializableContinuation::Delay {
3041 id,
3042 duration_ms,
3043 next,
3044 }),
3045 (
3047 arb_id(),
3048 arb_id(),
3049 prop::option::of(any::<u64>()),
3050 prop::option::of(arb_continuation(depth - 1).prop_map(Box::new)),
3051 )
3052 .prop_map(|(id, signal_name, timeout_ms, next)| {
3053 SerializableContinuation::AwaitSignal {
3054 id,
3055 signal_name,
3056 timeout_ms,
3057 next,
3058 }
3059 }),
3060 (
3062 arb_id(),
3063 prop::collection::hash_map(
3064 arb_id(),
3065 arb_continuation(depth - 1).prop_map(Box::new),
3066 0..3
3067 ),
3068 prop::option::of(arb_continuation(depth - 1).prop_map(Box::new)),
3069 prop::option::of(arb_continuation(depth - 1).prop_map(Box::new)),
3070 )
3071 .prop_map(|(id, branches, default, next)| {
3072 SerializableContinuation::Branch {
3073 id,
3074 branches,
3075 default,
3076 next,
3077 }
3078 }),
3079 (
3081 arb_id(),
3082 arb_continuation(depth - 1).prop_map(Box::new),
3083 1..100u32,
3084 prop::bool::ANY.prop_map(|b| if b {
3085 MaxIterationsPolicy::Fail
3086 } else {
3087 MaxIterationsPolicy::ExitWithLast
3088 }),
3089 prop::option::of(arb_continuation(depth - 1).prop_map(Box::new)),
3090 )
3091 .prop_map(|(id, body, max_iterations, on_max, next)| {
3092 SerializableContinuation::Loop {
3093 id,
3094 body,
3095 max_iterations,
3096 on_max,
3097 next,
3098 }
3099 }),
3100 (
3102 arb_id(),
3103 arb_continuation(depth - 1).prop_map(Box::new),
3104 prop::option::of(arb_continuation(depth - 1).prop_map(Box::new)),
3105 )
3106 .prop_map(|(id, child, next)| {
3107 SerializableContinuation::ChildWorkflow { id, child, next }
3108 }),
3109 ]
3110 .boxed()
3111 }
3112
3113 fn arb_unique_continuation(
3117 depth: usize,
3118 prefix: &str,
3119 ) -> BoxedStrategy<SerializableContinuation> {
3120 let id = format!("{prefix}n");
3121
3122 if depth == 0 {
3123 return Just(SerializableContinuation::Task {
3124 id,
3125 timeout_ms: None,
3126 retry_policy: None,
3127 version: None,
3128 priority: None,
3129
3130 tags: vec![],
3131 next: None,
3132 })
3133 .boxed();
3134 }
3135
3136 let id_clone = id.clone();
3137 prop_oneof![
3138 prop::option::of(
3140 arb_unique_continuation(depth - 1, &format!("{prefix}0_")).prop_map(Box::new),
3141 )
3142 .prop_map(move |next| SerializableContinuation::Task {
3143 id: id_clone.clone(),
3144 timeout_ms: None,
3145 retry_policy: None,
3146 version: None,
3147 priority: None,
3148
3149 tags: vec![],
3150 next,
3151 }),
3152 {
3154 let id_f = id.clone();
3155 let prefix_f = prefix.to_string();
3156 (0..3u8)
3157 .prop_flat_map(move |branch_count| {
3158 let id_inner = id_f.clone();
3159 let prefix_inner = prefix_f.clone();
3160 let branches: Vec<BoxedStrategy<SerializableContinuation>> = (0
3161 ..branch_count)
3162 .map(|i| {
3163 arb_unique_continuation(depth - 1, &format!("{prefix_inner}b{i}_"))
3164 })
3165 .collect();
3166 let join = prop::option::of(
3167 arb_unique_continuation(depth - 1, &format!("{prefix_inner}j_"))
3168 .prop_map(Box::new),
3169 );
3170 (branches, join).prop_map(move |(branches, join)| {
3171 SerializableContinuation::Fork {
3172 id: id_inner.clone(),
3173 branches,
3174 join,
3175 }
3176 })
3177 })
3178 .boxed()
3179 },
3180 {
3182 let id_d = id.clone();
3183 let prefix_d = prefix.to_string();
3184 (
3185 any::<u64>(),
3186 prop::option::of(
3187 arb_unique_continuation(depth - 1, &format!("{prefix_d}d_"))
3188 .prop_map(Box::new),
3189 ),
3190 )
3191 .prop_map(move |(duration_ms, next)| {
3192 SerializableContinuation::Delay {
3193 id: id_d.clone(),
3194 duration_ms,
3195 next,
3196 }
3197 })
3198 },
3199 {
3201 let id_s = id.clone();
3202 let prefix_s = prefix.to_string();
3203 (
3204 arb_id(),
3205 prop::option::of(any::<u64>()),
3206 prop::option::of(
3207 arb_unique_continuation(depth - 1, &format!("{prefix_s}s_"))
3208 .prop_map(Box::new),
3209 ),
3210 )
3211 .prop_map(move |(signal_name, timeout_ms, next)| {
3212 SerializableContinuation::AwaitSignal {
3213 id: id_s.clone(),
3214 signal_name,
3215 timeout_ms,
3216 next,
3217 }
3218 })
3219 },
3220 {
3222 let id_b = id.clone();
3223 let prefix_b = prefix.to_string();
3224 let b0 = arb_unique_continuation(depth - 1, &format!("{prefix_b}br0_"))
3225 .prop_map(Box::new);
3226 let b1 = arb_unique_continuation(depth - 1, &format!("{prefix_b}br1_"))
3227 .prop_map(Box::new);
3228 let default = prop::option::of(
3229 arb_unique_continuation(depth - 1, &format!("{prefix_b}bd_"))
3230 .prop_map(Box::new),
3231 );
3232 let next = prop::option::of(
3233 arb_unique_continuation(depth - 1, &format!("{prefix_b}bn_"))
3234 .prop_map(Box::new),
3235 );
3236 (b0, b1, default, next).prop_map(move |(branch0, branch1, default, next)| {
3237 let mut branches = std::collections::HashMap::new();
3238 branches.insert("k0".to_string(), branch0);
3239 branches.insert("k1".to_string(), branch1);
3240 SerializableContinuation::Branch {
3241 id: id_b.clone(),
3242 branches,
3243 default,
3244 next,
3245 }
3246 })
3247 },
3248 {
3250 let id_l = id.clone();
3251 let prefix_l = prefix.to_string();
3252 let body = arb_unique_continuation(depth - 1, &format!("{prefix_l}lb_"))
3253 .prop_map(Box::new);
3254 let next = prop::option::of(
3255 arb_unique_continuation(depth - 1, &format!("{prefix_l}ln_"))
3256 .prop_map(Box::new),
3257 );
3258 (
3259 body,
3260 1..100u32,
3261 prop::bool::ANY.prop_map(|b| {
3262 if b {
3263 MaxIterationsPolicy::Fail
3264 } else {
3265 MaxIterationsPolicy::ExitWithLast
3266 }
3267 }),
3268 next,
3269 )
3270 .prop_map(move |(body, max_iterations, on_max, next)| {
3271 SerializableContinuation::Loop {
3272 id: id_l.clone(),
3273 body,
3274 max_iterations,
3275 on_max,
3276 next,
3277 }
3278 })
3279 },
3280 {
3282 let id_cw = id;
3283 let prefix_cw = prefix.to_string();
3284 let child = arb_unique_continuation(depth - 1, &format!("{prefix_cw}cc_"))
3285 .prop_map(Box::new);
3286 let next = prop::option::of(
3287 arb_unique_continuation(depth - 1, &format!("{prefix_cw}cn_"))
3288 .prop_map(Box::new),
3289 );
3290 (child, next).prop_map(move |(child, next)| {
3291 SerializableContinuation::ChildWorkflow {
3292 id: id_cw.clone(),
3293 child,
3294 next,
3295 }
3296 })
3297 },
3298 ]
3299 .boxed()
3300 }
3301
3302 fn collect_ids(cont: &SerializableContinuation) -> Vec<String> {
3304 let mut ids = vec![];
3305 fn walk(c: &SerializableContinuation, out: &mut Vec<String>) {
3306 match c {
3307 SerializableContinuation::Task { id, next, .. }
3308 | SerializableContinuation::Delay { id, next, .. }
3309 | SerializableContinuation::AwaitSignal { id, next, .. } => {
3310 out.push(id.clone());
3311 if let Some(n) = next {
3312 walk(n, out);
3313 }
3314 }
3315 SerializableContinuation::Fork { id, branches, join } => {
3316 out.push(id.clone());
3317 for b in branches {
3318 walk(b, out);
3319 }
3320 if let Some(j) = join {
3321 walk(j, out);
3322 }
3323 }
3324 SerializableContinuation::Branch {
3325 id,
3326 branches,
3327 default,
3328 next,
3329 } => {
3330 out.push(id.clone());
3331 for b in branches.values() {
3332 walk(b, out);
3333 }
3334 if let Some(d) = default {
3335 walk(d, out);
3336 }
3337 if let Some(n) = next {
3338 walk(n, out);
3339 }
3340 }
3341 SerializableContinuation::Loop { id, body, next, .. } => {
3342 out.push(id.clone());
3343 walk(body, out);
3344 if let Some(n) = next {
3345 walk(n, out);
3346 }
3347 }
3348 SerializableContinuation::ChildWorkflow { id, child, next } => {
3349 out.push(id.clone());
3350 walk(child, out);
3351 if let Some(n) = next {
3352 walk(n, out);
3353 }
3354 }
3355 }
3356 }
3357 walk(cont, &mut ids);
3358 ids
3359 }
3360
3361 fn inject_duplicate(cont: &SerializableContinuation, dup_id: &str) -> SerializableContinuation {
3363 match cont {
3364 SerializableContinuation::Task {
3365 timeout_ms,
3366 retry_policy,
3367 version,
3368 next,
3369 ..
3370 } => SerializableContinuation::Task {
3371 id: dup_id.to_string(),
3372 timeout_ms: *timeout_ms,
3373 retry_policy: retry_policy.clone(),
3374 version: version.clone(),
3375 priority: None,
3376 tags: vec![],
3377 next: next.clone(),
3378 },
3379 SerializableContinuation::Fork { branches, join, .. } => {
3380 SerializableContinuation::Fork {
3381 id: dup_id.to_string(),
3382 branches: branches.clone(),
3383 join: join.clone(),
3384 }
3385 }
3386 SerializableContinuation::Delay {
3387 duration_ms, next, ..
3388 } => SerializableContinuation::Delay {
3389 id: dup_id.to_string(),
3390 duration_ms: *duration_ms,
3391 next: next.clone(),
3392 },
3393 SerializableContinuation::AwaitSignal {
3394 signal_name,
3395 timeout_ms,
3396 next,
3397 ..
3398 } => SerializableContinuation::AwaitSignal {
3399 id: dup_id.to_string(),
3400 signal_name: signal_name.clone(),
3401 timeout_ms: *timeout_ms,
3402 next: next.clone(),
3403 },
3404 SerializableContinuation::Branch {
3405 branches,
3406 default,
3407 next,
3408 ..
3409 } => SerializableContinuation::Branch {
3410 id: dup_id.to_string(),
3411 branches: branches.clone(),
3412 default: default.clone(),
3413 next: next.clone(),
3414 },
3415 SerializableContinuation::Loop {
3416 body,
3417 max_iterations,
3418 on_max,
3419 next,
3420 ..
3421 } => SerializableContinuation::Loop {
3422 id: dup_id.to_string(),
3423 body: body.clone(),
3424 max_iterations: *max_iterations,
3425 on_max: *on_max,
3426 next: next.clone(),
3427 },
3428 SerializableContinuation::ChildWorkflow { child, next, .. } => {
3429 SerializableContinuation::ChildWorkflow {
3430 id: dup_id.to_string(),
3431 child: child.clone(),
3432 next: next.clone(),
3433 }
3434 }
3435 }
3436 }
3437
3438 proptest! {
3439 #[test]
3441 fn hash_is_deterministic(cont in arb_continuation(3)) {
3442 let h1 = cont.compute_definition_hash();
3443 let h2 = cont.compute_definition_hash();
3444 prop_assert_eq!(h1, h2);
3445 }
3446
3447 #[test]
3449 fn serde_roundtrip_preserves_hash(cont in arb_continuation(3)) {
3450 let original_hash = cont.compute_definition_hash();
3451 let json = serde_json::to_string(&cont).unwrap();
3452 let recovered: SerializableContinuation = serde_json::from_str(&json).unwrap();
3453 prop_assert_eq!(original_hash, recovered.compute_definition_hash());
3454 }
3455
3456 #[test]
3458 fn unique_ids_means_none(cont in arb_unique_continuation(3, "r_")) {
3459 prop_assert!(cont.find_duplicate_id().is_none());
3460 }
3461
3462 #[test]
3464 fn injected_duplicate_is_detected(cont in arb_unique_continuation(3, "r_")) {
3465 let ids = collect_ids(&cont);
3466 if ids.len() >= 2 {
3468 let dup_id = &ids[1];
3470 let tampered = inject_duplicate(&cont, dup_id);
3471 prop_assert!(tampered.find_duplicate_id().is_some());
3472 }
3473 }
3474 }
3475}