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(&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(
248 &self,
249 id: &WorkflowId,
250 run: &RunId,
251 name: impl Into<String>,
252 payload: Payload,
253 ) -> Result<(), EngineError> {
254 let name = name.into();
255 let handle = if let Some(handle) = self.registry().get(id, run)? {
256 crate::signal::admission::admit_against_handle(
259 self.workflow_catalog(),
260 &handle,
261 &name,
262 &payload,
263 )?;
264 handle
265 } else {
266 let history = self.store().read_history(id).await?;
267 if run_has_terminal_history(&history, run) {
268 return Err(SignalRouterError::Terminal {
269 workflow_id: id.clone(),
270 run_id: run.clone(),
271 }
272 .into());
273 }
274 crate::signal::admission::admit_against_history(
280 self.workflow_catalog(),
281 id,
282 run,
283 &history,
284 &name,
285 &payload,
286 )?;
287 let segment = aion_core::run_segment(&history, run);
294 if matches!(
295 aion_core::status_from_events(segment),
296 aion_core::WorkflowStatus::Paused
297 ) {
298 let head = history.last().map(Event::seq).unwrap_or_default();
299 let mut recorder =
300 crate::durability::Recorder::resume_at(id.clone(), self.store(), head)
301 .with_visibility(run.clone(), self.visibility_store());
302 recorder
303 .record_signal_received(chrono::Utc::now(), name, payload)
304 .await?;
305 return Ok(());
306 }
307 self.handle_after_birth_window(id, run, &history)
308 .await?
309 .ok_or_else(|| workflow_not_found(id, run))?
310 };
311 self.delegated()
312 .signal_router()
313 .route(&handle, name, payload)
314 .await
315 }
316
317 pub async fn query(
326 &self,
327 id: &WorkflowId,
328 run: &RunId,
329 name: impl Into<String>,
330 ) -> Result<Payload, EngineError> {
331 let handle = if let Some(handle) = self.registry().get(id, run)? {
332 handle
333 } else {
334 let history = self.store().read_history(id).await?;
337 if run_has_terminal_history(&history, run) {
338 return Err(EngineError::Query(crate::query::QueryError::NotRunning(
339 id.clone(),
340 )));
341 }
342 self.handle_after_birth_window(id, run, &history)
343 .await?
344 .ok_or_else(|| workflow_not_found(id, run))?
345 };
346 self.delegated()
347 .query_service()
348 .query(&handle, name.into())
349 .await
350 }
351
352 pub(crate) async fn handle_after_birth_window(
363 &self,
364 id: &WorkflowId,
365 run: &RunId,
366 history: &[Event],
367 ) -> Result<Option<WorkflowHandle>, EngineError> {
368 let started = history
369 .iter()
370 .any(|event| matches!(event, Event::WorkflowStarted { run_id, .. } if run_id == run));
371 if !started {
372 return Ok(None);
373 }
374 wait_for_registered_handle(self.registry(), id, run, self.runtime().signal_delivery()).await
375 }
376
377 #[must_use]
382 pub fn subscribe(
383 &self,
384 filter: EventFilter,
385 ) -> BoxStream<'static, Result<Event, EventStreamLagged>> {
386 self.delegated().event_publisher().subscribe(filter)
387 }
388}
389
390pub(crate) fn run_has_terminal_history(history: &[Event], run: &RunId) -> bool {
391 current_lease_terminal(run_segment(history, run)).is_some()
396}
397
398pub(crate) async fn wait_for_registered_handle(
408 registry: &crate::registry::Registry,
409 id: &WorkflowId,
410 run: &RunId,
411 policy: crate::runtime::SignalDeliveryConfig,
412) -> Result<Option<WorkflowHandle>, EngineError> {
413 let budget = policy
414 .ready_timeout
415 .saturating_mul(policy.max_enqueue_attempts.max(1));
416 let deadline = std::time::Instant::now() + budget;
417 let mut backoff = policy.initial_backoff;
418 loop {
419 if let Some(handle) = registry.get(id, run)? {
420 return Ok(Some(handle));
421 }
422 if std::time::Instant::now() >= deadline {
423 return Ok(None);
424 }
425 tokio::time::sleep(backoff).await;
426 let doubled = backoff.saturating_mul(2);
427 backoff = if doubled > policy.max_backoff {
428 policy.max_backoff
429 } else {
430 doubled
431 };
432 }
433}
434
435const fn event_family(event: &Event) -> EventFamily {
436 match event {
437 Event::WorkflowStarted { .. }
438 | Event::WorkflowCompleted { .. }
439 | Event::WorkflowFailed { .. }
440 | Event::WorkflowCancelled { .. }
441 | Event::WorkflowTimedOut { .. }
442 | Event::WorkflowContinuedAsNew { .. }
443 | Event::WorkflowReopened { .. }
444 | Event::WorkflowPaused { .. }
445 | Event::WorkflowResumed { .. }
446 | Event::SearchAttributesUpdated { .. } => EventFamily::Workflow,
447 Event::ActivityScheduled { .. }
448 | Event::ActivityStarted { .. }
449 | Event::ActivityCompleted { .. }
450 | Event::ActivityFailed { .. }
451 | Event::ActivityAdvisoryExhausted { .. }
452 | Event::ActivityCancelled { .. } => EventFamily::Activity,
453 Event::TimerStarted { .. }
454 | Event::TimerFired { .. }
455 | Event::TimerCancelled { .. }
456 | Event::WithTimeoutCompleted { .. } => EventFamily::Timer,
457 Event::SignalReceived { .. } | Event::SignalSent { .. } => EventFamily::Signal,
458 Event::ChildWorkflowStarted { .. }
459 | Event::ChildWorkflowCompleted { .. }
460 | Event::ChildWorkflowFailed { .. }
461 | Event::ChildWorkflowCancelled { .. } => EventFamily::ChildWorkflow,
462 Event::ScheduleCreated { .. }
463 | Event::ScheduleUpdated { .. }
464 | Event::SchedulePaused { .. }
465 | Event::ScheduleResumed { .. }
466 | Event::ScheduleDeleted { .. }
467 | Event::ScheduleTriggered { .. } => EventFamily::Schedule,
468 }
469}
470
471#[cfg(test)]
472mod tests {
473 use std::sync::{Arc, Mutex};
474
475 use aion_core::{EventEnvelope, WorkflowStatus};
476 use aion_package::ContentHash;
477 use aion_store::visibility::VisibilityStore;
478 use aion_store::{EventStore, InMemoryStore};
479 use futures::{StreamExt, stream};
480 use serde_json::json;
481
482 use crate::durability::Recorder;
483 use crate::engine::api::EngineComponents;
484 use crate::registry::{CompletionNotifier, HandleResidency, WorkflowHandleParts};
485 use crate::{
486 Registry, RuntimeConfig, RuntimeHandle, SupervisionTree, WorkflowCatalog, WorkflowHandle,
487 };
488
489 use super::*;
490
491 #[derive(Debug, Default)]
492 struct SignalCapture {
493 calls: Mutex<Vec<(u64, String, Payload)>>,
494 }
495
496 #[async_trait]
497 impl SignalRouter for SignalCapture {
498 async fn route(
499 &self,
500 target: &WorkflowHandle,
501 name: String,
502 payload: Payload,
503 ) -> Result<(), EngineError> {
504 self.calls
505 .lock()
506 .map_err(|_| EngineError::RegistryPoisoned)?
507 .push((target.pid(), name, payload));
508 Ok(())
509 }
510 }
511
512 #[derive(Debug)]
513 struct QueryCapture {
514 calls: Mutex<Vec<(u64, String)>>,
515 reply: Payload,
516 }
517
518 #[async_trait]
519 impl QueryService for QueryCapture {
520 async fn query(
521 &self,
522 target: &WorkflowHandle,
523 name: String,
524 ) -> Result<Payload, EngineError> {
525 self.calls
526 .lock()
527 .map_err(|_| EngineError::RegistryPoisoned)?
528 .push((target.pid(), name));
529 Ok(self.reply.clone())
530 }
531 }
532
533 #[derive(Debug)]
534 struct FakePublisher {
535 events: Vec<Event>,
536 }
537
538 impl EventPublisher for FakePublisher {
539 fn subscribe(
540 &self,
541 filter: EventFilter,
542 ) -> BoxStream<'static, Result<Event, EventStreamLagged>> {
543 let events = self
544 .events
545 .iter()
546 .filter(|event| filter.matches(event))
547 .cloned()
548 .map(Ok)
549 .collect::<Vec<_>>();
550 stream::iter(events).boxed()
551 }
552 }
553
554 fn payload(label: &str) -> Result<Payload, aion_core::PayloadError> {
555 Payload::from_json(&json!({ "label": label }))
556 }
557
558 fn engine_with_seams(
559 signal_router: Arc<dyn SignalRouter>,
560 query_service: Arc<dyn QueryService>,
561 event_publisher: Arc<dyn EventPublisher>,
562 ) -> Result<Engine, EngineError> {
563 let backing = Arc::new(InMemoryStore::default());
564 let store: Arc<dyn EventStore> = Arc::clone(&backing) as _;
565 let visibility_store: Arc<dyn VisibilityStore> = backing;
566 Ok(Engine::new(EngineComponents {
567 store,
568 visibility_store,
569 runtime: Arc::new(RuntimeHandle::new(RuntimeConfig::new(Some(1)))?),
570 catalog: Arc::new(WorkflowCatalog::new()),
571 registry: Arc::new(Registry::default()),
572 supervision: Arc::new(SupervisionTree::new()),
573 delegated: DelegatedSeams::new(signal_router, query_service, event_publisher),
574 signal_handoff: Arc::new(crate::signal::SignalResumeHandoff::new()),
575 search_attribute_schema: Arc::new(aion_core::SearchAttributeSchema::new()),
576 visibility_reconciliation_task: None,
577 }))
578 }
579
580 async fn recorded_active_handle(
584 engine: &Engine,
585 ) -> Result<WorkflowHandle, Box<dyn std::error::Error>> {
586 let workflow_id = WorkflowId::new_v4();
587 let run_id = RunId::new_v4();
588 let store = engine.store();
589 let mut recorder = Recorder::new(workflow_id.clone(), Arc::clone(&store));
590 recorder
591 .record_workflow_started(
592 chrono::Utc::now(),
593 crate::durability::WorkflowStartRecord {
594 workflow_type: "checkout".to_owned(),
595 input: payload("input")?,
596 run_id: run_id.clone(),
597 parent_run_id: None,
598 package_version: aion_core::PackageVersion::new("a".repeat(64)),
599 },
600 )
601 .await?;
602 Ok(WorkflowHandle::new(WorkflowHandleParts {
603 workflow_id,
604 run_id,
605 pid: engine.runtime().spawn_test_process_with_trap_exit(true)?,
606 workflow_type: "checkout".to_owned(),
607 namespace: String::from("default"),
608 loaded_version: ContentHash::from_bytes([1; 32]),
609 cached_status: WorkflowStatus::Running,
610 residency: HandleResidency::Resident,
611 recorder,
612 completion: CompletionNotifier::new(),
613 }))
614 }
615
616 async fn insert_active_handle(
617 engine: &Engine,
618 ) -> Result<WorkflowHandle, Box<dyn std::error::Error>> {
619 let handle = recorded_active_handle(engine).await?;
620 engine.registry().insert(
621 (handle.workflow_id().clone(), handle.run_id().clone()),
622 handle.clone(),
623 )?;
624 Ok(handle)
625 }
626
627 fn envelope(seq: u64, workflow_id: &WorkflowId) -> EventEnvelope {
628 EventEnvelope {
629 seq,
630 recorded_at: chrono::Utc::now(),
631 workflow_id: workflow_id.clone(),
632 }
633 }
634
635 #[tokio::test(flavor = "multi_thread")]
642 async fn signal_inside_the_registration_birth_window_waits_for_the_handle()
643 -> Result<(), Box<dyn std::error::Error>> {
644 let signal = Arc::new(SignalCapture::default());
645 let engine = Arc::new(engine_with_seams(
646 signal.clone(),
647 Arc::new(DeferredQueryService),
648 Arc::new(DeferredEventPublisher),
649 )?);
650 let handle = recorded_active_handle(&engine).await?;
651
652 let late_engine = Arc::clone(&engine);
654 let late_handle = handle.clone();
655 let inserter = tokio::spawn(async move {
656 tokio::time::sleep(std::time::Duration::from_millis(15)).await;
657 late_engine.registry().insert(
658 (
659 late_handle.workflow_id().clone(),
660 late_handle.run_id().clone(),
661 ),
662 late_handle,
663 )
664 });
665
666 engine
667 .signal(
668 handle.workflow_id(),
669 handle.run_id(),
670 "approve",
671 payload("birth")?,
672 )
673 .await?;
674 inserter.await??;
675
676 let calls = signal
677 .calls
678 .lock()
679 .map_err(|_| EngineError::RegistryPoisoned)?;
680 assert_eq!(calls.len(), 1, "the signal must reach the routed handle");
681 drop(calls);
682 engine.shutdown()?;
683 Ok(())
684 }
685
686 #[tokio::test(flavor = "multi_thread")]
690 async fn signal_for_a_started_run_with_no_handle_fails_typed_after_the_budget()
691 -> Result<(), Box<dyn std::error::Error>> {
692 let engine = engine_with_seams(
693 Arc::new(SignalCapture::default()),
694 Arc::new(DeferredQueryService),
695 Arc::new(DeferredEventPublisher),
696 )?;
697 let handle = recorded_active_handle(&engine).await?;
698
699 let outcome = engine
700 .signal(
701 handle.workflow_id(),
702 handle.run_id(),
703 "approve",
704 payload("never")?,
705 )
706 .await;
707
708 assert!(matches!(outcome, Err(EngineError::WorkflowNotFound { .. })));
709 engine.shutdown()?;
710 Ok(())
711 }
712
713 #[tokio::test]
714 async fn signal_delegates_to_router_and_unknown_returns_not_found()
715 -> Result<(), Box<dyn std::error::Error>> {
716 let signal = Arc::new(SignalCapture::default());
717 let engine = engine_with_seams(
718 signal.clone(),
719 Arc::new(DeferredQueryService),
720 Arc::new(DeferredEventPublisher),
721 )?;
722 let handle = insert_active_handle(&engine).await?;
723 let sent_payload = payload("signal")?;
724
725 engine
726 .signal(
727 handle.workflow_id(),
728 handle.run_id(),
729 "approve",
730 sent_payload.clone(),
731 )
732 .await?;
733
734 {
735 let calls = signal
736 .calls
737 .lock()
738 .map_err(|_| EngineError::RegistryPoisoned)?;
739 assert_eq!(
740 calls.as_slice(),
741 &[(handle.pid(), "approve".to_owned(), sent_payload)]
742 );
743 }
744 let unknown = engine
745 .signal(
746 &WorkflowId::new_v4(),
747 &RunId::new_v4(),
748 "approve",
749 payload("unknown")?,
750 )
751 .await;
752 assert!(matches!(unknown, Err(EngineError::WorkflowNotFound { .. })));
753 engine.shutdown()?;
754 Ok(())
755 }
756
757 #[tokio::test]
758 async fn query_delegates_to_service_and_returns_payload()
759 -> Result<(), Box<dyn std::error::Error>> {
760 let reply = payload("reply")?;
761 let query = Arc::new(QueryCapture {
762 calls: Mutex::new(Vec::new()),
763 reply: reply.clone(),
764 });
765 let engine = engine_with_seams(
766 Arc::new(DeferredSignalRouter),
767 query.clone(),
768 Arc::new(DeferredEventPublisher),
769 )?;
770 let handle = insert_active_handle(&engine).await?;
771
772 let returned = engine
773 .query(handle.workflow_id(), handle.run_id(), "state")
774 .await?;
775
776 assert_eq!(returned, reply);
777 let calls = query
778 .calls
779 .lock()
780 .map_err(|_| EngineError::RegistryPoisoned)?;
781 assert_eq!(calls.as_slice(), &[(handle.pid(), "state".to_owned())]);
782 drop(calls);
783 engine.shutdown()?;
784 Ok(())
785 }
786
787 #[tokio::test]
788 async fn query_terminal_run_is_not_running_and_unknown_is_not_found()
789 -> Result<(), Box<dyn std::error::Error>> {
790 let engine = engine_with_seams(
791 Arc::new(DeferredSignalRouter),
792 Arc::new(DeferredQueryService),
793 Arc::new(DeferredEventPublisher),
794 )?;
795 let workflow_id = WorkflowId::new_v4();
797 let run_id = aion_core::RunId::new_v4();
798 let mut recorder = crate::durability::Recorder::new(workflow_id.clone(), engine.store());
799 recorder
800 .record_workflow_started(
801 chrono::Utc::now(),
802 crate::durability::WorkflowStartRecord {
803 workflow_type: "checkout".to_owned(),
804 input: payload("input")?,
805 run_id: run_id.clone(),
806 parent_run_id: None,
807 package_version: aion_core::PackageVersion::new("a".repeat(64)),
808 },
809 )
810 .await?;
811 recorder
812 .record_workflow_completed(chrono::Utc::now(), payload("result")?)
813 .await?;
814
815 let terminal = engine.query(&workflow_id, &run_id, "state").await;
816 assert!(matches!(
817 terminal,
818 Err(EngineError::Query(crate::query::QueryError::NotRunning(id))) if id == workflow_id
819 ));
820
821 let unknown = engine
822 .query(&WorkflowId::new_v4(), &RunId::new_v4(), "state")
823 .await;
824 assert!(matches!(unknown, Err(EngineError::WorkflowNotFound { .. })));
825 engine.shutdown()?;
826 Ok(())
827 }
828
829 #[tokio::test]
830 async fn subscribe_delegates_to_publisher_stream_with_filter()
831 -> Result<(), Box<dyn std::error::Error>> {
832 let workflow_id = WorkflowId::new_v4();
833 let other_id = WorkflowId::new_v4();
834 let matching = Event::SignalReceived {
835 envelope: envelope(1, &workflow_id),
836 name: "approved".to_owned(),
837 payload: payload("signal")?,
838 };
839 let filtered = Event::WorkflowStarted {
840 envelope: envelope(1, &other_id),
841 workflow_type: "checkout".to_owned(),
842 input: payload("input")?,
843 run_id: aion_core::RunId::new(uuid::Uuid::from_u128(1)),
844 parent_run_id: None,
845 package_version: aion_core::PackageVersion::new("a".repeat(64)),
846 };
847 let engine = engine_with_seams(
848 Arc::new(DeferredSignalRouter),
849 Arc::new(DeferredQueryService),
850 Arc::new(FakePublisher {
851 events: vec![matching.clone(), filtered],
852 }),
853 )?;
854
855 let events = engine
856 .subscribe(EventFilter {
857 workflow_id: Some(workflow_id),
858 run: None,
859 family: Some(EventFamily::Signal),
860 })
861 .collect::<Vec<_>>()
862 .await;
863
864 assert_eq!(events, vec![Ok(matching)]);
865 engine.shutdown()?;
866 Ok(())
867 }
868}