1use aion_core::{Event, Payload, RunId, WorkflowId};
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(&self, target: &WorkflowHandle, name: String) -> Result<Payload, EngineError>;
87}
88
89#[derive(thiserror::Error, Clone, Copy, Debug, PartialEq, Eq)]
95#[error("event subscription lagged behind the live stream and skipped {skipped} events")]
96pub struct EventStreamLagged {
97 pub skipped: u64,
99}
100
101pub trait EventPublisher: Send + Sync {
106 fn subscribe(
113 &self,
114 filter: EventFilter,
115 ) -> BoxStream<'static, Result<Event, EventStreamLagged>>;
116}
117
118#[derive(Clone)]
120pub struct DelegatedSeams {
121 signal_router: Arc<dyn SignalRouter>,
122 query_service: Arc<dyn QueryService>,
123 event_publisher: Arc<dyn EventPublisher>,
124}
125
126impl DelegatedSeams {
127 #[must_use]
129 pub const fn new(
130 signal_router: Arc<dyn SignalRouter>,
131 query_service: Arc<dyn QueryService>,
132 event_publisher: Arc<dyn EventPublisher>,
133 ) -> Self {
134 Self {
135 signal_router,
136 query_service,
137 event_publisher,
138 }
139 }
140
141 #[must_use]
143 pub fn signal_router(&self) -> &dyn SignalRouter {
144 self.signal_router.as_ref()
145 }
146
147 #[must_use]
149 pub fn query_service(&self) -> &dyn QueryService {
150 self.query_service.as_ref()
151 }
152
153 #[must_use]
155 pub fn event_publisher(&self) -> &dyn EventPublisher {
156 self.event_publisher.as_ref()
157 }
158
159 pub(crate) fn signal_router_arc(&self) -> Arc<dyn SignalRouter> {
160 Arc::clone(&self.signal_router)
161 }
162
163 pub(crate) fn query_service_arc(&self) -> Arc<dyn QueryService> {
164 Arc::clone(&self.query_service)
165 }
166
167 pub(crate) fn event_publisher_arc(&self) -> Arc<dyn EventPublisher> {
168 Arc::clone(&self.event_publisher)
169 }
170}
171
172impl Default for DelegatedSeams {
173 fn default() -> Self {
174 Self::new(
175 Arc::new(DeferredSignalRouter),
176 Arc::new(DeferredQueryService),
177 Arc::new(DeferredEventPublisher),
178 )
179 }
180}
181
182#[derive(Debug, Default)]
184pub struct DeferredSignalRouter;
185
186#[async_trait]
187impl SignalRouter for DeferredSignalRouter {
188 async fn route(
189 &self,
190 target: &WorkflowHandle,
191 name: String,
192 payload: Payload,
193 ) -> Result<(), EngineError> {
194 let _ = (target, name, payload);
195 Err(EngineError::Runtime {
196 reason: "signal routing seam is not configured".to_owned(),
197 })
198 }
199}
200
201#[derive(Debug, Default)]
203pub struct DeferredQueryService;
204
205#[async_trait]
206impl QueryService for DeferredQueryService {
207 async fn query(&self, target: &WorkflowHandle, name: String) -> Result<Payload, EngineError> {
208 let _ = (target, name);
209 Err(EngineError::Runtime {
210 reason: "query service seam is not configured".to_owned(),
211 })
212 }
213}
214
215#[derive(Debug, Default)]
217pub struct DeferredEventPublisher;
218
219impl EventPublisher for DeferredEventPublisher {
220 fn subscribe(
221 &self,
222 filter: EventFilter,
223 ) -> BoxStream<'static, Result<Event, EventStreamLagged>> {
224 let _ = filter;
225 Box::pin(stream::empty())
226 }
227}
228
229impl Engine {
230 pub async fn signal(
238 &self,
239 id: &WorkflowId,
240 run: &RunId,
241 name: impl Into<String>,
242 payload: Payload,
243 ) -> Result<(), EngineError> {
244 let handle = if let Some(handle) = self.registry().get(id, run)? {
245 handle
246 } else {
247 let history = self.store().read_history(id).await?;
248 if run_has_terminal_history(&history, run) {
249 return Err(SignalRouterError::Terminal {
250 workflow_id: id.clone(),
251 run_id: run.clone(),
252 }
253 .into());
254 }
255 self.handle_after_birth_window(id, run, &history)
256 .await?
257 .ok_or_else(|| workflow_not_found(id, run))?
258 };
259 self.delegated()
260 .signal_router()
261 .route(&handle, name.into(), payload)
262 .await
263 }
264
265 pub async fn query(
274 &self,
275 id: &WorkflowId,
276 run: &RunId,
277 name: impl Into<String>,
278 ) -> Result<Payload, EngineError> {
279 let handle = if let Some(handle) = self.registry().get(id, run)? {
280 handle
281 } else {
282 let history = self.store().read_history(id).await?;
285 if run_has_terminal_history(&history, run) {
286 return Err(EngineError::Query(crate::query::QueryError::NotRunning(
287 id.clone(),
288 )));
289 }
290 self.handle_after_birth_window(id, run, &history)
291 .await?
292 .ok_or_else(|| workflow_not_found(id, run))?
293 };
294 self.delegated()
295 .query_service()
296 .query(&handle, name.into())
297 .await
298 }
299
300 pub(crate) async fn handle_after_birth_window(
311 &self,
312 id: &WorkflowId,
313 run: &RunId,
314 history: &[Event],
315 ) -> Result<Option<WorkflowHandle>, EngineError> {
316 let started = history
317 .iter()
318 .any(|event| matches!(event, Event::WorkflowStarted { run_id, .. } if run_id == run));
319 if !started {
320 return Ok(None);
321 }
322 wait_for_registered_handle(self.registry(), id, run, self.runtime().signal_delivery()).await
323 }
324
325 #[must_use]
330 pub fn subscribe(
331 &self,
332 filter: EventFilter,
333 ) -> BoxStream<'static, Result<Event, EventStreamLagged>> {
334 self.delegated().event_publisher().subscribe(filter)
335 }
336}
337
338pub(crate) fn run_has_terminal_history(history: &[Event], run: &RunId) -> bool {
339 let mut in_requested_run = false;
340 for event in history {
341 match event {
342 Event::WorkflowStarted { run_id, .. } => {
343 if in_requested_run {
344 return false;
345 }
346 in_requested_run = run_id == run;
347 }
348 Event::WorkflowCompleted { .. }
349 | Event::WorkflowFailed { .. }
350 | Event::WorkflowCancelled { .. }
351 | Event::WorkflowTimedOut { .. }
352 | Event::WorkflowContinuedAsNew { .. }
353 if in_requested_run =>
354 {
355 return true;
356 }
357 Event::SearchAttributesUpdated { .. }
358 | Event::ActivityScheduled { .. }
359 | Event::ActivityStarted { .. }
360 | Event::ActivityCompleted { .. }
361 | Event::ActivityFailed { .. }
362 | Event::ActivityCancelled { .. }
363 | Event::TimerStarted { .. }
364 | Event::TimerFired { .. }
365 | Event::TimerCancelled { .. }
366 | Event::WithTimeoutCompleted { .. }
367 | Event::SignalReceived { .. }
368 | Event::SignalSent { .. }
369 | Event::ChildWorkflowStarted { .. }
370 | Event::ChildWorkflowCompleted { .. }
371 | Event::ChildWorkflowFailed { .. }
372 | Event::ChildWorkflowCancelled { .. }
373 | Event::ScheduleCreated { .. }
374 | Event::ScheduleUpdated { .. }
375 | Event::SchedulePaused { .. }
376 | Event::ScheduleResumed { .. }
377 | Event::ScheduleDeleted { .. }
378 | Event::ScheduleTriggered { .. }
379 | Event::WorkflowCompleted { .. }
380 | Event::WorkflowFailed { .. }
381 | Event::WorkflowCancelled { .. }
382 | Event::WorkflowTimedOut { .. }
383 | Event::WorkflowContinuedAsNew { .. } => {}
384 }
385 }
386 false
387}
388
389pub(crate) async fn wait_for_registered_handle(
399 registry: &crate::registry::Registry,
400 id: &WorkflowId,
401 run: &RunId,
402 policy: crate::runtime::SignalDeliveryConfig,
403) -> Result<Option<WorkflowHandle>, EngineError> {
404 let budget = policy
405 .ready_timeout
406 .saturating_mul(policy.max_enqueue_attempts.max(1));
407 let deadline = std::time::Instant::now() + budget;
408 let mut backoff = policy.initial_backoff;
409 loop {
410 if let Some(handle) = registry.get(id, run)? {
411 return Ok(Some(handle));
412 }
413 if std::time::Instant::now() >= deadline {
414 return Ok(None);
415 }
416 tokio::time::sleep(backoff).await;
417 let doubled = backoff.saturating_mul(2);
418 backoff = if doubled > policy.max_backoff {
419 policy.max_backoff
420 } else {
421 doubled
422 };
423 }
424}
425
426const fn event_family(event: &Event) -> EventFamily {
427 match event {
428 Event::WorkflowStarted { .. }
429 | Event::WorkflowCompleted { .. }
430 | Event::WorkflowFailed { .. }
431 | Event::WorkflowCancelled { .. }
432 | Event::WorkflowTimedOut { .. }
433 | Event::WorkflowContinuedAsNew { .. }
434 | Event::SearchAttributesUpdated { .. } => EventFamily::Workflow,
435 Event::ActivityScheduled { .. }
436 | Event::ActivityStarted { .. }
437 | Event::ActivityCompleted { .. }
438 | Event::ActivityFailed { .. }
439 | Event::ActivityCancelled { .. } => EventFamily::Activity,
440 Event::TimerStarted { .. }
441 | Event::TimerFired { .. }
442 | Event::TimerCancelled { .. }
443 | Event::WithTimeoutCompleted { .. } => EventFamily::Timer,
444 Event::SignalReceived { .. } | Event::SignalSent { .. } => EventFamily::Signal,
445 Event::ChildWorkflowStarted { .. }
446 | Event::ChildWorkflowCompleted { .. }
447 | Event::ChildWorkflowFailed { .. }
448 | Event::ChildWorkflowCancelled { .. } => EventFamily::ChildWorkflow,
449 Event::ScheduleCreated { .. }
450 | Event::ScheduleUpdated { .. }
451 | Event::SchedulePaused { .. }
452 | Event::ScheduleResumed { .. }
453 | Event::ScheduleDeleted { .. }
454 | Event::ScheduleTriggered { .. } => EventFamily::Schedule,
455 }
456}
457
458#[cfg(test)]
459mod tests {
460 use std::sync::{Arc, Mutex};
461
462 use aion_core::{EventEnvelope, WorkflowStatus};
463 use aion_package::ContentHash;
464 use aion_store::visibility::VisibilityStore;
465 use aion_store::{EventStore, InMemoryStore};
466 use futures::{StreamExt, stream};
467 use serde_json::json;
468
469 use crate::durability::Recorder;
470 use crate::engine::api::EngineComponents;
471 use crate::registry::{CompletionNotifier, HandleResidency, WorkflowHandleParts};
472 use crate::{
473 Registry, RuntimeConfig, RuntimeHandle, SupervisionTree, WorkflowCatalog, WorkflowHandle,
474 };
475
476 use super::*;
477
478 #[derive(Debug, Default)]
479 struct SignalCapture {
480 calls: Mutex<Vec<(u64, String, Payload)>>,
481 }
482
483 #[async_trait]
484 impl SignalRouter for SignalCapture {
485 async fn route(
486 &self,
487 target: &WorkflowHandle,
488 name: String,
489 payload: Payload,
490 ) -> Result<(), EngineError> {
491 self.calls
492 .lock()
493 .map_err(|_| EngineError::RegistryPoisoned)?
494 .push((target.pid(), name, payload));
495 Ok(())
496 }
497 }
498
499 #[derive(Debug)]
500 struct QueryCapture {
501 calls: Mutex<Vec<(u64, String)>>,
502 reply: Payload,
503 }
504
505 #[async_trait]
506 impl QueryService for QueryCapture {
507 async fn query(
508 &self,
509 target: &WorkflowHandle,
510 name: String,
511 ) -> Result<Payload, EngineError> {
512 self.calls
513 .lock()
514 .map_err(|_| EngineError::RegistryPoisoned)?
515 .push((target.pid(), name));
516 Ok(self.reply.clone())
517 }
518 }
519
520 #[derive(Debug)]
521 struct FakePublisher {
522 events: Vec<Event>,
523 }
524
525 impl EventPublisher for FakePublisher {
526 fn subscribe(
527 &self,
528 filter: EventFilter,
529 ) -> BoxStream<'static, Result<Event, EventStreamLagged>> {
530 let events = self
531 .events
532 .iter()
533 .filter(|event| filter.matches(event))
534 .cloned()
535 .map(Ok)
536 .collect::<Vec<_>>();
537 stream::iter(events).boxed()
538 }
539 }
540
541 fn payload(label: &str) -> Result<Payload, aion_core::PayloadError> {
542 Payload::from_json(&json!({ "label": label }))
543 }
544
545 fn engine_with_seams(
546 signal_router: Arc<dyn SignalRouter>,
547 query_service: Arc<dyn QueryService>,
548 event_publisher: Arc<dyn EventPublisher>,
549 ) -> Result<Engine, EngineError> {
550 let backing = Arc::new(InMemoryStore::default());
551 let store: Arc<dyn EventStore> = Arc::clone(&backing) as _;
552 let visibility_store: Arc<dyn VisibilityStore> = backing;
553 Ok(Engine::new(EngineComponents {
554 store,
555 visibility_store,
556 runtime: Arc::new(RuntimeHandle::new(RuntimeConfig::new(Some(1)))?),
557 catalog: Arc::new(WorkflowCatalog::new()),
558 registry: Arc::new(Registry::default()),
559 supervision: Arc::new(SupervisionTree::new()),
560 delegated: DelegatedSeams::new(signal_router, query_service, event_publisher),
561 signal_handoff: Arc::new(crate::signal::SignalResumeHandoff::new()),
562 search_attribute_schema: Arc::new(aion_core::SearchAttributeSchema::new()),
563 visibility_reconciliation_task: None,
564 }))
565 }
566
567 async fn recorded_active_handle(
571 engine: &Engine,
572 ) -> Result<WorkflowHandle, Box<dyn std::error::Error>> {
573 let workflow_id = WorkflowId::new_v4();
574 let run_id = RunId::new_v4();
575 let store = engine.store();
576 let mut recorder = Recorder::new(workflow_id.clone(), Arc::clone(&store));
577 recorder
578 .record_workflow_started(
579 chrono::Utc::now(),
580 crate::durability::WorkflowStartRecord {
581 workflow_type: "checkout".to_owned(),
582 input: payload("input")?,
583 run_id: run_id.clone(),
584 parent_run_id: None,
585 package_version: aion_core::PackageVersion::new("a".repeat(64)),
586 },
587 )
588 .await?;
589 Ok(WorkflowHandle::new(WorkflowHandleParts {
590 workflow_id,
591 run_id,
592 pid: engine.runtime().spawn_test_process_with_trap_exit(true)?,
593 workflow_type: "checkout".to_owned(),
594 loaded_version: ContentHash::from_bytes([1; 32]),
595 cached_status: WorkflowStatus::Running,
596 residency: HandleResidency::Resident,
597 recorder,
598 completion: CompletionNotifier::new(),
599 }))
600 }
601
602 async fn insert_active_handle(
603 engine: &Engine,
604 ) -> Result<WorkflowHandle, Box<dyn std::error::Error>> {
605 let handle = recorded_active_handle(engine).await?;
606 engine.registry().insert(
607 (handle.workflow_id().clone(), handle.run_id().clone()),
608 handle.clone(),
609 )?;
610 Ok(handle)
611 }
612
613 fn envelope(seq: u64, workflow_id: &WorkflowId) -> EventEnvelope {
614 EventEnvelope {
615 seq,
616 recorded_at: chrono::Utc::now(),
617 workflow_id: workflow_id.clone(),
618 }
619 }
620
621 #[tokio::test(flavor = "multi_thread")]
628 async fn signal_inside_the_registration_birth_window_waits_for_the_handle()
629 -> Result<(), Box<dyn std::error::Error>> {
630 let signal = Arc::new(SignalCapture::default());
631 let engine = Arc::new(engine_with_seams(
632 signal.clone(),
633 Arc::new(DeferredQueryService),
634 Arc::new(DeferredEventPublisher),
635 )?);
636 let handle = recorded_active_handle(&engine).await?;
637
638 let late_engine = Arc::clone(&engine);
640 let late_handle = handle.clone();
641 let inserter = tokio::spawn(async move {
642 tokio::time::sleep(std::time::Duration::from_millis(15)).await;
643 late_engine.registry().insert(
644 (
645 late_handle.workflow_id().clone(),
646 late_handle.run_id().clone(),
647 ),
648 late_handle,
649 )
650 });
651
652 engine
653 .signal(
654 handle.workflow_id(),
655 handle.run_id(),
656 "approve",
657 payload("birth")?,
658 )
659 .await?;
660 inserter.await??;
661
662 let calls = signal
663 .calls
664 .lock()
665 .map_err(|_| EngineError::RegistryPoisoned)?;
666 assert_eq!(calls.len(), 1, "the signal must reach the routed handle");
667 drop(calls);
668 engine.shutdown()?;
669 Ok(())
670 }
671
672 #[tokio::test(flavor = "multi_thread")]
676 async fn signal_for_a_started_run_with_no_handle_fails_typed_after_the_budget()
677 -> Result<(), Box<dyn std::error::Error>> {
678 let engine = engine_with_seams(
679 Arc::new(SignalCapture::default()),
680 Arc::new(DeferredQueryService),
681 Arc::new(DeferredEventPublisher),
682 )?;
683 let handle = recorded_active_handle(&engine).await?;
684
685 let outcome = engine
686 .signal(
687 handle.workflow_id(),
688 handle.run_id(),
689 "approve",
690 payload("never")?,
691 )
692 .await;
693
694 assert!(matches!(outcome, Err(EngineError::WorkflowNotFound { .. })));
695 engine.shutdown()?;
696 Ok(())
697 }
698
699 #[tokio::test]
700 async fn signal_delegates_to_router_and_unknown_returns_not_found()
701 -> Result<(), Box<dyn std::error::Error>> {
702 let signal = Arc::new(SignalCapture::default());
703 let engine = engine_with_seams(
704 signal.clone(),
705 Arc::new(DeferredQueryService),
706 Arc::new(DeferredEventPublisher),
707 )?;
708 let handle = insert_active_handle(&engine).await?;
709 let sent_payload = payload("signal")?;
710
711 engine
712 .signal(
713 handle.workflow_id(),
714 handle.run_id(),
715 "approve",
716 sent_payload.clone(),
717 )
718 .await?;
719
720 {
721 let calls = signal
722 .calls
723 .lock()
724 .map_err(|_| EngineError::RegistryPoisoned)?;
725 assert_eq!(
726 calls.as_slice(),
727 &[(handle.pid(), "approve".to_owned(), sent_payload)]
728 );
729 }
730 let unknown = engine
731 .signal(
732 &WorkflowId::new_v4(),
733 &RunId::new_v4(),
734 "approve",
735 payload("unknown")?,
736 )
737 .await;
738 assert!(matches!(unknown, Err(EngineError::WorkflowNotFound { .. })));
739 engine.shutdown()?;
740 Ok(())
741 }
742
743 #[tokio::test]
744 async fn query_delegates_to_service_and_returns_payload()
745 -> Result<(), Box<dyn std::error::Error>> {
746 let reply = payload("reply")?;
747 let query = Arc::new(QueryCapture {
748 calls: Mutex::new(Vec::new()),
749 reply: reply.clone(),
750 });
751 let engine = engine_with_seams(
752 Arc::new(DeferredSignalRouter),
753 query.clone(),
754 Arc::new(DeferredEventPublisher),
755 )?;
756 let handle = insert_active_handle(&engine).await?;
757
758 let returned = engine
759 .query(handle.workflow_id(), handle.run_id(), "state")
760 .await?;
761
762 assert_eq!(returned, reply);
763 let calls = query
764 .calls
765 .lock()
766 .map_err(|_| EngineError::RegistryPoisoned)?;
767 assert_eq!(calls.as_slice(), &[(handle.pid(), "state".to_owned())]);
768 drop(calls);
769 engine.shutdown()?;
770 Ok(())
771 }
772
773 #[tokio::test]
774 async fn query_terminal_run_is_not_running_and_unknown_is_not_found()
775 -> Result<(), Box<dyn std::error::Error>> {
776 let engine = engine_with_seams(
777 Arc::new(DeferredSignalRouter),
778 Arc::new(DeferredQueryService),
779 Arc::new(DeferredEventPublisher),
780 )?;
781 let workflow_id = WorkflowId::new_v4();
783 let run_id = aion_core::RunId::new_v4();
784 let mut recorder = crate::durability::Recorder::new(workflow_id.clone(), engine.store());
785 recorder
786 .record_workflow_started(
787 chrono::Utc::now(),
788 crate::durability::WorkflowStartRecord {
789 workflow_type: "checkout".to_owned(),
790 input: payload("input")?,
791 run_id: run_id.clone(),
792 parent_run_id: None,
793 package_version: aion_core::PackageVersion::new("a".repeat(64)),
794 },
795 )
796 .await?;
797 recorder
798 .record_workflow_completed(chrono::Utc::now(), payload("result")?)
799 .await?;
800
801 let terminal = engine.query(&workflow_id, &run_id, "state").await;
802 assert!(matches!(
803 terminal,
804 Err(EngineError::Query(crate::query::QueryError::NotRunning(id))) if id == workflow_id
805 ));
806
807 let unknown = engine
808 .query(&WorkflowId::new_v4(), &RunId::new_v4(), "state")
809 .await;
810 assert!(matches!(unknown, Err(EngineError::WorkflowNotFound { .. })));
811 engine.shutdown()?;
812 Ok(())
813 }
814
815 #[tokio::test]
816 async fn subscribe_delegates_to_publisher_stream_with_filter()
817 -> Result<(), Box<dyn std::error::Error>> {
818 let workflow_id = WorkflowId::new_v4();
819 let other_id = WorkflowId::new_v4();
820 let matching = Event::SignalReceived {
821 envelope: envelope(1, &workflow_id),
822 name: "approved".to_owned(),
823 payload: payload("signal")?,
824 };
825 let filtered = Event::WorkflowStarted {
826 envelope: envelope(1, &other_id),
827 workflow_type: "checkout".to_owned(),
828 input: payload("input")?,
829 run_id: aion_core::RunId::new(uuid::Uuid::from_u128(1)),
830 parent_run_id: None,
831 package_version: aion_core::PackageVersion::new("a".repeat(64)),
832 };
833 let engine = engine_with_seams(
834 Arc::new(DeferredSignalRouter),
835 Arc::new(DeferredQueryService),
836 Arc::new(FakePublisher {
837 events: vec![matching.clone(), filtered],
838 }),
839 )?;
840
841 let events = engine
842 .subscribe(EventFilter {
843 workflow_id: Some(workflow_id),
844 run: None,
845 family: Some(EventFamily::Signal),
846 })
847 .collect::<Vec<_>>()
848 .await;
849
850 assert_eq!(events, vec![Ok(matching)]);
851 engine.shutdown()?;
852 Ok(())
853 }
854}