1use std::future::Future;
4use std::sync::Arc;
5
6use aion_core::{ActivityError, ActivityId, Payload, RunId, WorkflowId};
7use aion_store::EventStore;
8use chrono::{DateTime, Utc};
9use tokio::runtime::Handle;
10use tokio::sync::Mutex;
11
12use crate::EngineError;
13use crate::durability::{
14 Command, DurabilityError, FanOutCompletionResult, FanOutItem, FanOutOutcome, HistoryCursor,
15 Recorder, ResolveOutcome, Resolver,
16};
17use crate::registry::{Registry, WorkflowHandle};
18
19#[derive(thiserror::Error, Debug)]
21pub enum NifContextError {
22 #[error("unknown workflow process pid {pid}")]
24 UnknownProcess {
25 pid: u64,
27 },
28 #[error("workflow recorder lock is poisoned")]
30 RecorderPoisoned,
31 #[error("durability error: {0}")]
33 Durability(#[from] DurabilityError),
34 #[error("term encoding error: {reason}")]
36 TermEncoding {
37 reason: String,
39 },
40}
41
42impl NifContextError {
43 pub(crate) fn error_reason(&self) -> String {
49 match self {
50 Self::UnknownProcess { pid } => format!("unknown_process:{pid}"),
51 Self::RecorderPoisoned => "recorder_poisoned".to_owned(),
52 Self::Durability(error) => format!("durability:{error}"),
53 Self::TermEncoding { reason } => format!("term_encoding:{reason}"),
54 }
55 }
56}
57
58pub struct NifContext {
60 handle: WorkflowHandle,
61 recorder: Arc<Mutex<Recorder>>,
62 tokio_handle: Handle,
63 resolver: Resolver,
64 last_recorded_at: Option<DateTime<Utc>>,
65}
66
67impl NifContext {
68 pub fn new(
78 pid: u64,
79 registry: &Registry,
80 tokio_handle: Handle,
81 birth_wait: crate::runtime::SignalDeliveryConfig,
82 ) -> Result<Self, NifContextError> {
83 Self::new_with_history_store(pid, registry, tokio_handle, None, birth_wait)
84 }
85
86 pub fn new_with_history_store(
97 pid: u64,
98 registry: &Registry,
99 tokio_handle: Handle,
100 store: Option<Arc<dyn EventStore>>,
101 birth_wait: crate::runtime::SignalDeliveryConfig,
102 ) -> Result<Self, NifContextError> {
103 let handle = resolve_handle_with_birth_wait(registry, pid, birth_wait)?;
104 let recorder = handle.recorder();
105 let workflow_id = handle.workflow_id().clone();
106 let history = match store {
107 Some(store) => tokio_handle
108 .block_on(store.read_history(&workflow_id))
109 .map_err(DurabilityError::from)?,
110 None => tokio_handle.block_on(async {
111 let recorder = recorder.lock().await;
112 recorder.read_history().await
113 })?,
114 };
115 let history = crate::durability::current_run_segment(history, handle.run_id())?;
118 let last_recorded_at = history.last().map(|event| *event.recorded_at());
119 let cursor = HistoryCursor::new(history)?;
120 let resolver = Resolver::new(workflow_id, cursor);
121
122 Ok(Self {
123 handle,
124 recorder,
125 tokio_handle,
126 resolver,
127 last_recorded_at,
128 })
129 }
130
131 #[must_use]
133 pub fn workflow_id(&self) -> &WorkflowId {
134 self.handle.workflow_id()
135 }
136
137 #[must_use]
139 pub fn run_id(&self) -> &RunId {
140 self.handle.run_id()
141 }
142
143 #[must_use]
150 pub fn next_activity_ordinal(&self) -> u64 {
151 self.handle.allocate_activity_ordinals(1)
152 }
153
154 #[must_use]
156 pub fn allocate_activity_ordinals(&self, count: u64) -> u64 {
157 self.handle.allocate_activity_ordinals(count)
158 }
159
160 #[must_use]
165 pub fn next_timer_ordinal(&self) -> u64 {
166 self.handle.allocate_timer_ordinals(1)
167 }
168
169 #[must_use]
178 pub fn next_child_ordinal(&self) -> u64 {
179 self.handle.allocate_child_ordinals(1)
180 }
181
182 #[must_use]
184 pub fn signal_receives_consumed(&self, name: &str) -> u64 {
185 self.handle.signal_receives_consumed(name)
186 }
187
188 pub fn mark_signal_receive_consumed(&self, name: &str) {
190 self.handle.mark_signal_receive_consumed(name);
191 }
192
193 #[must_use]
195 pub fn signal_sends_completed(&self, name: &str) -> u64 {
196 self.handle.signal_sends_completed(name)
197 }
198
199 pub fn mark_signal_send_completed(&self, name: &str) {
201 self.handle.mark_signal_send_completed(name);
202 }
203
204 #[must_use]
206 pub fn workflow_handle(&self) -> WorkflowHandle {
207 self.handle.clone()
208 }
209
210 #[must_use]
212 pub const fn pid(&self) -> u64 {
213 self.handle.pid()
214 }
215
216 #[must_use]
218 pub const fn last_recorded_at(&self) -> Option<DateTime<Utc>> {
219 self.last_recorded_at
220 }
221
222 #[must_use]
224 pub fn next_deterministic_sequence(&self) -> u64 {
225 self.handle.next_deterministic_nif_sequence()
226 }
227
228 #[must_use]
230 pub fn recorder(&self) -> Arc<Mutex<Recorder>> {
231 Arc::clone(&self.recorder)
232 }
233
234 pub fn block_on_recorder<T, F>(&self, f: F) -> Result<T, NifContextError>
240 where
241 F: for<'a> FnOnce(
242 &'a mut Recorder,
243 ) -> std::pin::Pin<
244 Box<dyn Future<Output = Result<T, DurabilityError>> + Send + 'a>,
245 >,
246 {
247 self.tokio_handle
248 .block_on(async {
249 let mut recorder = self.recorder.lock().await;
250 f(&mut recorder).await
251 })
252 .map_err(Into::into)
253 }
254
255 pub(crate) fn record_activity_scheduled_started(
261 &self,
262 recorded_at: chrono::DateTime<chrono::Utc>,
263 activity_id: ActivityId,
264 scheduled: super::nif_activity::ScheduledActivity,
265 ) -> Result<(), NifContextError> {
266 self.tokio_handle
267 .block_on(async {
268 let mut recorder = self.recorder.lock().await;
269 recorder
270 .record_activity_scheduled(
271 recorded_at,
272 activity_id.clone(),
273 scheduled.activity_type,
274 scheduled.input,
275 scheduled.task_queue,
278 scheduled.node,
281 )
282 .await?;
283 recorder
284 .record_activity_started(recorded_at, activity_id, scheduled.attempt)
286 .await
287 })
288 .map_err(Into::into)
289 }
290
291 pub fn record_activity_completed(
297 &self,
298 recorded_at: chrono::DateTime<chrono::Utc>,
299 activity_id: ActivityId,
300 result: Payload,
301 attempt: u32,
302 ) -> Result<(), NifContextError> {
303 self.tokio_handle
304 .block_on(async {
305 let mut recorder = self.recorder.lock().await;
306 recorder
307 .record_activity_completed(recorded_at, activity_id, result, attempt)
309 .await
310 })
311 .map_err(Into::into)
312 }
313
314 pub fn record_activity_failed(
320 &self,
321 recorded_at: chrono::DateTime<chrono::Utc>,
322 activity_id: ActivityId,
323 error: ActivityError,
324 attempt: u32,
325 ) -> Result<(), NifContextError> {
326 self.tokio_handle
327 .block_on(async {
328 let mut recorder = self.recorder.lock().await;
329 recorder
330 .record_activity_failed(recorded_at, activity_id, error, attempt)
331 .await
332 })
333 .map_err(Into::into)
334 }
335
336 pub fn record_activity_cancelled(
342 &self,
343 recorded_at: chrono::DateTime<chrono::Utc>,
344 activity_id: ActivityId,
345 attempt: u32,
346 ) -> Result<(), NifContextError> {
347 self.tokio_handle
348 .block_on(async {
349 let mut recorder = self.recorder.lock().await;
350 recorder
351 .record_activity_cancelled(recorded_at, activity_id, attempt)
353 .await
354 })
355 .map_err(Into::into)
356 }
357
358 pub fn record_activity_cancelled_and_settle_outbox(
364 &self,
365 recorded_at: chrono::DateTime<chrono::Utc>,
366 ordinal: u64,
367 attempt: u32,
368 ) -> Result<(), NifContextError> {
369 self.tokio_handle
370 .block_on(async {
371 let mut recorder = self.recorder.lock().await;
372 recorder
373 .record_activity_cancelled_and_settle_outbox(recorded_at, ordinal, attempt)
375 .await
376 })
377 .map_err(Into::into)
378 }
379
380 pub fn record_fan_out_dispatch(
386 &self,
387 recorded_at: chrono::DateTime<chrono::Utc>,
388 items: &[FanOutItem],
389 ) -> Result<(), NifContextError> {
390 self.tokio_handle
391 .block_on(async {
392 let mut recorder = self.recorder.lock().await;
393 recorder.record_fan_out_dispatch(recorded_at, items).await
394 })
395 .map_err(Into::into)
396 }
397
398 pub fn rearm_outbox_pending(
405 &self,
406 recorded_at: chrono::DateTime<chrono::Utc>,
407 items: &[FanOutItem],
408 ) -> Result<(), NifContextError> {
409 self.tokio_handle
410 .block_on(async {
411 let recorder = self.recorder.lock().await;
412 recorder.rearm_outbox_pending(recorded_at, items).await
413 })
414 .map_err(Into::into)
415 }
416
417 pub fn record_fan_out_completion(
423 &self,
424 recorded_at: chrono::DateTime<chrono::Utc>,
425 ordinal: u64,
426 outcome: FanOutOutcome,
427 ) -> Result<FanOutCompletionResult, NifContextError> {
428 self.tokio_handle
429 .block_on(async {
430 let mut recorder = self.recorder.lock().await;
431 recorder
432 .record_fan_out_completion(recorded_at, ordinal, None, outcome)
433 .await
434 })
435 .map_err(Into::into)
436 }
437
438 #[must_use]
440 pub fn history(&self) -> &[aion_core::Event] {
441 self.resolver.history()
442 }
443
444 #[must_use]
458 pub fn start_time_task_queue(&self) -> Option<String> {
459 aion_core::start_time_task_queue(self.history())
460 }
461
462 pub fn resolve_command(&mut self, command: Command) -> Result<ResolveOutcome, NifContextError> {
469 if let Some(key) = command.key() {
477 self.resolver.fast_forward_to(key);
478 } else if let Command::AwaitChild { child_workflow_id } = &command {
479 self.resolver
480 .fast_forward_to_child_terminal(child_workflow_id);
481 }
482 self.resolver.resolve(command).map_err(Into::into)
483 }
484}
485
486fn registry_error_to_context(error: &EngineError) -> NifContextError {
487 match error {
488 EngineError::RegistryPoisoned => NifContextError::RecorderPoisoned,
489 _ => NifContextError::TermEncoding {
490 reason: format!("registry lookup failed: {error}"),
491 },
492 }
493}
494
495fn resolve_handle_with_birth_wait(
516 registry: &Registry,
517 pid: u64,
518 birth_wait: crate::runtime::SignalDeliveryConfig,
519) -> Result<WorkflowHandle, NifContextError> {
520 let lookup = |registry: &Registry| -> Result<Option<WorkflowHandle>, NifContextError> {
521 Ok(registry
522 .list()
523 .map_err(|error| registry_error_to_context(&error))?
524 .into_iter()
525 .find(|handle| handle.pid() == pid))
526 };
527 if let Some(handle) = lookup(registry)? {
528 return Ok(handle);
529 }
530 let budget = birth_wait
531 .ready_timeout
532 .saturating_mul(birth_wait.max_enqueue_attempts.max(1));
533 let deadline = std::time::Instant::now() + budget;
534 let mut backoff = birth_wait.initial_backoff;
535 while std::time::Instant::now() < deadline {
536 std::thread::sleep(backoff);
537 let doubled = backoff.saturating_mul(2);
538 backoff = if doubled > birth_wait.max_backoff {
539 birth_wait.max_backoff
540 } else {
541 doubled
542 };
543 if let Some(handle) = lookup(registry)? {
544 return Ok(handle);
545 }
546 }
547 Err(NifContextError::UnknownProcess { pid })
548}
549
550#[cfg(test)]
551mod tests {
552 use std::sync::Arc;
553
554 use aion_core::{ActivityId, Event, EventEnvelope, Payload, WorkflowStatus};
555 use aion_package::ContentHash;
556 use aion_store::{EventStore, InMemoryStore, WriteToken};
557 use chrono::{TimeZone, Utc};
558 use serde_json::json;
559
560 use super::{NifContext, NifContextError};
561 use crate::durability::{Command, CorrelationKey, Recorder, Resolution, ResolveOutcome};
562 use crate::registry::{
563 CompletionNotifier, HandleResidency, Registry, WorkflowHandle, WorkflowHandleParts,
564 };
565
566 type TestResult = Result<(), Box<dyn std::error::Error>>;
567
568 fn hash() -> ContentHash {
569 ContentHash::from_bytes([7; 32])
570 }
571
572 fn birth_wait() -> crate::runtime::SignalDeliveryConfig {
574 crate::runtime::SignalDeliveryConfig::new(
575 std::time::Duration::from_millis(200),
576 1,
577 std::time::Duration::from_millis(2),
578 std::time::Duration::from_millis(8),
579 )
580 }
581
582 fn payload(label: &str) -> Result<Payload, Box<dyn std::error::Error>> {
583 Ok(Payload::from_json(&json!({ "label": label }))?)
584 }
585
586 fn envelope(
587 workflow_id: &aion_core::WorkflowId,
588 seq: u64,
589 ) -> Result<EventEnvelope, Box<dyn std::error::Error>> {
590 let recorded_at = Utc
591 .timestamp_opt(i64::try_from(seq)?, 0)
592 .single()
593 .ok_or_else(|| "invalid timestamp".to_owned())?;
594 Ok(EventEnvelope {
595 seq,
596 recorded_at,
597 workflow_id: workflow_id.clone(),
598 })
599 }
600
601 fn started_event(
602 workflow_id: &aion_core::WorkflowId,
603 run_id: &aion_core::RunId,
604 ) -> Result<Event, Box<dyn std::error::Error>> {
605 Ok(Event::WorkflowStarted {
606 envelope: envelope(workflow_id, 1)?,
607 workflow_type: "checkout".to_owned(),
608 input: payload("input")?,
609 run_id: run_id.clone(),
610 parent_run_id: None,
611 package_version: aion_core::PackageVersion::new("a".repeat(64)),
612 })
613 }
614
615 fn handle(
616 pid: u64,
617 store: Arc<dyn EventStore>,
618 workflow_id: aion_core::WorkflowId,
619 run_id: aion_core::RunId,
620 ) -> WorkflowHandle {
621 let recorder = Recorder::resume_at(workflow_id.clone(), store, 1);
622 WorkflowHandle::new(WorkflowHandleParts {
623 workflow_id,
624 run_id,
625 pid,
626 workflow_type: "checkout".to_owned(),
627 namespace: String::from("default"),
628 loaded_version: hash(),
629 cached_status: WorkflowStatus::Running,
630 residency: HandleResidency::Resident,
631 recorder,
632 completion: CompletionNotifier::new(),
633 })
634 }
635
636 type TestContext = (Registry, Arc<dyn EventStore>, WorkflowHandle);
637
638 fn context_with_history(
639 runtime: &tokio::runtime::Runtime,
640 pid: u64,
641 workflow_id: aion_core::WorkflowId,
642 history: &[Event],
643 ) -> Result<TestContext, Box<dyn std::error::Error>> {
644 let registry = Registry::default();
645 let run_id = aion_core::RunId::new_v4();
646 let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
647 let mut full_history = vec![started_event(&workflow_id, &run_id)?];
648 full_history.extend_from_slice(history);
649 runtime.block_on(store.append(WriteToken::recorder(), &workflow_id, &full_history, 0))?;
650 let recorder = Recorder::resume_at(
651 workflow_id.clone(),
652 Arc::clone(&store),
653 full_history.len() as u64,
654 );
655 let handle = WorkflowHandle::new(WorkflowHandleParts {
656 workflow_id: workflow_id.clone(),
657 run_id: run_id.clone(),
658 pid,
659 workflow_type: "checkout".to_owned(),
660 namespace: String::from("default"),
661 loaded_version: hash(),
662 cached_status: WorkflowStatus::Running,
663 residency: HandleResidency::Resident,
664 recorder,
665 completion: CompletionNotifier::new(),
666 });
667 registry.insert((workflow_id, run_id), handle.clone())?;
668 Ok((registry, store, handle))
669 }
670
671 #[test]
672 fn resolves_registered_pid_to_context() -> TestResult {
673 let runtime = tokio::runtime::Runtime::new()?;
674 let registry = Registry::default();
675 let workflow_id = aion_core::WorkflowId::new_v4();
676 let run_id = aion_core::RunId::new_v4();
677 let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
678 runtime.block_on(store.append(
679 WriteToken::recorder(),
680 &workflow_id,
681 &[started_event(&workflow_id, &run_id)?],
682 0,
683 ))?;
684 let handle = handle(44, Arc::clone(&store), workflow_id.clone(), run_id.clone());
685 registry.insert((workflow_id.clone(), run_id), handle)?;
686
687 let context = NifContext::new(44, ®istry, runtime.handle().clone(), birth_wait())?;
688
689 assert_eq!(context.workflow_id(), &workflow_id);
690 assert_eq!(context.pid(), 44);
691 Ok(())
692 }
693
694 #[test]
695 fn unknown_pid_returns_unknown_process() -> TestResult {
696 let runtime = tokio::runtime::Runtime::new()?;
697 let registry = Registry::default();
698
699 let error = NifContext::new(77, ®istry, runtime.handle().clone(), birth_wait())
700 .err()
701 .ok_or("expected unknown process error")?;
702
703 assert!(matches!(error, NifContextError::UnknownProcess { pid: 77 }));
704 Ok(())
705 }
706
707 #[test]
715 fn birth_window_registration_resolves_instead_of_failing() -> TestResult {
716 let runtime = tokio::runtime::Runtime::new()?;
717 let registry = Arc::new(Registry::default());
718 let workflow_id = aion_core::WorkflowId::new_v4();
719 let run_id = aion_core::RunId::new_v4();
720 let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
721 runtime.block_on(store.append(
722 WriteToken::recorder(),
723 &workflow_id,
724 &[started_event(&workflow_id, &run_id)?],
725 0,
726 ))?;
727 let handle = handle(91, Arc::clone(&store), workflow_id.clone(), run_id.clone());
728
729 let late_registry = Arc::clone(®istry);
732 let inserter = std::thread::spawn(move || {
733 std::thread::sleep(std::time::Duration::from_millis(30));
734 late_registry.insert((workflow_id.clone(), run_id), handle)
735 });
736
737 let context = NifContext::new(91, ®istry, runtime.handle().clone(), birth_wait())?;
738
739 assert_eq!(context.pid(), 91);
740 inserter
741 .join()
742 .map_err(|_| "registry insert thread panicked")??;
743 Ok(())
744 }
745
746 #[test]
747 fn block_on_recorder_reads_current_head_without_deadlock() -> TestResult {
748 let runtime = tokio::runtime::Runtime::new()?;
749 let registry = Registry::default();
750 let workflow_id = aion_core::WorkflowId::new_v4();
751 let run_id = aion_core::RunId::new_v4();
752 let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
753 runtime.block_on(store.append(
754 WriteToken::recorder(),
755 &workflow_id,
756 &[started_event(&workflow_id, &run_id)?],
757 0,
758 ))?;
759 let recorder = Recorder::resume_at(workflow_id.clone(), Arc::clone(&store), 5);
760 let handle = WorkflowHandle::new(WorkflowHandleParts {
761 workflow_id: workflow_id.clone(),
762 run_id: run_id.clone(),
763 pid: 55,
764 workflow_type: "checkout".to_owned(),
765 namespace: String::from("default"),
766 loaded_version: hash(),
767 cached_status: WorkflowStatus::Running,
768 residency: HandleResidency::Resident,
769 recorder,
770 completion: CompletionNotifier::new(),
771 });
772 registry.insert((workflow_id, run_id), handle)?;
773 let context = NifContext::new(55, ®istry, runtime.handle().clone(), birth_wait())?;
774
775 let head = context
776 .block_on_recorder(|recorder| Box::pin(async move { Ok(recorder.current_head()) }))?;
777
778 assert_eq!(head, 5);
779 Ok(())
780 }
781
782 #[test]
785 fn context_reads_the_recorded_start_time_task_queue() -> TestResult {
786 let runtime = tokio::runtime::Runtime::new()?;
787 let workflow_id = aion_core::WorkflowId::new_v4();
788 let history = vec![Event::SearchAttributesUpdated {
789 envelope: envelope(&workflow_id, 2)?,
790 workflow_id: workflow_id.clone(),
791 attributes: std::collections::HashMap::from([(
792 aion_core::START_TIME_TASK_QUEUE_ATTRIBUTE.to_owned(),
793 aion_core::SearchAttributeValue::String(String::from("started-on")),
794 )]),
795 }];
796 let (registry, store, _handle) = context_with_history(&runtime, 70, workflow_id, &history)?;
797 let context = NifContext::new_with_history_store(
798 70,
799 ®istry,
800 runtime.handle().clone(),
801 Some(store),
802 birth_wait(),
803 )?;
804
805 assert_eq!(
806 context.start_time_task_queue().as_deref(),
807 Some("started-on")
808 );
809 Ok(())
810 }
811
812 #[test]
816 fn context_without_start_time_attribute_projects_none() -> TestResult {
817 let runtime = tokio::runtime::Runtime::new()?;
818 let workflow_id = aion_core::WorkflowId::new_v4();
819 let (registry, store, _handle) = context_with_history(&runtime, 71, workflow_id, &[])?;
821 let context = NifContext::new_with_history_store(
822 71,
823 ®istry,
824 runtime.handle().clone(),
825 Some(store),
826 birth_wait(),
827 )?;
828
829 assert_eq!(context.start_time_task_queue(), None);
830 Ok(())
831 }
832
833 #[test]
834 fn resolve_command_returns_recorded_activity_resolution() -> TestResult {
835 let runtime = tokio::runtime::Runtime::new()?;
836 let workflow_id = aion_core::WorkflowId::new_v4();
837 let result = payload("activity-result")?;
838 let history = vec![
839 Event::ActivityScheduled {
840 envelope: envelope(&workflow_id, 2)?,
841 activity_id: ActivityId::from_sequence_position(0),
842 activity_type: "activity".to_owned(),
843 input: payload("activity-input")?,
844 task_queue: String::from("default"),
845 node: None,
846 },
847 Event::ActivityCompleted {
848 envelope: envelope(&workflow_id, 3)?,
849 activity_id: ActivityId::from_sequence_position(0),
850 result: result.clone(),
851 attempt: 1,
852 },
853 ];
854 let (registry, store, handle) = context_with_history(&runtime, 66, workflow_id, &history)?;
855 let mut context = NifContext::new_with_history_store(
856 66,
857 ®istry,
858 runtime.handle().clone(),
859 Some(store),
860 birth_wait(),
861 )?;
862
863 assert_eq!(context.workflow_id(), handle.workflow_id());
864 assert_eq!(
865 context.resolve_command(Command::RunActivity {
866 key: CorrelationKey::Activity(0),
867 activity_type: "activity".to_owned(),
868 input: payload("activity-input")?,
869 })?,
870 ResolveOutcome::Recorded(Resolution::ActivityCompleted(result))
871 );
872 Ok(())
873 }
874
875 fn child_history(
876 workflow_id: &aion_core::WorkflowId,
877 child_workflow_id: &aion_core::WorkflowId,
878 include_terminal: bool,
879 ) -> Result<Vec<Event>, Box<dyn std::error::Error>> {
880 let timer_id = aion_core::TimerId::anonymous(0);
881 let mut history = vec![
882 Event::ActivityScheduled {
883 envelope: envelope(workflow_id, 2)?,
884 activity_id: ActivityId::from_sequence_position(0),
885 activity_type: "activity".to_owned(),
886 input: payload("activity-input")?,
887 task_queue: String::from("default"),
888 node: None,
889 },
890 Event::ActivityCompleted {
891 envelope: envelope(workflow_id, 3)?,
892 activity_id: ActivityId::from_sequence_position(0),
893 result: payload("activity-result")?,
894 attempt: 1,
895 },
896 Event::TimerStarted {
897 envelope: envelope(workflow_id, 4)?,
898 timer_id: timer_id.clone(),
899 fire_at: Utc
900 .timestamp_opt(99, 0)
901 .single()
902 .ok_or_else(|| "invalid timestamp".to_owned())?,
903 },
904 Event::TimerFired {
905 envelope: envelope(workflow_id, 5)?,
906 timer_id,
907 },
908 Event::ChildWorkflowStarted {
909 envelope: envelope(workflow_id, 6)?,
910 child_workflow_id: child_workflow_id.clone(),
911 workflow_type: "child".to_owned(),
912 input: payload("child-input")?,
913 package_version: aion_core::PackageVersion::new("a".repeat(64)),
914 },
915 ];
916 if include_terminal {
917 history.push(Event::ChildWorkflowCompleted {
918 envelope: envelope(workflow_id, 7)?,
919 child_workflow_id: child_workflow_id.clone(),
920 result: payload("child-result")?,
921 });
922 }
923 Ok(history)
924 }
925
926 #[test]
927 fn await_child_skips_consumed_commands_to_recorded_terminal() -> TestResult {
928 let runtime = tokio::runtime::Runtime::new()?;
929 let workflow_id = aion_core::WorkflowId::new_v4();
930 let child_workflow_id = aion_core::WorkflowId::new_v4();
931 let history = child_history(&workflow_id, &child_workflow_id, true)?;
936 let (registry, store, _handle) = context_with_history(&runtime, 88, workflow_id, &history)?;
937 let mut context = NifContext::new_with_history_store(
938 88,
939 ®istry,
940 runtime.handle().clone(),
941 Some(store),
942 birth_wait(),
943 )?;
944
945 assert_eq!(
946 context.resolve_command(Command::AwaitChild {
947 child_workflow_id: child_workflow_id.clone(),
948 })?,
949 ResolveOutcome::Recorded(Resolution::ChildCompleted(payload("child-result")?))
950 );
951 Ok(())
952 }
953
954 #[test]
955 fn await_child_without_recorded_terminal_resumes_live() -> TestResult {
956 let runtime = tokio::runtime::Runtime::new()?;
957 let workflow_id = aion_core::WorkflowId::new_v4();
958 let child_workflow_id = aion_core::WorkflowId::new_v4();
959 let history = child_history(&workflow_id, &child_workflow_id, false)?;
963 let (registry, store, _handle) = context_with_history(&runtime, 89, workflow_id, &history)?;
964 let mut context = NifContext::new_with_history_store(
965 89,
966 ®istry,
967 runtime.handle().clone(),
968 Some(store),
969 birth_wait(),
970 )?;
971
972 assert_eq!(
973 context.resolve_command(Command::AwaitChild { child_workflow_id })?,
974 ResolveOutcome::ResumeLive
975 );
976 Ok(())
977 }
978}