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 workflow_handle_for_pid(
111 pid: u64,
112 registry: &Registry,
113 birth_wait: crate::runtime::SignalDeliveryConfig,
114 ) -> Result<WorkflowHandle, NifContextError> {
115 resolve_handle_with_birth_wait(registry, pid, birth_wait)
116 }
117
118 pub fn new_with_history_store(
129 pid: u64,
130 registry: &Registry,
131 tokio_handle: Handle,
132 store: Option<Arc<dyn EventStore>>,
133 birth_wait: crate::runtime::SignalDeliveryConfig,
134 ) -> Result<Self, NifContextError> {
135 let handle = resolve_handle_with_birth_wait(registry, pid, birth_wait)?;
136 let recorder = handle.recorder();
137 let workflow_id = handle.workflow_id().clone();
138 let history = match store {
139 Some(store) => tokio_handle
140 .block_on(store.read_history(&workflow_id))
141 .map_err(DurabilityError::from)?,
142 None => tokio_handle.block_on(async {
143 let recorder = recorder.lock().await;
144 recorder.read_history().await
145 })?,
146 };
147 let history = crate::durability::current_run_segment(history, handle.run_id())?;
150 let run_started_at = history.first().map(|event| *event.recorded_at());
157 let cursor = HistoryCursor::new(history)?;
158 let resolver = Resolver::new(workflow_id, cursor);
159
160 Ok(Self {
161 handle,
162 recorder,
163 tokio_handle,
164 resolver,
165 run_started_at,
166 })
167 }
168
169 #[must_use]
171 pub fn workflow_id(&self) -> &WorkflowId {
172 self.handle.workflow_id()
173 }
174
175 #[must_use]
177 pub fn run_id(&self) -> &RunId {
178 self.handle.run_id()
179 }
180
181 #[must_use]
188 pub fn next_activity_ordinal(&self) -> u64 {
189 self.handle.allocate_activity_ordinals(1)
190 }
191
192 #[must_use]
194 pub fn allocate_activity_ordinals(&self, count: u64) -> u64 {
195 self.handle.allocate_activity_ordinals(count)
196 }
197
198 #[must_use]
203 pub fn next_timer_ordinal(&self) -> u64 {
204 self.handle.allocate_timer_ordinals(1)
205 }
206
207 #[must_use]
216 pub fn next_child_ordinal(&self) -> u64 {
217 self.handle.allocate_child_ordinals(1)
218 }
219
220 #[must_use]
226 pub fn next_hatch_ordinal(&self) -> u64 {
227 self.handle.allocate_hatch_ordinals(1)
228 }
229
230 #[must_use]
232 pub fn signal_receives_consumed(&self, name: &str) -> u64 {
233 self.handle.signal_receives_consumed(name)
234 }
235
236 pub fn mark_signal_receive_consumed(&self, name: &str) {
238 self.handle.mark_signal_receive_consumed(name);
239 }
240
241 #[must_use]
243 pub fn signal_sends_completed(&self, name: &str) -> u64 {
244 self.handle.signal_sends_completed(name)
245 }
246
247 pub fn mark_signal_send_completed(&self, name: &str) {
249 self.handle.mark_signal_send_completed(name);
250 }
251
252 #[must_use]
254 pub fn workflow_handle(&self) -> WorkflowHandle {
255 self.handle.clone()
256 }
257
258 #[must_use]
260 pub const fn pid(&self) -> u64 {
261 self.handle.pid()
262 }
263
264 #[must_use]
284 pub fn workflow_now(&self) -> Option<DateTime<Utc>> {
285 let run_started_at = self.run_started_at?;
286 let Some(millis) = self.handle.workflow_now_millis() else {
287 return Some(run_started_at);
288 };
289 if millis <= run_started_at.timestamp_millis() {
290 return Some(run_started_at);
291 }
292 let Some(position) = Utc.timestamp_millis_opt(millis).single() else {
293 tracing::error!(
298 workflow_id = %self.handle.workflow_id(),
299 run_id = %self.handle.run_id(),
300 millis,
301 "workflow-visible now cell holds an unrepresentable timestamp; \
302 answering the run start"
303 );
304 return Some(run_started_at);
305 };
306 Some(position)
307 }
308
309 pub fn observe_recorded_at(&self, recorded_at: DateTime<Utc>) {
316 self.handle.advance_workflow_now(recorded_at);
317 }
318
319 #[must_use]
321 pub fn next_deterministic_sequence(&self) -> u64 {
322 self.handle.next_deterministic_nif_sequence()
323 }
324
325 #[must_use]
327 pub fn recorder(&self) -> Arc<Mutex<Recorder>> {
328 Arc::clone(&self.recorder)
329 }
330
331 pub fn block_on_recorder<T, F>(&self, f: F) -> Result<T, NifContextError>
337 where
338 F: for<'a> FnOnce(
339 &'a mut Recorder,
340 ) -> std::pin::Pin<
341 Box<dyn Future<Output = Result<T, DurabilityError>> + Send + 'a>,
342 >,
343 {
344 self.tokio_handle
345 .block_on(async {
346 let mut recorder = self.recorder.lock().await;
347 f(&mut recorder).await
348 })
349 .map_err(Into::into)
350 }
351
352 pub(crate) fn record_activity_scheduled_started(
358 &self,
359 recorded_at: chrono::DateTime<chrono::Utc>,
360 activity_id: ActivityId,
361 scheduled: super::nif_activity::ScheduledActivity,
362 ) -> Result<(), NifContextError> {
363 self.tokio_handle
364 .block_on(async {
365 let mut recorder = self.recorder.lock().await;
366 recorder
367 .record_activity_scheduled(
368 recorded_at,
369 activity_id.clone(),
370 scheduled.activity_type,
371 scheduled.input,
372 scheduled.task_queue,
375 scheduled.node,
378 )
379 .await?;
380 recorder
381 .record_activity_started(recorded_at, activity_id, scheduled.attempt)
383 .await
384 })
385 .map_err(Into::into)
386 }
387
388 pub(crate) fn record_activity_adoption_offered(
395 &self,
396 recorded_at: chrono::DateTime<chrono::Utc>,
397 activity_id: ActivityId,
398 attempt: u32,
399 ) -> Result<(), NifContextError> {
400 self.tokio_handle
401 .block_on(async {
402 let mut recorder = self.recorder.lock().await;
403 recorder
404 .record_activity_adoption_offered(recorded_at, activity_id, attempt)
405 .await
406 })
407 .map_err(Into::into)
408 }
409
410 pub fn record_activity_completed(
416 &self,
417 recorded_at: chrono::DateTime<chrono::Utc>,
418 activity_id: ActivityId,
419 result: Payload,
420 attempt: u32,
421 ) -> Result<(), NifContextError> {
422 self.tokio_handle
423 .block_on(async {
424 let mut recorder = self.recorder.lock().await;
425 recorder
426 .record_activity_completed(recorded_at, activity_id, result, attempt)
428 .await
429 })
430 .map_err(Into::into)
431 }
432
433 pub fn record_activity_failed(
443 &self,
444 recorded_at: chrono::DateTime<chrono::Utc>,
445 activity_id: ActivityId,
446 error: ActivityError,
447 attempt: u32,
448 ) -> Result<(), NifContextError> {
449 self.tokio_handle
450 .block_on(async {
451 let mut recorder = self.recorder.lock().await;
452 recorder
453 .record_activity_failed(recorded_at, activity_id, error, attempt)
454 .await
455 })
456 .map_err(Into::into)
457 }
458
459 pub fn record_activity_cancelled(
465 &self,
466 recorded_at: chrono::DateTime<chrono::Utc>,
467 activity_id: ActivityId,
468 attempt: u32,
469 ) -> Result<(), NifContextError> {
470 self.tokio_handle
471 .block_on(async {
472 let mut recorder = self.recorder.lock().await;
473 recorder
474 .record_activity_cancelled(recorded_at, activity_id, attempt)
476 .await
477 })
478 .map_err(Into::into)
479 }
480
481 pub fn record_activity_cancelled_and_settle_outbox(
487 &self,
488 recorded_at: chrono::DateTime<chrono::Utc>,
489 ordinal: u64,
490 attempt: u32,
491 ) -> Result<(), NifContextError> {
492 self.tokio_handle
493 .block_on(async {
494 let mut recorder = self.recorder.lock().await;
495 recorder
496 .record_activity_cancelled_and_settle_outbox(recorded_at, ordinal, attempt)
498 .await
499 })
500 .map_err(Into::into)
501 }
502
503 pub fn record_fan_out_dispatch(
509 &self,
510 recorded_at: chrono::DateTime<chrono::Utc>,
511 items: &[FanOutItem],
512 ) -> Result<(), NifContextError> {
513 self.tokio_handle
514 .block_on(async {
515 let mut recorder = self.recorder.lock().await;
516 recorder.record_fan_out_dispatch(recorded_at, items).await
517 })
518 .map_err(Into::into)
519 }
520
521 pub fn rearm_outbox_pending(
528 &self,
529 recorded_at: chrono::DateTime<chrono::Utc>,
530 items: &[FanOutItem],
531 ) -> Result<(), NifContextError> {
532 self.tokio_handle
533 .block_on(async {
534 let recorder = self.recorder.lock().await;
535 recorder.rearm_outbox_pending(recorded_at, items).await
536 })
537 .map_err(Into::into)
538 }
539
540 pub fn record_fan_out_completion(
546 &self,
547 recorded_at: chrono::DateTime<chrono::Utc>,
548 ordinal: u64,
549 outcome: FanOutOutcome,
550 ) -> Result<FanOutCompletionResult, NifContextError> {
551 self.tokio_handle
552 .block_on(async {
553 let mut recorder = self.recorder.lock().await;
554 recorder
555 .record_fan_out_completion(recorded_at, ordinal, None, outcome)
556 .await
557 })
558 .map_err(Into::into)
559 }
560
561 #[must_use]
563 pub fn history(&self) -> &[aion_core::Event] {
564 self.resolver.history()
565 }
566
567 #[must_use]
581 pub fn start_time_task_queue(&self) -> Option<String> {
582 aion_core::start_time_task_queue(self.history())
583 }
584
585 pub fn resolve_command_observed(
609 &mut self,
610 command: Command,
611 ) -> Result<ResolveOutcome, NifContextError> {
612 self.position_resolver_for(&command);
613 match self.resolver.resolve_with_consumed(command)? {
614 ResolvedCommand::Recorded {
615 resolution,
616 recorded_at,
617 } => {
618 self.observe_recorded_at(recorded_at);
619 Ok(ResolveOutcome::Recorded(resolution))
620 }
621 ResolvedCommand::ResumeLive { recorded_at } => {
622 if let Some(recorded_at) = recorded_at {
623 self.observe_recorded_at(recorded_at);
624 }
625 Ok(ResolveOutcome::ResumeLive)
626 }
627 }
628 }
629
630 pub fn resolve_command_unobserved(
645 &mut self,
646 command: Command,
647 ) -> Result<ResolveOutcome, NifContextError> {
648 self.position_resolver_for(&command);
649 self.resolver.resolve(command).map_err(Into::into)
650 }
651
652 fn position_resolver_for(&mut self, command: &Command) {
662 if let Some(key) = command.key() {
663 self.resolver.fast_forward_to(key);
664 } else if let Command::AwaitChild { child_workflow_id } = command {
665 self.resolver
666 .fast_forward_to_child_terminal(child_workflow_id);
667 }
668 }
669}
670
671fn registry_error_to_context(error: &EngineError) -> NifContextError {
672 match error {
673 EngineError::RegistryPoisoned => NifContextError::RecorderPoisoned,
674 _ => NifContextError::TermEncoding {
675 reason: format!("registry lookup failed: {error}"),
676 },
677 }
678}
679
680fn resolve_handle_with_birth_wait(
701 registry: &Registry,
702 pid: u64,
703 birth_wait: crate::runtime::SignalDeliveryConfig,
704) -> Result<WorkflowHandle, NifContextError> {
705 let lookup = |registry: &Registry| -> Result<Option<WorkflowHandle>, NifContextError> {
706 Ok(registry
707 .list()
708 .map_err(|error| registry_error_to_context(&error))?
709 .into_iter()
710 .find(|handle| handle.pid() == pid))
711 };
712 if let Some(handle) = lookup(registry)? {
713 return Ok(handle);
714 }
715 let budget = birth_wait
716 .ready_timeout
717 .saturating_mul(birth_wait.max_enqueue_attempts.max(1));
718 let deadline = std::time::Instant::now() + budget;
719 let mut backoff = birth_wait.initial_backoff;
720 while std::time::Instant::now() < deadline {
721 std::thread::sleep(backoff);
722 let doubled = backoff.saturating_mul(2);
723 backoff = if doubled > birth_wait.max_backoff {
724 birth_wait.max_backoff
725 } else {
726 doubled
727 };
728 if let Some(handle) = lookup(registry)? {
729 return Ok(handle);
730 }
731 }
732 Err(NifContextError::UnknownProcess { pid })
733}
734
735#[cfg(test)]
736mod tests {
737 use std::sync::Arc;
738
739 use aion_core::{ActivityId, Event, EventEnvelope, Payload, WorkflowStatus};
740 use aion_package::ContentHash;
741 use aion_store::{EventStore, InMemoryStore, WriteToken};
742 use chrono::{TimeZone, Utc};
743 use serde_json::json;
744
745 use super::{NifContext, NifContextError};
746 use crate::durability::{Command, CorrelationKey, Recorder, Resolution, ResolveOutcome};
747 use crate::registry::{
748 CompletionNotifier, HandleResidency, Registry, WorkflowHandle, WorkflowHandleParts,
749 };
750
751 type TestResult = Result<(), Box<dyn std::error::Error>>;
752
753 fn hash() -> ContentHash {
754 ContentHash::from_bytes([7; 32])
755 }
756
757 fn birth_wait() -> crate::runtime::SignalDeliveryConfig {
759 crate::runtime::SignalDeliveryConfig::new(
760 std::time::Duration::from_millis(200),
761 1,
762 std::time::Duration::from_millis(2),
763 std::time::Duration::from_millis(8),
764 )
765 }
766
767 fn payload(label: &str) -> Result<Payload, Box<dyn std::error::Error>> {
768 Ok(Payload::from_json(&json!({ "label": label }))?)
769 }
770
771 fn envelope(
772 workflow_id: &aion_core::WorkflowId,
773 seq: u64,
774 ) -> Result<EventEnvelope, Box<dyn std::error::Error>> {
775 let recorded_at = Utc
776 .timestamp_opt(i64::try_from(seq)?, 0)
777 .single()
778 .ok_or_else(|| "invalid timestamp".to_owned())?;
779 Ok(EventEnvelope {
780 seq,
781 recorded_at,
782 workflow_id: workflow_id.clone(),
783 })
784 }
785
786 fn started_event(
787 workflow_id: &aion_core::WorkflowId,
788 run_id: &aion_core::RunId,
789 ) -> Result<Event, Box<dyn std::error::Error>> {
790 Ok(Event::WorkflowStarted {
791 envelope: envelope(workflow_id, 1)?,
792 workflow_type: "checkout".to_owned(),
793 input: payload("input")?,
794 run_id: run_id.clone(),
795 parent_run_id: None,
796 parent_workflow_id: None,
797 package_version: aion_core::PackageVersion::new("a".repeat(64)),
798 })
799 }
800
801 fn handle(
802 pid: u64,
803 store: Arc<dyn EventStore>,
804 workflow_id: aion_core::WorkflowId,
805 run_id: aion_core::RunId,
806 ) -> WorkflowHandle {
807 let recorder = Recorder::resume_at(workflow_id.clone(), store, 1);
808 WorkflowHandle::new(WorkflowHandleParts {
809 workflow_id,
810 run_id,
811 pid,
812 workflow_type: "checkout".to_owned(),
813 namespace: String::from("default"),
814 loaded_version: hash(),
815 cached_status: WorkflowStatus::Running,
816 residency: HandleResidency::Resident,
817 recorder,
818 completion: CompletionNotifier::new(),
819 })
820 }
821
822 type TestContext = (Registry, Arc<dyn EventStore>, WorkflowHandle);
823
824 fn context_with_history(
825 runtime: &tokio::runtime::Runtime,
826 pid: u64,
827 workflow_id: aion_core::WorkflowId,
828 history: &[Event],
829 ) -> Result<TestContext, Box<dyn std::error::Error>> {
830 let registry = Registry::default();
831 let run_id = aion_core::RunId::new_v4();
832 let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
833 let mut full_history = vec![started_event(&workflow_id, &run_id)?];
834 full_history.extend_from_slice(history);
835 runtime.block_on(store.append(WriteToken::recorder(), &workflow_id, &full_history, 0))?;
836 let recorder = Recorder::resume_at(
837 workflow_id.clone(),
838 Arc::clone(&store),
839 full_history.len() as u64,
840 );
841 let handle = WorkflowHandle::new(WorkflowHandleParts {
842 workflow_id: workflow_id.clone(),
843 run_id: run_id.clone(),
844 pid,
845 workflow_type: "checkout".to_owned(),
846 namespace: String::from("default"),
847 loaded_version: hash(),
848 cached_status: WorkflowStatus::Running,
849 residency: HandleResidency::Resident,
850 recorder,
851 completion: CompletionNotifier::new(),
852 });
853 registry.insert((workflow_id, run_id), handle.clone())?;
854 Ok((registry, store, handle))
855 }
856
857 #[test]
858 fn resolves_registered_pid_to_context() -> TestResult {
859 let runtime = tokio::runtime::Runtime::new()?;
860 let registry = Registry::default();
861 let workflow_id = aion_core::WorkflowId::new_v4();
862 let run_id = aion_core::RunId::new_v4();
863 let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
864 runtime.block_on(store.append(
865 WriteToken::recorder(),
866 &workflow_id,
867 &[started_event(&workflow_id, &run_id)?],
868 0,
869 ))?;
870 let handle = handle(44, Arc::clone(&store), workflow_id.clone(), run_id.clone());
871 registry.insert((workflow_id.clone(), run_id), handle)?;
872
873 let context = NifContext::new(44, ®istry, runtime.handle().clone(), birth_wait())?;
874
875 assert_eq!(context.workflow_id(), &workflow_id);
876 assert_eq!(context.pid(), 44);
877 Ok(())
878 }
879
880 #[test]
881 fn unknown_pid_returns_unknown_process() -> TestResult {
882 let runtime = tokio::runtime::Runtime::new()?;
883 let registry = Registry::default();
884
885 let error = NifContext::new(77, ®istry, runtime.handle().clone(), birth_wait())
886 .err()
887 .ok_or("expected unknown process error")?;
888
889 assert!(matches!(error, NifContextError::UnknownProcess { pid: 77 }));
890 Ok(())
891 }
892
893 #[test]
901 fn birth_window_registration_resolves_instead_of_failing() -> TestResult {
902 let runtime = tokio::runtime::Runtime::new()?;
903 let registry = Arc::new(Registry::default());
904 let workflow_id = aion_core::WorkflowId::new_v4();
905 let run_id = aion_core::RunId::new_v4();
906 let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
907 runtime.block_on(store.append(
908 WriteToken::recorder(),
909 &workflow_id,
910 &[started_event(&workflow_id, &run_id)?],
911 0,
912 ))?;
913 let handle = handle(91, Arc::clone(&store), workflow_id.clone(), run_id.clone());
914
915 let late_registry = Arc::clone(®istry);
918 let inserter = std::thread::spawn(move || {
919 std::thread::sleep(std::time::Duration::from_millis(30));
920 late_registry.insert((workflow_id.clone(), run_id), handle)
921 });
922
923 let context = NifContext::new(91, ®istry, runtime.handle().clone(), birth_wait())?;
924
925 assert_eq!(context.pid(), 91);
926 inserter
927 .join()
928 .map_err(|_| "registry insert thread panicked")??;
929 Ok(())
930 }
931
932 #[test]
933 fn block_on_recorder_reads_current_head_without_deadlock() -> TestResult {
934 let runtime = tokio::runtime::Runtime::new()?;
935 let registry = Registry::default();
936 let workflow_id = aion_core::WorkflowId::new_v4();
937 let run_id = aion_core::RunId::new_v4();
938 let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
939 runtime.block_on(store.append(
940 WriteToken::recorder(),
941 &workflow_id,
942 &[started_event(&workflow_id, &run_id)?],
943 0,
944 ))?;
945 let recorder = Recorder::resume_at(workflow_id.clone(), Arc::clone(&store), 5);
946 let handle = WorkflowHandle::new(WorkflowHandleParts {
947 workflow_id: workflow_id.clone(),
948 run_id: run_id.clone(),
949 pid: 55,
950 workflow_type: "checkout".to_owned(),
951 namespace: String::from("default"),
952 loaded_version: hash(),
953 cached_status: WorkflowStatus::Running,
954 residency: HandleResidency::Resident,
955 recorder,
956 completion: CompletionNotifier::new(),
957 });
958 registry.insert((workflow_id, run_id), handle)?;
959 let context = NifContext::new(55, ®istry, runtime.handle().clone(), birth_wait())?;
960
961 let head = context
962 .block_on_recorder(|recorder| Box::pin(async move { Ok(recorder.current_head()) }))?;
963
964 assert_eq!(head, 5);
965 Ok(())
966 }
967
968 #[test]
971 fn context_reads_the_recorded_start_time_task_queue() -> TestResult {
972 let runtime = tokio::runtime::Runtime::new()?;
973 let workflow_id = aion_core::WorkflowId::new_v4();
974 let history = vec![Event::SearchAttributesUpdated {
975 envelope: envelope(&workflow_id, 2)?,
976 workflow_id: workflow_id.clone(),
977 attributes: std::collections::HashMap::from([(
978 aion_core::START_TIME_TASK_QUEUE_ATTRIBUTE.to_owned(),
979 aion_core::SearchAttributeValue::String(String::from("started-on")),
980 )]),
981 }];
982 let (registry, store, _handle) = context_with_history(&runtime, 70, workflow_id, &history)?;
983 let context = NifContext::new_with_history_store(
984 70,
985 ®istry,
986 runtime.handle().clone(),
987 Some(store),
988 birth_wait(),
989 )?;
990
991 assert_eq!(
992 context.start_time_task_queue().as_deref(),
993 Some("started-on")
994 );
995 Ok(())
996 }
997
998 #[test]
1002 fn context_without_start_time_attribute_projects_none() -> TestResult {
1003 let runtime = tokio::runtime::Runtime::new()?;
1004 let workflow_id = aion_core::WorkflowId::new_v4();
1005 let (registry, store, _handle) = context_with_history(&runtime, 71, workflow_id, &[])?;
1007 let context = NifContext::new_with_history_store(
1008 71,
1009 ®istry,
1010 runtime.handle().clone(),
1011 Some(store),
1012 birth_wait(),
1013 )?;
1014
1015 assert_eq!(context.start_time_task_queue(), None);
1016 Ok(())
1017 }
1018
1019 #[test]
1020 fn resolve_command_returns_recorded_activity_resolution() -> TestResult {
1021 let runtime = tokio::runtime::Runtime::new()?;
1022 let workflow_id = aion_core::WorkflowId::new_v4();
1023 let result = payload("activity-result")?;
1024 let history = vec![
1025 Event::ActivityScheduled {
1026 envelope: envelope(&workflow_id, 2)?,
1027 activity_id: ActivityId::from_sequence_position(0),
1028 activity_type: "activity".to_owned(),
1029 input: payload("activity-input")?,
1030 task_queue: String::from("default"),
1031 node: None,
1032 },
1033 Event::ActivityCompleted {
1034 envelope: envelope(&workflow_id, 3)?,
1035 activity_id: ActivityId::from_sequence_position(0),
1036 result: result.clone(),
1037 attempt: 1,
1038 },
1039 ];
1040 let (registry, store, handle) = context_with_history(&runtime, 66, workflow_id, &history)?;
1041 let mut context = NifContext::new_with_history_store(
1042 66,
1043 ®istry,
1044 runtime.handle().clone(),
1045 Some(store),
1046 birth_wait(),
1047 )?;
1048
1049 assert_eq!(context.workflow_id(), handle.workflow_id());
1050 assert_eq!(
1051 context.resolve_command_observed(Command::RunActivity {
1052 key: CorrelationKey::Activity(0),
1053 activity_type: "activity".to_owned(),
1054 input: payload("activity-input")?,
1055 })?,
1056 ResolveOutcome::Recorded(Resolution::ActivityCompleted(result))
1057 );
1058 Ok(())
1059 }
1060
1061 fn child_history(
1062 workflow_id: &aion_core::WorkflowId,
1063 child_workflow_id: &aion_core::WorkflowId,
1064 include_terminal: bool,
1065 ) -> Result<Vec<Event>, Box<dyn std::error::Error>> {
1066 let timer_id = aion_core::TimerId::anonymous(0);
1067 let mut history = vec![
1068 Event::ActivityScheduled {
1069 envelope: envelope(workflow_id, 2)?,
1070 activity_id: ActivityId::from_sequence_position(0),
1071 activity_type: "activity".to_owned(),
1072 input: payload("activity-input")?,
1073 task_queue: String::from("default"),
1074 node: None,
1075 },
1076 Event::ActivityCompleted {
1077 envelope: envelope(workflow_id, 3)?,
1078 activity_id: ActivityId::from_sequence_position(0),
1079 result: payload("activity-result")?,
1080 attempt: 1,
1081 },
1082 Event::TimerStarted {
1083 envelope: envelope(workflow_id, 4)?,
1084 timer_id: timer_id.clone(),
1085 fire_at: Utc
1086 .timestamp_opt(99, 0)
1087 .single()
1088 .ok_or_else(|| "invalid timestamp".to_owned())?,
1089 },
1090 Event::TimerFired {
1091 envelope: envelope(workflow_id, 5)?,
1092 timer_id,
1093 },
1094 Event::ChildWorkflowStarted {
1095 envelope: envelope(workflow_id, 6)?,
1096 child_workflow_id: child_workflow_id.clone(),
1097 workflow_type: "child".to_owned(),
1098 input: payload("child-input")?,
1099 package_version: aion_core::PackageVersion::new("a".repeat(64)),
1100 },
1101 ];
1102 if include_terminal {
1103 history.push(Event::ChildWorkflowCompleted {
1104 envelope: envelope(workflow_id, 7)?,
1105 child_workflow_id: child_workflow_id.clone(),
1106 result: payload("child-result")?,
1107 });
1108 }
1109 Ok(history)
1110 }
1111
1112 #[test]
1113 fn await_child_skips_consumed_commands_to_recorded_terminal() -> TestResult {
1114 let runtime = tokio::runtime::Runtime::new()?;
1115 let workflow_id = aion_core::WorkflowId::new_v4();
1116 let child_workflow_id = aion_core::WorkflowId::new_v4();
1117 let history = child_history(&workflow_id, &child_workflow_id, true)?;
1122 let (registry, store, _handle) = context_with_history(&runtime, 88, workflow_id, &history)?;
1123 let mut context = NifContext::new_with_history_store(
1124 88,
1125 ®istry,
1126 runtime.handle().clone(),
1127 Some(store),
1128 birth_wait(),
1129 )?;
1130
1131 assert_eq!(
1132 context.resolve_command_observed(Command::AwaitChild {
1133 child_workflow_id: child_workflow_id.clone(),
1134 })?,
1135 ResolveOutcome::Recorded(Resolution::ChildCompleted(payload("child-result")?))
1136 );
1137 Ok(())
1138 }
1139
1140 #[test]
1141 fn await_child_without_recorded_terminal_resumes_live() -> TestResult {
1142 let runtime = tokio::runtime::Runtime::new()?;
1143 let workflow_id = aion_core::WorkflowId::new_v4();
1144 let child_workflow_id = aion_core::WorkflowId::new_v4();
1145 let history = child_history(&workflow_id, &child_workflow_id, false)?;
1149 let (registry, store, _handle) = context_with_history(&runtime, 89, workflow_id, &history)?;
1150 let mut context = NifContext::new_with_history_store(
1151 89,
1152 ®istry,
1153 runtime.handle().clone(),
1154 Some(store),
1155 birth_wait(),
1156 )?;
1157
1158 assert_eq!(
1159 context.resolve_command_observed(Command::AwaitChild { child_workflow_id })?,
1160 ResolveOutcome::ResumeLive
1161 );
1162 Ok(())
1163 }
1164}