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