Skip to main content

aion/
engine_seam.rs

1//! Engine-facing seam for time, signal, query, child, and concurrency services.
2//!
3//! AE implements [`EngineHandle`] for the real engine. This AT cluster consumes the seam to resolve
4//! workflow residency, deliver already-recorded observations to mailboxes, request linked child
5//! workflow starts, arm timer-wheel entries, and route asynchronous-arrival events through the
6//! target workflow's single AD Recorder. AT does not manage workflow process lifecycle,
7//! supervision, or module loading directly.
8
9use aion_core::{Event, Payload, TimerId, WorkflowError, WorkflowId};
10
11use crate::Pid;
12use chrono::{DateTime, Utc};
13use tokio::sync::oneshot;
14
15/// Narrow live-process handle used by AT services after AE resolves workflow residency.
16///
17/// The wrapper intentionally exposes only an opaque process identifier. Real AE implementations can
18/// adapt their concrete BEAM process handle into this type without giving AT lifecycle ownership.
19#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
20pub struct WorkflowProcessHandle {
21    pid: u64,
22}
23
24impl WorkflowProcessHandle {
25    /// Creates a workflow process handle from an opaque process identifier.
26    #[must_use]
27    pub const fn new(pid: u64) -> Self {
28        Self { pid }
29    }
30
31    /// Returns the opaque process identifier backing this handle.
32    #[must_use]
33    pub const fn pid(self) -> u64 {
34        self.pid
35    }
36}
37
38/// AE's answer when AT resolves a logical workflow to a live process.
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub enum WorkflowResidency {
41    /// The workflow is currently resident and can receive mailbox messages.
42    Resident(WorkflowProcessHandle),
43    /// The workflow exists durably but has no live process at the moment.
44    NonResident,
45    /// The workflow is terminal and should not receive live interactions.
46    Terminal,
47    /// AE has no durable or live workflow for the requested identifier.
48    Unknown,
49}
50
51/// One-shot reply path carried by read-only query mailbox messages.
52///
53/// Workflow processes answer query messages at yield points from registered read-only handlers.
54/// Queries are distinct from signals, do not mutate deterministic workflow state, and never record
55/// events; the reply sender carries either the handler payload or a typed query error.
56pub type QueryReplySender = oneshot::Sender<crate::query::service::QueryResult>;
57
58/// Message kinds AT may ask AE to deliver to a workflow process mailbox.
59#[derive(Debug)]
60pub enum WorkflowMailboxMessage {
61    /// A durable timer fired and has been recorded.
62    TimerFired {
63        /// Timer that fired.
64        timer_id: TimerId,
65        /// Deterministic fire timestamp carried for service/replay correlation.
66        fire_at: DateTime<Utc>,
67    },
68    /// A durable signal arrived and has been recorded.
69    SignalReceived {
70        /// Signal name selected by the sender.
71        name: String,
72        /// Opaque signal payload.
73        payload: Payload,
74    },
75    /// A read-only query request. Query dispatch records no event.
76    Query {
77        /// Query name selected by the caller.
78        name: String,
79        /// Opaque query input payload.
80        payload: Payload,
81        /// One-shot channel for the workflow query handler's reply.
82        reply_to: QueryReplySender,
83    },
84    /// A linked child workflow completed successfully and has been recorded.
85    ChildWorkflowCompleted {
86        /// Child workflow that produced the result.
87        child_workflow_id: WorkflowId,
88        /// Spawn correlation token used by collectors.
89        correlation: u64,
90        /// Opaque child result payload.
91        result: Payload,
92    },
93    /// A linked child workflow failed terminally and has been recorded.
94    ChildWorkflowFailed {
95        /// Child workflow that failed.
96        child_workflow_id: WorkflowId,
97        /// Spawn correlation token used by collectors.
98        correlation: u64,
99        /// Terminal child workflow failure.
100        error: WorkflowError,
101    },
102    /// A linked child workflow was cancelled and has been recorded.
103    ChildWorkflowCancelled {
104        /// Child workflow that was cancelled.
105        child_workflow_id: WorkflowId,
106        /// Spawn correlation token used by collectors.
107        correlation: u64,
108    },
109}
110
111/// Request from AT to AE to spawn a child workflow under a parent process.
112///
113/// The child workflow identifier is pre-allocated by the parent and durably
114/// recorded as `ChildWorkflowStarted` in the parent's history *before* this
115/// request is issued (record-then-spawn), so a crash between the record and
116/// the start leaves a recoverable record instead of an unrecorded orphan.
117/// AE must start the child under exactly this identifier. Children are not
118/// process-linked to their parents: parent death leaves children running,
119/// and awaited terminals are observed through the child-terminal watcher.
120#[derive(Clone, Debug, PartialEq, Eq)]
121pub struct ChildWorkflowSpawnRequest {
122    /// Parent workflow requesting the child execution.
123    pub parent_workflow_id: WorkflowId,
124    /// Pre-allocated child workflow identifier already recorded by the parent.
125    pub child_workflow_id: WorkflowId,
126    /// Child workflow type selected by the parent workflow.
127    pub workflow_type: String,
128    /// Opaque child workflow input payload.
129    pub input: Payload,
130    /// Package version resolved for the child at record time.
131    pub package_version: aion_core::PackageVersion,
132}
133
134/// AE's result after starting a linked child workflow execution.
135#[derive(Clone, Debug, PartialEq, Eq)]
136pub struct ChildWorkflowSpawnResult {
137    /// Logical child workflow identifier.
138    pub child_workflow_id: WorkflowId,
139    /// Live process handle for the linked child execution.
140    pub child_process: WorkflowProcessHandle,
141}
142
143/// Timer-wheel entry requested by AT for a live workflow process.
144#[derive(Clone, Debug, PartialEq, Eq)]
145pub struct TimerWheelEntry {
146    /// Workflow process that should receive the timer fire path.
147    pub process: WorkflowProcessHandle,
148    /// Timer selected by workflow code or assigned by the engine.
149    pub timer_id: TimerId,
150    /// UTC timestamp at which the wheel should fire.
151    pub fire_at: DateTime<Utc>,
152}
153
154/// Errors returned by the engine seam.
155#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
156pub enum EngineSeamError {
157    /// The target workflow has no current live process.
158    #[error("workflow {workflow_id} is not resident")]
159    NonResident {
160        /// Workflow that had no current live process.
161        workflow_id: WorkflowId,
162    },
163
164    /// The target workflow is terminal.
165    #[error("workflow {workflow_id} is terminal")]
166    Terminal {
167        /// Terminal workflow identifier.
168        workflow_id: WorkflowId,
169    },
170
171    /// The target workflow is unknown to AE.
172    #[error("workflow {workflow_id} is unknown")]
173    UnknownWorkflow {
174        /// Unknown workflow identifier.
175        workflow_id: WorkflowId,
176    },
177
178    /// AE could not deliver a mailbox message.
179    #[error("mailbox delivery failed: {reason}")]
180    Delivery {
181        /// Human-readable delivery failure reason.
182        reason: String,
183    },
184
185    /// AE could not spawn a linked child workflow.
186    #[error("child workflow spawn failed: {reason}")]
187    ChildSpawn {
188        /// Human-readable child-spawn failure reason.
189        reason: String,
190    },
191
192    /// AE could not arm or disarm the timer wheel.
193    #[error("timer wheel operation failed: {reason}")]
194    TimerWheel {
195        /// Human-readable timer-wheel failure reason.
196        reason: String,
197    },
198
199    /// AE could not terminate a linked child process.
200    #[error("linked child termination failed: {reason}")]
201    ChildTermination {
202        /// Human-readable child termination failure reason.
203        reason: String,
204    },
205
206    /// AD's single Recorder path could not record the event.
207    #[error("workflow recorder failed: {reason}")]
208    Recorder {
209        /// Human-readable recorder failure reason.
210        reason: String,
211    },
212}
213
214/// Engine-facing capabilities consumed by AT services and implemented by AE.
215///
216/// This trait deliberately does not expose operations that start, supervise, tear down, or load
217/// top-level workflow processes. Child spawning, residency resolution, and recording are requests
218/// into AE/AD-owned infrastructure. In particular, [`EngineHandle::record_workflow_event`] must
219/// route asynchronous-arrival events through the target workflow's single Recorder; AT services must
220/// not append directly to the event store.
221pub trait EngineHandle: Send + Sync {
222    /// Resolves a workflow identifier to its current residency state.
223    ///
224    /// # Errors
225    ///
226    /// Returns [`EngineSeamError`] when AE cannot inspect residency for the requested workflow.
227    fn resolve_workflow(
228        &self,
229        workflow_id: &WorkflowId,
230    ) -> Result<WorkflowResidency, EngineSeamError>;
231
232    /// Delivers a message to a resident workflow process mailbox.
233    ///
234    /// # Errors
235    ///
236    /// Returns [`EngineSeamError`] when AE cannot enqueue the message on the target mailbox.
237    fn deliver_workflow_message(
238        &self,
239        process: WorkflowProcessHandle,
240        message: WorkflowMailboxMessage,
241    ) -> Result<(), EngineSeamError>;
242
243    /// Requests AE to spawn a child workflow execution linked to the parent process.
244    ///
245    /// # Errors
246    ///
247    /// Returns [`EngineSeamError`] when AE rejects or fails the linked child-spawn request.
248    fn spawn_child_workflow(
249        &self,
250        request: ChildWorkflowSpawnRequest,
251    ) -> Result<ChildWorkflowSpawnResult, EngineSeamError>;
252
253    /// Terminates a linked child workflow process through AE's process-link boundary.
254    ///
255    /// # Errors
256    ///
257    /// Returns [`EngineSeamError`] when AE cannot send the cancellation exit to the linked child.
258    fn terminate_linked_child_workflow(
259        &self,
260        parent_workflow_id: &WorkflowId,
261        child_process: WorkflowProcessHandle,
262        correlation: u64,
263    ) -> Result<(), EngineSeamError>;
264
265    /// Terminates a linked in-VM activity process through AE's process-link boundary.
266    ///
267    /// # Errors
268    ///
269    /// Returns [`EngineSeamError`] when AE cannot send the cancellation exit to the linked child.
270    fn terminate_linked_activity(
271        &self,
272        parent_workflow_id: &WorkflowId,
273        activity_process: Pid,
274        correlation: u64,
275    ) -> Result<(), EngineSeamError>;
276
277    /// Arms a timer-wheel entry for a resident workflow process.
278    ///
279    /// # Errors
280    ///
281    /// Returns [`EngineSeamError`] when AE cannot register the timer with the live wheel.
282    fn arm_timer(&self, entry: TimerWheelEntry) -> Result<(), EngineSeamError>;
283
284    /// Disarms a timer-wheel entry for a resident workflow process.
285    ///
286    /// # Errors
287    ///
288    /// Returns [`EngineSeamError`] when AE cannot remove the timer from the live wheel.
289    fn disarm_timer(
290        &self,
291        process: WorkflowProcessHandle,
292        timer_id: &TimerId,
293    ) -> Result<(), EngineSeamError>;
294
295    /// Records an event through the target workflow's single AD Recorder.
296    ///
297    /// # Errors
298    ///
299    /// Returns [`EngineSeamError`] when the target workflow's Recorder cannot append the event.
300    fn record_workflow_event(
301        &self,
302        workflow_id: &WorkflowId,
303        event: Event,
304    ) -> Result<(), EngineSeamError>;
305}
306
307#[cfg(test)]
308pub(crate) mod test_support {
309    use std::collections::{HashMap, VecDeque};
310    use std::sync::Arc;
311    use std::sync::{Mutex, MutexGuard};
312
313    use aion_store::{WritableEventStore, WriteToken};
314
315    use super::*;
316
317    /// Operation captured by [`FakeEngineHandle`] in observed order.
318    #[derive(Clone, Debug, PartialEq)]
319    pub enum FakeEngineOperation {
320        /// A mailbox message was delivered.
321        Delivered {
322            /// Target process handle.
323            process: WorkflowProcessHandle,
324            /// Delivered message projection.
325            message: DeliveredWorkflowMessage,
326        },
327        /// A child spawn was requested.
328        ChildSpawnRequested(ChildWorkflowSpawnRequest),
329        /// A timer-wheel entry was armed.
330        TimerArmed(TimerWheelEntry),
331        /// A timer-wheel entry was disarmed.
332        TimerDisarmed {
333            /// Target process handle.
334            process: WorkflowProcessHandle,
335            /// Timer that was disarmed.
336            timer_id: TimerId,
337        },
338        /// A linked child workflow process was terminated.
339        LinkedChildWorkflowTerminated {
340            /// Parent workflow owning the link.
341            parent_workflow_id: WorkflowId,
342            /// Linked child workflow process.
343            child_process: WorkflowProcessHandle,
344            /// Spawn correlation token.
345            correlation: u64,
346        },
347        /// A linked activity process was terminated.
348        LinkedActivityTerminated {
349            /// Parent workflow owning the link.
350            parent_workflow_id: WorkflowId,
351            /// Linked activity process.
352            activity_process: Pid,
353            /// Spawn correlation token.
354            correlation: u64,
355        },
356        /// An event was recorded through the recorder seam.
357        EventRecorded {
358            /// Workflow whose recorder received the event.
359            workflow_id: WorkflowId,
360            /// Recorded event.
361            event: Event,
362        },
363    }
364
365    #[derive(Default)]
366    struct FakeEngineState {
367        residency: HashMap<WorkflowId, WorkflowResidency>,
368        delivered: Vec<(WorkflowProcessHandle, DeliveredWorkflowMessage)>,
369        delivery_responses: VecDeque<Result<(), EngineSeamError>>,
370        child_spawn_responses: VecDeque<Result<ChildWorkflowSpawnResult, EngineSeamError>>,
371        armed_timers: Vec<TimerWheelEntry>,
372        disarmed_timers: Vec<(WorkflowProcessHandle, TimerId)>,
373        recorded_events: Vec<(WorkflowId, Event)>,
374        operations: Vec<FakeEngineOperation>,
375        recorder_store: Option<Arc<dyn WritableEventStore>>,
376        record_responses: VecDeque<Result<(), EngineSeamError>>,
377    }
378
379    /// Cloneable projection of delivered mailbox messages for seam tests.
380    #[derive(Clone, Debug, PartialEq, Eq)]
381    pub enum DeliveredWorkflowMessage {
382        /// A timer-fired delivery was observed.
383        TimerFired {
384            timer_id: TimerId,
385            fire_at: DateTime<Utc>,
386        },
387        /// A signal delivery was observed.
388        SignalReceived { name: String, payload: Payload },
389        /// A query delivery was observed; the one-shot sender is intentionally not retained.
390        Query { name: String, payload: Payload },
391        /// A child completion delivery was observed.
392        ChildWorkflowCompleted {
393            child_workflow_id: WorkflowId,
394            correlation: u64,
395            result: Payload,
396        },
397        /// A child failure delivery was observed.
398        ChildWorkflowFailed {
399            child_workflow_id: WorkflowId,
400            correlation: u64,
401            error: WorkflowError,
402        },
403        /// A child cancellation delivery was observed.
404        ChildWorkflowCancelled {
405            child_workflow_id: WorkflowId,
406            correlation: u64,
407        },
408    }
409
410    impl DeliveredWorkflowMessage {
411        pub(crate) fn from_message(message: &WorkflowMailboxMessage) -> Self {
412            match message {
413                WorkflowMailboxMessage::TimerFired { timer_id, fire_at } => Self::TimerFired {
414                    timer_id: timer_id.clone(),
415                    fire_at: *fire_at,
416                },
417                WorkflowMailboxMessage::SignalReceived { name, payload } => Self::SignalReceived {
418                    name: name.clone(),
419                    payload: payload.clone(),
420                },
421                WorkflowMailboxMessage::Query {
422                    name,
423                    payload,
424                    reply_to: _,
425                } => Self::Query {
426                    name: name.clone(),
427                    payload: payload.clone(),
428                },
429                WorkflowMailboxMessage::ChildWorkflowCompleted {
430                    child_workflow_id,
431                    correlation,
432                    result,
433                } => Self::ChildWorkflowCompleted {
434                    child_workflow_id: child_workflow_id.clone(),
435                    correlation: *correlation,
436                    result: result.clone(),
437                },
438                WorkflowMailboxMessage::ChildWorkflowFailed {
439                    child_workflow_id,
440                    correlation,
441                    error,
442                } => Self::ChildWorkflowFailed {
443                    child_workflow_id: child_workflow_id.clone(),
444                    correlation: *correlation,
445                    error: error.clone(),
446                },
447                WorkflowMailboxMessage::ChildWorkflowCancelled {
448                    child_workflow_id,
449                    correlation,
450                } => Self::ChildWorkflowCancelled {
451                    child_workflow_id: child_workflow_id.clone(),
452                    correlation: *correlation,
453                },
454            }
455        }
456    }
457
458    /// Test-only fake implementation of [`EngineHandle`].
459    #[derive(Default)]
460    pub struct FakeEngineHandle {
461        state: Mutex<FakeEngineState>,
462    }
463
464    impl FakeEngineHandle {
465        /// Creates an empty fake engine handle.
466        #[must_use]
467        pub fn new() -> Self {
468            Self::default()
469        }
470
471        /// Creates a fake whose recorder seam appends to the supplied store with event sequencing.
472        #[must_use]
473        pub fn recording_to(store: Arc<dyn WritableEventStore>) -> Self {
474            Self {
475                state: Mutex::new(FakeEngineState {
476                    recorder_store: Some(store),
477                    ..FakeEngineState::default()
478                }),
479            }
480        }
481
482        /// Sets the residency response returned for a workflow.
483        ///
484        /// # Errors
485        ///
486        /// Returns [`EngineSeamError::EngineOffline`] if the fake's state lock is poisoned.
487        pub fn set_residency(
488            &self,
489            workflow_id: WorkflowId,
490            residency: WorkflowResidency,
491        ) -> Result<(), EngineSeamError> {
492            self.state()?.residency.insert(workflow_id, residency);
493            Ok(())
494        }
495
496        /// Queues the next response returned by mailbox-delivery seam calls.
497        ///
498        /// # Errors
499        ///
500        /// Returns [`EngineSeamError::EngineOffline`] if the fake's state lock is poisoned.
501        pub fn push_delivery_response(
502            &self,
503            response: Result<(), EngineSeamError>,
504        ) -> Result<(), EngineSeamError> {
505            self.state()?.delivery_responses.push_back(response);
506            Ok(())
507        }
508
509        /// Queues the next response returned by workflow-event recording seam calls.
510        ///
511        /// Used to simulate the engine rejecting a recorded event — e.g. firing a
512        /// timer for a workflow that no longer exists ([`EngineSeamError::UnknownWorkflow`]).
513        ///
514        /// # Errors
515        ///
516        /// Returns [`EngineSeamError::EngineOffline`] if the fake's state lock is poisoned.
517        pub fn push_record_response(
518            &self,
519            response: Result<(), EngineSeamError>,
520        ) -> Result<(), EngineSeamError> {
521            self.state()?.record_responses.push_back(response);
522            Ok(())
523        }
524
525        /// Returns a snapshot of seam operations in observed order.
526        ///
527        /// # Errors
528        ///
529        /// Returns [`EngineSeamError::EngineOffline`] if the fake's state lock is poisoned.
530        pub fn operations(&self) -> Result<Vec<FakeEngineOperation>, EngineSeamError> {
531            Ok(self.state()?.operations.clone())
532        }
533
534        /// Returns a snapshot of delivered mailbox messages.
535        ///
536        /// # Errors
537        ///
538        /// Returns [`EngineSeamError::EngineOffline`] if the fake's state lock is poisoned.
539        pub fn delivered_messages(
540            &self,
541        ) -> Result<Vec<(WorkflowProcessHandle, DeliveredWorkflowMessage)>, EngineSeamError>
542        {
543            Ok(self.state()?.delivered.clone())
544        }
545
546        /// Returns a snapshot of armed timer-wheel entries.
547        ///
548        /// # Errors
549        ///
550        /// Returns [`EngineSeamError::EngineOffline`] if the fake's state lock is poisoned.
551        pub fn armed_timers(&self) -> Result<Vec<TimerWheelEntry>, EngineSeamError> {
552            Ok(self.state()?.armed_timers.clone())
553        }
554
555        /// Queues the next child-spawn response returned by the fake.
556        ///
557        /// # Errors
558        ///
559        /// Returns [`EngineSeamError::EngineOffline`] if the fake's state lock is poisoned.
560        pub fn push_child_spawn_response(
561            &self,
562            response: Result<ChildWorkflowSpawnResult, EngineSeamError>,
563        ) -> Result<(), EngineSeamError> {
564            self.state()?.child_spawn_responses.push_back(response);
565            Ok(())
566        }
567
568        /// Returns events recorded through the fake recorder seam.
569        ///
570        /// # Errors
571        ///
572        /// Returns [`EngineSeamError::EngineOffline`] if the fake's state lock is poisoned.
573        pub fn recorded_events(&self) -> Result<Vec<(WorkflowId, Event)>, EngineSeamError> {
574            Ok(self.state()?.recorded_events.clone())
575        }
576
577        fn state(&self) -> Result<MutexGuard<'_, FakeEngineState>, EngineSeamError> {
578            self.state.lock().map_err(|_| EngineSeamError::Recorder {
579                reason: "fake engine state lock was poisoned".to_owned(),
580            })
581        }
582    }
583
584    impl EngineHandle for FakeEngineHandle {
585        fn resolve_workflow(
586            &self,
587            workflow_id: &WorkflowId,
588        ) -> Result<WorkflowResidency, EngineSeamError> {
589            Ok(self
590                .state()?
591                .residency
592                .get(workflow_id)
593                .copied()
594                .unwrap_or(WorkflowResidency::Unknown))
595        }
596
597        fn deliver_workflow_message(
598            &self,
599            process: WorkflowProcessHandle,
600            message: WorkflowMailboxMessage,
601        ) -> Result<(), EngineSeamError> {
602            let mut state = self.state()?;
603            if let Some(response) = state.delivery_responses.pop_front() {
604                response?;
605            }
606            let delivered = DeliveredWorkflowMessage::from_message(&message);
607            state.delivered.push((process, delivered.clone()));
608            state.operations.push(FakeEngineOperation::Delivered {
609                process,
610                message: delivered,
611            });
612            Ok(())
613        }
614
615        fn spawn_child_workflow(
616            &self,
617            request: ChildWorkflowSpawnRequest,
618        ) -> Result<ChildWorkflowSpawnResult, EngineSeamError> {
619            let mut state = self.state()?;
620            state
621                .operations
622                .push(FakeEngineOperation::ChildSpawnRequested(request.clone()));
623            if let Some(response) = state.child_spawn_responses.pop_front() {
624                response
625            } else {
626                Err(EngineSeamError::ChildSpawn {
627                    reason: "fake child spawn response was not queued".to_owned(),
628                })
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 mut state = self.state()?;
639            state
640                .operations
641                .push(FakeEngineOperation::LinkedChildWorkflowTerminated {
642                    parent_workflow_id: parent_workflow_id.clone(),
643                    child_process,
644                    correlation,
645                });
646            Ok(())
647        }
648
649        fn terminate_linked_activity(
650            &self,
651            parent_workflow_id: &WorkflowId,
652            activity_process: Pid,
653            correlation: u64,
654        ) -> Result<(), EngineSeamError> {
655            let mut state = self.state()?;
656            state
657                .operations
658                .push(FakeEngineOperation::LinkedActivityTerminated {
659                    parent_workflow_id: parent_workflow_id.clone(),
660                    activity_process,
661                    correlation,
662                });
663            Ok(())
664        }
665
666        fn arm_timer(&self, entry: TimerWheelEntry) -> Result<(), EngineSeamError> {
667            let mut state = self.state()?;
668            state.armed_timers.push(entry.clone());
669            state
670                .operations
671                .push(FakeEngineOperation::TimerArmed(entry));
672            Ok(())
673        }
674
675        fn disarm_timer(
676            &self,
677            process: WorkflowProcessHandle,
678            timer_id: &TimerId,
679        ) -> Result<(), EngineSeamError> {
680            let mut state = self.state()?;
681            state
682                .armed_timers
683                .retain(|entry| !(entry.process == process && &entry.timer_id == timer_id));
684            state.disarmed_timers.push((process, timer_id.clone()));
685            state.operations.push(FakeEngineOperation::TimerDisarmed {
686                process,
687                timer_id: timer_id.clone(),
688            });
689            Ok(())
690        }
691
692        fn record_workflow_event(
693            &self,
694            workflow_id: &WorkflowId,
695            event: Event,
696        ) -> Result<(), EngineSeamError> {
697            let mut state = self.state()?;
698            if let Some(response) = state.record_responses.pop_front() {
699                response?;
700            }
701            state
702                .recorded_events
703                .push((workflow_id.clone(), event.clone()));
704            let recorder_store = state.recorder_store.clone();
705            state.operations.push(FakeEngineOperation::EventRecorded {
706                workflow_id: workflow_id.clone(),
707                event: event.clone(),
708            });
709            drop(state);
710
711            if let Some(store) = recorder_store {
712                let expected_seq = event.seq().saturating_sub(1);
713                futures::executor::block_on(store.append(
714                    WriteToken::recorder(),
715                    workflow_id,
716                    &[event],
717                    expected_seq,
718                ))
719                .map_err(|error| EngineSeamError::Recorder {
720                    reason: error.to_string(),
721                })?;
722            }
723            Ok(())
724        }
725    }
726}
727
728#[cfg(test)]
729mod tests {
730    use aion_core::{ContentType, Payload, WorkflowId};
731
732    use super::test_support::{DeliveredWorkflowMessage, FakeEngineHandle};
733    use super::{
734        EngineHandle, EngineSeamError, WorkflowMailboxMessage, WorkflowProcessHandle,
735        WorkflowResidency,
736    };
737
738    #[test]
739    fn fake_captures_delivered_message_for_resident_workflow()
740    -> Result<(), Box<dyn std::error::Error>> {
741        let engine = FakeEngineHandle::new();
742        let workflow_id = WorkflowId::new_v4();
743        let process = WorkflowProcessHandle::new(42);
744        engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
745
746        let resolved = engine.resolve_workflow(&workflow_id)?;
747        assert_eq!(resolved, WorkflowResidency::Resident(process));
748
749        let payload = Payload::new(ContentType::Json, b"null".to_vec());
750        let message = WorkflowMailboxMessage::SignalReceived {
751            name: "wake".to_owned(),
752            payload: payload.clone(),
753        };
754        engine.deliver_workflow_message(process, message)?;
755
756        assert_eq!(
757            engine.delivered_messages()?,
758            vec![(
759                process,
760                DeliveredWorkflowMessage::SignalReceived {
761                    name: "wake".to_owned(),
762                    payload,
763                }
764            )]
765        );
766        Ok(())
767    }
768
769    #[test]
770    fn fake_can_inject_delivery_failure() -> Result<(), Box<dyn std::error::Error>> {
771        let engine = FakeEngineHandle::new();
772        let process = WorkflowProcessHandle::new(43);
773        engine.push_delivery_response(Err(EngineSeamError::Delivery {
774            reason: "mailbox unavailable".to_owned(),
775        }))?;
776
777        let error = engine
778            .deliver_workflow_message(
779                process,
780                WorkflowMailboxMessage::SignalReceived {
781                    name: "wake".to_owned(),
782                    payload: Payload::new(ContentType::Json, b"null".to_vec()),
783                },
784            )
785            .err()
786            .ok_or_else(|| std::io::Error::other("delivery failure was not returned"))?;
787
788        assert!(matches!(error, EngineSeamError::Delivery { .. }));
789        assert!(engine.delivered_messages()?.is_empty());
790        assert!(engine.operations()?.is_empty());
791        Ok(())
792    }
793}