1use std::collections::BTreeMap;
49use std::sync::{Arc, OnceLock};
50use std::time::{Duration, Instant};
51
52use aion::{ActivityDispatch, ActivityDispatcher};
53use aion_core::{ActivityId, ContentType, Payload, RunId, WorkflowId};
54use aion_proto::{ProtoActivityId, ProtoActivityTask, ProtoPayload, ProtoWorkflowId};
55use dashmap::DashMap;
56
57use super::dispatch::{ActivityCompletion, ActivityCompletionOutcome, ActivityCompletionSink};
58use super::heartbeat::{HeartbeatTracker, InFlightActivity};
59use super::registry::{ConnectedWorkerRegistry, WorkerHandle, WorkerId, WorkerMessage};
60use crate::error::ServerError;
61use crate::shutdown::DrainState;
62use tracing::info_span;
63
64type SyncSender = std::sync::mpsc::SyncSender<Result<String, String>>;
65type SyncReceiver = std::sync::mpsc::Receiver<Result<String, String>>;
66
67type PendingActivityKey = (WorkflowId, ActivityId);
85
86pub trait OutboxDeliveryCallback: Send + Sync {
96 fn deliver_completion(
106 &self,
107 workflow_id: &WorkflowId,
108 activity_id: &ActivityId,
109 run_id: Option<&RunId>,
110 result: String,
111 ) -> Result<bool, ServerError>;
112
113 fn deliver_failure(
120 &self,
121 workflow_id: &WorkflowId,
122 activity_id: &ActivityId,
123 run_id: Option<&RunId>,
124 reason: String,
125 ) -> Result<bool, ServerError>;
126}
127
128#[derive(Clone, Default)]
140pub struct PendingActivities {
141 pending: Arc<DashMap<PendingActivityKey, SyncSender>>,
142 outbox_delivery: Arc<OnceLock<Arc<dyn OutboxDeliveryCallback>>>,
143}
144
145impl std::fmt::Debug for PendingActivities {
146 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147 formatter
148 .debug_struct("PendingActivities")
149 .field("pending", &self.pending.len())
150 .field(
151 "outbox_delivery_installed",
152 &self.outbox_delivery.get().is_some(),
153 )
154 .finish()
155 }
156}
157
158impl PendingActivities {
159 fn insert(&self, workflow_id: WorkflowId, activity_id: ActivityId) -> SyncReceiver {
160 let (tx, rx) = std::sync::mpsc::sync_channel(1);
161 self.pending.insert((workflow_id, activity_id), tx);
162 rx
163 }
164
165 pub fn set_outbox_delivery(&self, callback: Arc<dyn OutboxDeliveryCallback>) {
171 if self.outbox_delivery.set(callback).is_err() {
172 tracing::warn!("outbox delivery callback already installed; ignoring duplicate set");
173 }
174 }
175
176 fn complete(
184 &self,
185 workflow_id: &WorkflowId,
186 activity_id: &ActivityId,
187 run_id: Option<&RunId>,
188 result: Result<String, String>,
189 ) -> bool {
190 let matched = self
193 .pending
194 .remove(&(workflow_id.clone(), activity_id.clone()));
195 if let Some((_, sender)) = matched {
196 return sender.send(result).is_ok();
197 }
198 let Some(callback) = self.outbox_delivery.get() else {
199 return false;
201 };
202 let outcome = match result {
203 Ok(payload) => callback.deliver_completion(workflow_id, activity_id, run_id, payload),
204 Err(reason) => callback.deliver_failure(workflow_id, activity_id, run_id, reason),
205 };
206 match outcome {
207 Ok(true) => true,
208 Ok(false) => {
209 tracing::debug!(
211 workflow_id = %workflow_id,
212 activity_id = %activity_id,
213 "unmatched outbox completion for a workflow that is not currently live; \
214 recovery will re-arm it"
215 );
216 false
217 }
218 Err(error) => {
219 tracing::warn!(
220 workflow_id = %workflow_id,
221 activity_id = %activity_id,
222 %error,
223 "failed to deliver unmatched outbox completion to the live workflow"
224 );
225 false
226 }
227 }
228 }
229}
230
231impl ActivityCompletionSink for PendingActivities {
232 fn complete_activity(&self, completion: ActivityCompletion) -> Result<(), ServerError> {
233 let result = match completion.outcome {
234 ActivityCompletionOutcome::Succeeded(payload) => {
235 payload_to_string(&payload).map_err(|reason| {
236 tracing::error!(
237 operation = "activity_complete",
238 workflow_id = %completion.workflow_id,
239 activity_id = %completion.activity_id,
240 error_type = "ActivityResultDecode",
241 %reason,
242 "activity completion failed"
243 );
244 ServerError::worker_dispatch("", "", format!("payload decode: {reason}"))
245 })?
246 }
247 ActivityCompletionOutcome::Failed(error) => {
248 let prefix = if error.is_retryable() {
249 "retryable"
250 } else {
251 "terminal"
252 };
253 tracing::error!(
254 operation = "activity_complete",
255 workflow_id = %completion.workflow_id,
256 activity_id = %completion.activity_id,
257 error_type = "ActivityFailed",
258 error_kind = prefix,
259 reason = %error.message,
260 "activity completion failed"
261 );
262 Err(format!("{prefix}:{}", error.message))
263 }
264 };
265 self.complete(
266 &completion.workflow_id,
267 &completion.activity_id,
268 completion.run_id.as_ref(),
269 result,
270 );
271 Ok(())
272 }
273}
274
275fn payload_to_string(payload: &Payload) -> Result<Result<String, String>, String> {
276 match payload.content_type() {
277 ContentType::Json => String::from_utf8(payload.bytes().to_vec())
278 .map(Ok)
279 .map_err(|_| "activity result payload is not valid UTF-8".to_owned()),
280 }
281}
282
283pub struct WorkerActivityDispatcher {
291 registry: ConnectedWorkerRegistry,
292 namespace: String,
293 pending: PendingActivities,
294 heartbeat_tracker: HeartbeatTracker,
295 drain_state: DrainState,
296 tokio_handle: Option<tokio::runtime::Handle>,
297}
298
299impl std::fmt::Debug for WorkerActivityDispatcher {
300 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301 f.debug_struct("WorkerActivityDispatcher")
302 .field("namespace", &self.namespace)
303 .finish_non_exhaustive()
304 }
305}
306
307impl WorkerActivityDispatcher {
308 #[must_use]
316 pub fn new(
317 registry: ConnectedWorkerRegistry,
318 namespace: impl Into<String>,
319 heartbeat_tracker: HeartbeatTracker,
320 ) -> Self {
321 Self {
322 registry,
323 namespace: namespace.into(),
324 pending: PendingActivities::default(),
325 heartbeat_tracker,
326 drain_state: DrainState::default(),
327 tokio_handle: None,
328 }
329 }
330
331 #[must_use]
333 pub fn with_pending(mut self, pending: PendingActivities) -> Self {
334 self.pending = pending;
335 self
336 }
337
338 #[must_use]
340 pub fn with_drain_state(mut self, drain_state: DrainState) -> Self {
341 self.drain_state = drain_state;
342 self
343 }
344
345 #[must_use]
347 pub fn with_tokio_handle(mut self, tokio_handle: tokio::runtime::Handle) -> Self {
348 self.tokio_handle = Some(tokio_handle);
349 self
350 }
351}
352
353impl WorkerActivityDispatcher {
354 fn ensure_accepting(
355 &self,
356 namespace: &str,
357 activity_type: &str,
358 workflow_id: &WorkflowId,
359 activity_id: &ActivityId,
360 worker_id: Option<WorkerId>,
361 ) -> Result<(), String> {
362 self.drain_state
363 .ensure_accepting(namespace, activity_type)
364 .map_err(|error| {
365 let reason = error.to_string();
366 log_worker_error(
367 "WorkerDispatch",
368 namespace,
369 activity_type,
370 workflow_id,
371 activity_id,
372 worker_id,
373 &reason,
374 );
375 reason
376 })
377 }
378
379 fn select_worker_or_wait(
383 &self,
384 namespace: &str,
385 task_queue: &str,
386 activity_type: &str,
387 node: Option<&str>,
388 workflow_id: &WorkflowId,
389 activity_id: &ActivityId,
390 ) -> Result<WorkerHandle, String> {
391 loop {
392 match self
399 .registry
400 .select_worker(namespace, task_queue, activity_type, node)
401 {
402 Ok(Some(worker)) => return Ok(worker),
403 Ok(None) => {
404 self.ensure_accepting(
405 namespace,
406 activity_type,
407 workflow_id,
408 activity_id,
409 None,
410 )?;
411 tracing::info!(
412 namespace,
413 activity_type,
414 node,
415 workflow_id = %workflow_id,
416 activity_id = %activity_id,
417 "no connected worker; waiting for a matching worker to register"
418 );
419 match &self.tokio_handle {
420 Some(handle) => {
421 handle.block_on(self.registry.wait_for_worker());
422 }
423 None => match tokio::runtime::Handle::try_current() {
424 Ok(handle) => {
425 handle.block_on(self.registry.wait_for_worker());
426 }
427 Err(_) => {
428 std::thread::sleep(Duration::from_millis(500));
429 }
430 },
431 }
432 }
433 Err(error) => {
434 let reason = format!("registry error: {error}");
435 log_worker_error(
436 "WorkerRegistry",
437 namespace,
438 activity_type,
439 workflow_id,
440 activity_id,
441 None,
442 &reason,
443 );
444 return Err(reason);
445 }
446 }
447 }
448 }
449
450 fn track_worker_task(
451 &self,
452 worker_id: WorkerId,
453 activity_type: &str,
454 workflow_id: &WorkflowId,
455 activity_id: &ActivityId,
456 ) -> Result<(), String> {
457 self.heartbeat_tracker
458 .track_task(
459 worker_id,
460 InFlightActivity {
461 workflow_id: workflow_id.clone(),
462 activity_id: activity_id.clone(),
463 },
464 Instant::now(),
465 )
466 .map_err(|error| {
467 let reason = error.to_string();
468 log_worker_error(
469 "WorkerHeartbeatTracker",
470 &self.namespace,
471 activity_type,
472 workflow_id,
473 activity_id,
474 Some(worker_id),
475 &reason,
476 );
477 reason
478 })
479 }
480
481 fn cleanup_activity(
482 &self,
483 worker_id: WorkerId,
484 workflow_id: &WorkflowId,
485 activity_id: &ActivityId,
486 ) {
487 self.pending
488 .pending
489 .remove(&(workflow_id.clone(), activity_id.clone()));
490 let _ = self
491 .heartbeat_tracker
492 .complete_task(worker_id, workflow_id, activity_id);
493 self.drain_state.notify_activity_drained();
494 }
495
496 fn send_activity_task(
497 &self,
498 worker: &WorkerHandle,
499 task: ProtoActivityTask,
500 activity_type: &str,
501 workflow_id: &WorkflowId,
502 activity_id: &ActivityId,
503 ) -> Result<(), String> {
504 let Some(sender) = worker.sender() else {
505 let worker_id = worker.id();
509 self.cleanup_activity(worker_id, workflow_id, activity_id);
510 return Err(format!(
511 "worker {worker_id:?} has no gRPC stream sender (non-gRPC transport)"
512 ));
513 };
514 match sender.try_send(WorkerMessage::ActivityTask(task)) {
515 Ok(()) => Ok(()),
516 Err(error) => {
517 let worker_id = worker.id();
518 let reason = format!("worker task channel full or closed: {error}");
519 self.cleanup_activity(worker_id, workflow_id, activity_id);
520 log_worker_error(
521 "WorkerChannelClosed",
522 &self.namespace,
523 activity_type,
524 workflow_id,
525 activity_id,
526 Some(worker_id),
527 &reason,
528 );
529 Err(reason)
530 }
531 }
532 }
533
534 fn await_activity_result(
538 &self,
539 context: &ActivityDispatchContext<'_>,
540 rx: &SyncReceiver,
541 ) -> Result<String, String> {
542 match self.registry.is_registered(context.worker_id) {
550 Ok(true) => {}
551 Ok(false) => {
552 if let Ok(result) = rx.try_recv() {
556 return self.deliver_result(context, result);
557 }
558 self.cleanup_activity(context.worker_id, context.workflow_id, context.activity_id);
559 let reason = format!(
560 "retryable:{}",
561 super::dispatch::lost_worker_error(context.worker_id).message
562 );
563 log_worker_error(
564 "WorkerLost",
565 &self.namespace,
566 context.activity_type,
567 context.workflow_id,
568 context.activity_id,
569 Some(context.worker_id),
570 &reason,
571 );
572 return Err(reason);
573 }
574 Err(error) => {
575 self.cleanup_activity(context.worker_id, context.workflow_id, context.activity_id);
576 let reason = format!("worker registry inspection failed: {error}");
577 log_worker_error(
578 "WorkerRegistry",
579 &self.namespace,
580 context.activity_type,
581 context.workflow_id,
582 context.activity_id,
583 Some(context.worker_id),
584 &reason,
585 );
586 return Err(reason);
587 }
588 }
589 if let Ok(result) = rx.recv() {
590 return self.deliver_result(context, result);
591 }
592 self.cleanup_activity(context.worker_id, context.workflow_id, context.activity_id);
595 let reason = "activity response channel dropped".to_owned();
596 log_worker_error(
597 "WorkerChannelClosed",
598 &self.namespace,
599 context.activity_type,
600 context.workflow_id,
601 context.activity_id,
602 Some(context.worker_id),
603 &reason,
604 );
605 Err(reason)
606 }
607
608 fn deliver_result(
609 &self,
610 context: &ActivityDispatchContext<'_>,
611 result: Result<String, String>,
612 ) -> Result<String, String> {
613 self.pending
614 .pending
615 .remove(&(context.workflow_id.clone(), context.activity_id.clone()));
616 log_activity_completion(context, result.is_ok());
617 result.inspect_err(|reason| {
618 log_worker_error(
619 "ActivityFailed",
620 &self.namespace,
621 context.activity_type,
622 context.workflow_id,
623 context.activity_id,
624 Some(context.worker_id),
625 reason,
626 );
627 })
628 }
629}
630
631impl ActivityDispatcher for WorkerActivityDispatcher {
632 fn dispatch(&self, request: ActivityDispatch) -> Result<String, String> {
633 match tokio::runtime::Handle::try_current() {
634 Ok(handle) => match handle.runtime_flavor() {
635 tokio::runtime::RuntimeFlavor::MultiThread => {
636 tokio::task::block_in_place(|| self.dispatch_blocking(request))
643 }
644 flavor => Err(format!(
645 "activity dispatch blocks the calling thread until the worker responds; \
646 a {flavor:?} tokio runtime cannot host that wait because the worker \
647 stream forwarder shares its only executor thread and the task could \
648 never be delivered — run the engine on a multi-thread tokio runtime"
649 )),
650 },
651 Err(_) => self.dispatch_blocking(request),
655 }
656 }
657}
658
659impl WorkerActivityDispatcher {
660 fn dispatch_blocking(&self, request: ActivityDispatch) -> Result<String, String> {
677 let ActivityDispatch {
678 namespace,
679 task_queue,
680 node,
684 workflow_id,
685 activity_id,
686 name,
687 input,
688 config: _,
689 attempt,
690 labels,
691 } = request;
692 let started_at = Instant::now();
693 self.ensure_accepting(&namespace, &name, &workflow_id, &activity_id, None)?;
694 let worker = self.select_worker_or_wait(
695 &namespace,
696 &task_queue,
697 &name,
698 node.as_deref(),
699 &workflow_id,
700 &activity_id,
701 )?;
702 let worker_id = worker.id();
703 let span = info_span!(
704 "activity_dispatch",
705 operation = "activity_dispatch",
706 namespace = %namespace,
707 task_queue = %task_queue,
708 node = node.as_deref(),
709 workflow_id = %workflow_id,
710 activity_id = %activity_id,
711 activity_type = %name,
712 worker_id = ?worker_id,
713 );
714 let _span_guard = span.enter();
715 self.ensure_accepting(
716 &namespace,
717 &name,
718 &workflow_id,
719 &activity_id,
720 Some(worker_id),
721 )?;
722
723 let task = activity_task(&name, &input, &workflow_id, &activity_id, attempt, labels);
724 let rx = self
725 .pending
726 .insert(workflow_id.clone(), activity_id.clone());
727 self.track_worker_task(worker_id, &name, &workflow_id, &activity_id)?;
728 self.send_activity_task(&worker, task, &name, &workflow_id, &activity_id)?;
729 let context = ActivityDispatchContext {
730 namespace: &namespace,
731 activity_type: &name,
732 worker_id,
733 workflow_id: &workflow_id,
734 activity_id: &activity_id,
735 started_at,
736 };
737 self.await_activity_result(&context, &rx)
738 }
739}
740
741struct ActivityDispatchContext<'a> {
742 namespace: &'a str,
743 activity_type: &'a str,
744 worker_id: WorkerId,
745 workflow_id: &'a WorkflowId,
746 activity_id: &'a ActivityId,
747 started_at: Instant,
748}
749
750fn activity_task(
751 activity_type: &str,
752 input: &str,
753 workflow_id: &WorkflowId,
754 activity_id: &ActivityId,
755 attempt: u32,
756 labels: BTreeMap<String, String>,
757) -> ProtoActivityTask {
758 ProtoActivityTask {
759 workflow_id: Some(ProtoWorkflowId::from(workflow_id.clone())),
760 activity_id: Some(ProtoActivityId::from(activity_id.clone())),
761 activity_type: activity_type.to_owned(),
762 input: Some(ProtoPayload {
763 content_type: String::from("application/json"),
764 bytes: input.as_bytes().to_vec(),
765 }),
766 attempt,
767 labels: labels.into_iter().collect(),
768 run_id: None,
771 }
772}
773
774fn log_activity_completion(context: &ActivityDispatchContext<'_>, succeeded: bool) {
775 let duration_ms = duration_ms(context.started_at.elapsed());
776 tracing::info!(
777 operation = "activity_complete",
778 namespace = context.namespace,
779 workflow_id = %context.workflow_id,
780 activity_id = %context.activity_id,
781 activity_type = context.activity_type,
782 worker_id = ?context.worker_id,
783 duration_ms,
784 outcome = if succeeded { "succeeded" } else { "failed" },
785 "activity completed"
786 );
787}
788
789fn duration_ms(duration: Duration) -> u64 {
790 u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
791}
792
793fn log_worker_error(
794 error_type: &'static str,
795 namespace: &str,
796 activity_type: &str,
797 workflow_id: &WorkflowId,
798 activity_id: &ActivityId,
799 worker_id: Option<super::registry::WorkerId>,
800 reason: &str,
801) {
802 tracing::error!(
803 operation = "activity_dispatch",
804 namespace,
805 workflow_id = %workflow_id,
806 activity_id = %activity_id,
807 activity_type,
808 worker_id = ?worker_id,
809 error_type,
810 reason,
811 "worker interaction failed"
812 );
813}
814
815#[cfg(test)]
816mod tests {
817 use std::sync::Mutex;
818
819 use aion_core::{ActivityError, ActivityErrorKind, ContentType, Payload};
820
821 use super::*;
822
823 fn activity_id(pos: u64) -> ActivityId {
824 ActivityId::from_sequence_position(pos)
825 }
826
827 #[test]
828 fn pending_insert_and_complete_delivers_result() {
829 let pending = PendingActivities::default();
830 let workflow_id = WorkflowId::new_v4();
831 let id = activity_id(1);
832 let rx = pending.insert(workflow_id.clone(), id.clone());
833
834 assert!(pending.complete(&workflow_id, &id, None, Ok("done".to_owned())));
835 assert_eq!(
836 rx.recv_timeout(Duration::from_millis(50)),
837 Ok(Ok("done".to_owned()))
838 );
839 }
840
841 #[test]
842 fn pending_complete_unknown_returns_false() {
843 let pending = PendingActivities::default();
844 assert!(!pending.complete(
845 &WorkflowId::new_v4(),
846 &activity_id(99),
847 None,
848 Ok("orphan".to_owned())
849 ));
850 }
851
852 #[derive(Default)]
853 struct RecordingOutboxCallback {
854 completions: Mutex<Vec<(WorkflowId, ActivityId, String)>>,
855 failures: Mutex<Vec<(WorkflowId, ActivityId, String)>>,
856 live: bool,
857 }
858
859 impl OutboxDeliveryCallback for RecordingOutboxCallback {
860 fn deliver_completion(
861 &self,
862 workflow_id: &WorkflowId,
863 activity_id: &ActivityId,
864 run_id: Option<&RunId>,
865 result: String,
866 ) -> Result<bool, ServerError> {
867 let _ = run_id;
868 self.completions
869 .lock()
870 .map_err(|_| ServerError::lock_poisoned("recording outbox callback"))?
871 .push((workflow_id.clone(), activity_id.clone(), result));
872 Ok(self.live)
873 }
874
875 fn deliver_failure(
876 &self,
877 workflow_id: &WorkflowId,
878 activity_id: &ActivityId,
879 run_id: Option<&RunId>,
880 reason: String,
881 ) -> Result<bool, ServerError> {
882 let _ = run_id;
883 self.failures
884 .lock()
885 .map_err(|_| ServerError::lock_poisoned("recording outbox callback"))?
886 .push((workflow_id.clone(), activity_id.clone(), reason));
887 Ok(self.live)
888 }
889 }
890
891 #[test]
892 fn unmatched_completion_routes_to_outbox_callback_when_installed() -> Result<(), ServerError> {
893 let pending = PendingActivities::default();
894 let callback = Arc::new(RecordingOutboxCallback {
895 live: true,
896 ..RecordingOutboxCallback::default()
897 });
898 pending.clone().set_outbox_delivery(callback.clone());
900
901 let workflow_id = WorkflowId::new_v4();
902 let id = activity_id(7);
903
904 assert!(pending.complete(&workflow_id, &id, None, Ok("done".to_owned())));
907 let completions = callback
908 .completions
909 .lock()
910 .map_err(|_| ServerError::lock_poisoned("recording outbox callback"))?;
911 assert_eq!(completions.len(), 1);
912 assert_eq!(completions[0].0, workflow_id);
913 assert_eq!(completions[0].1, id);
914 assert_eq!(completions[0].2, "done");
915 Ok(())
916 }
917
918 #[test]
919 fn unmatched_failure_routes_to_outbox_callback_and_not_live_reports_false()
920 -> Result<(), ServerError> {
921 let pending = PendingActivities::default();
922 let callback = Arc::new(RecordingOutboxCallback::default());
924 pending.set_outbox_delivery(callback.clone());
925
926 let workflow_id = WorkflowId::new_v4();
927 let id = activity_id(8);
928
929 assert!(!pending.complete(&workflow_id, &id, None, Err("retryable:boom".to_owned())));
930 let failures = callback
931 .failures
932 .lock()
933 .map_err(|_| ServerError::lock_poisoned("recording outbox callback"))?;
934 assert_eq!(failures.len(), 1);
935 assert_eq!(failures[0].2, "retryable:boom");
936 Ok(())
937 }
938
939 #[test]
940 fn unmatched_completion_is_silent_drop_when_no_callback_installed() {
941 let pending = PendingActivities::default();
944 assert!(!pending.complete(
945 &WorkflowId::new_v4(),
946 &activity_id(9),
947 None,
948 Ok("x".to_owned())
949 ));
950 }
951
952 #[test]
953 fn matched_completion_never_reaches_outbox_callback() -> Result<(), ServerError> {
954 let pending = PendingActivities::default();
955 let callback = Arc::new(RecordingOutboxCallback {
956 live: true,
957 ..RecordingOutboxCallback::default()
958 });
959 pending.set_outbox_delivery(callback.clone());
960
961 let workflow_id = WorkflowId::new_v4();
962 let id = activity_id(10);
963 let rx = pending.insert(workflow_id.clone(), id.clone());
964
965 assert!(pending.complete(&workflow_id, &id, None, Ok("matched".to_owned())));
966 assert_eq!(
967 rx.recv_timeout(Duration::from_millis(50)),
968 Ok(Ok("matched".to_owned()))
969 );
970 assert!(
971 callback
972 .completions
973 .lock()
974 .map_err(|_| ServerError::lock_poisoned("recording outbox callback"))?
975 .is_empty(),
976 "a matched completion must deliver to its waiter, not the outbox callback"
977 );
978 Ok(())
979 }
980
981 #[test]
982 fn completion_sink_routes_success() -> Result<(), ServerError> {
983 let pending = PendingActivities::default();
984 let workflow_id = WorkflowId::new_v4();
985 let id = activity_id(2);
986 let rx = pending.insert(workflow_id.clone(), id.clone());
987 let payload = Payload::new(ContentType::Json, br#"{"greeting":"hi"}"#.to_vec());
988
989 pending.complete_activity(ActivityCompletion {
990 workflow_id,
991 activity_id: id,
992 run_id: None,
993 outcome: ActivityCompletionOutcome::Succeeded(payload),
994 })?;
995
996 let result = rx
997 .recv_timeout(Duration::from_millis(50))
998 .map_err(|e| ServerError::worker_dispatch("", "", format!("channel: {e}")))?;
999 assert_eq!(result, Ok(r#"{"greeting":"hi"}"#.to_owned()));
1000 Ok(())
1001 }
1002
1003 #[test]
1004 fn completion_sink_routes_retryable_error() -> Result<(), ServerError> {
1005 let pending = PendingActivities::default();
1006 let workflow_id = WorkflowId::new_v4();
1007 let id = activity_id(3);
1008 let rx = pending.insert(workflow_id.clone(), id.clone());
1009
1010 pending.complete_activity(ActivityCompletion {
1011 workflow_id,
1012 activity_id: id,
1013 run_id: None,
1014 outcome: ActivityCompletionOutcome::Failed(ActivityError {
1015 kind: ActivityErrorKind::Retryable,
1016 message: "temporary".to_owned(),
1017 details: None,
1018 }),
1019 })?;
1020
1021 let result = rx
1022 .recv_timeout(Duration::from_millis(50))
1023 .map_err(|e| ServerError::worker_dispatch("", "", format!("channel: {e}")))?;
1024 assert_eq!(result, Err("retryable:temporary".to_owned()));
1025 Ok(())
1026 }
1027
1028 #[test]
1037 fn stale_result_for_other_workflow_does_not_complete_pending_dispatch()
1038 -> Result<(), ServerError> {
1039 let pending = PendingActivities::default();
1040 let post_restart_workflow = WorkflowId::new_v4();
1041 let pre_restart_workflow = WorkflowId::new_v4();
1042 let id = activity_id(1);
1044 let rx = pending.insert(post_restart_workflow.clone(), id.clone());
1045
1046 pending.complete_activity(ActivityCompletion {
1048 workflow_id: pre_restart_workflow,
1049 activity_id: id.clone(),
1050 run_id: None,
1051 outcome: ActivityCompletionOutcome::Succeeded(Payload::new(
1052 ContentType::Json,
1053 br#""stale""#.to_vec(),
1054 )),
1055 })?;
1056 assert!(
1057 rx.try_recv().is_err(),
1058 "stale result for a different workflow must not complete this dispatch"
1059 );
1060
1061 pending.complete_activity(ActivityCompletion {
1063 workflow_id: post_restart_workflow,
1064 activity_id: id,
1065 run_id: None,
1066 outcome: ActivityCompletionOutcome::Succeeded(Payload::new(
1067 ContentType::Json,
1068 br#""fresh""#.to_vec(),
1069 )),
1070 })?;
1071 let result = rx
1072 .recv_timeout(Duration::from_millis(50))
1073 .map_err(|e| ServerError::worker_dispatch("", "", format!("channel: {e}")))?;
1074 assert_eq!(result, Ok(r#""fresh""#.to_owned()));
1075 Ok(())
1076 }
1077
1078 fn test_tracker() -> HeartbeatTracker {
1081 HeartbeatTracker::new(Duration::from_secs(5))
1082 }
1083
1084 fn greet_request() -> ActivityDispatch {
1087 ActivityDispatch {
1088 namespace: "default".to_owned(),
1089 task_queue: "default".to_owned(),
1090 node: None,
1091 workflow_id: WorkflowId::new_v4(),
1092 activity_id: ActivityId::from_sequence_position(0),
1093 name: "greet".to_owned(),
1094 input: "{}".to_owned(),
1095 config: "{}".to_owned(),
1096 attempt: 1,
1097 labels: std::collections::BTreeMap::new(),
1098 }
1099 }
1100
1101 #[test]
1102 fn dispatcher_fails_immediately_when_draining_without_workers() {
1103 let registry = ConnectedWorkerRegistry::default();
1104 let drain = DrainState::default();
1105 let dispatcher = WorkerActivityDispatcher::new(registry, "default", test_tracker())
1106 .with_drain_state(drain.clone());
1107
1108 let _ = drain.begin();
1109
1110 let result = dispatcher.dispatch(greet_request());
1111
1112 assert!(result.is_err());
1113 let err = result.err().unwrap_or_default();
1114 assert!(
1115 err.contains("drain"),
1116 "expected drain rejection, got: {err}"
1117 );
1118 }
1119
1120 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1136 async fn dispatch_inside_runtime_task_delivers_promptly_and_round_trips()
1137 -> Result<(), Box<dyn std::error::Error>> {
1138 let registry = ConnectedWorkerRegistry::default();
1139 let pending = PendingActivities::default();
1140 let (worker_tx, mut worker_rx) = tokio::sync::mpsc::channel(32);
1141 let activity_types = [String::from("greet")];
1142 let registration = registry.register("default", activity_types.iter(), worker_tx)?;
1143
1144 let sink = pending.clone();
1145 let echo_worker = tokio::spawn(async move {
1146 let Some(WorkerMessage::ActivityTask(task)) = worker_rx.recv().await else {
1147 return Err("expected an activity task on the worker channel".to_owned());
1148 };
1149 let workflow_id = task
1150 .workflow_id
1151 .ok_or("task missing workflow id")
1152 .and_then(|id| WorkflowId::try_from(id).map_err(|_| "bad workflow id"))?;
1153 let activity_id = task
1154 .activity_id
1155 .map(ActivityId::from)
1156 .ok_or("task missing activity id")?;
1157 sink.complete_activity(ActivityCompletion {
1158 workflow_id,
1159 activity_id,
1160 run_id: None,
1161 outcome: ActivityCompletionOutcome::Succeeded(Payload::new(
1162 ContentType::Json,
1163 br#"{"greeting":"hello"}"#.to_vec(),
1164 )),
1165 })
1166 .map_err(|error| error.to_string())
1167 });
1168
1169 let dispatcher = Arc::new(
1170 WorkerActivityDispatcher::new(registry, "default", test_tracker())
1171 .with_pending(pending),
1172 );
1173 let started = Instant::now();
1174 let dispatch_task = tokio::spawn(futures::future::lazy(move |_| {
1177 dispatcher.dispatch(greet_request())
1178 }));
1179 let result = dispatch_task.await.map_err(|error| error.to_string())?;
1180 let elapsed = started.elapsed();
1181
1182 assert_eq!(result, Ok(r#"{"greeting":"hello"}"#.to_owned()));
1183 assert!(
1184 elapsed < Duration::from_secs(5),
1185 "dispatch round trip took {elapsed:?}; task delivery must not \
1186 depend on the blocked dispatch thread"
1187 );
1188 echo_worker.await.map_err(|error| error.to_string())??;
1189 registration.deregister()?;
1190 Ok(())
1191 }
1192
1193 #[tokio::test]
1197 async fn dispatch_on_current_thread_runtime_fails_fast()
1198 -> Result<(), Box<dyn std::error::Error>> {
1199 let registry = ConnectedWorkerRegistry::default();
1200 let (worker_tx, _worker_rx) = tokio::sync::mpsc::channel(32);
1201 let activity_types = [String::from("greet")];
1202 let registration = registry.register("default", activity_types.iter(), worker_tx)?;
1203 let dispatcher = WorkerActivityDispatcher::new(registry, "default", test_tracker());
1204
1205 let started = Instant::now();
1206 let result = dispatcher.dispatch(greet_request());
1207 let elapsed = started.elapsed();
1208
1209 let err = result.err().ok_or("expected dispatch to fail")?;
1210 assert!(
1211 err.contains("multi-thread tokio runtime"),
1212 "unexpected error: {err}"
1213 );
1214 assert!(
1215 elapsed < Duration::from_secs(5),
1216 "fail-fast path took {elapsed:?}"
1217 );
1218 registration.deregister()?;
1219 Ok(())
1220 }
1221
1222 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1228 async fn dispatch_pinned_to_node_reaches_only_that_node()
1229 -> Result<(), Box<dyn std::error::Error>> {
1230 let registry = ConnectedWorkerRegistry::default();
1231 let pending = PendingActivities::default();
1232 let activity_types = [String::from("greet")];
1233 let (n1_tx, mut n1_rx) = tokio::sync::mpsc::channel(32);
1234 let (n2_tx, mut n2_rx) = tokio::sync::mpsc::channel(32);
1235 let on_n2 = registry.register_namespaces(
1241 [String::from("default")],
1242 "default",
1243 Some(String::from("n2")),
1244 activity_types.iter(),
1245 n2_tx,
1246 )?;
1247 let on_n1 = registry.register_namespaces(
1248 [String::from("default")],
1249 "default",
1250 Some(String::from("n1")),
1251 activity_types.iter(),
1252 n1_tx,
1253 )?;
1254
1255 let sink = pending.clone();
1259 let echo_n1 = tokio::spawn(async move {
1260 let Some(WorkerMessage::ActivityTask(task)) = n1_rx.recv().await else {
1261 return Err("expected an activity task on the n1 worker channel".to_owned());
1262 };
1263 let workflow_id = task
1264 .workflow_id
1265 .ok_or("task missing workflow id")
1266 .and_then(|id| WorkflowId::try_from(id).map_err(|_| "bad workflow id"))?;
1267 let activity_id = task
1268 .activity_id
1269 .map(ActivityId::from)
1270 .ok_or("task missing activity id")?;
1271 sink.complete_activity(ActivityCompletion {
1272 workflow_id,
1273 activity_id,
1274 run_id: None,
1275 outcome: ActivityCompletionOutcome::Succeeded(Payload::new(
1276 ContentType::Json,
1277 br#"{"greeting":"hello"}"#.to_vec(),
1278 )),
1279 })
1280 .map_err(|error| error.to_string())
1281 });
1282
1283 let dispatcher = Arc::new(
1284 WorkerActivityDispatcher::new(registry.clone(), "default", test_tracker())
1285 .with_pending(pending),
1286 );
1287
1288 let pinned = ActivityDispatch {
1289 node: Some(String::from("n1")),
1290 ..greet_request()
1291 };
1292 let started = Instant::now();
1293 let result = tokio::spawn(futures::future::lazy(move |_| dispatcher.dispatch(pinned)))
1294 .await
1295 .map_err(|error| error.to_string())?;
1296 let elapsed = started.elapsed();
1297
1298 assert_eq!(result, Ok(r#"{"greeting":"hello"}"#.to_owned()));
1299 assert!(
1300 elapsed < Duration::from_secs(5),
1301 "pinned dispatch round trip took {elapsed:?}; the task must route to n1"
1302 );
1303 echo_n1.await.map_err(|error| error.to_string())??;
1304
1305 assert!(
1307 n2_rx.try_recv().is_err(),
1308 "node=Some(\"n1\") dispatch must not reach the n2 worker"
1309 );
1310
1311 on_n1.deregister()?;
1312 on_n2.deregister()?;
1313 Ok(())
1314 }
1315
1316 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1319 async fn unpinned_dispatch_reaches_a_pooled_worker_regardless_of_node()
1320 -> Result<(), Box<dyn std::error::Error>> {
1321 let registry = ConnectedWorkerRegistry::default();
1322 let pending = PendingActivities::default();
1323 let activity_types = [String::from("greet")];
1324 let (n1_tx, mut n1_rx) = tokio::sync::mpsc::channel(32);
1325 let on_n1 = registry.register_namespaces(
1326 [String::from("default")],
1327 "default",
1328 Some(String::from("n1")),
1329 activity_types.iter(),
1330 n1_tx,
1331 )?;
1332
1333 let sink = pending.clone();
1334 let echo = tokio::spawn(async move {
1335 let Some(WorkerMessage::ActivityTask(task)) = n1_rx.recv().await else {
1336 return Err("expected an activity task on the worker channel".to_owned());
1337 };
1338 let workflow_id = task
1339 .workflow_id
1340 .ok_or("task missing workflow id")
1341 .and_then(|id| WorkflowId::try_from(id).map_err(|_| "bad workflow id"))?;
1342 let activity_id = task
1343 .activity_id
1344 .map(ActivityId::from)
1345 .ok_or("task missing activity id")?;
1346 sink.complete_activity(ActivityCompletion {
1347 workflow_id,
1348 activity_id,
1349 run_id: None,
1350 outcome: ActivityCompletionOutcome::Succeeded(Payload::new(
1351 ContentType::Json,
1352 br#"{"greeting":"hello"}"#.to_vec(),
1353 )),
1354 })
1355 .map_err(|error| error.to_string())
1356 });
1357
1358 let dispatcher = Arc::new(
1359 WorkerActivityDispatcher::new(registry.clone(), "default", test_tracker())
1360 .with_pending(pending),
1361 );
1362
1363 let result = tokio::spawn(futures::future::lazy(move |_| {
1365 dispatcher.dispatch(greet_request())
1366 }))
1367 .await
1368 .map_err(|error| error.to_string())?;
1369
1370 assert_eq!(result, Ok(r#"{"greeting":"hello"}"#.to_owned()));
1371 echo.await.map_err(|error| error.to_string())??;
1372 on_n1.deregister()?;
1373 Ok(())
1374 }
1375}