Skip to main content

aion/lifecycle/
start.rs

1//! Start path: spawn, `WorkflowStarted`, and register.
2
3use 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
33/// Dependencies required to start one workflow execution.
34pub struct StartWorkflowContext {
35    /// Durable event store used by the workflow's single recorder.
36    pub store: Arc<dyn EventStore>,
37    /// Visibility index updated after state-changing workflow events.
38    pub visibility_store: Arc<dyn VisibilityStore>,
39    /// Shared workflow catalog resolving types to loaded package versions.
40    pub catalog: Arc<WorkflowCatalog>,
41    /// Runtime boundary used to spawn the workflow process.
42    pub runtime: Arc<RuntimeHandle>,
43    /// Structural supervision tree recording the per-type supervisor placement.
44    pub supervision: Arc<SupervisionTree>,
45    /// Active execution registry keyed by workflow/run identifiers.
46    pub registry: Arc<Registry>,
47    /// Shared non-resident signal handoff to flush after resident registration.
48    pub signal_handoff: Option<Arc<SignalResumeHandoff>>,
49    /// Schema validating any initial search attributes before they are recorded.
50    pub search_attribute_schema: Arc<SearchAttributeSchema>,
51    /// Tokio handle the spawned workflow's exit monitor captures for its
52    /// completion work. Must be epoch-stable — the host runtime's handle,
53    /// never an engine-owned task runtime's — because the monitor outlives
54    /// the start call and blocks on this handle when the process exits.
55    pub monitor_tokio_handle: tokio::runtime::Handle,
56}
57
58/// Optional identifiers used by internal start callers such as continue-as-new.
59#[derive(Clone, Debug, Default)]
60pub struct StartWorkflowOptions {
61    /// Existing workflow identifier to reuse; omitted for a fresh workflow.
62    pub workflow_id: Option<WorkflowId>,
63    /// Caller-chosen R-4 steered-start routing key. When set (and no explicit
64    /// `workflow_id` is supplied), the request-routing edge derives a fresh id on
65    /// `shard_for(routing_key)` so the start is *steered* to that shard's owner —
66    /// forwarded there when this node is not the owner. `None` (the default) keeps
67    /// the unsteered R-1 remint behaviour, so the single-node path is unchanged.
68    pub routing_key: Option<String>,
69    /// Parent run that continued into this run, when applicable.
70    pub parent_run_id: Option<RunId>,
71    /// Exact loaded package version to spawn; omitted to use the latest version.
72    pub loaded_version: Option<ContentHash>,
73    /// Initial search attributes recorded atomically with `WorkflowStarted`.
74    pub search_attributes: HashMap<String, SearchAttributeValue>,
75    /// Namespace that owns this workflow execution; defaults to `"default"`.
76    pub namespace: Option<String>,
77}
78
79/// Starts a loaded workflow execution and returns its active handle.
80///
81/// # Errors
82///
83/// Returns [`EngineError::WorkflowNotFound`] before appending anything when
84/// `workflow_type` is not loaded. Recorder failures surface as
85/// [`EngineError::Durability`] and stop before any process is spawned. Runtime,
86/// supervision, and registry failures surface as their typed [`EngineError`]
87/// variants.
88pub 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
102/// Starts a loaded workflow execution with caller-supplied lifecycle options.
103///
104/// # Errors
105///
106/// Returns the same typed errors as [`start_workflow`].
107pub async fn start_workflow_with_options(
108    context: StartWorkflowContext,
109    workflow_type: &str,
110    input: Payload,
111    options: StartWorkflowOptions,
112) -> Result<WorkflowHandle, EngineError> {
113    // The pinned resolution is held for the whole start: from here until the
114    // registry insert lands (or the start fails), unload verification sees
115    // this version as in use, closing the registration birth window.
116    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    // Single deterministic clock read: `WorkflowStarted.recorded_at` AND the
141    // deadline's `fire_at` both derive from it, so a replay/adoption re-arm
142    // computes the identical fire time (never a second live-clock read).
143    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
220/// Establish the declared deadline LIVE before acknowledging the start, failing
221/// the start honestly if it cannot be armed.
222///
223/// The durable timer row and the live wheel are armed here (after registration,
224/// so the workflow is resident and the wheel actually arms), while the recorded
225/// `TimerStarted` is the recovery anchor. A declared deadline MUST arm — a run
226/// that proceeded with a silently-inert deadline would run forever if this single
227/// attempt failed and the engine stayed alive — so a persistence or arming
228/// failure retracts the registry publication and aborts the still-unmonitored
229/// process, returning the typed error with no half-started run. A run with no
230/// declared deadline (`armed_deadline == None`) is a no-op.
231///
232/// # Errors
233///
234/// Returns the typed [`EngineError`] from [`arm_declared_deadline`] after
235/// retracting the partial start.
236async 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
251/// Fails a partially-started run without orphaning its still-unmonitored process.
252///
253/// Ordering is the invariant: the registry publication (ownership) is retracted
254/// ONLY after the process has been terminated and that termination synchronously
255/// observed by [`RuntimeHandle::abort_unmonitored_process`]. If termination
256/// cannot be guaranteed — the bounded cleanup queue is
257/// `Unavailable`/`Exhausted`/`Poisoned` — the publication is RETAINED so the live
258/// process stays owned rather than becoming an unowned, unmonitored orphan, and a
259/// completion monitor is installed so its eventual exit is reconciled and cleanup
260/// stays retryable. The original `cause` is surfaced either way (a retain-path
261/// abort failure is logged at error level).
262fn 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            // Terminated and synchronously observed; retracting cannot orphan.
271            retract_registry_publication(context, handle);
272        }
273        Err(UnmonitoredProcessAbortError::CleanupFailed { reason, .. }) => {
274            // The abort DID terminate the process — `terminate_process` runs
275            // before the ancillary cleanup that failed — so the retained
276            // publication is now stale: retract it. The ancillary cleanup failure
277            // is the runtime's own to retry; surface it loudly.
278            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            // The abort job is still in flight and WILL terminate the process.
288            // Defer retraction to the job's completion finalizer rather than
289            // racing a monitor install the in-flight job would reject.
290            defer_retraction_to_abort_job(context, handle, pid);
291        }
292        Err(
293            error @ (UnmonitoredProcessAbortError::ExecutorUnavailable { .. }
294            | UnmonitoredProcessAbortError::ExecutorExhausted { .. }
295            | UnmonitoredProcessAbortError::ExecutorPoisoned { .. }),
296        ) => {
297            // No abort job was submitted (the reservation was released) and the
298            // process is still live and unmonitored: retain ownership and install
299            // a completion monitor so its eventual exit is reconciled.
300            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            // Poisoned/degraded abort state: termination cannot be proven, so
310            // retain ownership rather than orphan. The typed cause already reaches
311            // the caller.
312            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
323/// Defer failed-start registry retraction to the in-flight abort job's
324/// completion: when the job proves termination it runs the retraction finalizer.
325/// If no job is found the abort completed between the wait timeout and here, so
326/// the process is already terminated and retraction runs now. A poisoned attach
327/// retains ownership (the safe backstop).
328fn 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
362/// Keeps a failed-start run owned when its process could not be aborted:
363/// installs a completion monitor so the eventual exit is reconciled. A failed
364/// installation is logged — the retained registry publication remains the
365/// cleanup backstop, so ownership is never dropped on the floor.
366fn 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
376/// Record the run's declared-timeout deadline `TimerStarted`, if one is declared.
377///
378/// LAW 1: a `None` `declared_timeout` records nothing — no `TimerStarted`, no
379/// durable row, no deadline object of any kind — and returns `None`. When a
380/// timeout is declared, this records the durable anchor (`timer_is_live` and
381/// `outstanding_future_timers` recover the deadline from it after
382/// failover/adoption) and returns the id and `fire_at` for the caller to arm the
383/// live wheel after registration.
384///
385/// # Errors
386///
387/// Returns [`EngineError`] when the fire time is out of range, the deadline id
388/// cannot be minted, or the `TimerStarted` append fails.
389async 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
409/// The deadline fire time for a run started at `started_at` with `timeout`.
410///
411/// Deterministic: derived from the same `started_at` recorded on
412/// `WorkflowStarted`, never a second clock read.
413///
414/// # Errors
415///
416/// Returns [`EngineError::Runtime`] when `timeout` is out of `chrono` range or
417/// the addition overflows the representable timestamp range.
418fn 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
432/// Persist the durable deadline row and arm the live wheel for a resident run.
433///
434/// The durable timer row and the live-wheel task are both established here,
435/// before the start is acknowledged. Failure is returned to the caller — the
436/// start path fails the start rather than proceeding with an inert deadline; the
437/// recorded `TimerStarted` remains the recovery anchor for a re-drive.
438///
439/// # Errors
440///
441/// Returns [`EngineError`] when the timer bridge is unavailable or the durable
442/// row/wheel could not be armed.
443async 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
464/// Publishes the started handle into the active registry (with the test-only
465/// start-publication pause), retracting a partial publication if the insert
466/// fails.
467fn 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
488/// Installs the completion monitor for an already-published handle, retracting
489/// the registry publication if installation fails.
490fn 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        // The handle is already published; do not retract ownership until the
496        // process is confirmed terminated. `fail_started_run` aborts first and
497        // retracts only on success, retaining ownership otherwise.
498        return Err(fail_started_run(context, handle, error));
499    }
500    Ok(())
501}
502
503/// Best-effort retraction of a handle's registry publication during a failed
504/// start; a failure to remove is logged (the entry is superseded on re-drive).
505fn 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        // Epoch-stable by the StartWorkflowContext contract: the monitor
545        // fires whenever the process exits — potentially long after the
546        // start call's own executor is gone — so it must never capture
547        // `Handle::current()` of an engine-owned task runtime.
548        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;