1use std::future::Future;
4use std::sync::Arc;
5
6use aion_core::{ActivityError, ActivityId, Payload, RunId, WorkflowId};
7use aion_store::EventStore;
8use chrono::{DateTime, TimeZone, 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, ResolvedCommand, 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 run_started_at: Option<DateTime<Utc>>,
67}
68
69impl NifContext {
70 pub fn new(
80 pid: u64,
81 registry: &Registry,
82 tokio_handle: Handle,
83 birth_wait: crate::runtime::SignalDeliveryConfig,
84 ) -> Result<Self, NifContextError> {
85 Self::new_with_history_store(pid, registry, tokio_handle, None, birth_wait)
86 }
87
88 pub fn new_with_history_store(
99 pid: u64,
100 registry: &Registry,
101 tokio_handle: Handle,
102 store: Option<Arc<dyn EventStore>>,
103 birth_wait: crate::runtime::SignalDeliveryConfig,
104 ) -> Result<Self, NifContextError> {
105 let handle = resolve_handle_with_birth_wait(registry, pid, birth_wait)?;
106 let recorder = handle.recorder();
107 let workflow_id = handle.workflow_id().clone();
108 let history = match store {
109 Some(store) => tokio_handle
110 .block_on(store.read_history(&workflow_id))
111 .map_err(DurabilityError::from)?,
112 None => tokio_handle.block_on(async {
113 let recorder = recorder.lock().await;
114 recorder.read_history().await
115 })?,
116 };
117 let history = crate::durability::current_run_segment(history, handle.run_id())?;
120 let run_started_at = history.first().map(|event| *event.recorded_at());
127 let cursor = HistoryCursor::new(history)?;
128 let resolver = Resolver::new(workflow_id, cursor);
129
130 Ok(Self {
131 handle,
132 recorder,
133 tokio_handle,
134 resolver,
135 run_started_at,
136 })
137 }
138
139 #[must_use]
141 pub fn workflow_id(&self) -> &WorkflowId {
142 self.handle.workflow_id()
143 }
144
145 #[must_use]
147 pub fn run_id(&self) -> &RunId {
148 self.handle.run_id()
149 }
150
151 #[must_use]
158 pub fn next_activity_ordinal(&self) -> u64 {
159 self.handle.allocate_activity_ordinals(1)
160 }
161
162 #[must_use]
164 pub fn allocate_activity_ordinals(&self, count: u64) -> u64 {
165 self.handle.allocate_activity_ordinals(count)
166 }
167
168 #[must_use]
173 pub fn next_timer_ordinal(&self) -> u64 {
174 self.handle.allocate_timer_ordinals(1)
175 }
176
177 #[must_use]
186 pub fn next_child_ordinal(&self) -> u64 {
187 self.handle.allocate_child_ordinals(1)
188 }
189
190 #[must_use]
192 pub fn signal_receives_consumed(&self, name: &str) -> u64 {
193 self.handle.signal_receives_consumed(name)
194 }
195
196 pub fn mark_signal_receive_consumed(&self, name: &str) {
198 self.handle.mark_signal_receive_consumed(name);
199 }
200
201 #[must_use]
203 pub fn signal_sends_completed(&self, name: &str) -> u64 {
204 self.handle.signal_sends_completed(name)
205 }
206
207 pub fn mark_signal_send_completed(&self, name: &str) {
209 self.handle.mark_signal_send_completed(name);
210 }
211
212 #[must_use]
214 pub fn workflow_handle(&self) -> WorkflowHandle {
215 self.handle.clone()
216 }
217
218 #[must_use]
220 pub const fn pid(&self) -> u64 {
221 self.handle.pid()
222 }
223
224 #[must_use]
244 pub fn workflow_now(&self) -> Option<DateTime<Utc>> {
245 let run_started_at = self.run_started_at?;
246 let Some(millis) = self.handle.workflow_now_millis() else {
247 return Some(run_started_at);
248 };
249 if millis <= run_started_at.timestamp_millis() {
250 return Some(run_started_at);
251 }
252 let Some(position) = Utc.timestamp_millis_opt(millis).single() else {
253 tracing::error!(
258 workflow_id = %self.handle.workflow_id(),
259 run_id = %self.handle.run_id(),
260 millis,
261 "workflow-visible now cell holds an unrepresentable timestamp; \
262 answering the run start"
263 );
264 return Some(run_started_at);
265 };
266 Some(position)
267 }
268
269 pub fn observe_recorded_at(&self, recorded_at: DateTime<Utc>) {
276 self.handle.advance_workflow_now(recorded_at);
277 }
278
279 #[must_use]
281 pub fn next_deterministic_sequence(&self) -> u64 {
282 self.handle.next_deterministic_nif_sequence()
283 }
284
285 #[must_use]
287 pub fn recorder(&self) -> Arc<Mutex<Recorder>> {
288 Arc::clone(&self.recorder)
289 }
290
291 pub fn block_on_recorder<T, F>(&self, f: F) -> Result<T, NifContextError>
297 where
298 F: for<'a> FnOnce(
299 &'a mut Recorder,
300 ) -> std::pin::Pin<
301 Box<dyn Future<Output = Result<T, DurabilityError>> + Send + 'a>,
302 >,
303 {
304 self.tokio_handle
305 .block_on(async {
306 let mut recorder = self.recorder.lock().await;
307 f(&mut recorder).await
308 })
309 .map_err(Into::into)
310 }
311
312 pub(crate) fn record_activity_scheduled_started(
318 &self,
319 recorded_at: chrono::DateTime<chrono::Utc>,
320 activity_id: ActivityId,
321 scheduled: super::nif_activity::ScheduledActivity,
322 ) -> Result<(), NifContextError> {
323 self.tokio_handle
324 .block_on(async {
325 let mut recorder = self.recorder.lock().await;
326 recorder
327 .record_activity_scheduled(
328 recorded_at,
329 activity_id.clone(),
330 scheduled.activity_type,
331 scheduled.input,
332 scheduled.task_queue,
335 scheduled.node,
338 )
339 .await?;
340 recorder
341 .record_activity_started(recorded_at, activity_id, scheduled.attempt)
343 .await
344 })
345 .map_err(Into::into)
346 }
347
348 pub(crate) fn record_activity_adoption_offered(
355 &self,
356 recorded_at: chrono::DateTime<chrono::Utc>,
357 activity_id: ActivityId,
358 attempt: u32,
359 ) -> Result<(), NifContextError> {
360 self.tokio_handle
361 .block_on(async {
362 let mut recorder = self.recorder.lock().await;
363 recorder
364 .record_activity_adoption_offered(recorded_at, activity_id, attempt)
365 .await
366 })
367 .map_err(Into::into)
368 }
369
370 pub fn record_activity_completed(
376 &self,
377 recorded_at: chrono::DateTime<chrono::Utc>,
378 activity_id: ActivityId,
379 result: Payload,
380 attempt: u32,
381 ) -> Result<(), NifContextError> {
382 self.tokio_handle
383 .block_on(async {
384 let mut recorder = self.recorder.lock().await;
385 recorder
386 .record_activity_completed(recorded_at, activity_id, result, attempt)
388 .await
389 })
390 .map_err(Into::into)
391 }
392
393 pub fn record_activity_failed(
403 &self,
404 recorded_at: chrono::DateTime<chrono::Utc>,
405 activity_id: ActivityId,
406 error: ActivityError,
407 attempt: u32,
408 ) -> Result<(), NifContextError> {
409 self.tokio_handle
410 .block_on(async {
411 let mut recorder = self.recorder.lock().await;
412 recorder
413 .record_activity_failed(recorded_at, activity_id, error, attempt)
414 .await
415 })
416 .map_err(Into::into)
417 }
418
419 pub fn record_activity_cancelled(
425 &self,
426 recorded_at: chrono::DateTime<chrono::Utc>,
427 activity_id: ActivityId,
428 attempt: u32,
429 ) -> Result<(), NifContextError> {
430 self.tokio_handle
431 .block_on(async {
432 let mut recorder = self.recorder.lock().await;
433 recorder
434 .record_activity_cancelled(recorded_at, activity_id, attempt)
436 .await
437 })
438 .map_err(Into::into)
439 }
440
441 pub fn record_activity_cancelled_and_settle_outbox(
447 &self,
448 recorded_at: chrono::DateTime<chrono::Utc>,
449 ordinal: u64,
450 attempt: u32,
451 ) -> Result<(), NifContextError> {
452 self.tokio_handle
453 .block_on(async {
454 let mut recorder = self.recorder.lock().await;
455 recorder
456 .record_activity_cancelled_and_settle_outbox(recorded_at, ordinal, attempt)
458 .await
459 })
460 .map_err(Into::into)
461 }
462
463 pub fn record_fan_out_dispatch(
469 &self,
470 recorded_at: chrono::DateTime<chrono::Utc>,
471 items: &[FanOutItem],
472 ) -> Result<(), NifContextError> {
473 self.tokio_handle
474 .block_on(async {
475 let mut recorder = self.recorder.lock().await;
476 recorder.record_fan_out_dispatch(recorded_at, items).await
477 })
478 .map_err(Into::into)
479 }
480
481 pub fn rearm_outbox_pending(
488 &self,
489 recorded_at: chrono::DateTime<chrono::Utc>,
490 items: &[FanOutItem],
491 ) -> Result<(), NifContextError> {
492 self.tokio_handle
493 .block_on(async {
494 let recorder = self.recorder.lock().await;
495 recorder.rearm_outbox_pending(recorded_at, items).await
496 })
497 .map_err(Into::into)
498 }
499
500 pub fn record_fan_out_completion(
506 &self,
507 recorded_at: chrono::DateTime<chrono::Utc>,
508 ordinal: u64,
509 outcome: FanOutOutcome,
510 ) -> Result<FanOutCompletionResult, NifContextError> {
511 self.tokio_handle
512 .block_on(async {
513 let mut recorder = self.recorder.lock().await;
514 recorder
515 .record_fan_out_completion(recorded_at, ordinal, None, outcome)
516 .await
517 })
518 .map_err(Into::into)
519 }
520
521 #[must_use]
523 pub fn history(&self) -> &[aion_core::Event] {
524 self.resolver.history()
525 }
526
527 #[must_use]
541 pub fn start_time_task_queue(&self) -> Option<String> {
542 aion_core::start_time_task_queue(self.history())
543 }
544
545 pub fn resolve_command_observed(
569 &mut self,
570 command: Command,
571 ) -> Result<ResolveOutcome, NifContextError> {
572 self.position_resolver_for(&command);
573 match self.resolver.resolve_with_consumed(command)? {
574 ResolvedCommand::Recorded {
575 resolution,
576 recorded_at,
577 } => {
578 self.observe_recorded_at(recorded_at);
579 Ok(ResolveOutcome::Recorded(resolution))
580 }
581 ResolvedCommand::ResumeLive { recorded_at } => {
582 if let Some(recorded_at) = recorded_at {
583 self.observe_recorded_at(recorded_at);
584 }
585 Ok(ResolveOutcome::ResumeLive)
586 }
587 }
588 }
589
590 pub fn resolve_command_unobserved(
605 &mut self,
606 command: Command,
607 ) -> Result<ResolveOutcome, NifContextError> {
608 self.position_resolver_for(&command);
609 self.resolver.resolve(command).map_err(Into::into)
610 }
611
612 fn position_resolver_for(&mut self, command: &Command) {
622 if let Some(key) = command.key() {
623 self.resolver.fast_forward_to(key);
624 } else if let Command::AwaitChild { child_workflow_id } = command {
625 self.resolver
626 .fast_forward_to_child_terminal(child_workflow_id);
627 }
628 }
629}
630
631fn registry_error_to_context(error: &EngineError) -> NifContextError {
632 match error {
633 EngineError::RegistryPoisoned => NifContextError::RecorderPoisoned,
634 _ => NifContextError::TermEncoding {
635 reason: format!("registry lookup failed: {error}"),
636 },
637 }
638}
639
640fn resolve_handle_with_birth_wait(
661 registry: &Registry,
662 pid: u64,
663 birth_wait: crate::runtime::SignalDeliveryConfig,
664) -> Result<WorkflowHandle, NifContextError> {
665 let lookup = |registry: &Registry| -> Result<Option<WorkflowHandle>, NifContextError> {
666 Ok(registry
667 .list()
668 .map_err(|error| registry_error_to_context(&error))?
669 .into_iter()
670 .find(|handle| handle.pid() == pid))
671 };
672 if let Some(handle) = lookup(registry)? {
673 return Ok(handle);
674 }
675 let budget = birth_wait
676 .ready_timeout
677 .saturating_mul(birth_wait.max_enqueue_attempts.max(1));
678 let deadline = std::time::Instant::now() + budget;
679 let mut backoff = birth_wait.initial_backoff;
680 while std::time::Instant::now() < deadline {
681 std::thread::sleep(backoff);
682 let doubled = backoff.saturating_mul(2);
683 backoff = if doubled > birth_wait.max_backoff {
684 birth_wait.max_backoff
685 } else {
686 doubled
687 };
688 if let Some(handle) = lookup(registry)? {
689 return Ok(handle);
690 }
691 }
692 Err(NifContextError::UnknownProcess { pid })
693}
694
695#[cfg(test)]
696mod tests {
697 use std::sync::Arc;
698
699 use aion_core::{ActivityId, Event, EventEnvelope, Payload, WorkflowStatus};
700 use aion_package::ContentHash;
701 use aion_store::{EventStore, InMemoryStore, WriteToken};
702 use chrono::{TimeZone, Utc};
703 use serde_json::json;
704
705 use super::{NifContext, NifContextError};
706 use crate::durability::{Command, CorrelationKey, Recorder, Resolution, ResolveOutcome};
707 use crate::registry::{
708 CompletionNotifier, HandleResidency, Registry, WorkflowHandle, WorkflowHandleParts,
709 };
710
711 type TestResult = Result<(), Box<dyn std::error::Error>>;
712
713 fn hash() -> ContentHash {
714 ContentHash::from_bytes([7; 32])
715 }
716
717 fn birth_wait() -> crate::runtime::SignalDeliveryConfig {
719 crate::runtime::SignalDeliveryConfig::new(
720 std::time::Duration::from_millis(200),
721 1,
722 std::time::Duration::from_millis(2),
723 std::time::Duration::from_millis(8),
724 )
725 }
726
727 fn payload(label: &str) -> Result<Payload, Box<dyn std::error::Error>> {
728 Ok(Payload::from_json(&json!({ "label": label }))?)
729 }
730
731 fn envelope(
732 workflow_id: &aion_core::WorkflowId,
733 seq: u64,
734 ) -> Result<EventEnvelope, Box<dyn std::error::Error>> {
735 let recorded_at = Utc
736 .timestamp_opt(i64::try_from(seq)?, 0)
737 .single()
738 .ok_or_else(|| "invalid timestamp".to_owned())?;
739 Ok(EventEnvelope {
740 seq,
741 recorded_at,
742 workflow_id: workflow_id.clone(),
743 })
744 }
745
746 fn started_event(
747 workflow_id: &aion_core::WorkflowId,
748 run_id: &aion_core::RunId,
749 ) -> Result<Event, Box<dyn std::error::Error>> {
750 Ok(Event::WorkflowStarted {
751 envelope: envelope(workflow_id, 1)?,
752 workflow_type: "checkout".to_owned(),
753 input: payload("input")?,
754 run_id: run_id.clone(),
755 parent_run_id: None,
756 package_version: aion_core::PackageVersion::new("a".repeat(64)),
757 })
758 }
759
760 fn handle(
761 pid: u64,
762 store: Arc<dyn EventStore>,
763 workflow_id: aion_core::WorkflowId,
764 run_id: aion_core::RunId,
765 ) -> WorkflowHandle {
766 let recorder = Recorder::resume_at(workflow_id.clone(), store, 1);
767 WorkflowHandle::new(WorkflowHandleParts {
768 workflow_id,
769 run_id,
770 pid,
771 workflow_type: "checkout".to_owned(),
772 namespace: String::from("default"),
773 loaded_version: hash(),
774 cached_status: WorkflowStatus::Running,
775 residency: HandleResidency::Resident,
776 recorder,
777 completion: CompletionNotifier::new(),
778 })
779 }
780
781 type TestContext = (Registry, Arc<dyn EventStore>, WorkflowHandle);
782
783 fn context_with_history(
784 runtime: &tokio::runtime::Runtime,
785 pid: u64,
786 workflow_id: aion_core::WorkflowId,
787 history: &[Event],
788 ) -> Result<TestContext, Box<dyn std::error::Error>> {
789 let registry = Registry::default();
790 let run_id = aion_core::RunId::new_v4();
791 let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
792 let mut full_history = vec![started_event(&workflow_id, &run_id)?];
793 full_history.extend_from_slice(history);
794 runtime.block_on(store.append(WriteToken::recorder(), &workflow_id, &full_history, 0))?;
795 let recorder = Recorder::resume_at(
796 workflow_id.clone(),
797 Arc::clone(&store),
798 full_history.len() as u64,
799 );
800 let handle = WorkflowHandle::new(WorkflowHandleParts {
801 workflow_id: workflow_id.clone(),
802 run_id: run_id.clone(),
803 pid,
804 workflow_type: "checkout".to_owned(),
805 namespace: String::from("default"),
806 loaded_version: hash(),
807 cached_status: WorkflowStatus::Running,
808 residency: HandleResidency::Resident,
809 recorder,
810 completion: CompletionNotifier::new(),
811 });
812 registry.insert((workflow_id, run_id), handle.clone())?;
813 Ok((registry, store, handle))
814 }
815
816 #[test]
817 fn resolves_registered_pid_to_context() -> TestResult {
818 let runtime = tokio::runtime::Runtime::new()?;
819 let registry = Registry::default();
820 let workflow_id = aion_core::WorkflowId::new_v4();
821 let run_id = aion_core::RunId::new_v4();
822 let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
823 runtime.block_on(store.append(
824 WriteToken::recorder(),
825 &workflow_id,
826 &[started_event(&workflow_id, &run_id)?],
827 0,
828 ))?;
829 let handle = handle(44, Arc::clone(&store), workflow_id.clone(), run_id.clone());
830 registry.insert((workflow_id.clone(), run_id), handle)?;
831
832 let context = NifContext::new(44, ®istry, runtime.handle().clone(), birth_wait())?;
833
834 assert_eq!(context.workflow_id(), &workflow_id);
835 assert_eq!(context.pid(), 44);
836 Ok(())
837 }
838
839 #[test]
840 fn unknown_pid_returns_unknown_process() -> TestResult {
841 let runtime = tokio::runtime::Runtime::new()?;
842 let registry = Registry::default();
843
844 let error = NifContext::new(77, ®istry, runtime.handle().clone(), birth_wait())
845 .err()
846 .ok_or("expected unknown process error")?;
847
848 assert!(matches!(error, NifContextError::UnknownProcess { pid: 77 }));
849 Ok(())
850 }
851
852 #[test]
860 fn birth_window_registration_resolves_instead_of_failing() -> TestResult {
861 let runtime = tokio::runtime::Runtime::new()?;
862 let registry = Arc::new(Registry::default());
863 let workflow_id = aion_core::WorkflowId::new_v4();
864 let run_id = aion_core::RunId::new_v4();
865 let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
866 runtime.block_on(store.append(
867 WriteToken::recorder(),
868 &workflow_id,
869 &[started_event(&workflow_id, &run_id)?],
870 0,
871 ))?;
872 let handle = handle(91, Arc::clone(&store), workflow_id.clone(), run_id.clone());
873
874 let late_registry = Arc::clone(®istry);
877 let inserter = std::thread::spawn(move || {
878 std::thread::sleep(std::time::Duration::from_millis(30));
879 late_registry.insert((workflow_id.clone(), run_id), handle)
880 });
881
882 let context = NifContext::new(91, ®istry, runtime.handle().clone(), birth_wait())?;
883
884 assert_eq!(context.pid(), 91);
885 inserter
886 .join()
887 .map_err(|_| "registry insert thread panicked")??;
888 Ok(())
889 }
890
891 #[test]
892 fn block_on_recorder_reads_current_head_without_deadlock() -> TestResult {
893 let runtime = tokio::runtime::Runtime::new()?;
894 let registry = Registry::default();
895 let workflow_id = aion_core::WorkflowId::new_v4();
896 let run_id = aion_core::RunId::new_v4();
897 let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
898 runtime.block_on(store.append(
899 WriteToken::recorder(),
900 &workflow_id,
901 &[started_event(&workflow_id, &run_id)?],
902 0,
903 ))?;
904 let recorder = Recorder::resume_at(workflow_id.clone(), Arc::clone(&store), 5);
905 let handle = WorkflowHandle::new(WorkflowHandleParts {
906 workflow_id: workflow_id.clone(),
907 run_id: run_id.clone(),
908 pid: 55,
909 workflow_type: "checkout".to_owned(),
910 namespace: String::from("default"),
911 loaded_version: hash(),
912 cached_status: WorkflowStatus::Running,
913 residency: HandleResidency::Resident,
914 recorder,
915 completion: CompletionNotifier::new(),
916 });
917 registry.insert((workflow_id, run_id), handle)?;
918 let context = NifContext::new(55, ®istry, runtime.handle().clone(), birth_wait())?;
919
920 let head = context
921 .block_on_recorder(|recorder| Box::pin(async move { Ok(recorder.current_head()) }))?;
922
923 assert_eq!(head, 5);
924 Ok(())
925 }
926
927 #[test]
930 fn context_reads_the_recorded_start_time_task_queue() -> TestResult {
931 let runtime = tokio::runtime::Runtime::new()?;
932 let workflow_id = aion_core::WorkflowId::new_v4();
933 let history = vec![Event::SearchAttributesUpdated {
934 envelope: envelope(&workflow_id, 2)?,
935 workflow_id: workflow_id.clone(),
936 attributes: std::collections::HashMap::from([(
937 aion_core::START_TIME_TASK_QUEUE_ATTRIBUTE.to_owned(),
938 aion_core::SearchAttributeValue::String(String::from("started-on")),
939 )]),
940 }];
941 let (registry, store, _handle) = context_with_history(&runtime, 70, workflow_id, &history)?;
942 let context = NifContext::new_with_history_store(
943 70,
944 ®istry,
945 runtime.handle().clone(),
946 Some(store),
947 birth_wait(),
948 )?;
949
950 assert_eq!(
951 context.start_time_task_queue().as_deref(),
952 Some("started-on")
953 );
954 Ok(())
955 }
956
957 #[test]
961 fn context_without_start_time_attribute_projects_none() -> TestResult {
962 let runtime = tokio::runtime::Runtime::new()?;
963 let workflow_id = aion_core::WorkflowId::new_v4();
964 let (registry, store, _handle) = context_with_history(&runtime, 71, workflow_id, &[])?;
966 let context = NifContext::new_with_history_store(
967 71,
968 ®istry,
969 runtime.handle().clone(),
970 Some(store),
971 birth_wait(),
972 )?;
973
974 assert_eq!(context.start_time_task_queue(), None);
975 Ok(())
976 }
977
978 #[test]
979 fn resolve_command_returns_recorded_activity_resolution() -> TestResult {
980 let runtime = tokio::runtime::Runtime::new()?;
981 let workflow_id = aion_core::WorkflowId::new_v4();
982 let result = payload("activity-result")?;
983 let history = vec![
984 Event::ActivityScheduled {
985 envelope: envelope(&workflow_id, 2)?,
986 activity_id: ActivityId::from_sequence_position(0),
987 activity_type: "activity".to_owned(),
988 input: payload("activity-input")?,
989 task_queue: String::from("default"),
990 node: None,
991 },
992 Event::ActivityCompleted {
993 envelope: envelope(&workflow_id, 3)?,
994 activity_id: ActivityId::from_sequence_position(0),
995 result: result.clone(),
996 attempt: 1,
997 },
998 ];
999 let (registry, store, handle) = context_with_history(&runtime, 66, workflow_id, &history)?;
1000 let mut context = NifContext::new_with_history_store(
1001 66,
1002 ®istry,
1003 runtime.handle().clone(),
1004 Some(store),
1005 birth_wait(),
1006 )?;
1007
1008 assert_eq!(context.workflow_id(), handle.workflow_id());
1009 assert_eq!(
1010 context.resolve_command_observed(Command::RunActivity {
1011 key: CorrelationKey::Activity(0),
1012 activity_type: "activity".to_owned(),
1013 input: payload("activity-input")?,
1014 })?,
1015 ResolveOutcome::Recorded(Resolution::ActivityCompleted(result))
1016 );
1017 Ok(())
1018 }
1019
1020 fn child_history(
1021 workflow_id: &aion_core::WorkflowId,
1022 child_workflow_id: &aion_core::WorkflowId,
1023 include_terminal: bool,
1024 ) -> Result<Vec<Event>, Box<dyn std::error::Error>> {
1025 let timer_id = aion_core::TimerId::anonymous(0);
1026 let mut history = vec![
1027 Event::ActivityScheduled {
1028 envelope: envelope(workflow_id, 2)?,
1029 activity_id: ActivityId::from_sequence_position(0),
1030 activity_type: "activity".to_owned(),
1031 input: payload("activity-input")?,
1032 task_queue: String::from("default"),
1033 node: None,
1034 },
1035 Event::ActivityCompleted {
1036 envelope: envelope(workflow_id, 3)?,
1037 activity_id: ActivityId::from_sequence_position(0),
1038 result: payload("activity-result")?,
1039 attempt: 1,
1040 },
1041 Event::TimerStarted {
1042 envelope: envelope(workflow_id, 4)?,
1043 timer_id: timer_id.clone(),
1044 fire_at: Utc
1045 .timestamp_opt(99, 0)
1046 .single()
1047 .ok_or_else(|| "invalid timestamp".to_owned())?,
1048 },
1049 Event::TimerFired {
1050 envelope: envelope(workflow_id, 5)?,
1051 timer_id,
1052 },
1053 Event::ChildWorkflowStarted {
1054 envelope: envelope(workflow_id, 6)?,
1055 child_workflow_id: child_workflow_id.clone(),
1056 workflow_type: "child".to_owned(),
1057 input: payload("child-input")?,
1058 package_version: aion_core::PackageVersion::new("a".repeat(64)),
1059 },
1060 ];
1061 if include_terminal {
1062 history.push(Event::ChildWorkflowCompleted {
1063 envelope: envelope(workflow_id, 7)?,
1064 child_workflow_id: child_workflow_id.clone(),
1065 result: payload("child-result")?,
1066 });
1067 }
1068 Ok(history)
1069 }
1070
1071 #[test]
1072 fn await_child_skips_consumed_commands_to_recorded_terminal() -> TestResult {
1073 let runtime = tokio::runtime::Runtime::new()?;
1074 let workflow_id = aion_core::WorkflowId::new_v4();
1075 let child_workflow_id = aion_core::WorkflowId::new_v4();
1076 let history = child_history(&workflow_id, &child_workflow_id, true)?;
1081 let (registry, store, _handle) = context_with_history(&runtime, 88, workflow_id, &history)?;
1082 let mut context = NifContext::new_with_history_store(
1083 88,
1084 ®istry,
1085 runtime.handle().clone(),
1086 Some(store),
1087 birth_wait(),
1088 )?;
1089
1090 assert_eq!(
1091 context.resolve_command_observed(Command::AwaitChild {
1092 child_workflow_id: child_workflow_id.clone(),
1093 })?,
1094 ResolveOutcome::Recorded(Resolution::ChildCompleted(payload("child-result")?))
1095 );
1096 Ok(())
1097 }
1098
1099 #[test]
1100 fn await_child_without_recorded_terminal_resumes_live() -> TestResult {
1101 let runtime = tokio::runtime::Runtime::new()?;
1102 let workflow_id = aion_core::WorkflowId::new_v4();
1103 let child_workflow_id = aion_core::WorkflowId::new_v4();
1104 let history = child_history(&workflow_id, &child_workflow_id, false)?;
1108 let (registry, store, _handle) = context_with_history(&runtime, 89, workflow_id, &history)?;
1109 let mut context = NifContext::new_with_history_store(
1110 89,
1111 ®istry,
1112 runtime.handle().clone(),
1113 Some(store),
1114 birth_wait(),
1115 )?;
1116
1117 assert_eq!(
1118 context.resolve_command_observed(Command::AwaitChild { child_workflow_id })?,
1119 ResolveOutcome::ResumeLive
1120 );
1121 Ok(())
1122 }
1123}