1use aion_core::{Event, Payload, RunId, WorkflowId, current_lease_terminal, run_segment};
4use async_trait::async_trait;
5use futures::stream::{self, BoxStream};
6use serde::{Deserialize, Serialize};
7use std::sync::Arc;
8
9use crate::{Engine, EngineError, SignalRouterError, WorkflowHandle};
10
11use super::api::workflow_not_found;
12
13#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)]
19pub struct EventFilter {
20 pub workflow_id: Option<WorkflowId>,
22 pub run: Option<RunId>,
24 pub family: Option<EventFamily>,
26}
27
28impl EventFilter {
29 #[must_use]
35 pub fn matches(&self, event: &Event) -> bool {
36 self.workflow_id
37 .as_ref()
38 .is_none_or(|workflow_id| event.workflow_id() == workflow_id)
39 && self
40 .family
41 .is_none_or(|family| family == event_family(event))
42 }
43}
44
45#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq)]
47pub enum EventFamily {
48 Workflow,
50 Activity,
52 Timer,
54 Signal,
56 ChildWorkflow,
58 Schedule,
60}
61
62#[async_trait]
68pub trait SignalRouter: Send + Sync {
69 async fn route(
71 &self,
72 target: &WorkflowHandle,
73 name: String,
74 payload: Payload,
75 ) -> Result<(), EngineError>;
76}
77
78#[async_trait]
84pub trait QueryService: Send + Sync {
85 async fn query(
93 &self,
94 target: &WorkflowHandle,
95 name: String,
96 arguments: Payload,
97 ) -> Result<Payload, EngineError>;
98}
99
100#[derive(thiserror::Error, Clone, Copy, Debug, PartialEq, Eq)]
106#[error("event subscription lagged behind the live stream and skipped {skipped} events")]
107pub struct EventStreamLagged {
108 pub skipped: u64,
110}
111
112pub trait EventPublisher: Send + Sync {
117 fn subscribe(
124 &self,
125 filter: EventFilter,
126 ) -> BoxStream<'static, Result<Event, EventStreamLagged>>;
127}
128
129#[derive(Clone)]
131pub struct DelegatedSeams {
132 signal_router: Arc<dyn SignalRouter>,
133 query_service: Arc<dyn QueryService>,
134 event_publisher: Arc<dyn EventPublisher>,
135}
136
137impl DelegatedSeams {
138 #[must_use]
140 pub const fn new(
141 signal_router: Arc<dyn SignalRouter>,
142 query_service: Arc<dyn QueryService>,
143 event_publisher: Arc<dyn EventPublisher>,
144 ) -> Self {
145 Self {
146 signal_router,
147 query_service,
148 event_publisher,
149 }
150 }
151
152 #[must_use]
154 pub fn signal_router(&self) -> &dyn SignalRouter {
155 self.signal_router.as_ref()
156 }
157
158 #[must_use]
160 pub fn query_service(&self) -> &dyn QueryService {
161 self.query_service.as_ref()
162 }
163
164 #[must_use]
166 pub fn event_publisher(&self) -> &dyn EventPublisher {
167 self.event_publisher.as_ref()
168 }
169
170 pub(crate) fn signal_router_arc(&self) -> Arc<dyn SignalRouter> {
171 Arc::clone(&self.signal_router)
172 }
173
174 pub(crate) fn query_service_arc(&self) -> Arc<dyn QueryService> {
175 Arc::clone(&self.query_service)
176 }
177
178 pub(crate) fn event_publisher_arc(&self) -> Arc<dyn EventPublisher> {
179 Arc::clone(&self.event_publisher)
180 }
181}
182
183impl Default for DelegatedSeams {
184 fn default() -> Self {
185 Self::new(
186 Arc::new(DeferredSignalRouter),
187 Arc::new(DeferredQueryService),
188 Arc::new(DeferredEventPublisher),
189 )
190 }
191}
192
193#[derive(Debug, Default)]
195pub struct DeferredSignalRouter;
196
197#[async_trait]
198impl SignalRouter for DeferredSignalRouter {
199 async fn route(
200 &self,
201 target: &WorkflowHandle,
202 name: String,
203 payload: Payload,
204 ) -> Result<(), EngineError> {
205 let _ = (target, name, payload);
206 Err(EngineError::Runtime {
207 reason: "signal routing seam is not configured".to_owned(),
208 })
209 }
210}
211
212#[derive(Debug, Default)]
214pub struct DeferredQueryService;
215
216#[async_trait]
217impl QueryService for DeferredQueryService {
218 async fn query(
219 &self,
220 target: &WorkflowHandle,
221 name: String,
222 arguments: Payload,
223 ) -> Result<Payload, EngineError> {
224 let _ = (target, name, arguments);
225 Err(EngineError::Runtime {
226 reason: "query service seam is not configured".to_owned(),
227 })
228 }
229}
230
231#[derive(Debug, Default)]
233pub struct DeferredEventPublisher;
234
235impl EventPublisher for DeferredEventPublisher {
236 fn subscribe(
237 &self,
238 filter: EventFilter,
239 ) -> BoxStream<'static, Result<Event, EventStreamLagged>> {
240 let _ = filter;
241 Box::pin(stream::empty())
242 }
243}
244
245impl Engine {
246 pub async fn signal(
264 &self,
265 id: &WorkflowId,
266 run: &RunId,
267 name: impl Into<String>,
268 payload: Payload,
269 ) -> Result<(), EngineError> {
270 let name = name.into();
271 let handle = if let Some(handle) = self.registry().get(id, run)? {
272 crate::signal::admission::admit_against_handle(
275 self.workflow_catalog(),
276 &handle,
277 &name,
278 &payload,
279 )?;
280 handle
281 } else {
282 let history = self.store().read_history(id).await?;
283 if run_has_terminal_history(&history, run) {
284 return Err(SignalRouterError::Terminal {
285 workflow_id: id.clone(),
286 run_id: run.clone(),
287 }
288 .into());
289 }
290 crate::signal::admission::admit_against_history(
296 self.workflow_catalog(),
297 id,
298 run,
299 &history,
300 &name,
301 &payload,
302 )?;
303 let segment = aion_core::run_segment(&history, run);
310 if matches!(
311 aion_core::status_from_events(segment),
312 aion_core::WorkflowStatus::Paused
313 ) {
314 let head = history.last().map(Event::seq).unwrap_or_default();
315 let mut recorder =
316 crate::durability::Recorder::resume_at(id.clone(), self.store(), head)
317 .with_visibility(run.clone(), self.visibility_store());
318 recorder
319 .record_signal_received(chrono::Utc::now(), name, payload)
320 .await?;
321 return Ok(());
322 }
323 self.handle_after_birth_window(id, run, &history)
324 .await?
325 .ok_or_else(|| workflow_not_found(id, run))?
326 };
327 self.delegated()
328 .signal_router()
329 .route(&handle, name, payload)
330 .await
331 }
332
333 pub async fn query(
350 &self,
351 id: &WorkflowId,
352 run: &RunId,
353 name: impl Into<String>,
354 arguments: Payload,
355 ) -> Result<Payload, EngineError> {
356 let handle = if let Some(handle) = self.registry().get(id, run)? {
357 handle
358 } else {
359 let history = self.store().read_history(id).await?;
362 if run_has_terminal_history(&history, run) {
363 return Err(EngineError::Query(crate::query::QueryError::NotRunning(
364 id.clone(),
365 )));
366 }
367 self.handle_after_birth_window(id, run, &history)
368 .await?
369 .ok_or_else(|| workflow_not_found(id, run))?
370 };
371 self.delegated()
372 .query_service()
373 .query(&handle, name.into(), arguments)
374 .await
375 }
376
377 pub(crate) async fn handle_after_birth_window(
388 &self,
389 id: &WorkflowId,
390 run: &RunId,
391 history: &[Event],
392 ) -> Result<Option<WorkflowHandle>, EngineError> {
393 let started = history
394 .iter()
395 .any(|event| matches!(event, Event::WorkflowStarted { run_id, .. } if run_id == run));
396 if !started {
397 return Ok(None);
398 }
399 wait_for_registered_handle(self.registry(), id, run, self.runtime().signal_delivery()).await
400 }
401
402 #[must_use]
407 pub fn subscribe(
408 &self,
409 filter: EventFilter,
410 ) -> BoxStream<'static, Result<Event, EventStreamLagged>> {
411 self.delegated().event_publisher().subscribe(filter)
412 }
413}
414
415pub(crate) fn run_has_terminal_history(history: &[Event], run: &RunId) -> bool {
416 current_lease_terminal(run_segment(history, run)).is_some()
421}
422
423pub(crate) async fn wait_for_registered_handle(
433 registry: &crate::registry::Registry,
434 id: &WorkflowId,
435 run: &RunId,
436 policy: crate::runtime::SignalDeliveryConfig,
437) -> Result<Option<WorkflowHandle>, EngineError> {
438 let budget = policy
439 .ready_timeout
440 .saturating_mul(policy.max_enqueue_attempts.max(1));
441 let deadline = std::time::Instant::now() + budget;
442 let mut backoff = policy.initial_backoff;
443 loop {
444 if let Some(handle) = registry.get(id, run)? {
445 return Ok(Some(handle));
446 }
447 if std::time::Instant::now() >= deadline {
448 return Ok(None);
449 }
450 tokio::time::sleep(backoff).await;
451 let doubled = backoff.saturating_mul(2);
452 backoff = if doubled > policy.max_backoff {
453 policy.max_backoff
454 } else {
455 doubled
456 };
457 }
458}
459
460const fn event_family(event: &Event) -> EventFamily {
461 match event {
462 Event::WorkflowStarted { .. }
463 | Event::WorkflowCompleted { .. }
464 | Event::WorkflowFailed { .. }
465 | Event::WorkflowCancelled { .. }
466 | Event::WorkflowTimedOut { .. }
467 | Event::WorkflowContinuedAsNew { .. }
468 | Event::WorkflowReopened { .. }
469 | Event::WorkflowPaused { .. }
470 | Event::WorkflowResumed { .. }
471 | Event::SearchAttributesUpdated { .. } => EventFamily::Workflow,
472 Event::ActivityScheduled { .. }
473 | Event::ActivityStarted { .. }
474 | Event::ActivityAdoptionOffered { .. }
475 | Event::ActivityCompleted { .. }
476 | Event::ActivityFailed { .. }
477 | Event::ActivityAdvisoryExhausted { .. }
478 | Event::ActivityFallbackRouted { .. }
479 | Event::ActivityCancelled { .. } => EventFamily::Activity,
480 Event::TimerStarted { .. }
481 | Event::TimerFired { .. }
482 | Event::TimerCancelled { .. }
483 | Event::WithTimeoutCompleted { .. } => EventFamily::Timer,
484 Event::SignalReceived { .. } | Event::SignalSent { .. } => EventFamily::Signal,
485 Event::ChildWorkflowStarted { .. }
486 | Event::ChildWorkflowCompleted { .. }
487 | Event::ChildWorkflowFailed { .. }
488 | Event::ChildWorkflowCancelled { .. } => EventFamily::ChildWorkflow,
489 Event::ScheduleCreated { .. }
490 | Event::ScheduleUpdated { .. }
491 | Event::SchedulePaused { .. }
492 | Event::ScheduleResumed { .. }
493 | Event::ScheduleDeleted { .. }
494 | Event::ScheduleTriggered { .. } => EventFamily::Schedule,
495 }
496}
497
498#[cfg(test)]
499mod tests {
500 use std::sync::{Arc, Mutex};
501
502 use aion_core::{EventEnvelope, WorkflowStatus};
503 use aion_package::ContentHash;
504 use aion_store::visibility::VisibilityStore;
505 use aion_store::{EventStore, InMemoryStore};
506 use futures::{StreamExt, stream};
507 use serde_json::json;
508
509 use crate::durability::Recorder;
510 use crate::engine::api::EngineComponents;
511 use crate::registry::{CompletionNotifier, HandleResidency, WorkflowHandleParts};
512 use crate::{
513 Registry, RuntimeConfig, RuntimeHandle, SupervisionTree, WorkflowCatalog, WorkflowHandle,
514 };
515
516 use super::*;
517
518 #[derive(Debug, Default)]
519 struct SignalCapture {
520 calls: Mutex<Vec<(u64, String, Payload)>>,
521 }
522
523 #[async_trait]
524 impl SignalRouter for SignalCapture {
525 async fn route(
526 &self,
527 target: &WorkflowHandle,
528 name: String,
529 payload: Payload,
530 ) -> Result<(), EngineError> {
531 self.calls
532 .lock()
533 .map_err(|_| EngineError::RegistryPoisoned)?
534 .push((target.pid(), name, payload));
535 Ok(())
536 }
537 }
538
539 #[derive(Debug)]
540 struct QueryCapture {
541 calls: Mutex<Vec<(u64, String, Payload)>>,
542 reply: Payload,
543 }
544
545 #[async_trait]
546 impl QueryService for QueryCapture {
547 async fn query(
548 &self,
549 target: &WorkflowHandle,
550 name: String,
551 arguments: Payload,
552 ) -> Result<Payload, EngineError> {
553 self.calls
554 .lock()
555 .map_err(|_| EngineError::RegistryPoisoned)?
556 .push((target.pid(), name, arguments));
557 Ok(self.reply.clone())
558 }
559 }
560
561 #[derive(Debug)]
562 struct FakePublisher {
563 events: Vec<Event>,
564 }
565
566 impl EventPublisher for FakePublisher {
567 fn subscribe(
568 &self,
569 filter: EventFilter,
570 ) -> BoxStream<'static, Result<Event, EventStreamLagged>> {
571 let events = self
572 .events
573 .iter()
574 .filter(|event| filter.matches(event))
575 .cloned()
576 .map(Ok)
577 .collect::<Vec<_>>();
578 stream::iter(events).boxed()
579 }
580 }
581
582 fn payload(label: &str) -> Result<Payload, aion_core::PayloadError> {
583 Payload::from_json(&json!({ "label": label }))
584 }
585
586 fn engine_with_seams(
587 signal_router: Arc<dyn SignalRouter>,
588 query_service: Arc<dyn QueryService>,
589 event_publisher: Arc<dyn EventPublisher>,
590 ) -> Result<Engine, EngineError> {
591 let backing = Arc::new(InMemoryStore::default());
592 let store: Arc<dyn EventStore> = Arc::clone(&backing) as _;
593 let visibility_store: Arc<dyn VisibilityStore> = backing;
594 Ok(Engine::new(EngineComponents {
595 store,
596 visibility_store,
597 runtime: Arc::new(RuntimeHandle::new(RuntimeConfig::new(Some(1)))?),
598 catalog: Arc::new(WorkflowCatalog::new()),
599 registry: Arc::new(Registry::default()),
600 supervision: Arc::new(SupervisionTree::new()),
601 delegated: DelegatedSeams::new(signal_router, query_service, event_publisher),
602 signal_handoff: Arc::new(crate::signal::SignalResumeHandoff::new()),
603 search_attribute_schema: Arc::new(aion_core::SearchAttributeSchema::new()),
604 visibility_reconciliation_task: None,
605 deferred_startup_recovery: None,
606 }))
607 }
608
609 async fn recorded_active_handle(
613 engine: &Engine,
614 ) -> Result<WorkflowHandle, Box<dyn std::error::Error>> {
615 let workflow_id = WorkflowId::new_v4();
616 let run_id = RunId::new_v4();
617 let store = engine.store();
618 let mut recorder = Recorder::new(workflow_id.clone(), Arc::clone(&store));
619 recorder
620 .record_workflow_started(
621 chrono::Utc::now(),
622 crate::durability::WorkflowStartRecord {
623 workflow_type: "checkout".to_owned(),
624 input: payload("input")?,
625 run_id: run_id.clone(),
626 parent_run_id: None,
627 parent_workflow_id: None,
628 package_version: aion_core::PackageVersion::new("a".repeat(64)),
629 },
630 )
631 .await?;
632 Ok(WorkflowHandle::new(WorkflowHandleParts {
633 workflow_id,
634 run_id,
635 pid: engine.runtime().spawn_test_process_with_trap_exit(true)?,
636 workflow_type: "checkout".to_owned(),
637 namespace: String::from("default"),
638 loaded_version: ContentHash::from_bytes([1; 32]),
639 cached_status: WorkflowStatus::Running,
640 residency: HandleResidency::Resident,
641 recorder,
642 completion: CompletionNotifier::new(),
643 }))
644 }
645
646 async fn insert_active_handle(
647 engine: &Engine,
648 ) -> Result<WorkflowHandle, Box<dyn std::error::Error>> {
649 let handle = recorded_active_handle(engine).await?;
650 engine.registry().insert(
651 (handle.workflow_id().clone(), handle.run_id().clone()),
652 handle.clone(),
653 )?;
654 Ok(handle)
655 }
656
657 fn envelope(seq: u64, workflow_id: &WorkflowId) -> EventEnvelope {
658 EventEnvelope {
659 seq,
660 recorded_at: chrono::Utc::now(),
661 workflow_id: workflow_id.clone(),
662 }
663 }
664
665 #[tokio::test(flavor = "multi_thread")]
672 async fn signal_inside_the_registration_birth_window_waits_for_the_handle()
673 -> Result<(), Box<dyn std::error::Error>> {
674 let signal = Arc::new(SignalCapture::default());
675 let engine = Arc::new(engine_with_seams(
676 signal.clone(),
677 Arc::new(DeferredQueryService),
678 Arc::new(DeferredEventPublisher),
679 )?);
680 let handle = recorded_active_handle(&engine).await?;
681
682 let late_engine = Arc::clone(&engine);
684 let late_handle = handle.clone();
685 let inserter = tokio::spawn(async move {
686 tokio::time::sleep(std::time::Duration::from_millis(15)).await;
687 late_engine.registry().insert(
688 (
689 late_handle.workflow_id().clone(),
690 late_handle.run_id().clone(),
691 ),
692 late_handle,
693 )
694 });
695
696 engine
697 .signal(
698 handle.workflow_id(),
699 handle.run_id(),
700 "approve",
701 payload("birth")?,
702 )
703 .await?;
704 inserter.await??;
705
706 let calls = signal
707 .calls
708 .lock()
709 .map_err(|_| EngineError::RegistryPoisoned)?;
710 assert_eq!(calls.len(), 1, "the signal must reach the routed handle");
711 drop(calls);
712 engine.shutdown()?;
713 Ok(())
714 }
715
716 #[tokio::test(flavor = "multi_thread")]
720 async fn signal_for_a_started_run_with_no_handle_fails_typed_after_the_budget()
721 -> Result<(), Box<dyn std::error::Error>> {
722 let engine = engine_with_seams(
723 Arc::new(SignalCapture::default()),
724 Arc::new(DeferredQueryService),
725 Arc::new(DeferredEventPublisher),
726 )?;
727 let handle = recorded_active_handle(&engine).await?;
728
729 let outcome = engine
730 .signal(
731 handle.workflow_id(),
732 handle.run_id(),
733 "approve",
734 payload("never")?,
735 )
736 .await;
737
738 assert!(matches!(outcome, Err(EngineError::WorkflowNotFound { .. })));
739 engine.shutdown()?;
740 Ok(())
741 }
742
743 #[tokio::test]
744 async fn signal_delegates_to_router_and_unknown_returns_not_found()
745 -> Result<(), Box<dyn std::error::Error>> {
746 let signal = Arc::new(SignalCapture::default());
747 let engine = engine_with_seams(
748 signal.clone(),
749 Arc::new(DeferredQueryService),
750 Arc::new(DeferredEventPublisher),
751 )?;
752 let handle = insert_active_handle(&engine).await?;
753 let sent_payload = payload("signal")?;
754
755 engine
756 .signal(
757 handle.workflow_id(),
758 handle.run_id(),
759 "approve",
760 sent_payload.clone(),
761 )
762 .await?;
763
764 {
765 let calls = signal
766 .calls
767 .lock()
768 .map_err(|_| EngineError::RegistryPoisoned)?;
769 assert_eq!(
770 calls.as_slice(),
771 &[(handle.pid(), "approve".to_owned(), sent_payload)]
772 );
773 }
774 let unknown = engine
775 .signal(
776 &WorkflowId::new_v4(),
777 &RunId::new_v4(),
778 "approve",
779 payload("unknown")?,
780 )
781 .await;
782 assert!(matches!(unknown, Err(EngineError::WorkflowNotFound { .. })));
783 engine.shutdown()?;
784 Ok(())
785 }
786
787 #[tokio::test]
788 async fn query_delegates_to_service_and_returns_payload()
789 -> Result<(), Box<dyn std::error::Error>> {
790 let reply = payload("reply")?;
791 let query = Arc::new(QueryCapture {
792 calls: Mutex::new(Vec::new()),
793 reply: reply.clone(),
794 });
795 let engine = engine_with_seams(
796 Arc::new(DeferredSignalRouter),
797 query.clone(),
798 Arc::new(DeferredEventPublisher),
799 )?;
800 let handle = insert_active_handle(&engine).await?;
801
802 let arguments = payload("arguments")?;
803 let returned = engine
804 .query(
805 handle.workflow_id(),
806 handle.run_id(),
807 "state",
808 arguments.clone(),
809 )
810 .await?;
811
812 assert_eq!(returned, reply);
813 let calls = query
814 .calls
815 .lock()
816 .map_err(|_| EngineError::RegistryPoisoned)?;
817 assert_eq!(
820 calls.as_slice(),
821 &[(handle.pid(), "state".to_owned(), arguments)]
822 );
823 drop(calls);
824 engine.shutdown()?;
825 Ok(())
826 }
827
828 #[tokio::test]
829 async fn query_terminal_run_is_not_running_and_unknown_is_not_found()
830 -> Result<(), Box<dyn std::error::Error>> {
831 let engine = engine_with_seams(
832 Arc::new(DeferredSignalRouter),
833 Arc::new(DeferredQueryService),
834 Arc::new(DeferredEventPublisher),
835 )?;
836 let workflow_id = WorkflowId::new_v4();
838 let run_id = aion_core::RunId::new_v4();
839 let mut recorder = crate::durability::Recorder::new(workflow_id.clone(), engine.store());
840 recorder
841 .record_workflow_started(
842 chrono::Utc::now(),
843 crate::durability::WorkflowStartRecord {
844 workflow_type: "checkout".to_owned(),
845 input: payload("input")?,
846 run_id: run_id.clone(),
847 parent_run_id: None,
848 parent_workflow_id: None,
849 package_version: aion_core::PackageVersion::new("a".repeat(64)),
850 },
851 )
852 .await?;
853 recorder
854 .record_workflow_completed(chrono::Utc::now(), payload("result")?)
855 .await?;
856
857 let terminal = engine
858 .query(&workflow_id, &run_id, "state", Payload::json_null())
859 .await;
860 assert!(matches!(
861 terminal,
862 Err(EngineError::Query(crate::query::QueryError::NotRunning(id))) if id == workflow_id
863 ));
864
865 let unknown = engine
866 .query(
867 &WorkflowId::new_v4(),
868 &RunId::new_v4(),
869 "state",
870 Payload::json_null(),
871 )
872 .await;
873 assert!(matches!(unknown, Err(EngineError::WorkflowNotFound { .. })));
874 engine.shutdown()?;
875 Ok(())
876 }
877
878 #[tokio::test]
879 async fn subscribe_delegates_to_publisher_stream_with_filter()
880 -> Result<(), Box<dyn std::error::Error>> {
881 let workflow_id = WorkflowId::new_v4();
882 let other_id = WorkflowId::new_v4();
883 let matching = Event::SignalReceived {
884 envelope: envelope(1, &workflow_id),
885 name: "approved".to_owned(),
886 payload: payload("signal")?,
887 };
888 let filtered = Event::WorkflowStarted {
889 envelope: envelope(1, &other_id),
890 workflow_type: "checkout".to_owned(),
891 input: payload("input")?,
892 run_id: aion_core::RunId::new(uuid::Uuid::from_u128(1)),
893 parent_run_id: None,
894 parent_workflow_id: None,
895 package_version: aion_core::PackageVersion::new("a".repeat(64)),
896 };
897 let engine = engine_with_seams(
898 Arc::new(DeferredSignalRouter),
899 Arc::new(DeferredQueryService),
900 Arc::new(FakePublisher {
901 events: vec![matching.clone(), filtered],
902 }),
903 )?;
904
905 let events = engine
906 .subscribe(EventFilter {
907 workflow_id: Some(workflow_id),
908 run: None,
909 family: Some(EventFamily::Signal),
910 })
911 .collect::<Vec<_>>()
912 .await;
913
914 assert_eq!(events, vec![Ok(matching)]);
915 engine.shutdown()?;
916 Ok(())
917 }
918}