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