1use std::collections::HashMap;
4use std::sync::Arc;
5
6use aion_core::{
7 Event, Payload, RunId, SearchAttributeSchema, SearchAttributeValue, WorkflowId, WorkflowStatus,
8};
9use aion_package::ContentHash;
10use aion_store::EventStore;
11use aion_store::visibility::VisibilityStore;
12use chrono::Utc;
13
14use super::completion::{ProcessExitContext, handle_process_exit};
15use super::visibility::upsert_workflow_visibility;
16use crate::durability::Recorder;
17use crate::loader::WorkflowCatalog;
18use crate::registry::{
19 CompletionNotifier, HandleResidency, Registry, WorkflowHandle, WorkflowHandleParts,
20};
21use crate::runtime::monitor::UnmonitoredProcessAbortError;
22use crate::runtime::{RuntimeHandle, RuntimeInput};
23use crate::supervision::{SupervisionTree, spawn_workflow_with_policy};
24use crate::{
25 EngineError,
26 engine_seam::{
27 ChildWorkflowSpawnRequest, ChildWorkflowSpawnResult, EngineHandle, EngineSeamError,
28 TimerWheelEntry, WorkflowMailboxMessage, WorkflowProcessHandle, WorkflowResidency,
29 },
30 signal::SignalResumeHandoff,
31};
32
33pub struct StartWorkflowContext {
35 pub store: Arc<dyn EventStore>,
37 pub visibility_store: Arc<dyn VisibilityStore>,
39 pub catalog: Arc<WorkflowCatalog>,
41 pub runtime: Arc<RuntimeHandle>,
43 pub supervision: Arc<SupervisionTree>,
45 pub registry: Arc<Registry>,
47 pub signal_handoff: Option<Arc<SignalResumeHandoff>>,
49 pub search_attribute_schema: Arc<SearchAttributeSchema>,
51 pub monitor_tokio_handle: tokio::runtime::Handle,
56}
57
58#[derive(Clone, Debug, Default)]
60pub struct StartWorkflowOptions {
61 pub workflow_id: Option<WorkflowId>,
63 pub routing_key: Option<String>,
69 pub parent_run_id: Option<RunId>,
71 pub loaded_version: Option<ContentHash>,
73 pub search_attributes: HashMap<String, SearchAttributeValue>,
75 pub namespace: Option<String>,
77}
78
79pub async fn start_workflow(
89 context: StartWorkflowContext,
90 workflow_type: &str,
91 input: Payload,
92) -> Result<WorkflowHandle, EngineError> {
93 start_workflow_with_options(
94 context,
95 workflow_type,
96 input,
97 StartWorkflowOptions::default(),
98 )
99 .await
100}
101
102pub async fn start_workflow_with_options(
108 context: StartWorkflowContext,
109 workflow_type: &str,
110 input: Payload,
111 options: StartWorkflowOptions,
112) -> Result<WorkflowHandle, EngineError> {
113 let pinned = match &options.loaded_version {
117 Some(version) => context.catalog.resolve_exact(workflow_type, version)?,
118 None => context.catalog.resolve_routed(workflow_type)?,
119 }
120 .ok_or_else(|| EngineError::WorkflowNotFound {
121 workflow_type: workflow_type.to_owned(),
122 })?;
123 let loaded = pinned.workflow();
124
125 let supplied_workflow_id = options.workflow_id.is_some();
126 let workflow_id = options.workflow_id.unwrap_or_else(WorkflowId::new_v4);
127 let run_id = RunId::new_v4();
128 let initial_head = if supplied_workflow_id {
129 context
130 .store
131 .read_history(&workflow_id)
132 .await?
133 .iter()
134 .map(Event::seq)
135 .max()
136 .unwrap_or_default()
137 } else {
138 0
139 };
140 let started_at = Utc::now();
144 let mut recorder = Recorder::resume_at(
145 workflow_id.clone(),
146 Arc::clone(&context.store),
147 initial_head,
148 )
149 .with_visibility(run_id.clone(), Arc::clone(&context.visibility_store));
150 recorder
151 .record_workflow_started_with_attributes(
152 started_at,
153 crate::durability::WorkflowStartRecord {
154 workflow_type: workflow_type.to_owned(),
155 input: input.clone(),
156 run_id: run_id.clone(),
157 parent_run_id: options.parent_run_id,
158 package_version: crate::loader::package_version_of(loaded.version()),
159 },
160 options.search_attributes,
161 &context.search_attribute_schema,
162 )
163 .await?;
164 let armed_deadline = record_declared_deadline(
165 &mut recorder,
166 &run_id,
167 loaded.declared_timeout(),
168 started_at,
169 )
170 .await?;
171 upsert_workflow_visibility(
172 Arc::clone(&context.store),
173 Arc::clone(&context.visibility_store),
174 &workflow_id,
175 &run_id,
176 )
177 .await?;
178
179 context
180 .supervision
181 .ensure_type_supervisor(loaded.workflow_type())?;
182 let runtime_input = RuntimeInput::from_payload(&input)?;
183 let pid = spawn_workflow_with_policy(
184 &context.runtime,
185 loaded.deployed_entry_module(),
186 loaded.entry_function(),
187 runtime_input,
188 )?;
189 if let Err(error) = context
190 .supervision
191 .place_workflow(loaded.workflow_type(), pid)
192 {
193 return Err(abort_unmonitored_start(&context.runtime, pid, error));
194 }
195
196 let completion = CompletionNotifier::new();
197 let handle = WorkflowHandle::new(WorkflowHandleParts {
198 workflow_id: workflow_id.clone(),
199 run_id: run_id.clone(),
200 pid,
201 workflow_type: loaded.workflow_type().to_owned(),
202 namespace: options.namespace.unwrap_or_else(|| String::from("default")),
203 loaded_version: loaded.version().clone(),
204 cached_status: WorkflowStatus::Running,
205 residency: HandleResidency::Resident,
206 recorder,
207 completion,
208 });
209
210 publish_started_handle(&context, &handle)?;
211
212 arm_declared_deadline_or_fail_start(&context, &workflow_id, &handle, armed_deadline).await?;
213
214 install_started_monitor(&context, &handle)?;
215 deliver_deferred_signals(&context, &handle);
216
217 Ok(handle)
218}
219
220async fn arm_declared_deadline_or_fail_start(
237 context: &StartWorkflowContext,
238 workflow_id: &WorkflowId,
239 handle: &WorkflowHandle,
240 armed_deadline: Option<(aion_core::TimerId, chrono::DateTime<Utc>)>,
241) -> Result<(), EngineError> {
242 let Some((deadline_id, fire_at)) = armed_deadline else {
243 return Ok(());
244 };
245 if let Err(error) = arm_declared_deadline(context, workflow_id, deadline_id, fire_at).await {
246 return Err(fail_started_run(context, handle, error));
247 }
248 Ok(())
249}
250
251fn fail_started_run(
263 context: &StartWorkflowContext,
264 handle: &WorkflowHandle,
265 cause: EngineError,
266) -> EngineError {
267 let pid = handle.pid();
268 match context.runtime.abort_unmonitored_process(pid) {
269 Ok(()) => {
270 retract_registry_publication(context, handle);
272 }
273 Err(UnmonitoredProcessAbortError::CleanupFailed { reason, .. }) => {
274 tracing::error!(
279 workflow_pid = pid,
280 %reason,
281 cause = %cause,
282 "failed-start abort terminated the process but ancillary cleanup failed; retracting stale ownership"
283 );
284 retract_registry_publication(context, handle);
285 }
286 Err(UnmonitoredProcessAbortError::TimedOut { .. }) => {
287 defer_retraction_to_abort_job(context, handle, pid);
291 }
292 Err(
293 error @ (UnmonitoredProcessAbortError::ExecutorUnavailable { .. }
294 | UnmonitoredProcessAbortError::ExecutorExhausted { .. }
295 | UnmonitoredProcessAbortError::ExecutorPoisoned { .. }),
296 ) => {
297 tracing::error!(
301 workflow_pid = pid,
302 %error,
303 cause = %cause,
304 "failed-start abort could not be submitted; retaining ownership and installing a completion monitor"
305 );
306 retain_ownership_with_monitor(context, handle);
307 }
308 Err(error) => {
309 tracing::error!(
313 workflow_pid = pid,
314 %error,
315 cause = %cause,
316 "failed-start abort returned a degraded state; retaining ownership as the safe cleanup backstop"
317 );
318 }
319 }
320 cause
321}
322
323fn defer_retraction_to_abort_job(
329 context: &StartWorkflowContext,
330 handle: &WorkflowHandle,
331 pid: crate::Pid,
332) {
333 let registry = Arc::clone(&context.registry);
334 let workflow_id = handle.workflow_id().clone();
335 let run_id = handle.run_id().clone();
336 let finalizer = move || {
337 if let Err(error) = registry.remove(&workflow_id, &run_id) {
338 tracing::warn!(
339 workflow_pid = pid,
340 %error,
341 "failed to retract failed-start ownership after its abort job terminated the process"
342 );
343 }
344 };
345 match context
346 .runtime
347 .attach_unmonitored_abort_finalizer(pid, finalizer)
348 {
349 Ok(true) => tracing::warn!(
350 workflow_pid = pid,
351 "failed-start abort timed out; registry retraction deferred to the abort job's termination"
352 ),
353 Ok(false) => retract_registry_publication(context, handle),
354 Err(error) => tracing::error!(
355 workflow_pid = pid,
356 %error,
357 "could not attach failed-start retraction to the abort job; retaining ownership"
358 ),
359 }
360}
361
362fn retain_ownership_with_monitor(context: &StartWorkflowContext, handle: &WorkflowHandle) {
367 if let Err(error) = install_completion_monitor(context, handle.pid(), handle) {
368 tracing::error!(
369 workflow_pid = handle.pid(),
370 error = %error,
371 "failed to install a completion monitor for a retained failed-start run; registry ownership remains the cleanup backstop"
372 );
373 }
374}
375
376async fn record_declared_deadline(
390 recorder: &mut Recorder,
391 run_id: &RunId,
392 declared_timeout: Option<std::time::Duration>,
393 started_at: chrono::DateTime<Utc>,
394) -> Result<Option<(aion_core::TimerId, chrono::DateTime<Utc>)>, EngineError> {
395 let Some(timeout) = declared_timeout else {
396 return Ok(None);
397 };
398 let fire_at = deadline_fire_at(started_at, timeout)?;
399 let deadline_id =
400 crate::time::deadline_timer_id(run_id).map_err(|error| EngineError::Runtime {
401 reason: format!("failed to mint deadline timer id: {error}"),
402 })?;
403 recorder
404 .record_timer_started(started_at, deadline_id.clone(), fire_at)
405 .await?;
406 Ok(Some((deadline_id, fire_at)))
407}
408
409fn deadline_fire_at(
419 started_at: chrono::DateTime<Utc>,
420 timeout: std::time::Duration,
421) -> Result<chrono::DateTime<Utc>, EngineError> {
422 let delta = chrono::Duration::from_std(timeout).map_err(|error| EngineError::Runtime {
423 reason: format!("declared workflow timeout is out of range: {error}"),
424 })?;
425 started_at
426 .checked_add_signed(delta)
427 .ok_or_else(|| EngineError::Runtime {
428 reason: String::from("declared workflow timeout overflowed the deadline fire time"),
429 })
430}
431
432async fn arm_declared_deadline(
444 context: &StartWorkflowContext,
445 workflow_id: &WorkflowId,
446 deadline_id: aion_core::TimerId,
447 fire_at: chrono::DateTime<Utc>,
448) -> Result<(), EngineError> {
449 let timer_service =
450 crate::runtime::nif_timer_bridge::installed_timer_service(context.runtime.nif_state())
451 .map_err(|error| EngineError::Runtime {
452 reason: format!(
453 "timer service unavailable while arming workflow deadline: {error}"
454 ),
455 })?;
456 timer_service
457 .schedule(workflow_id.clone(), deadline_id.clone(), fire_at)
458 .await
459 .map_err(|error| EngineError::Runtime {
460 reason: format!("failed to arm workflow deadline {deadline_id}: {error}"),
461 })
462}
463
464fn publish_started_handle(
468 context: &StartWorkflowContext,
469 handle: &WorkflowHandle,
470) -> Result<(), EngineError> {
471 let pid = handle.pid();
472 if let Err(error) = context
473 .registry
474 .insert(
475 (handle.workflow_id().clone(), handle.run_id().clone()),
476 handle.clone(),
477 )
478 .map(|_| ())
479 {
480 retract_registry_publication(context, handle);
481 return Err(abort_unmonitored_start(&context.runtime, pid, error));
482 }
483 #[cfg(test)]
484 context.runtime.pause_at_start_publication_for_test(pid)?;
485 Ok(())
486}
487
488fn install_started_monitor(
491 context: &StartWorkflowContext,
492 handle: &WorkflowHandle,
493) -> Result<(), EngineError> {
494 if let Err(error) = install_completion_monitor(context, handle.pid(), handle) {
495 return Err(fail_started_run(context, handle, error));
499 }
500 Ok(())
501}
502
503fn retract_registry_publication(context: &StartWorkflowContext, handle: &WorkflowHandle) {
506 if let Err(error) = context
507 .registry
508 .remove(handle.workflow_id(), handle.run_id())
509 {
510 tracing::warn!(
511 workflow_pid = handle.pid(),
512 error = %error,
513 "failed to retract workflow registry publication during failed start"
514 );
515 }
516}
517
518fn abort_unmonitored_start(
519 runtime: &Arc<RuntimeHandle>,
520 pid: crate::Pid,
521 cause: EngineError,
522) -> EngineError {
523 match runtime.abort_unmonitored_process(pid) {
524 Ok(()) => cause,
525 Err(abort_error) => {
526 tracing::error!(pid, error = %abort_error, cause = %cause, "bounded workflow abort failed after start registration error");
527 abort_error.into_engine_error()
528 }
529 }
530}
531
532fn install_completion_monitor(
533 context: &StartWorkflowContext,
534 pid: crate::Pid,
535 handle: &WorkflowHandle,
536) -> Result<(), EngineError> {
537 let completion_context = ProcessExitContext {
538 store: Arc::clone(&context.store),
539 visibility_store: Arc::clone(&context.visibility_store),
540 registry: Arc::clone(&context.registry),
541 catalog: Arc::clone(&context.catalog),
542 runtime: Arc::clone(&context.runtime),
543 supervision: Arc::clone(&context.supervision),
544 tokio_handle: context.monitor_tokio_handle.clone(),
549 search_attribute_schema: Arc::clone(&context.search_attribute_schema),
550 };
551 let completion_handle = handle.clone();
552 context.runtime.monitor_process(pid, move |outcome| {
553 if let Err(error) = handle_process_exit(completion_context, completion_handle, outcome) {
554 tracing::error!(workflow_pid = pid, error = %error, "workflow process monitor completion failed");
555 }
556 })?;
557 Ok(())
558}
559
560fn deliver_deferred_signals(context: &StartWorkflowContext, handle: &WorkflowHandle) {
561 let Some(handoff) = &context.signal_handoff else {
562 return;
563 };
564 let adapter = StartResumeEngineHandle {
565 runtime: &context.runtime,
566 registry: &context.registry,
567 };
568 if let Err(error) = handoff.deliver_deferred(&adapter, handle.workflow_id()) {
569 tracing::warn!(
570 workflow_id = %handle.workflow_id(),
571 error = %error,
572 "failed to flush deferred signals after workflow became resident"
573 );
574 }
575}
576
577struct StartResumeEngineHandle<'a> {
578 runtime: &'a RuntimeHandle,
579 registry: &'a Registry,
580}
581
582impl EngineHandle for StartResumeEngineHandle<'_> {
583 fn resolve_workflow(
584 &self,
585 workflow_id: &WorkflowId,
586 ) -> Result<WorkflowResidency, EngineSeamError> {
587 let handle = self
588 .registry
589 .list()
590 .map_err(|error| EngineSeamError::Delivery {
591 reason: error.to_string(),
592 })?
593 .into_iter()
594 .find(|handle| handle.workflow_id() == workflow_id);
595 match handle {
596 Some(handle) if handle.residency() == HandleResidency::Resident => Ok(
597 WorkflowResidency::Resident(WorkflowProcessHandle::new(handle.pid())),
598 ),
599 Some(_) => Ok(WorkflowResidency::NonResident),
600 None => Ok(WorkflowResidency::Unknown),
601 }
602 }
603
604 fn deliver_workflow_message(
605 &self,
606 process: WorkflowProcessHandle,
607 message: WorkflowMailboxMessage,
608 ) -> Result<(), EngineSeamError> {
609 match message {
610 WorkflowMailboxMessage::SignalReceived { .. } => self
611 .runtime
612 .deliver_signal_received(process.pid())
613 .map_err(|error| EngineSeamError::Delivery {
614 reason: error.to_string(),
615 }),
616 other => Err(EngineSeamError::Delivery {
617 reason: format!("unsupported resume handoff message: {other:?}"),
618 }),
619 }
620 }
621
622 fn spawn_child_workflow(
623 &self,
624 request: ChildWorkflowSpawnRequest,
625 ) -> Result<ChildWorkflowSpawnResult, EngineSeamError> {
626 let _ = request;
627 Err(EngineSeamError::ChildSpawn {
628 reason: "start resume handoff cannot spawn child workflows".to_owned(),
629 })
630 }
631
632 fn terminate_linked_child_workflow(
633 &self,
634 parent_workflow_id: &WorkflowId,
635 child_process: WorkflowProcessHandle,
636 correlation: u64,
637 ) -> Result<(), EngineSeamError> {
638 let _ = (parent_workflow_id, child_process, correlation);
639 Err(EngineSeamError::ChildTermination {
640 reason: "start resume handoff cannot terminate child workflows".to_owned(),
641 })
642 }
643
644 fn terminate_linked_activity(
645 &self,
646 parent_workflow_id: &WorkflowId,
647 activity_process: crate::Pid,
648 correlation: u64,
649 ) -> Result<(), EngineSeamError> {
650 let _ = (parent_workflow_id, activity_process, correlation);
651 Err(EngineSeamError::ChildTermination {
652 reason: "start resume handoff cannot terminate activities".to_owned(),
653 })
654 }
655
656 fn arm_timer(&self, entry: TimerWheelEntry) -> Result<(), EngineSeamError> {
657 let _ = entry;
658 Err(EngineSeamError::TimerWheel {
659 reason: "start resume handoff cannot arm timers".to_owned(),
660 })
661 }
662
663 fn disarm_timer(
664 &self,
665 process: WorkflowProcessHandle,
666 timer_id: &aion_core::TimerId,
667 ) -> Result<(), EngineSeamError> {
668 let _ = (process, timer_id);
669 Err(EngineSeamError::TimerWheel {
670 reason: "start resume handoff cannot disarm timers".to_owned(),
671 })
672 }
673
674 fn record_workflow_event(
675 &self,
676 workflow_id: &WorkflowId,
677 event: Event,
678 ) -> Result<crate::engine_seam::RecordOutcome, EngineSeamError> {
679 let _ = (workflow_id, event);
680 Err(EngineSeamError::Recorder {
681 reason: "start resume handoff cannot record workflow events".to_owned(),
682 })
683 }
684}
685
686#[cfg(test)]
687#[path = "start_tests.rs"]
688mod start_tests;