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, RunAdmission,
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.admitted_recorder().await?;
347 f(&mut recorder).await
348 })
349 .map_err(Into::into)
350 }
351
352 async fn admitted_recorder(
379 &self,
380 ) -> Result<tokio::sync::MutexGuard<'_, Recorder>, DurabilityError> {
381 let recorder = self.recorder.lock().await;
382 match recorder.admit_run_append(self.handle.run_id()).await? {
383 RunAdmission::Open => Ok(recorder),
384 RunAdmission::RefusedTerminal => Err(DurabilityError::RunSuperseded {
385 workflow_id: self.handle.workflow_id().clone(),
386 run_id: self.handle.run_id().clone(),
387 }),
388 }
389 }
390
391 pub(crate) fn record_activity_scheduled_started(
397 &self,
398 recorded_at: chrono::DateTime<chrono::Utc>,
399 activity_id: ActivityId,
400 scheduled: super::nif_activity::ScheduledActivity,
401 ) -> Result<(), NifContextError> {
402 self.tokio_handle
403 .block_on(async {
404 let mut recorder = self.admitted_recorder().await?;
405 recorder
406 .record_activity_scheduled(
407 recorded_at,
408 activity_id.clone(),
409 scheduled.activity_type,
410 scheduled.input,
411 scheduled.task_queue,
414 scheduled.node,
417 )
418 .await?;
419 recorder
420 .record_activity_started(recorded_at, activity_id, scheduled.attempt)
422 .await
423 })
424 .map_err(Into::into)
425 }
426
427 pub(crate) fn record_activity_adoption_offered(
434 &self,
435 recorded_at: chrono::DateTime<chrono::Utc>,
436 activity_id: ActivityId,
437 attempt: u32,
438 ) -> Result<(), NifContextError> {
439 self.tokio_handle
440 .block_on(async {
441 let mut recorder = self.admitted_recorder().await?;
442 recorder
443 .record_activity_adoption_offered(recorded_at, activity_id, attempt)
444 .await
445 })
446 .map_err(Into::into)
447 }
448
449 pub fn record_activity_completed(
455 &self,
456 recorded_at: chrono::DateTime<chrono::Utc>,
457 activity_id: ActivityId,
458 result: Payload,
459 attempt: u32,
460 ) -> Result<(), NifContextError> {
461 self.tokio_handle
462 .block_on(async {
463 let mut recorder = self.admitted_recorder().await?;
464 recorder
465 .record_activity_completed(recorded_at, activity_id, result, attempt)
467 .await
468 })
469 .map_err(Into::into)
470 }
471
472 pub fn record_activity_failed(
482 &self,
483 recorded_at: chrono::DateTime<chrono::Utc>,
484 activity_id: ActivityId,
485 error: ActivityError,
486 attempt: u32,
487 ) -> Result<(), NifContextError> {
488 self.tokio_handle
489 .block_on(async {
490 let mut recorder = self.admitted_recorder().await?;
491 recorder
492 .record_activity_failed(recorded_at, activity_id, error, attempt)
493 .await
494 })
495 .map_err(Into::into)
496 }
497
498 pub fn record_activity_cancelled(
504 &self,
505 recorded_at: chrono::DateTime<chrono::Utc>,
506 activity_id: ActivityId,
507 attempt: u32,
508 ) -> Result<(), NifContextError> {
509 self.tokio_handle
510 .block_on(async {
511 let mut recorder = self.admitted_recorder().await?;
512 recorder
513 .record_activity_cancelled(recorded_at, activity_id, attempt)
515 .await
516 })
517 .map_err(Into::into)
518 }
519
520 pub fn record_activity_cancelled_and_settle_outbox(
526 &self,
527 recorded_at: chrono::DateTime<chrono::Utc>,
528 ordinal: u64,
529 attempt: u32,
530 ) -> Result<(), NifContextError> {
531 self.tokio_handle
532 .block_on(async {
533 let mut recorder = self.admitted_recorder().await?;
534 recorder
535 .record_activity_cancelled_and_settle_outbox(recorded_at, ordinal, attempt)
537 .await
538 })
539 .map_err(Into::into)
540 }
541
542 pub fn record_fan_out_dispatch(
548 &self,
549 recorded_at: chrono::DateTime<chrono::Utc>,
550 items: &[FanOutItem],
551 ) -> Result<(), NifContextError> {
552 self.tokio_handle
553 .block_on(async {
554 let mut recorder = self.admitted_recorder().await?;
555 recorder.record_fan_out_dispatch(recorded_at, items).await
556 })
557 .map_err(Into::into)
558 }
559
560 pub fn rearm_outbox_pending(
567 &self,
568 recorded_at: chrono::DateTime<chrono::Utc>,
569 items: &[FanOutItem],
570 ) -> Result<(), NifContextError> {
571 self.tokio_handle
572 .block_on(async {
573 let recorder = self.admitted_recorder().await?;
574 recorder.rearm_outbox_pending(recorded_at, items).await
575 })
576 .map_err(Into::into)
577 }
578
579 pub fn record_fan_out_completion(
585 &self,
586 recorded_at: chrono::DateTime<chrono::Utc>,
587 ordinal: u64,
588 outcome: FanOutOutcome,
589 ) -> Result<FanOutCompletionResult, NifContextError> {
590 self.tokio_handle
591 .block_on(async {
592 let mut recorder = self.admitted_recorder().await?;
593 recorder
594 .record_fan_out_completion(recorded_at, ordinal, None, outcome)
595 .await
596 })
597 .map_err(Into::into)
598 }
599
600 #[must_use]
602 pub fn history(&self) -> &[aion_core::Event] {
603 self.resolver.history()
604 }
605
606 #[must_use]
620 pub fn start_time_task_queue(&self) -> Option<String> {
621 aion_core::start_time_task_queue(self.history())
622 }
623
624 pub fn resolve_command_observed(
648 &mut self,
649 command: Command,
650 ) -> Result<ResolveOutcome, NifContextError> {
651 self.position_resolver_for(&command);
652 match self.resolver.resolve_with_consumed(command)? {
653 ResolvedCommand::Recorded {
654 resolution,
655 recorded_at,
656 } => {
657 self.observe_recorded_at(recorded_at);
658 Ok(ResolveOutcome::Recorded(resolution))
659 }
660 ResolvedCommand::ResumeLive { recorded_at } => {
661 if let Some(recorded_at) = recorded_at {
662 self.observe_recorded_at(recorded_at);
663 }
664 Ok(ResolveOutcome::ResumeLive)
665 }
666 }
667 }
668
669 pub fn resolve_command_unobserved(
684 &mut self,
685 command: Command,
686 ) -> Result<ResolveOutcome, NifContextError> {
687 self.position_resolver_for(&command);
688 self.resolver.resolve(command).map_err(Into::into)
689 }
690
691 fn position_resolver_for(&mut self, command: &Command) {
701 if let Some(key) = command.key() {
702 self.resolver.fast_forward_to(key);
703 } else if let Command::AwaitChild { child_workflow_id } = command {
704 self.resolver
705 .fast_forward_to_child_terminal(child_workflow_id);
706 }
707 }
708}
709
710fn registry_error_to_context(error: &EngineError) -> NifContextError {
711 match error {
712 EngineError::RegistryPoisoned => NifContextError::RecorderPoisoned,
713 _ => NifContextError::TermEncoding {
714 reason: format!("registry lookup failed: {error}"),
715 },
716 }
717}
718
719fn resolve_handle_with_birth_wait(
740 registry: &Registry,
741 pid: u64,
742 birth_wait: crate::runtime::SignalDeliveryConfig,
743) -> Result<WorkflowHandle, NifContextError> {
744 let lookup = |registry: &Registry| -> Result<Option<WorkflowHandle>, NifContextError> {
745 Ok(registry
746 .list()
747 .map_err(|error| registry_error_to_context(&error))?
748 .into_iter()
749 .find(|handle| handle.pid() == pid))
750 };
751 if let Some(handle) = lookup(registry)? {
752 return Ok(handle);
753 }
754 let budget = birth_wait
755 .ready_timeout
756 .saturating_mul(birth_wait.max_enqueue_attempts.max(1));
757 let deadline = std::time::Instant::now() + budget;
758 let mut backoff = birth_wait.initial_backoff;
759 while std::time::Instant::now() < deadline {
760 std::thread::sleep(backoff);
761 let doubled = backoff.saturating_mul(2);
762 backoff = if doubled > birth_wait.max_backoff {
763 birth_wait.max_backoff
764 } else {
765 doubled
766 };
767 if let Some(handle) = lookup(registry)? {
768 return Ok(handle);
769 }
770 }
771 Err(NifContextError::UnknownProcess { pid })
772}
773
774#[cfg(test)]
775mod tests {
776 use std::sync::Arc;
777
778 use aion_core::{ActivityId, Event, EventEnvelope, Payload, WorkflowStatus};
779 use aion_package::ContentHash;
780 use aion_store::{EventStore, InMemoryStore, WriteToken};
781 use chrono::{TimeZone, Utc};
782 use serde_json::json;
783
784 use super::{NifContext, NifContextError};
785 use crate::durability::{Command, CorrelationKey, Recorder, Resolution, ResolveOutcome};
786 use crate::registry::{
787 CompletionNotifier, HandleResidency, Registry, WorkflowHandle, WorkflowHandleParts,
788 };
789
790 type TestResult = Result<(), Box<dyn std::error::Error>>;
791
792 fn hash() -> ContentHash {
793 ContentHash::from_bytes([7; 32])
794 }
795
796 fn birth_wait() -> crate::runtime::SignalDeliveryConfig {
798 crate::runtime::SignalDeliveryConfig::new(
799 std::time::Duration::from_millis(200),
800 1,
801 std::time::Duration::from_millis(2),
802 std::time::Duration::from_millis(8),
803 )
804 }
805
806 fn payload(label: &str) -> Result<Payload, Box<dyn std::error::Error>> {
807 Ok(Payload::from_json(&json!({ "label": label }))?)
808 }
809
810 fn envelope(
811 workflow_id: &aion_core::WorkflowId,
812 seq: u64,
813 ) -> Result<EventEnvelope, Box<dyn std::error::Error>> {
814 let recorded_at = Utc
815 .timestamp_opt(i64::try_from(seq)?, 0)
816 .single()
817 .ok_or_else(|| "invalid timestamp".to_owned())?;
818 Ok(EventEnvelope {
819 seq,
820 recorded_at,
821 workflow_id: workflow_id.clone(),
822 })
823 }
824
825 fn started_event(
826 workflow_id: &aion_core::WorkflowId,
827 run_id: &aion_core::RunId,
828 ) -> Result<Event, Box<dyn std::error::Error>> {
829 Ok(Event::WorkflowStarted {
830 envelope: envelope(workflow_id, 1)?,
831 workflow_type: "checkout".to_owned(),
832 input: payload("input")?,
833 run_id: run_id.clone(),
834 parent_run_id: None,
835 parent_workflow_id: None,
836 package_version: aion_core::PackageVersion::new("a".repeat(64)),
837 })
838 }
839
840 fn handle(
841 pid: u64,
842 store: Arc<dyn EventStore>,
843 workflow_id: aion_core::WorkflowId,
844 run_id: aion_core::RunId,
845 ) -> WorkflowHandle {
846 let recorder = Recorder::resume_at(workflow_id.clone(), store, 1);
847 WorkflowHandle::new(WorkflowHandleParts {
848 workflow_id,
849 run_id,
850 pid,
851 workflow_type: "checkout".to_owned(),
852 namespace: String::from("default"),
853 loaded_version: hash(),
854 cached_status: WorkflowStatus::Running,
855 residency: HandleResidency::Resident,
856 recorder,
857 completion: CompletionNotifier::new(),
858 })
859 }
860
861 type TestContext = (Registry, Arc<dyn EventStore>, WorkflowHandle);
862
863 fn context_with_history(
864 runtime: &tokio::runtime::Runtime,
865 pid: u64,
866 workflow_id: aion_core::WorkflowId,
867 history: &[Event],
868 ) -> Result<TestContext, Box<dyn std::error::Error>> {
869 let registry = Registry::default();
870 let run_id = aion_core::RunId::new_v4();
871 let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
872 let mut full_history = vec![started_event(&workflow_id, &run_id)?];
873 full_history.extend_from_slice(history);
874 runtime.block_on(store.append(WriteToken::recorder(), &workflow_id, &full_history, 0))?;
875 let recorder = Recorder::resume_at(
876 workflow_id.clone(),
877 Arc::clone(&store),
878 full_history.len() as u64,
879 );
880 let handle = WorkflowHandle::new(WorkflowHandleParts {
881 workflow_id: workflow_id.clone(),
882 run_id: run_id.clone(),
883 pid,
884 workflow_type: "checkout".to_owned(),
885 namespace: String::from("default"),
886 loaded_version: hash(),
887 cached_status: WorkflowStatus::Running,
888 residency: HandleResidency::Resident,
889 recorder,
890 completion: CompletionNotifier::new(),
891 });
892 registry.insert((workflow_id, run_id), handle.clone())?;
893 Ok((registry, store, handle))
894 }
895
896 #[test]
897 fn resolves_registered_pid_to_context() -> TestResult {
898 let runtime = tokio::runtime::Runtime::new()?;
899 let registry = Registry::default();
900 let workflow_id = aion_core::WorkflowId::new_v4();
901 let run_id = aion_core::RunId::new_v4();
902 let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
903 runtime.block_on(store.append(
904 WriteToken::recorder(),
905 &workflow_id,
906 &[started_event(&workflow_id, &run_id)?],
907 0,
908 ))?;
909 let handle = handle(44, Arc::clone(&store), workflow_id.clone(), run_id.clone());
910 registry.insert((workflow_id.clone(), run_id), handle)?;
911
912 let context = NifContext::new(44, ®istry, runtime.handle().clone(), birth_wait())?;
913
914 assert_eq!(context.workflow_id(), &workflow_id);
915 assert_eq!(context.pid(), 44);
916 Ok(())
917 }
918
919 #[test]
920 fn unknown_pid_returns_unknown_process() -> TestResult {
921 let runtime = tokio::runtime::Runtime::new()?;
922 let registry = Registry::default();
923
924 let error = NifContext::new(77, ®istry, runtime.handle().clone(), birth_wait())
925 .err()
926 .ok_or("expected unknown process error")?;
927
928 assert!(matches!(error, NifContextError::UnknownProcess { pid: 77 }));
929 Ok(())
930 }
931
932 #[test]
940 fn birth_window_registration_resolves_instead_of_failing() -> TestResult {
941 let runtime = tokio::runtime::Runtime::new()?;
942 let registry = Arc::new(Registry::default());
943 let workflow_id = aion_core::WorkflowId::new_v4();
944 let run_id = aion_core::RunId::new_v4();
945 let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
946 runtime.block_on(store.append(
947 WriteToken::recorder(),
948 &workflow_id,
949 &[started_event(&workflow_id, &run_id)?],
950 0,
951 ))?;
952 let handle = handle(91, Arc::clone(&store), workflow_id.clone(), run_id.clone());
953
954 let late_registry = Arc::clone(®istry);
957 let inserter = std::thread::spawn(move || {
958 std::thread::sleep(std::time::Duration::from_millis(30));
959 late_registry.insert((workflow_id.clone(), run_id), handle)
960 });
961
962 let context = NifContext::new(91, ®istry, runtime.handle().clone(), birth_wait())?;
963
964 assert_eq!(context.pid(), 91);
965 inserter
966 .join()
967 .map_err(|_| "registry insert thread panicked")??;
968 Ok(())
969 }
970
971 #[test]
972 fn block_on_recorder_reads_current_head_without_deadlock() -> TestResult {
973 let runtime = tokio::runtime::Runtime::new()?;
974 let registry = Registry::default();
975 let workflow_id = aion_core::WorkflowId::new_v4();
976 let run_id = aion_core::RunId::new_v4();
977 let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
978 runtime.block_on(store.append(
979 WriteToken::recorder(),
980 &workflow_id,
981 &[started_event(&workflow_id, &run_id)?],
982 0,
983 ))?;
984 let recorder = Recorder::resume_at(workflow_id.clone(), Arc::clone(&store), 5);
985 let handle = WorkflowHandle::new(WorkflowHandleParts {
986 workflow_id: workflow_id.clone(),
987 run_id: run_id.clone(),
988 pid: 55,
989 workflow_type: "checkout".to_owned(),
990 namespace: String::from("default"),
991 loaded_version: hash(),
992 cached_status: WorkflowStatus::Running,
993 residency: HandleResidency::Resident,
994 recorder,
995 completion: CompletionNotifier::new(),
996 });
997 registry.insert((workflow_id, run_id), handle)?;
998 let context = NifContext::new(55, ®istry, runtime.handle().clone(), birth_wait())?;
999
1000 let head = context
1001 .block_on_recorder(|recorder| Box::pin(async move { Ok(recorder.current_head()) }))?;
1002
1003 assert_eq!(head, 5);
1004 Ok(())
1005 }
1006
1007 #[test]
1010 fn context_reads_the_recorded_start_time_task_queue() -> TestResult {
1011 let runtime = tokio::runtime::Runtime::new()?;
1012 let workflow_id = aion_core::WorkflowId::new_v4();
1013 let history = vec![Event::SearchAttributesUpdated {
1014 envelope: envelope(&workflow_id, 2)?,
1015 workflow_id: workflow_id.clone(),
1016 attributes: std::collections::HashMap::from([(
1017 aion_core::START_TIME_TASK_QUEUE_ATTRIBUTE.to_owned(),
1018 aion_core::SearchAttributeValue::String(String::from("started-on")),
1019 )]),
1020 }];
1021 let (registry, store, _handle) = context_with_history(&runtime, 70, workflow_id, &history)?;
1022 let context = NifContext::new_with_history_store(
1023 70,
1024 ®istry,
1025 runtime.handle().clone(),
1026 Some(store),
1027 birth_wait(),
1028 )?;
1029
1030 assert_eq!(
1031 context.start_time_task_queue().as_deref(),
1032 Some("started-on")
1033 );
1034 Ok(())
1035 }
1036
1037 #[test]
1041 fn context_without_start_time_attribute_projects_none() -> TestResult {
1042 let runtime = tokio::runtime::Runtime::new()?;
1043 let workflow_id = aion_core::WorkflowId::new_v4();
1044 let (registry, store, _handle) = context_with_history(&runtime, 71, workflow_id, &[])?;
1046 let context = NifContext::new_with_history_store(
1047 71,
1048 ®istry,
1049 runtime.handle().clone(),
1050 Some(store),
1051 birth_wait(),
1052 )?;
1053
1054 assert_eq!(context.start_time_task_queue(), None);
1055 Ok(())
1056 }
1057
1058 #[test]
1059 fn resolve_command_returns_recorded_activity_resolution() -> TestResult {
1060 let runtime = tokio::runtime::Runtime::new()?;
1061 let workflow_id = aion_core::WorkflowId::new_v4();
1062 let result = payload("activity-result")?;
1063 let history = vec![
1064 Event::ActivityScheduled {
1065 envelope: envelope(&workflow_id, 2)?,
1066 activity_id: ActivityId::from_sequence_position(0),
1067 activity_type: "activity".to_owned(),
1068 input: payload("activity-input")?,
1069 task_queue: String::from("default"),
1070 node: None,
1071 },
1072 Event::ActivityCompleted {
1073 envelope: envelope(&workflow_id, 3)?,
1074 activity_id: ActivityId::from_sequence_position(0),
1075 result: result.clone(),
1076 attempt: 1,
1077 },
1078 ];
1079 let (registry, store, handle) = context_with_history(&runtime, 66, workflow_id, &history)?;
1080 let mut context = NifContext::new_with_history_store(
1081 66,
1082 ®istry,
1083 runtime.handle().clone(),
1084 Some(store),
1085 birth_wait(),
1086 )?;
1087
1088 assert_eq!(context.workflow_id(), handle.workflow_id());
1089 assert_eq!(
1090 context.resolve_command_observed(Command::RunActivity {
1091 key: CorrelationKey::Activity(0),
1092 activity_type: "activity".to_owned(),
1093 input: payload("activity-input")?,
1094 })?,
1095 ResolveOutcome::Recorded(Resolution::ActivityCompleted(result))
1096 );
1097 Ok(())
1098 }
1099
1100 fn child_history(
1101 workflow_id: &aion_core::WorkflowId,
1102 child_workflow_id: &aion_core::WorkflowId,
1103 include_terminal: bool,
1104 ) -> Result<Vec<Event>, Box<dyn std::error::Error>> {
1105 let timer_id = aion_core::TimerId::anonymous(0);
1106 let mut history = vec![
1107 Event::ActivityScheduled {
1108 envelope: envelope(workflow_id, 2)?,
1109 activity_id: ActivityId::from_sequence_position(0),
1110 activity_type: "activity".to_owned(),
1111 input: payload("activity-input")?,
1112 task_queue: String::from("default"),
1113 node: None,
1114 },
1115 Event::ActivityCompleted {
1116 envelope: envelope(workflow_id, 3)?,
1117 activity_id: ActivityId::from_sequence_position(0),
1118 result: payload("activity-result")?,
1119 attempt: 1,
1120 },
1121 Event::TimerStarted {
1122 envelope: envelope(workflow_id, 4)?,
1123 timer_id: timer_id.clone(),
1124 fire_at: Utc
1125 .timestamp_opt(99, 0)
1126 .single()
1127 .ok_or_else(|| "invalid timestamp".to_owned())?,
1128 },
1129 Event::TimerFired {
1130 envelope: envelope(workflow_id, 5)?,
1131 timer_id,
1132 },
1133 Event::ChildWorkflowStarted {
1134 envelope: envelope(workflow_id, 6)?,
1135 child_workflow_id: child_workflow_id.clone(),
1136 workflow_type: "child".to_owned(),
1137 input: payload("child-input")?,
1138 package_version: aion_core::PackageVersion::new("a".repeat(64)),
1139 },
1140 ];
1141 if include_terminal {
1142 history.push(Event::ChildWorkflowCompleted {
1143 envelope: envelope(workflow_id, 7)?,
1144 child_workflow_id: child_workflow_id.clone(),
1145 result: payload("child-result")?,
1146 });
1147 }
1148 Ok(history)
1149 }
1150
1151 #[test]
1152 fn await_child_skips_consumed_commands_to_recorded_terminal() -> TestResult {
1153 let runtime = tokio::runtime::Runtime::new()?;
1154 let workflow_id = aion_core::WorkflowId::new_v4();
1155 let child_workflow_id = aion_core::WorkflowId::new_v4();
1156 let history = child_history(&workflow_id, &child_workflow_id, true)?;
1161 let (registry, store, _handle) = context_with_history(&runtime, 88, workflow_id, &history)?;
1162 let mut context = NifContext::new_with_history_store(
1163 88,
1164 ®istry,
1165 runtime.handle().clone(),
1166 Some(store),
1167 birth_wait(),
1168 )?;
1169
1170 assert_eq!(
1171 context.resolve_command_observed(Command::AwaitChild {
1172 child_workflow_id: child_workflow_id.clone(),
1173 })?,
1174 ResolveOutcome::Recorded(Resolution::ChildCompleted(payload("child-result")?))
1175 );
1176 Ok(())
1177 }
1178
1179 #[test]
1180 fn await_child_without_recorded_terminal_resumes_live() -> TestResult {
1181 let runtime = tokio::runtime::Runtime::new()?;
1182 let workflow_id = aion_core::WorkflowId::new_v4();
1183 let child_workflow_id = aion_core::WorkflowId::new_v4();
1184 let history = child_history(&workflow_id, &child_workflow_id, false)?;
1188 let (registry, store, _handle) = context_with_history(&runtime, 89, workflow_id, &history)?;
1189 let mut context = NifContext::new_with_history_store(
1190 89,
1191 ®istry,
1192 runtime.handle().clone(),
1193 Some(store),
1194 birth_wait(),
1195 )?;
1196
1197 assert_eq!(
1198 context.resolve_command_observed(Command::AwaitChild { child_workflow_id })?,
1199 ResolveOutcome::ResumeLive
1200 );
1201 Ok(())
1202 }
1203}