1use std::collections::HashMap;
4use std::sync::Arc;
5
6use aion_core::{
7 Event, Payload, RunId, SearchAttributeSchema, SearchAttributeValue, TimerCancelCause,
8 WorkflowError, WorkflowFilter, WorkflowId, WorkflowSummary,
9};
10use tokio::sync::Mutex as AsyncMutex;
11use tokio::task::JoinHandle;
12
13use crate::durability::Recorder;
14use crate::schedule::ScheduleEvaluator;
15use aion_store::EventStore;
16use aion_store::visibility::VisibilityStore;
17
18use crate::lifecycle::continue_as_new::{self, ContinueAsNewContext, ContinueAsNewRequest};
19use crate::lifecycle::reopen::{self, ReopenWorkflowContext};
20use crate::lifecycle::start::{self, StartWorkflowContext};
21use crate::lifecycle::terminate::{self, TerminateWorkflowContext};
22use crate::lifecycle::transition;
23use crate::registry::{TerminalOutcome, WorkflowHandle};
24use crate::{
25 EngineError, Registry, RuntimeHandle, SupervisionTree, WorkflowCatalog,
26 signal::SignalResumeHandoff,
27};
28
29use super::api_schedule::{
30 ScheduleRuntimeDeps, default_schedule_evaluator, schedule_coordinator_workflow_id,
31};
32use super::delegated::DelegatedSeams;
33use super::shutdown_gate::ShutdownGate;
34use crate::time::timer_service::live_timers_in_active_segment;
35
36pub struct Engine {
38 store: Arc<dyn EventStore>,
39 visibility_store: Arc<dyn VisibilityStore>,
40 pub(super) schedule_recorder: Arc<AsyncMutex<Recorder>>,
41 pub(super) schedule_evaluator: Arc<AsyncMutex<ScheduleEvaluator>>,
42 pub(super) schedule_coordinator_workflow_id: WorkflowId,
43 runtime: Arc<RuntimeHandle>,
44 catalog: Arc<WorkflowCatalog>,
45 registry: Arc<Registry>,
46 supervision: Arc<SupervisionTree>,
47 delegated: DelegatedSeams,
48 signal_handoff: Arc<SignalResumeHandoff>,
49 search_attribute_schema: Arc<SearchAttributeSchema>,
50 pub(super) shutdown_gate: ShutdownGate,
51 pub(super) deploy_mutations: AsyncMutex<()>,
58 visibility_reconciliation_task: Option<JoinHandle<()>>,
59 paused_runs: crate::lifecycle::PausedRuns,
64}
65
66pub(crate) struct EngineComponents {
68 pub(crate) store: Arc<dyn EventStore>,
69 pub(crate) visibility_store: Arc<dyn VisibilityStore>,
70 pub(crate) runtime: Arc<RuntimeHandle>,
71 pub(crate) catalog: Arc<WorkflowCatalog>,
72 pub(crate) registry: Arc<Registry>,
73 pub(crate) supervision: Arc<SupervisionTree>,
74 pub(crate) delegated: DelegatedSeams,
75 pub(crate) signal_handoff: Arc<SignalResumeHandoff>,
76 pub(crate) search_attribute_schema: Arc<SearchAttributeSchema>,
77 pub(crate) visibility_reconciliation_task: Option<JoinHandle<()>>,
78}
79
80impl Engine {
81 #[must_use]
83 pub(crate) fn new(components: EngineComponents) -> Self {
84 let EngineComponents {
85 store,
86 visibility_store,
87 runtime,
88 catalog,
89 registry,
90 supervision,
91 delegated,
92 signal_handoff,
93 search_attribute_schema,
94 visibility_reconciliation_task,
95 } = components;
96 let schedule_coordinator_workflow_id = schedule_coordinator_workflow_id();
97 let schedule_recorder = Arc::new(AsyncMutex::new(Recorder::new(
98 schedule_coordinator_workflow_id.clone(),
99 Arc::clone(&store),
100 )));
101 let runtime_arc = runtime;
102 let registry_arc = registry;
103 let supervision_arc = supervision;
104 let schedule_evaluator = Arc::new(AsyncMutex::new(default_schedule_evaluator(
105 schedule_coordinator_workflow_id.clone(),
106 Arc::clone(&schedule_recorder),
107 ScheduleRuntimeDeps {
108 store: Arc::clone(&store),
109 visibility_store: Arc::clone(&visibility_store),
110 runtime: Arc::clone(&runtime_arc),
111 catalog: Arc::clone(&catalog),
112 registry: Arc::clone(®istry_arc),
113 supervision: Arc::clone(&supervision_arc),
114 search_attribute_schema: Arc::clone(&search_attribute_schema),
115 },
116 )));
117 Self {
118 store,
119 visibility_store,
120 schedule_recorder,
121 schedule_evaluator,
122 schedule_coordinator_workflow_id,
123 runtime: runtime_arc,
124 catalog,
125 registry: registry_arc,
126 supervision: supervision_arc,
127 delegated,
128 signal_handoff,
129 search_attribute_schema,
130 shutdown_gate: ShutdownGate::default(),
131 deploy_mutations: AsyncMutex::new(()),
132 visibility_reconciliation_task,
133 paused_runs: crate::lifecycle::PausedRuns::default(),
134 }
135 }
136
137 pub(crate) async fn catchup_schedule_coordinator(&self) -> Result<(), EngineError> {
145 let history = self
146 .store
147 .read_history(&self.schedule_coordinator_workflow_id)
148 .await?;
149 let head = u64::try_from(history.len()).unwrap_or(u64::MAX);
150 if head > 0 {
151 let mut recorder = self.schedule_recorder.lock().await;
152 *recorder = Recorder::resume_at(
153 self.schedule_coordinator_workflow_id.clone(),
154 Arc::clone(&self.store),
155 head,
156 );
157 }
158 Ok(())
159 }
160
161 #[must_use]
163 pub fn store(&self) -> Arc<dyn EventStore> {
164 Arc::clone(&self.store)
165 }
166
167 #[must_use]
169 pub fn visibility_store(&self) -> Arc<dyn VisibilityStore> {
170 Arc::clone(&self.visibility_store)
171 }
172
173 #[must_use]
175 pub fn runtime(&self) -> &RuntimeHandle {
176 &self.runtime
177 }
178
179 #[must_use]
181 pub fn workflow_catalog(&self) -> &Arc<WorkflowCatalog> {
182 &self.catalog
183 }
184
185 #[must_use]
187 pub fn registry(&self) -> &Registry {
188 &self.registry
189 }
190
191 #[must_use]
193 pub fn supervision(&self) -> &SupervisionTree {
194 &self.supervision
195 }
196
197 #[must_use]
199 pub const fn delegated(&self) -> &DelegatedSeams {
200 &self.delegated
201 }
202
203 #[must_use]
205 pub fn signal_handoff(&self) -> Arc<SignalResumeHandoff> {
206 Arc::clone(&self.signal_handoff)
207 }
208
209 pub async fn start_workflow(
223 &self,
224 workflow_type: &str,
225 input: Payload,
226 search_attributes: HashMap<String, SearchAttributeValue>,
227 namespace: String,
228 ) -> Result<WorkflowHandle, EngineError> {
229 self.start_workflow_with_id(
230 workflow_type,
231 input,
232 search_attributes,
233 namespace,
234 None,
235 None,
236 )
237 .await
238 }
239
240 pub async fn start_workflow_with_id(
262 &self,
263 workflow_type: &str,
264 input: Payload,
265 search_attributes: HashMap<String, SearchAttributeValue>,
266 namespace: String,
267 workflow_id: Option<WorkflowId>,
268 routing_key: Option<String>,
269 ) -> Result<WorkflowHandle, EngineError> {
270 let operation = self.shutdown_gate.begin_start()?;
271 let result = start::start_workflow_with_options(
272 StartWorkflowContext {
273 store: self.store(),
274 visibility_store: self.visibility_store(),
275 catalog: Arc::clone(&self.catalog),
276 runtime: Arc::clone(&self.runtime),
277 supervision: Arc::clone(&self.supervision),
278 registry: Arc::clone(&self.registry),
279 signal_handoff: Some(self.signal_handoff()),
280 search_attribute_schema: Arc::clone(&self.search_attribute_schema),
281 monitor_tokio_handle: tokio::runtime::Handle::current(),
282 },
283 workflow_type,
284 input,
285 start::StartWorkflowOptions {
286 namespace: Some(namespace),
287 search_attributes,
288 workflow_id,
289 routing_key,
290 ..start::StartWorkflowOptions::default()
291 },
292 )
293 .await;
294 drop(operation);
295 result
296 }
297
298 pub async fn adopt_shards(&self, shards: &[usize]) -> Result<(), EngineError> {
340 let operation = self.shutdown_gate.begin_start()?;
341 let result = self.adopt_shards_inner(shards).await;
342 drop(operation);
343 result
344 }
345
346 async fn adopt_shards_inner(&self, shards: &[usize]) -> Result<(), EngineError> {
362 let _recoverable = super::fence::plan_adopted_shards(
374 &super::fence::StoreFenceSeam {
375 store: &*self.store,
376 },
377 shards,
378 )?;
379 match self.store.list_paused().await {
388 Ok(paused) => self.paused_runs.extend(paused),
389 Err(error) => {
390 tracing::warn!(%error, "failed to rebuild paused-runs dispatch hold at shard adoption");
391 }
392 }
393 super::startup::recover_adopted_shards(super::startup::StartupRecoveryContext {
398 store: Arc::clone(&self.store),
399 visibility_store: Arc::clone(&self.visibility_store),
400 runtime: Arc::clone(&self.runtime),
401 catalog: Arc::clone(&self.catalog),
402 registry: Arc::clone(&self.registry),
403 supervision: Arc::clone(&self.supervision),
404 recovery: None,
405 search_attribute_schema: Arc::clone(&self.search_attribute_schema),
406 bootstrap_schedule_coordinator: false,
407 })
408 .await?;
409 super::startup::recover_timers_on_startup(self.runtime.nif_state(), Arc::clone(&self.store))
424 .await
425 }
426
427 pub fn resume_workflow(
435 &self,
436 id: &WorkflowId,
437 run: &RunId,
438 ) -> Result<WorkflowHandle, EngineError> {
439 let handle = transition::resume(self.registry(), id, run)?;
440 if let Err(error) = self.signal_handoff.deliver_deferred(self, id) {
441 tracing::warn!(
442 workflow_id = %id,
443 run_id = %run,
444 error = %error,
445 "failed to flush deferred signals after workflow resume"
446 );
447 }
448 Ok(handle)
449 }
450
451 pub async fn cancel(
459 &self,
460 id: &WorkflowId,
461 run: &RunId,
462 reason: impl Into<String>,
463 ) -> Result<(), EngineError> {
464 let operation = self.shutdown_gate.begin_operation()?;
465 self.cancel_inflight_timers(id).await;
470 let result = terminate::cancel(
471 TerminateWorkflowContext {
472 runtime: &self.runtime,
473 store: self.store(),
474 visibility_store: self.visibility_store(),
475 registry: &self.registry,
476 },
477 id,
478 run,
479 reason,
480 )
481 .await;
482 if result.is_ok() {
486 self.paused_runs.remove(id);
487 }
488 drop(operation);
489 result
490 }
491
492 async fn cancel_inflight_timers(&self, id: &WorkflowId) {
518 let timer_service = match crate::runtime::nif_timer_bridge::installed_timer_service(
519 self.runtime.nif_state(),
520 ) {
521 Ok(service) => service,
522 Err(error) => {
523 tracing::warn!(
524 %error,
525 workflow_id = %id,
526 "timer service unavailable during cancel; any in-flight timers will be skipped by recovery"
527 );
528 return;
529 }
530 };
531 let history = match self.store.read_history(id).await {
532 Ok(history) => history,
533 Err(error) => {
534 tracing::warn!(
535 %error,
536 workflow_id = %id,
537 "could not read history for timer cleanup during cancel; any in-flight timers will be skipped by recovery"
538 );
539 return;
540 }
541 };
542 for timer_id in live_timers_in_active_segment(&history) {
543 let cause = if crate::time::is_deadline_timer(&timer_id) {
549 TimerCancelCause::WorkflowIntent
550 } else {
551 TimerCancelCause::CancelTeardown
552 };
553 if let Err(error) = timer_service
554 .cancel(id.clone(), timer_id.clone(), cause)
555 .await
556 {
557 tracing::warn!(
558 %error,
559 workflow_id = %id,
560 %timer_id,
561 "failed to cancel in-flight timer during workflow cancel; recovery will skip it if orphaned"
562 );
563 }
564 }
565 }
566
567 pub async fn continue_as_new(
575 &self,
576 id: &WorkflowId,
577 run: &RunId,
578 input: Payload,
579 workflow_type: Option<String>,
580 ) -> Result<WorkflowHandle, EngineError> {
581 let operation = self.shutdown_gate.begin_operation()?;
582 let result = continue_as_new::continue_as_new(
583 ContinueAsNewContext {
584 store: self.store(),
585 visibility_store: Arc::clone(&self.visibility_store),
586 catalog: Arc::clone(&self.catalog),
587 runtime: &self.runtime,
588 supervision: Arc::clone(&self.supervision),
589 registry: &self.registry,
590 search_attribute_schema: Arc::clone(&self.search_attribute_schema),
591 },
592 id,
593 run,
594 ContinueAsNewRequest {
595 input,
596 workflow_type,
597 },
598 )
599 .await;
600 drop(operation);
601 result
602 }
603
604 pub async fn reopen_workflow(
620 &self,
621 id: &WorkflowId,
622 run: &RunId,
623 ) -> Result<WorkflowHandle, EngineError> {
624 let operation = self.shutdown_gate.begin_operation()?;
625 let result = reopen::reopen(
626 ReopenWorkflowContext {
627 store: self.store(),
628 visibility_store: Arc::clone(&self.visibility_store),
629 catalog: Arc::clone(&self.catalog),
630 runtime: &self.runtime,
631 supervision: Arc::clone(&self.supervision),
632 registry: &self.registry,
633 search_attribute_schema: Arc::clone(&self.search_attribute_schema),
634 },
635 id,
636 run,
637 )
638 .await;
639 drop(operation);
640 result
641 }
642
643 #[must_use]
649 pub fn paused_runs(&self) -> crate::lifecycle::PausedRuns {
650 self.paused_runs.clone()
651 }
652
653 pub async fn rebuild_paused_runs(&self) -> Result<(), EngineError> {
662 let paused = self.store.list_paused().await?;
663 self.paused_runs.replace_all(paused);
664 Ok(())
665 }
666
667 fn pause_context(&self) -> crate::lifecycle::PauseWorkflowContext<'_> {
668 crate::lifecycle::PauseWorkflowContext {
669 store: self.store(),
670 visibility_store: Arc::clone(&self.visibility_store),
671 catalog: Arc::clone(&self.catalog),
672 runtime: &self.runtime,
673 supervision: Arc::clone(&self.supervision),
674 registry: &self.registry,
675 search_attribute_schema: Arc::clone(&self.search_attribute_schema),
676 paused_runs: self.paused_runs.clone(),
677 }
678 }
679
680 pub async fn pause_workflow(
693 &self,
694 id: &WorkflowId,
695 run: &RunId,
696 reason: Option<String>,
697 operator: Option<String>,
698 ) -> Result<WorkflowHandle, EngineError> {
699 let operation = self.shutdown_gate.begin_operation()?;
700 let result =
701 crate::lifecycle::pause::pause(&self.pause_context(), id, run, reason, operator).await;
702 drop(operation);
703 result
704 }
705
706 pub async fn resume_paused_workflow(
722 &self,
723 id: &WorkflowId,
724 run: &RunId,
725 operator: Option<String>,
726 ) -> Result<WorkflowHandle, EngineError> {
727 let operation = self.shutdown_gate.begin_operation()?;
728 let result =
729 crate::lifecycle::pause::resume(&self.pause_context(), id, run, operator).await;
730 drop(operation);
731 result
732 }
733
734 pub async fn result(
745 &self,
746 id: &WorkflowId,
747 run: &RunId,
748 ) -> Result<Result<Payload, WorkflowError>, EngineError> {
749 let history = self.store.read_history(id).await?;
750 if let Some(outcome) = terminal_outcome_from_history(&history) {
751 return Ok(outcome_to_result(outcome));
752 }
753
754 let handle = match self.registry.get(id, run)? {
755 Some(handle) => handle,
756 None => self
760 .handle_after_birth_window(id, run, &history)
761 .await?
762 .ok_or_else(|| workflow_not_found(id, run))?,
763 };
764 let mut receiver = handle.completion().subscribe();
765 loop {
766 if let Some(outcome) = receiver.borrow().clone() {
767 return Ok(outcome_to_result(outcome));
768 }
769 if receiver.changed().await.is_err() {
770 if let Some(outcome) =
771 terminal_outcome_from_history(&self.store.read_history(id).await?)
772 {
773 return Ok(outcome_to_result(outcome));
774 }
775 return Err(EngineError::Runtime {
776 reason: format!(
777 "completion channel closed before workflow `{id}/{run}` finished"
778 ),
779 });
780 }
781 }
782 }
783
784 pub async fn list_workflows(
793 &self,
794 filter: WorkflowFilter,
795 ) -> Result<Vec<WorkflowSummary>, EngineError> {
796 let mut summaries = self
797 .store
798 .query(&filter)
799 .await?
800 .into_iter()
801 .map(|summary| (summary.workflow_id.clone(), summary))
802 .collect::<HashMap<_, _>>();
803
804 for handle in self.registry.list()? {
805 let history = self.store.read_history(handle.workflow_id()).await?;
806 self.registry
807 .reconcile(handle.workflow_id(), handle.run_id(), &history)?;
808 if let Some(summary) = WorkflowSummary::from_history(&history) {
809 if filter.matches(&summary) {
810 summaries.insert(summary.workflow_id.clone(), summary);
811 }
812 }
813 }
814
815 let mut summaries = summaries.into_values().collect::<Vec<_>>();
816 summaries.sort_by(|left, right| {
817 left.started_at.cmp(&right.started_at).then_with(|| {
818 left.workflow_id
819 .to_string()
820 .cmp(&right.workflow_id.to_string())
821 })
822 });
823 Ok(summaries)
824 }
825
826 pub fn shutdown(&self) -> Result<(), EngineError> {
832 if let Some(task) = &self.visibility_reconciliation_task {
833 task.abort();
834 }
835 self.shutdown_gate.close_and_wait()?;
836 self.runtime.shutdown()?;
844 self.runtime.nif_state().shutdown_child_tasks();
845 self.runtime.nif_state().shutdown_timer_wheel();
853 self.runtime.nif_state().clear_engine_seams();
862 Ok(())
863 }
864}
865
866pub(crate) fn terminal_outcome_from_history(events: &[Event]) -> Option<TerminalOutcome> {
867 match aion_core::current_lease_terminal(events)? {
871 Event::WorkflowCompleted { result, .. } => Some(TerminalOutcome::Completed(result.clone())),
872 Event::WorkflowFailed { error, .. } => Some(TerminalOutcome::Failed(error.clone())),
873 Event::WorkflowCancelled { reason, .. } => Some(TerminalOutcome::Cancelled(reason.clone())),
874 Event::WorkflowTimedOut { timeout, .. } => Some(TerminalOutcome::TimedOut(timeout.clone())),
875 Event::WorkflowContinuedAsNew {
876 input,
877 workflow_type,
878 parent_run_id,
879 ..
880 } => Some(TerminalOutcome::ContinuedAsNew {
881 input: input.clone(),
882 workflow_type: workflow_type.clone(),
883 parent_run_id: parent_run_id.clone(),
884 }),
885 _ => None,
886 }
887}
888
889fn outcome_to_result(outcome: TerminalOutcome) -> Result<Payload, WorkflowError> {
890 match outcome {
891 TerminalOutcome::Completed(payload) => Ok(payload),
892 TerminalOutcome::Failed(error) => Err(error),
893 TerminalOutcome::Cancelled(reason) => Err(WorkflowError {
894 message: format!("workflow cancelled: {reason}"),
895 details: None,
896 }),
897 TerminalOutcome::TimedOut(timeout) => Err(WorkflowError {
898 message: format!("workflow timed out: {timeout}"),
899 details: None,
900 }),
901 TerminalOutcome::ContinuedAsNew { parent_run_id, .. } => Err(WorkflowError {
902 message: format!("workflow continued as new from run {parent_run_id}"),
903 details: None,
904 }),
905 }
906}
907
908pub(crate) fn workflow_not_found(id: &WorkflowId, run: &RunId) -> EngineError {
909 EngineError::WorkflowNotFound {
910 workflow_type: format!("{id}/{run}"),
911 }
912}
913
914#[cfg(test)]
915mod tests {
916 use std::collections::HashMap;
917 use std::sync::Arc;
918 use std::time::Duration;
919
920 use aion_core::{
921 Event, EventEnvelope, PackageVersion, Payload, RunId, SearchAttributeSchema,
922 TimerCancelCause, TimerId, WorkflowFilter, WorkflowId, WorkflowStatus,
923 };
924 use aion_package::ContentHash;
925 use aion_store::visibility::VisibilityStore;
926 use aion_store::{EventStore, InMemoryStore, ReadableEventStore};
927 use serde_json::json;
928
929 use super::{DelegatedSeams, Engine, EngineComponents, live_timers_in_active_segment};
930 use crate::durability::Recorder;
931 use crate::lifecycle::terminate::{self, TerminateWorkflowContext};
932 use crate::registry::{CompletionNotifier, HandleResidency, WorkflowHandleParts};
933 use crate::time::TimerRecovery;
934 use crate::{
935 EngineError, Registry, RuntimeConfig, RuntimeHandle, SupervisionTree, WorkflowCatalog,
936 WorkflowHandle,
937 };
938
939 fn payload(label: &str) -> Result<Payload, aion_core::PayloadError> {
940 Payload::from_json(&json!({ "label": label }))
941 }
942
943 fn workflow_error(message: &str) -> aion_core::WorkflowError {
944 aion_core::WorkflowError {
945 message: message.to_owned(),
946 details: None,
947 }
948 }
949
950 fn workflow_catalog(workflow_type: &str, deployed_module: &str) -> Arc<WorkflowCatalog> {
951 let catalog = Arc::new(WorkflowCatalog::new());
952 catalog.note_loaded_workflow_for_test(
953 workflow_type,
954 deployed_module,
955 "run",
956 ContentHash::from_bytes([5; 32]),
957 );
958 catalog
959 }
960
961 fn engine_with_loaded_workflow(
962 store: Arc<dyn EventStore>,
963 workflow_type: &str,
964 deployed_module: &str,
965 ) -> Result<Engine, EngineError> {
966 let runtime = RuntimeHandle::new(RuntimeConfig::new(Some(1)))?;
967 runtime.register_waiting_test_module(deployed_module, "run");
968 let visibility_store: Arc<dyn VisibilityStore> = Arc::new(InMemoryStore::default());
969 Ok(Engine::new(EngineComponents {
970 store,
971 visibility_store,
972 runtime: Arc::new(runtime),
973 catalog: workflow_catalog(workflow_type, deployed_module),
974 registry: Arc::new(Registry::default()),
975 supervision: Arc::new(SupervisionTree::new()),
976 delegated: DelegatedSeams::default(),
977 signal_handoff: Arc::new(crate::signal::SignalResumeHandoff::new()),
978 search_attribute_schema: Arc::new(SearchAttributeSchema::new()),
979 visibility_reconciliation_task: None,
980 }))
981 }
982
983 fn termination_context(engine: &Engine) -> TerminateWorkflowContext<'_> {
984 TerminateWorkflowContext {
985 runtime: engine.runtime(),
986 store: engine.store(),
987 visibility_store: engine.visibility_store(),
988 registry: engine.registry(),
989 }
990 }
991
992 async fn insert_active_handle(
993 engine: &Engine,
994 store: Arc<dyn EventStore>,
995 workflow_type: &str,
996 ) -> Result<WorkflowHandle, Box<dyn std::error::Error>> {
997 let workflow_id = aion_core::WorkflowId::new_v4();
998 let run_id = aion_core::RunId::new_v4();
999 let mut recorder = Recorder::new(workflow_id.clone(), store);
1000 recorder
1001 .record_workflow_started(
1002 chrono::Utc::now(),
1003 crate::durability::WorkflowStartRecord {
1004 workflow_type: workflow_type.to_owned(),
1005 input: payload("input")?,
1006 run_id: run_id.clone(),
1007 parent_run_id: None,
1008 package_version: aion_core::PackageVersion::new("a".repeat(64)),
1009 },
1010 )
1011 .await?;
1012 let pid = engine.runtime().spawn_test_process_with_trap_exit(true)?;
1013 let handle = WorkflowHandle::new(WorkflowHandleParts {
1014 workflow_id: workflow_id.clone(),
1015 run_id: run_id.clone(),
1016 pid,
1017 workflow_type: workflow_type.to_owned(),
1018 namespace: String::from("default"),
1019 loaded_version: ContentHash::from_bytes([9; 32]),
1020 cached_status: WorkflowStatus::Running,
1021 residency: HandleResidency::Resident,
1022 recorder,
1023 completion: CompletionNotifier::new(),
1024 });
1025 engine
1026 .registry()
1027 .insert((workflow_id, run_id), handle.clone())?;
1028 Ok(handle)
1029 }
1030
1031 #[tokio::test]
1032 async fn start_then_cancel_records_started_then_cancelled()
1033 -> Result<(), Box<dyn std::error::Error>> {
1034 let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
1035 let engine =
1036 engine_with_loaded_workflow(Arc::clone(&store), "checkout", "checkout_deployed")?;
1037 let handle = engine
1038 .start_workflow(
1039 "checkout",
1040 payload("input")?,
1041 HashMap::new(),
1042 String::from("default"),
1043 )
1044 .await?;
1045
1046 engine
1047 .cancel(
1048 handle.workflow_id(),
1049 handle.run_id(),
1050 "caller requested cancellation",
1051 )
1052 .await?;
1053
1054 let history = store.read_history(handle.workflow_id()).await?;
1055 match history.as_slice() {
1056 [
1057 Event::WorkflowStarted { .. },
1058 Event::WorkflowCancelled { reason, .. },
1059 ] => {
1060 assert_eq!(reason, "caller requested cancellation");
1061 }
1062 other => return Err(format!("expected started then cancelled, found {other:?}").into()),
1063 }
1064 engine.shutdown()?;
1065 Ok(())
1066 }
1067
1068 fn test_envelope(workflow_id: &WorkflowId, seq: u64) -> EventEnvelope {
1069 EventEnvelope {
1070 seq,
1071 recorded_at: chrono::DateTime::from_timestamp(1_700_000_000, 0).unwrap_or_default(),
1072 workflow_id: workflow_id.clone(),
1073 }
1074 }
1075
1076 fn started_event(workflow_id: &WorkflowId, seq: u64) -> Event {
1077 Event::WorkflowStarted {
1078 envelope: test_envelope(workflow_id, seq),
1079 workflow_type: String::from("checkout"),
1080 input: Payload::new(aion_core::ContentType::Json, b"{}".to_vec()),
1081 run_id: RunId::new_v4(),
1082 parent_run_id: None,
1083 package_version: PackageVersion::new("a".repeat(64)),
1084 }
1085 }
1086
1087 fn timer_started_event(workflow_id: &WorkflowId, seq: u64, timer_id: &TimerId) -> Event {
1088 Event::TimerStarted {
1089 envelope: test_envelope(workflow_id, seq),
1090 timer_id: timer_id.clone(),
1091 fire_at: chrono::DateTime::from_timestamp(1_700_000_500, 0).unwrap_or_default(),
1092 }
1093 }
1094
1095 fn timer_fired_event(workflow_id: &WorkflowId, seq: u64, timer_id: &TimerId) -> Event {
1096 Event::TimerFired {
1097 envelope: test_envelope(workflow_id, seq),
1098 timer_id: timer_id.clone(),
1099 }
1100 }
1101
1102 fn timer_cancelled_event(workflow_id: &WorkflowId, seq: u64, timer_id: &TimerId) -> Event {
1103 Event::TimerCancelled {
1104 envelope: test_envelope(workflow_id, seq),
1105 timer_id: timer_id.clone(),
1106 cause: TimerCancelCause::WorkflowIntent,
1107 }
1108 }
1109
1110 #[test]
1111 fn live_timers_lists_started_and_unterminated() {
1112 let workflow_id = WorkflowId::new_v4();
1113 let first = TimerId::anonymous(0);
1114 let second = TimerId::anonymous(1);
1115 let history = vec![
1116 started_event(&workflow_id, 0),
1117 timer_started_event(&workflow_id, 1, &first),
1118 timer_started_event(&workflow_id, 2, &second),
1119 ];
1120 assert_eq!(
1121 live_timers_in_active_segment(&history),
1122 vec![first, second],
1123 "both started, unterminated timers should be live, in start order"
1124 );
1125 }
1126
1127 #[test]
1128 fn live_timers_excludes_fired_and_cancelled() {
1129 let workflow_id = WorkflowId::new_v4();
1130 let fired = TimerId::anonymous(0);
1131 let cancelled = TimerId::anonymous(1);
1132 let live = TimerId::anonymous(2);
1133 let history = vec![
1134 started_event(&workflow_id, 0),
1135 timer_started_event(&workflow_id, 1, &fired),
1136 timer_started_event(&workflow_id, 2, &cancelled),
1137 timer_started_event(&workflow_id, 3, &live),
1138 timer_fired_event(&workflow_id, 4, &fired),
1139 timer_cancelled_event(&workflow_id, 5, &cancelled),
1140 ];
1141 assert_eq!(
1142 live_timers_in_active_segment(&history),
1143 vec![live],
1144 "only the timer with no terminal event remains live"
1145 );
1146 }
1147
1148 #[test]
1149 fn live_timers_dedups_repeated_start() {
1150 let workflow_id = WorkflowId::new_v4();
1151 let timer = TimerId::anonymous(0);
1152 let history = vec![
1153 started_event(&workflow_id, 0),
1154 timer_started_event(&workflow_id, 1, &timer),
1155 timer_started_event(&workflow_id, 2, &timer),
1156 ];
1157 assert_eq!(live_timers_in_active_segment(&history), vec![timer]);
1158 }
1159
1160 #[test]
1161 fn live_timers_scopes_to_active_run_segment() {
1162 let workflow_id = WorkflowId::new_v4();
1165 let prior_run = TimerId::anonymous(0);
1166 let current_run = TimerId::anonymous(0);
1167 let history = vec![
1168 started_event(&workflow_id, 0),
1169 timer_started_event(&workflow_id, 1, &prior_run),
1170 started_event(&workflow_id, 2),
1171 timer_started_event(&workflow_id, 3, ¤t_run),
1172 ];
1173 assert_eq!(
1174 live_timers_in_active_segment(&history),
1175 vec![current_run],
1176 "only timers from the latest WorkflowStarted segment are live"
1177 );
1178 }
1179
1180 #[test]
1181 fn live_timers_empty_history_is_empty() {
1182 assert!(live_timers_in_active_segment(&[]).is_empty());
1183 }
1184
1185 fn engine_with_timer_bridge(
1190 store: Arc<dyn EventStore>,
1191 registry: Arc<Registry>,
1192 ) -> Result<Engine, EngineError> {
1193 let runtime = RuntimeHandle::new(RuntimeConfig::new(Some(1)))?;
1194 runtime.register_waiting_test_module("checkout_deployed", "run");
1195 crate::runtime::nif_timer_bridge::install_timer_nif_bridge(
1196 runtime.nif_state(),
1197 Arc::clone(®istry),
1198 Arc::clone(&store),
1199 tokio::runtime::Handle::current(),
1200 crate::runtime::SignalDeliveryConfig::default(),
1201 );
1202 let visibility_store: Arc<dyn VisibilityStore> = Arc::new(InMemoryStore::default());
1203 Ok(Engine::new(EngineComponents {
1204 store,
1205 visibility_store,
1206 runtime: Arc::new(runtime),
1207 catalog: workflow_catalog("checkout", "checkout_deployed"),
1208 registry,
1209 supervision: Arc::new(SupervisionTree::new()),
1210 delegated: DelegatedSeams::default(),
1211 signal_handoff: Arc::new(crate::signal::SignalResumeHandoff::new()),
1212 search_attribute_schema: Arc::new(SearchAttributeSchema::new()),
1213 visibility_reconciliation_task: None,
1214 }))
1215 }
1216
1217 #[tokio::test(flavor = "multi_thread")]
1223 async fn cancel_records_timer_cancelled_before_workflow_cancelled()
1224 -> Result<(), Box<dyn std::error::Error>> {
1225 let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
1226 let registry = Arc::new(Registry::default());
1227 let engine = engine_with_timer_bridge(Arc::clone(&store), Arc::clone(®istry))?;
1228
1229 let handle = engine
1230 .start_workflow(
1231 "checkout",
1232 payload("input")?,
1233 HashMap::new(),
1234 String::from("default"),
1235 )
1236 .await?;
1237
1238 let timer_id = TimerId::anonymous(0);
1241 let fire_at = chrono::Utc::now() + chrono::Duration::hours(1);
1242 handle
1243 .recorder()
1244 .lock()
1245 .await
1246 .record_timer_started(chrono::Utc::now(), timer_id.clone(), fire_at)
1247 .await?;
1248 let timer_service =
1249 crate::runtime::nif_timer_bridge::installed_timer_service(engine.runtime().nif_state())
1250 .map_err(|error| format!("timer service unavailable: {error}"))?;
1251 timer_service
1252 .schedule(handle.workflow_id().clone(), timer_id.clone(), fire_at)
1253 .await?;
1254
1255 engine
1256 .cancel(
1257 handle.workflow_id(),
1258 handle.run_id(),
1259 "caller requested cancellation",
1260 )
1261 .await?;
1262
1263 let history = store.read_history(handle.workflow_id()).await?;
1264 match history.as_slice() {
1265 [
1266 Event::WorkflowStarted { .. },
1267 Event::TimerStarted {
1268 timer_id: started, ..
1269 },
1270 Event::TimerCancelled {
1271 timer_id: cancelled,
1272 ..
1273 },
1274 Event::WorkflowCancelled { reason, .. },
1275 ] => {
1276 assert_eq!(started, &timer_id);
1277 assert_eq!(cancelled, &timer_id, "the live timer must be cancelled");
1278 assert_eq!(reason, "caller requested cancellation");
1279 }
1280 other => {
1281 return Err(format!(
1282 "expected [started, timer-started, timer-cancelled, cancelled], found {other:?}"
1283 )
1284 .into());
1285 }
1286 }
1287 engine.shutdown()?;
1288 Ok(())
1289 }
1290
1291 #[tokio::test(flavor = "multi_thread")]
1294 async fn cancel_cancels_multiple_live_timers() -> Result<(), Box<dyn std::error::Error>> {
1295 let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
1296 let registry = Arc::new(Registry::default());
1297 let engine = engine_with_timer_bridge(Arc::clone(&store), Arc::clone(®istry))?;
1298 let handle = engine
1299 .start_workflow(
1300 "checkout",
1301 payload("input")?,
1302 HashMap::new(),
1303 String::from("default"),
1304 )
1305 .await?;
1306
1307 let first = TimerId::anonymous(0);
1308 let second = TimerId::anonymous(1);
1309 let fire_at = chrono::Utc::now() + chrono::Duration::hours(1);
1310 {
1311 let recorder = handle.recorder();
1312 let mut recorder = recorder.lock().await;
1313 recorder
1314 .record_timer_started(chrono::Utc::now(), first.clone(), fire_at)
1315 .await?;
1316 recorder
1317 .record_timer_started(chrono::Utc::now(), second.clone(), fire_at)
1318 .await?;
1319 }
1320
1321 engine
1322 .cancel(handle.workflow_id(), handle.run_id(), "stop")
1323 .await?;
1324
1325 let history = store.read_history(handle.workflow_id()).await?;
1326 match history.as_slice() {
1327 [
1328 Event::WorkflowStarted { .. },
1329 Event::TimerStarted {
1330 timer_id: started_first,
1331 ..
1332 },
1333 Event::TimerStarted {
1334 timer_id: started_second,
1335 ..
1336 },
1337 Event::TimerCancelled {
1338 timer_id: cancelled_first,
1339 ..
1340 },
1341 Event::TimerCancelled {
1342 timer_id: cancelled_second,
1343 ..
1344 },
1345 Event::WorkflowCancelled { .. },
1346 ] => {
1347 assert_eq!(started_first, &first);
1348 assert_eq!(started_second, &second);
1349 assert_eq!(cancelled_first, &first, "first live timer cancelled first");
1350 assert_eq!(
1351 cancelled_second, &second,
1352 "second live timer cancelled second"
1353 );
1354 }
1355 other => {
1356 return Err(format!(
1357 "expected two timer-cancels before workflow-cancel, found {other:?}"
1358 )
1359 .into());
1360 }
1361 }
1362 engine.shutdown()?;
1363 Ok(())
1364 }
1365
1366 #[tokio::test(flavor = "multi_thread")]
1373 async fn cancelled_workflow_leaves_no_orphan_for_recovery()
1374 -> Result<(), Box<dyn std::error::Error>> {
1375 let concrete: Arc<InMemoryStore> = Arc::new(InMemoryStore::default());
1376 let store: Arc<dyn EventStore> = concrete.clone();
1377 let registry = Arc::new(Registry::default());
1378 let engine = engine_with_timer_bridge(Arc::clone(&store), Arc::clone(®istry))?;
1379 let handle = engine
1380 .start_workflow(
1381 "checkout",
1382 payload("input")?,
1383 HashMap::new(),
1384 String::from("default"),
1385 )
1386 .await?;
1387 let workflow_id = handle.workflow_id().clone();
1388
1389 let timer_id = TimerId::anonymous(0);
1392 let fire_at = chrono::Utc::now() - chrono::Duration::hours(1);
1393 handle
1394 .recorder()
1395 .lock()
1396 .await
1397 .record_timer_started(chrono::Utc::now(), timer_id.clone(), fire_at)
1398 .await?;
1399 concrete
1400 .schedule_timer(&workflow_id, &timer_id, fire_at)
1401 .await?;
1402
1403 let timer_service =
1404 crate::runtime::nif_timer_bridge::installed_timer_service(engine.runtime().nif_state())
1405 .map_err(|error| format!("timer service unavailable: {error}"))?;
1406
1407 engine.cancel(&workflow_id, handle.run_id(), "stop").await?;
1408
1409 let readable: Arc<dyn ReadableEventStore> = concrete.clone();
1414 TimerRecovery::new(readable, timer_service, Duration::ZERO)
1415 .recover_on_startup(chrono::Utc::now())
1416 .await?;
1417
1418 let history = concrete.read_history(&workflow_id).await?;
1419 assert!(
1420 !history
1421 .iter()
1422 .any(|event| matches!(event, Event::TimerFired { .. })),
1423 "no timer should fire for a cancelled workflow during recovery"
1424 );
1425 assert!(
1426 history
1427 .iter()
1428 .any(|event| matches!(event, Event::TimerCancelled { .. })),
1429 "cancel must have recorded TimerCancelled at the source"
1430 );
1431 engine.shutdown()?;
1432 Ok(())
1433 }
1434
1435 #[tokio::test]
1436 async fn result_returns_completed_payload() -> Result<(), Box<dyn std::error::Error>> {
1437 let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
1438 let engine =
1439 engine_with_loaded_workflow(Arc::clone(&store), "checkout", "checkout_deployed")?;
1440 let handle = engine
1441 .start_workflow(
1442 "checkout",
1443 payload("input")?,
1444 HashMap::new(),
1445 String::from("default"),
1446 )
1447 .await?;
1448 let result_payload = payload("result")?;
1449
1450 terminate::complete(
1451 termination_context(&engine),
1452 handle.workflow_id(),
1453 handle.run_id(),
1454 result_payload.clone(),
1455 )
1456 .await?;
1457
1458 assert_eq!(
1459 engine.result(handle.workflow_id(), handle.run_id()).await?,
1460 Ok(result_payload)
1461 );
1462 engine.shutdown()?;
1463 Ok(())
1464 }
1465
1466 #[tokio::test]
1467 async fn result_returns_failed_workflow_error() -> Result<(), Box<dyn std::error::Error>> {
1468 let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
1469 let engine =
1470 engine_with_loaded_workflow(Arc::clone(&store), "checkout", "checkout_deployed")?;
1471 let handle = engine
1472 .start_workflow(
1473 "checkout",
1474 payload("input")?,
1475 HashMap::new(),
1476 String::from("default"),
1477 )
1478 .await?;
1479 let error = workflow_error("workflow failed");
1480
1481 terminate::fail(
1482 termination_context(&engine),
1483 handle.workflow_id(),
1484 handle.run_id(),
1485 error.clone(),
1486 )
1487 .await?;
1488
1489 assert_eq!(
1490 engine.result(handle.workflow_id(), handle.run_id()).await?,
1491 Err(error)
1492 );
1493 engine.shutdown()?;
1494 Ok(())
1495 }
1496
1497 #[tokio::test]
1498 async fn result_unknown_workflow_returns_not_found() -> Result<(), Box<dyn std::error::Error>> {
1499 let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
1500 let engine = engine_with_loaded_workflow(store, "checkout", "checkout_deployed")?;
1501 let workflow_id = aion_core::WorkflowId::new_v4();
1502 let run_id = aion_core::RunId::new_v4();
1503
1504 let result = engine.result(&workflow_id, &run_id).await;
1505
1506 assert!(matches!(result, Err(EngineError::WorkflowNotFound { .. })));
1507 engine.shutdown()?;
1508 Ok(())
1509 }
1510
1511 #[tokio::test]
1512 async fn continue_as_new_unknown_workflow_returns_not_found()
1513 -> Result<(), Box<dyn std::error::Error>> {
1514 let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
1515 let engine = engine_with_loaded_workflow(store, "checkout", "checkout_deployed")?;
1516 let workflow_id = aion_core::WorkflowId::new_v4();
1517 let run_id = aion_core::RunId::new_v4();
1518
1519 let result = engine
1520 .continue_as_new(&workflow_id, &run_id, payload("next")?, None)
1521 .await;
1522
1523 assert!(matches!(result, Err(EngineError::WorkflowNotFound { .. })));
1524 engine.shutdown()?;
1525 Ok(())
1526 }
1527
1528 #[tokio::test]
1529 async fn list_workflows_merges_live_and_terminal_without_duplicates()
1530 -> Result<(), Box<dyn std::error::Error>> {
1531 let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
1532 let engine =
1533 engine_with_loaded_workflow(Arc::clone(&store), "checkout", "checkout_deployed")?;
1534 let running = insert_active_handle(&engine, Arc::clone(&store), "checkout").await?;
1535 let completed = engine
1536 .start_workflow(
1537 "checkout",
1538 payload("input")?,
1539 HashMap::new(),
1540 String::from("default"),
1541 )
1542 .await?;
1543 terminate::complete(
1544 termination_context(&engine),
1545 completed.workflow_id(),
1546 completed.run_id(),
1547 payload("result")?,
1548 )
1549 .await?;
1550
1551 let summaries = engine.list_workflows(WorkflowFilter::default()).await?;
1552 assert_eq!(summaries.len(), 2);
1553 assert!(summaries.iter().any(|summary| {
1554 &summary.workflow_id == running.workflow_id()
1555 && summary.status == WorkflowStatus::Running
1556 }));
1557 assert!(summaries.iter().any(|summary| {
1558 &summary.workflow_id == completed.workflow_id()
1559 && summary.status == WorkflowStatus::Completed
1560 }));
1561
1562 let completed_only = engine
1563 .list_workflows(WorkflowFilter {
1564 status: Some(WorkflowStatus::Completed),
1565 ..WorkflowFilter::default()
1566 })
1567 .await?;
1568 assert_eq!(completed_only.len(), 1);
1569 assert_eq!(&completed_only[0].workflow_id, completed.workflow_id());
1570 engine.shutdown()?;
1571 Ok(())
1572 }
1573
1574 #[tokio::test]
1575 async fn shutdown_rejects_subsequent_starts() -> Result<(), Box<dyn std::error::Error>> {
1576 let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
1577 let engine =
1578 engine_with_loaded_workflow(Arc::clone(&store), "checkout", "checkout_deployed")?;
1579 let handle = engine
1580 .start_workflow(
1581 "checkout",
1582 payload("input")?,
1583 HashMap::new(),
1584 String::from("default"),
1585 )
1586 .await?;
1587 terminate::complete(
1588 termination_context(&engine),
1589 handle.workflow_id(),
1590 handle.run_id(),
1591 payload("result")?,
1592 )
1593 .await?;
1594
1595 engine.shutdown()?;
1596 let result = engine
1597 .start_workflow(
1598 "checkout",
1599 payload("after-shutdown")?,
1600 HashMap::new(),
1601 String::from("default"),
1602 )
1603 .await;
1604
1605 assert!(matches!(result, Err(EngineError::ShuttingDown)));
1606 Ok(())
1607 }
1608
1609 #[tokio::test]
1610 async fn shutdown_is_idempotent() -> Result<(), Box<dyn std::error::Error>> {
1611 let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
1612 let engine =
1613 engine_with_loaded_workflow(Arc::clone(&store), "checkout", "checkout_deployed")?;
1614 let handle = engine
1615 .start_workflow(
1616 "checkout",
1617 payload("input")?,
1618 HashMap::new(),
1619 String::from("default"),
1620 )
1621 .await?;
1622 terminate::complete(
1623 termination_context(&engine),
1624 handle.workflow_id(),
1625 handle.run_id(),
1626 payload("result")?,
1627 )
1628 .await?;
1629
1630 engine.shutdown()?;
1631 let second = engine.shutdown();
1632
1633 assert!(
1634 second.is_ok(),
1635 "double shutdown should succeed; got {second:?}"
1636 );
1637 Ok(())
1638 }
1639
1640 #[tokio::test]
1641 async fn shutdown_rejects_schedule_creation() -> Result<(), Box<dyn std::error::Error>> {
1642 let store: Arc<dyn EventStore> = Arc::new(InMemoryStore::default());
1643 let engine =
1644 engine_with_loaded_workflow(Arc::clone(&store), "checkout", "checkout_deployed")?;
1645 let handle = engine
1646 .start_workflow(
1647 "checkout",
1648 payload("input")?,
1649 HashMap::new(),
1650 String::from("default"),
1651 )
1652 .await?;
1653 terminate::complete(
1654 termination_context(&engine),
1655 handle.workflow_id(),
1656 handle.run_id(),
1657 payload("result")?,
1658 )
1659 .await?;
1660 engine.shutdown()?;
1661
1662 let config = aion_core::ScheduleConfig {
1663 trigger: aion_core::TriggerSpec::Interval {
1664 period: Duration::from_secs(60),
1665 },
1666 overlap_policy: aion_core::OverlapPolicy::Skip,
1667 catch_up_policy: aion_core::CatchUpPolicy::Skip,
1668 workflow_type: String::from("checkout"),
1669 input: payload("scheduled")?,
1670 search_attributes: HashMap::new(),
1671 };
1672 let result = engine.create_schedule(config).await;
1673
1674 assert!(
1675 matches!(result, Err(EngineError::ShuttingDown)),
1676 "create_schedule after shutdown should return ShuttingDown; got {result:?}"
1677 );
1678 Ok(())
1679 }
1680}