1use aion_core::{Event, Payload, TimerId, WorkflowError, WorkflowId};
10
11use crate::Pid;
12use chrono::{DateTime, Utc};
13use tokio::sync::oneshot;
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
20pub struct WorkflowProcessHandle {
21 pid: u64,
22}
23
24impl WorkflowProcessHandle {
25 #[must_use]
27 pub const fn new(pid: u64) -> Self {
28 Self { pid }
29 }
30
31 #[must_use]
33 pub const fn pid(self) -> u64 {
34 self.pid
35 }
36}
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub enum WorkflowResidency {
41 Resident(WorkflowProcessHandle),
43 NonResident,
45 Terminal,
47 Unknown,
49}
50
51pub type QueryReplySender = oneshot::Sender<crate::query::service::QueryResult>;
57
58#[derive(Debug)]
60pub enum WorkflowMailboxMessage {
61 TimerFired {
63 timer_id: TimerId,
65 fire_at: DateTime<Utc>,
67 },
68 SignalReceived {
70 name: String,
72 payload: Payload,
74 },
75 Query {
77 name: String,
79 payload: Payload,
81 reply_to: QueryReplySender,
83 },
84 ChildWorkflowCompleted {
86 child_workflow_id: WorkflowId,
88 correlation: u64,
90 result: Payload,
92 },
93 ChildWorkflowFailed {
95 child_workflow_id: WorkflowId,
97 correlation: u64,
99 error: WorkflowError,
101 },
102 ChildWorkflowCancelled {
104 child_workflow_id: WorkflowId,
106 correlation: u64,
108 },
109}
110
111#[derive(Clone, Debug, PartialEq, Eq)]
121pub struct ChildWorkflowSpawnRequest {
122 pub parent_workflow_id: WorkflowId,
124 pub child_workflow_id: WorkflowId,
126 pub workflow_type: String,
128 pub input: Payload,
130 pub package_version: aion_core::PackageVersion,
132}
133
134#[derive(Clone, Debug, PartialEq, Eq)]
136pub struct ChildWorkflowSpawnResult {
137 pub child_workflow_id: WorkflowId,
139 pub child_process: WorkflowProcessHandle,
141}
142
143#[derive(Clone, Debug, PartialEq, Eq)]
145pub struct TimerWheelEntry {
146 pub process: WorkflowProcessHandle,
148 pub timer_id: TimerId,
150 pub fire_at: DateTime<Utc>,
152}
153
154#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
156pub enum EngineSeamError {
157 #[error("workflow {workflow_id} is not resident")]
159 NonResident {
160 workflow_id: WorkflowId,
162 },
163
164 #[error("workflow {workflow_id} is terminal")]
166 Terminal {
167 workflow_id: WorkflowId,
169 },
170
171 #[error("workflow {workflow_id} is unknown")]
173 UnknownWorkflow {
174 workflow_id: WorkflowId,
176 },
177
178 #[error("mailbox delivery failed: {reason}")]
180 Delivery {
181 reason: String,
183 },
184
185 #[error("child workflow spawn failed: {reason}")]
187 ChildSpawn {
188 reason: String,
190 },
191
192 #[error("timer wheel operation failed: {reason}")]
194 TimerWheel {
195 reason: String,
197 },
198
199 #[error("linked child termination failed: {reason}")]
201 ChildTermination {
202 reason: String,
204 },
205
206 #[error("workflow recorder failed: {reason}")]
208 Recorder {
209 reason: String,
211 },
212}
213
214pub trait EngineHandle: Send + Sync {
222 fn resolve_workflow(
228 &self,
229 workflow_id: &WorkflowId,
230 ) -> Result<WorkflowResidency, EngineSeamError>;
231
232 fn deliver_workflow_message(
238 &self,
239 process: WorkflowProcessHandle,
240 message: WorkflowMailboxMessage,
241 ) -> Result<(), EngineSeamError>;
242
243 fn spawn_child_workflow(
249 &self,
250 request: ChildWorkflowSpawnRequest,
251 ) -> Result<ChildWorkflowSpawnResult, EngineSeamError>;
252
253 fn terminate_linked_child_workflow(
259 &self,
260 parent_workflow_id: &WorkflowId,
261 child_process: WorkflowProcessHandle,
262 correlation: u64,
263 ) -> Result<(), EngineSeamError>;
264
265 fn terminate_linked_activity(
271 &self,
272 parent_workflow_id: &WorkflowId,
273 activity_process: Pid,
274 correlation: u64,
275 ) -> Result<(), EngineSeamError>;
276
277 fn arm_timer(&self, entry: TimerWheelEntry) -> Result<(), EngineSeamError>;
283
284 fn disarm_timer(
290 &self,
291 process: WorkflowProcessHandle,
292 timer_id: &TimerId,
293 ) -> Result<(), EngineSeamError>;
294
295 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 #[derive(Clone, Debug, PartialEq)]
319 pub enum FakeEngineOperation {
320 Delivered {
322 process: WorkflowProcessHandle,
324 message: DeliveredWorkflowMessage,
326 },
327 ChildSpawnRequested(ChildWorkflowSpawnRequest),
329 TimerArmed(TimerWheelEntry),
331 TimerDisarmed {
333 process: WorkflowProcessHandle,
335 timer_id: TimerId,
337 },
338 LinkedChildWorkflowTerminated {
340 parent_workflow_id: WorkflowId,
342 child_process: WorkflowProcessHandle,
344 correlation: u64,
346 },
347 LinkedActivityTerminated {
349 parent_workflow_id: WorkflowId,
351 activity_process: Pid,
353 correlation: u64,
355 },
356 EventRecorded {
358 workflow_id: WorkflowId,
360 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 #[derive(Clone, Debug, PartialEq, Eq)]
381 pub enum DeliveredWorkflowMessage {
382 TimerFired {
384 timer_id: TimerId,
385 fire_at: DateTime<Utc>,
386 },
387 SignalReceived { name: String, payload: Payload },
389 Query { name: String, payload: Payload },
391 ChildWorkflowCompleted {
393 child_workflow_id: WorkflowId,
394 correlation: u64,
395 result: Payload,
396 },
397 ChildWorkflowFailed {
399 child_workflow_id: WorkflowId,
400 correlation: u64,
401 error: WorkflowError,
402 },
403 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 #[derive(Default)]
460 pub struct FakeEngineHandle {
461 state: Mutex<FakeEngineState>,
462 }
463
464 impl FakeEngineHandle {
465 #[must_use]
467 pub fn new() -> Self {
468 Self::default()
469 }
470
471 #[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 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 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 pub fn operations(&self) -> Result<Vec<FakeEngineOperation>, EngineSeamError> {
515 Ok(self.state()?.operations.clone())
516 }
517
518 pub fn delivered_messages(
524 &self,
525 ) -> Result<Vec<(WorkflowProcessHandle, DeliveredWorkflowMessage)>, EngineSeamError>
526 {
527 Ok(self.state()?.delivered.clone())
528 }
529
530 pub fn armed_timers(&self) -> Result<Vec<TimerWheelEntry>, EngineSeamError> {
536 Ok(self.state()?.armed_timers.clone())
537 }
538
539 pub fn push_child_spawn_response(
545 &self,
546 response: Result<ChildWorkflowSpawnResult, EngineSeamError>,
547 ) -> Result<(), EngineSeamError> {
548 self.state()?.child_spawn_responses.push_back(response);
549 Ok(())
550 }
551
552 pub fn recorded_events(&self) -> Result<Vec<(WorkflowId, Event)>, EngineSeamError> {
558 Ok(self.state()?.recorded_events.clone())
559 }
560
561 fn state(&self) -> Result<MutexGuard<'_, FakeEngineState>, EngineSeamError> {
562 self.state.lock().map_err(|_| EngineSeamError::Recorder {
563 reason: "fake engine state lock was poisoned".to_owned(),
564 })
565 }
566 }
567
568 impl EngineHandle for FakeEngineHandle {
569 fn resolve_workflow(
570 &self,
571 workflow_id: &WorkflowId,
572 ) -> Result<WorkflowResidency, EngineSeamError> {
573 Ok(self
574 .state()?
575 .residency
576 .get(workflow_id)
577 .copied()
578 .unwrap_or(WorkflowResidency::Unknown))
579 }
580
581 fn deliver_workflow_message(
582 &self,
583 process: WorkflowProcessHandle,
584 message: WorkflowMailboxMessage,
585 ) -> Result<(), EngineSeamError> {
586 let mut state = self.state()?;
587 if let Some(response) = state.delivery_responses.pop_front() {
588 response?;
589 }
590 let delivered = DeliveredWorkflowMessage::from_message(&message);
591 state.delivered.push((process, delivered.clone()));
592 state.operations.push(FakeEngineOperation::Delivered {
593 process,
594 message: delivered,
595 });
596 Ok(())
597 }
598
599 fn spawn_child_workflow(
600 &self,
601 request: ChildWorkflowSpawnRequest,
602 ) -> Result<ChildWorkflowSpawnResult, EngineSeamError> {
603 let mut state = self.state()?;
604 state
605 .operations
606 .push(FakeEngineOperation::ChildSpawnRequested(request.clone()));
607 if let Some(response) = state.child_spawn_responses.pop_front() {
608 response
609 } else {
610 Err(EngineSeamError::ChildSpawn {
611 reason: "fake child spawn response was not queued".to_owned(),
612 })
613 }
614 }
615
616 fn terminate_linked_child_workflow(
617 &self,
618 parent_workflow_id: &WorkflowId,
619 child_process: WorkflowProcessHandle,
620 correlation: u64,
621 ) -> Result<(), EngineSeamError> {
622 let mut state = self.state()?;
623 state
624 .operations
625 .push(FakeEngineOperation::LinkedChildWorkflowTerminated {
626 parent_workflow_id: parent_workflow_id.clone(),
627 child_process,
628 correlation,
629 });
630 Ok(())
631 }
632
633 fn terminate_linked_activity(
634 &self,
635 parent_workflow_id: &WorkflowId,
636 activity_process: Pid,
637 correlation: u64,
638 ) -> Result<(), EngineSeamError> {
639 let mut state = self.state()?;
640 state
641 .operations
642 .push(FakeEngineOperation::LinkedActivityTerminated {
643 parent_workflow_id: parent_workflow_id.clone(),
644 activity_process,
645 correlation,
646 });
647 Ok(())
648 }
649
650 fn arm_timer(&self, entry: TimerWheelEntry) -> Result<(), EngineSeamError> {
651 let mut state = self.state()?;
652 state.armed_timers.push(entry.clone());
653 state
654 .operations
655 .push(FakeEngineOperation::TimerArmed(entry));
656 Ok(())
657 }
658
659 fn disarm_timer(
660 &self,
661 process: WorkflowProcessHandle,
662 timer_id: &TimerId,
663 ) -> Result<(), EngineSeamError> {
664 let mut state = self.state()?;
665 state
666 .armed_timers
667 .retain(|entry| !(entry.process == process && &entry.timer_id == timer_id));
668 state.disarmed_timers.push((process, timer_id.clone()));
669 state.operations.push(FakeEngineOperation::TimerDisarmed {
670 process,
671 timer_id: timer_id.clone(),
672 });
673 Ok(())
674 }
675
676 fn record_workflow_event(
677 &self,
678 workflow_id: &WorkflowId,
679 event: Event,
680 ) -> Result<(), EngineSeamError> {
681 let mut state = self.state()?;
682 if let Some(response) = state.record_responses.pop_front() {
683 response?;
684 }
685 state
686 .recorded_events
687 .push((workflow_id.clone(), event.clone()));
688 let recorder_store = state.recorder_store.clone();
689 state.operations.push(FakeEngineOperation::EventRecorded {
690 workflow_id: workflow_id.clone(),
691 event: event.clone(),
692 });
693 drop(state);
694
695 if let Some(store) = recorder_store {
696 let expected_seq = event.seq().saturating_sub(1);
697 futures::executor::block_on(store.append(
698 WriteToken::recorder(),
699 workflow_id,
700 &[event],
701 expected_seq,
702 ))
703 .map_err(|error| EngineSeamError::Recorder {
704 reason: error.to_string(),
705 })?;
706 }
707 Ok(())
708 }
709 }
710}
711
712#[cfg(test)]
713mod tests {
714 use aion_core::{ContentType, Payload, WorkflowId};
715
716 use super::test_support::{DeliveredWorkflowMessage, FakeEngineHandle};
717 use super::{
718 EngineHandle, EngineSeamError, WorkflowMailboxMessage, WorkflowProcessHandle,
719 WorkflowResidency,
720 };
721
722 #[test]
723 fn fake_captures_delivered_message_for_resident_workflow()
724 -> Result<(), Box<dyn std::error::Error>> {
725 let engine = FakeEngineHandle::new();
726 let workflow_id = WorkflowId::new_v4();
727 let process = WorkflowProcessHandle::new(42);
728 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
729
730 let resolved = engine.resolve_workflow(&workflow_id)?;
731 assert_eq!(resolved, WorkflowResidency::Resident(process));
732
733 let payload = Payload::new(ContentType::Json, b"null".to_vec());
734 let message = WorkflowMailboxMessage::SignalReceived {
735 name: "wake".to_owned(),
736 payload: payload.clone(),
737 };
738 engine.deliver_workflow_message(process, message)?;
739
740 assert_eq!(
741 engine.delivered_messages()?,
742 vec![(
743 process,
744 DeliveredWorkflowMessage::SignalReceived {
745 name: "wake".to_owned(),
746 payload,
747 }
748 )]
749 );
750 Ok(())
751 }
752
753 #[test]
754 fn fake_can_inject_delivery_failure() -> Result<(), Box<dyn std::error::Error>> {
755 let engine = FakeEngineHandle::new();
756 let process = WorkflowProcessHandle::new(43);
757 engine.push_delivery_response(Err(EngineSeamError::Delivery {
758 reason: "mailbox unavailable".to_owned(),
759 }))?;
760
761 let error = engine
762 .deliver_workflow_message(
763 process,
764 WorkflowMailboxMessage::SignalReceived {
765 name: "wake".to_owned(),
766 payload: Payload::new(ContentType::Json, b"null".to_vec()),
767 },
768 )
769 .err()
770 .ok_or_else(|| std::io::Error::other("delivery failure was not returned"))?;
771
772 assert!(matches!(error, EngineSeamError::Delivery { .. }));
773 assert!(engine.delivered_messages()?.is_empty());
774 assert!(engine.operations()?.is_empty());
775 Ok(())
776 }
777}