1#![warn(missing_docs)] #[macro_use]
65extern crate tracing;
66extern crate self as temporalio_sdk;
67
68pub mod activities;
69pub mod error;
70pub mod interceptors;
71pub mod runtime {
77 pub use temporalio_sdk_core::{
78 ActivitySlotKind, FixedSizeSlotSupplier, LocalActivitySlotKind, NexusSlotKind,
79 PollerBehavior, ResourceBasedSlotsOptions, ResourceBasedSlotsOptionsBuilder,
80 ResourceBasedTuner, ResourceBasedTunerConfig, ResourceController, ResourceSlotOptions,
81 RuntimeOptions, RuntimeOptionsBuilder, SlotInfo, SlotInfoTrait, SlotKind, SlotKindType,
82 SlotMarkUsedContext, SlotReleaseContext, SlotReservationContext, SlotSupplier,
83 SlotSupplierOptions, SlotSupplierPermit, TokioRuntimeBuilder, TunerBuilder, TunerHolder,
84 TunerHolderOptions, TunerHolderOptionsBuilder, Worker as CoreWorker, WorkerConfig,
85 WorkerConfigBuilder, WorkerTuner, WorkerVersioningStrategy, WorkflowErrorType,
86 WorkflowSlotKind, init_replay_worker, replay,
87 };
88}
89mod workflow_executor;
90mod workflow_future;
91pub mod workflow_interceptors;
92mod workflow_registry;
93#[cfg(feature = "wasm-workflows")]
94mod workflow_wasm;
95pub mod workflows;
96
97pub use crate::error::{
98 ActivityExecutionError, ApplicationFailure, ChildWorkflowExecutionError,
99 ChildWorkflowStartError, OutgoingActivityError, OutgoingError, OutgoingWorkflowError,
100 RetryState, TimeoutType, WorkflowRegistrationError, WorkflowSignalError,
101};
102pub use temporalio_client::Namespace;
103pub use temporalio_sdk_core::CoreRuntime as Runtime;
104pub use temporalio_workflow::{
105 ActivityCancellationType, ActivityCloseTimeouts, ActivityOptions, BaseWorkflowContext,
106 CancellableFuture, CancellableFutureWithReason, ChildWorkflowCancellationType,
107 ChildWorkflowOptions, ContinueAsNewOptions, ContinueAsNewVersioningBehavior,
108 ExternalWorkflowHandle, LocalActivityOptions, MemoValue, NexusOperationCancellationType,
109 NexusOperationOptions, ParentClosePolicy, PatchActivationCallback, Signal, SignalData,
110 StartChildWorkflowExecutionFailedCause, StartChildWorkflowOutput, StartedChildWorkflow,
111 StartedNexusOperation, SyncWorkflowContext, TimerOptions, TimerResult, VersioningIntent,
112 WorkflowContext, WorkflowContextView, WorkflowIdReusePolicy, WorkflowRandomValue,
113 WorkflowResult, WorkflowTermination,
114};
115#[cfg(feature = "wasm-workflows")]
116pub use workflow_wasm::WasmWorkflowComponent;
117
118use crate::{
119 activities::{
120 ActivityContext, ActivityDefinitions, ActivityImplementer, ExecutableActivity,
121 activity_error_to_core_result,
122 },
123 interceptors::{ActivityInboundInterceptor, WorkerInterceptor},
124 workflow_executor::{TaskHandle, WorkflowExecutor},
125 workflow_future::start_workflow,
126 workflow_interceptors::WorkflowInterceptorConstructor,
127 workflow_registry::WorkflowDefinitions,
128};
129use anyhow::{Context, anyhow, bail};
130use futures_util::{FutureExt, StreamExt, TryFutureExt, TryStreamExt};
131use std::{
132 any::{Any, TypeId},
133 cell::RefCell,
134 collections::{HashMap, HashSet},
135 fmt::{Debug, Display, Formatter},
136 future::Future,
137 sync::Arc,
138 time::Duration,
139};
140use temporalio_client::{Client, ClientOptions, NamespacedClient};
141use temporalio_common::{
142 ActivityDefinition, WorkflowDefinition,
143 data_converters::{DataConverter, SerializationContext, SerializationContextData},
144 payload_visitor::{decode_payloads, encode_payloads},
145 protos::{
146 TaskToken,
147 coresdk::{
148 ActivityTaskCompletion, AsJsonPayloadExt,
149 activity_result::ActivityExecutionResult,
150 activity_task::{ActivityTask, activity_task},
151 workflow_activation::{WorkflowActivation, workflow_activation_job::Variant},
152 workflow_completion::WorkflowActivationCompletion,
153 },
154 temporal::api::{
155 common::v1::Payload, enums::v1::WorkflowTaskFailedCause, failure::v1::Failure,
156 },
157 },
158 worker::{WorkerDeploymentOptions, WorkerTaskTypes, build_id_from_current_exe},
159};
160use temporalio_sdk_core::{PollError, init_worker};
161use temporalio_workflow::runtime::entry::WorkflowImplementation;
162use tokio::sync::{
163 Notify,
164 mpsc::{UnboundedSender, unbounded_channel},
165};
166use tokio_stream::wrappers::UnboundedReceiverStream;
167use tokio_util::sync::CancellationToken;
168use tracing::{Instrument, Span, field};
169use uuid::Uuid;
170
171use crate::runtime::{
172 CoreWorker, PollerBehavior, TunerBuilder, WorkerConfig, WorkerTuner, WorkerVersioningStrategy,
173 WorkflowErrorType,
174};
175
176#[derive(bon::Builder, Clone)]
178#[builder(start_fn = new, on(String, into), state_mod(vis = "pub"))]
179#[non_exhaustive]
180pub struct WorkerOptions {
181 #[builder(start_fn)]
184 pub task_queue: String,
185
186 #[builder(field)]
187 activities: ActivityDefinitions,
188
189 #[builder(field)]
190 workflows: WorkflowDefinitions,
191
192 #[builder(field)]
193 workflow_interceptor_constructors: Vec<WorkflowInterceptorConstructor>,
194
195 #[cfg(feature = "wasm-workflows")]
196 #[builder(field)]
197 wasm_workflow_components: Vec<WasmWorkflowComponent>,
198
199 #[builder(default = def_build_id())]
202 pub deployment_options: WorkerDeploymentOptions,
203 pub client_identity_override: Option<String>,
207 #[builder(default = 1000)]
213 pub max_cached_workflows: usize,
214 #[builder(default = Arc::new(TunerBuilder::default().build()))]
217 pub tuner: Arc<dyn WorkerTuner + Send + Sync>,
218 pub workflow_task_poller_behavior: Option<PollerBehavior>,
225 #[builder(default = 0.2)]
233 pub nonsticky_to_sticky_poll_ratio: f32,
234 pub activity_task_poller_behavior: Option<PollerBehavior>,
239 pub nexus_task_poller_behavior: Option<PollerBehavior>,
244 #[builder(default = WorkerTaskTypes::all())]
250 pub task_types: WorkerTaskTypes,
251 #[builder(default = Duration::from_secs(10))]
254 pub sticky_queue_schedule_to_start_timeout: Duration,
255 #[builder(default = Duration::from_secs(60))]
257 pub max_heartbeat_throttle_interval: Duration,
258 #[builder(default = Duration::from_secs(30))]
263 pub default_heartbeat_throttle_interval: Duration,
264 pub max_task_queue_activities_per_second: Option<f64>,
271 pub max_worker_activities_per_second: Option<f64>,
276 #[builder(default = 3)]
279 pub max_eager_activity_reservations_per_workflow_task: usize,
280 #[builder(default)]
283 pub workflow_failure_errors: HashSet<WorkflowErrorType>,
284 #[builder(default)]
287 pub workflow_types_to_failure_errors: HashMap<String, HashSet<WorkflowErrorType>>,
288 pub graceful_shutdown_period: Option<Duration>,
291 #[builder(default = true)]
295 pub detect_nondeterministic_futures: bool,
296 #[builder(default = false)]
301 pub disable_payload_error_limit: bool,
302 pub patch_activation_callback: Option<PatchActivationCallback>,
310}
311
312impl<S: worker_options_builder::State> WorkerOptionsBuilder<S> {
313 pub fn register_activities<AI: ActivityImplementer>(mut self, instance: AI) -> Self {
315 self.activities.register_activities::<AI>(instance);
316 self
317 }
318 pub fn register_activity<AD>(mut self, instance: Arc<AD::Implementer>) -> Self
320 where
321 AD: ActivityDefinition + ExecutableActivity,
322 AD::Input: Send + Sync,
323 AD::Output: Send + Sync,
324 {
325 self.activities.register_activity::<AD>(instance);
326 self
327 }
328
329 pub fn register_workflow<W>(mut self) -> Result<Self, WorkflowRegistrationError>
331 where
332 W: WorkflowImplementation,
333 <W::Run as WorkflowDefinition>::Input: Send,
334 {
335 self.workflows.register_workflow::<W>()?;
336 Ok(self)
337 }
338
339 pub fn register_workflow_with_factory<W, F>(
357 mut self,
358 factory: F,
359 ) -> Result<Self, WorkflowRegistrationError>
360 where
361 W: WorkflowImplementation,
362 <W::Run as WorkflowDefinition>::Input: Send,
363 F: Fn() -> W + Send + Sync + 'static,
364 {
365 self.workflows
366 .register_workflow_run_with_factory::<W, F>(factory)?;
367 Ok(self)
368 }
369
370 pub fn register_workflow_interceptors(
374 mut self,
375 constructors: Vec<WorkflowInterceptorConstructor>,
376 ) -> Self {
377 self.workflow_interceptor_constructors = constructors;
378 self
379 }
380
381 pub fn workflow_interceptor_constructors_mut(
383 &mut self,
384 ) -> &mut Vec<WorkflowInterceptorConstructor> {
385 &mut self.workflow_interceptor_constructors
386 }
387
388 #[cfg(feature = "wasm-workflows")]
390 pub fn register_wasm_workflow(mut self, component: WasmWorkflowComponent) -> Self {
391 self.wasm_workflow_components.push(component);
392 self
393 }
394}
395
396fn def_build_id() -> WorkerDeploymentOptions {
398 WorkerDeploymentOptions::from_build_id(build_id_from_current_exe().to_owned())
399}
400
401impl WorkerOptions {
402 pub fn register_activities<AI: ActivityImplementer>(&mut self, instance: AI) -> &mut Self {
404 self.activities.register_activities::<AI>(instance);
405 self
406 }
407 pub fn register_activity<AD>(&mut self, instance: Arc<AD::Implementer>) -> &mut Self
409 where
410 AD: ActivityDefinition + ExecutableActivity,
411 AD::Input: Send + Sync,
412 AD::Output: Send + Sync,
413 {
414 self.activities.register_activity::<AD>(instance);
415 self
416 }
417 pub fn activities(&self) -> ActivityDefinitions {
419 self.activities.clone()
420 }
421
422 pub fn register_workflow<W>(&mut self) -> Result<&mut Self, WorkflowRegistrationError>
424 where
425 W: WorkflowImplementation,
426 <W::Run as WorkflowDefinition>::Input: Send,
427 {
428 self.workflows.register_workflow::<W>()?;
429 Ok(self)
430 }
431
432 pub fn register_workflow_with_factory<W, F>(
437 &mut self,
438 factory: F,
439 ) -> Result<&mut Self, WorkflowRegistrationError>
440 where
441 W: WorkflowImplementation,
442 <W::Run as WorkflowDefinition>::Input: Send,
443 F: Fn() -> W + Send + Sync + 'static,
444 {
445 self.workflows
446 .register_workflow_run_with_factory::<W, F>(factory)?;
447 Ok(self)
448 }
449
450 pub fn register_workflow_interceptors(
454 &mut self,
455 constructors: Vec<WorkflowInterceptorConstructor>,
456 ) -> &mut Self {
457 self.workflow_interceptor_constructors = constructors;
458 self
459 }
460
461 #[cfg(feature = "wasm-workflows")]
463 pub fn register_wasm_workflow(&mut self, component: WasmWorkflowComponent) -> &mut Self {
464 self.wasm_workflow_components.push(component);
465 self
466 }
467
468 pub fn workflows(&self) -> WorkflowDefinitions {
470 self.workflows.clone()
471 }
472
473 #[doc(hidden)]
474 pub fn to_core_options(
475 &self,
476 namespace: String,
477 connection_identity: String,
478 ) -> Result<WorkerConfig, String> {
479 WorkerConfig::builder()
480 .namespace(namespace)
481 .task_queue(self.task_queue.clone())
482 .maybe_client_identity_override(self.client_identity_override.clone().or_else(|| {
483 connection_identity.is_empty().then(|| {
484 format!(
485 "{}@{}",
486 std::process::id(),
487 gethostname::gethostname().to_string_lossy()
488 )
489 })
490 }))
491 .max_cached_workflows(self.max_cached_workflows)
492 .tuner(self.tuner.clone())
493 .maybe_workflow_task_poller_behavior(self.workflow_task_poller_behavior)
494 .maybe_activity_task_poller_behavior(self.activity_task_poller_behavior)
495 .maybe_nexus_task_poller_behavior(self.nexus_task_poller_behavior)
496 .task_types(self.task_types)
497 .sticky_queue_schedule_to_start_timeout(self.sticky_queue_schedule_to_start_timeout)
498 .max_heartbeat_throttle_interval(self.max_heartbeat_throttle_interval)
499 .default_heartbeat_throttle_interval(self.default_heartbeat_throttle_interval)
500 .maybe_max_task_queue_activities_per_second(self.max_task_queue_activities_per_second)
501 .maybe_max_worker_activities_per_second(self.max_worker_activities_per_second)
502 .max_eager_activity_reservations_per_workflow_task(
503 self.max_eager_activity_reservations_per_workflow_task,
504 )
505 .maybe_graceful_shutdown_period(self.graceful_shutdown_period)
506 .versioning_strategy(WorkerVersioningStrategy::WorkerDeploymentBased(
507 self.deployment_options.clone(),
508 ))
509 .workflow_failure_errors(self.workflow_failure_errors.clone())
510 .workflow_types_to_failure_errors(self.workflow_types_to_failure_errors.clone())
511 .disable_payload_error_limit(self.disable_payload_error_limit)
512 .build()
513 }
514}
515
516pub struct Worker {
520 common: CommonWorker,
521 workflow_half: WorkflowHalf,
522 activity_half: ActivityHalf,
523}
524
525struct CommonWorker {
526 worker: Arc<CoreWorker>,
527 task_queue: String,
528 worker_interceptor: Option<Box<dyn WorkerInterceptor>>,
529 activity_inbound_interceptors: Vec<Arc<dyn ActivityInboundInterceptor>>,
530 workflow_interceptor_constructors: Vec<WorkflowInterceptorConstructor>,
531 client_options: ClientOptions,
532 data_converter: DataConverter,
533}
534
535struct WorkflowHalf {
536 workflows: RefCell<HashMap<String, WorkflowData>>,
538 workflow_definitions: WorkflowDefinitions,
539 workflow_removed_from_map: Notify,
540 detect_nondeterministic_futures: bool,
541 patch_activation_callback: Option<PatchActivationCallback>,
542}
543struct WorkflowData {
544 activation_chan: UnboundedSender<WorkflowActivation>,
546}
547
548struct WorkflowFutureHandle<F: Future> {
549 join_handle: F,
550 run_id: String,
551}
552
553#[derive(Default)]
554struct ActivityHalf {
555 activities: ActivityDefinitions,
557 task_tokens_to_cancels: HashMap<TaskToken, CancellationToken>,
558}
559
560#[derive(Debug, thiserror::Error)]
561enum ActivityTaskHandlerError {
562 #[error("{source}")]
563 UnregisteredActivity {
564 source: ActivityNotRegisteredError,
565 task_token: Vec<u8>,
566 },
567 #[error(transparent)]
568 Fatal(#[from] anyhow::Error),
569}
570
571#[derive(Debug, thiserror::Error)]
572enum ActivityNotRegisteredError {
573 #[error(
574 "Activity {activity_type} is not registered on this worker, available activities: {}",
575 .available_activities.join(", ")
576 )]
577 HasAvailable {
578 activity_type: String,
579 available_activities: Vec<String>,
580 },
581 #[error("Activity {activity_type} is not registered on this worker, no available activities.")]
582 NoAvailable { activity_type: String },
583}
584
585impl ActivityNotRegisteredError {
586 fn new(activity_type: String, available_activities: Vec<String>) -> Self {
587 if available_activities.is_empty() {
588 Self::NoAvailable { activity_type }
589 } else {
590 Self::HasAvailable {
591 activity_type,
592 available_activities,
593 }
594 }
595 }
596}
597
598async fn encode_workflow_completion(
599 completion: &mut WorkflowActivationCompletion,
600 data_converter: &DataConverter,
601) {
602 let run_id = completion.run_id.clone();
603 if let Err(err) = encode_payloads(
604 completion,
605 data_converter.codec(),
606 &SerializationContextData::Workflow,
607 )
608 .await
609 {
610 error!(run_id, error = %err, "Failed encoding workflow activation completion");
611 *completion = WorkflowActivationCompletion::fail(
612 run_id,
613 Failure {
614 message: format!("Failed encoding completion: {err}"),
615 ..Default::default()
616 },
617 Some(WorkflowTaskFailedCause::WorkflowWorkerUnhandledFailure),
618 );
619 }
620}
621
622async fn encode_activity_completion(
623 completion: &mut ActivityTaskCompletion,
624 data_converter: &DataConverter,
625) {
626 if let Err(err) = encode_payloads(
627 completion,
628 data_converter.codec(),
629 &SerializationContextData::Activity,
630 )
631 .await
632 {
633 error!(error = %err, "Failed encoding activity task completion");
634 completion.result = Some(ActivityExecutionResult::fail(Failure::application_failure(
635 format!("Failed encoding activity completion: {err}"),
636 false,
637 )));
638 }
639}
640
641impl Worker {
642 pub fn new(
644 runtime: &Runtime,
645 client: Client,
646 options: WorkerOptions,
647 ) -> Result<Self, Box<dyn std::error::Error>> {
648 let wc = options
649 .to_core_options(client.namespace(), client.identity())
650 .map_err(|s| anyhow::anyhow!("{s}"))?;
651 let core = init_worker(runtime, wc, client.connection().clone())?;
652 Self::new_from_core_options(Arc::new(core), client.options().clone(), options)
653 }
654
655 #[doc(hidden)]
657 pub fn new_from_core(worker: Arc<CoreWorker>, data_converter: DataConverter) -> Self {
658 let client_options = ClientOptions::new(worker.get_config().namespace.clone())
659 .data_converter(data_converter)
660 .build();
661 Self::new_from_core_definitions(
662 worker,
663 client_options,
664 Default::default(),
665 Default::default(),
666 Default::default(),
667 )
668 }
669
670 #[doc(hidden)]
672 pub fn new_from_core_options(
673 worker: Arc<CoreWorker>,
674 client_options: ClientOptions,
675 mut options: WorkerOptions,
676 ) -> Result<Self, Box<dyn std::error::Error>> {
677 let acts = std::mem::take(&mut options.activities);
678 let wfs = std::mem::take(&mut options.workflows);
679 let workflow_interceptor_constructors =
680 std::mem::take(&mut options.workflow_interceptor_constructors);
681 #[cfg(feature = "wasm-workflows")]
682 let wasm_components = std::mem::take(&mut options.wasm_workflow_components);
683 let mut me = Self::new_from_core_definitions(
684 worker,
685 client_options,
686 acts,
687 wfs,
688 workflow_interceptor_constructors,
689 );
690 me.set_detect_nondeterministic_futures(options.detect_nondeterministic_futures);
691 me.workflow_half.patch_activation_callback = options.patch_activation_callback;
692 #[cfg(feature = "wasm-workflows")]
693 me.workflow_half
694 .workflow_definitions
695 .register_wasm_workflows(
696 wasm_components,
697 !me.common.workflow_interceptor_constructors.is_empty(),
698 )?;
699 Ok(me)
700 }
701
702 fn new_from_core_definitions(
703 worker: Arc<CoreWorker>,
704 client_options: ClientOptions,
705 activities: ActivityDefinitions,
706 workflows: WorkflowDefinitions,
707 workflow_interceptor_constructors: Vec<WorkflowInterceptorConstructor>,
708 ) -> Self {
709 let data_converter = client_options.data_converter.clone();
710 Self {
711 common: CommonWorker {
712 task_queue: worker.get_config().task_queue.clone(),
713 worker,
714 worker_interceptor: None,
715 activity_inbound_interceptors: Vec::new(),
716 workflow_interceptor_constructors,
717 client_options,
718 data_converter,
719 },
720 workflow_half: WorkflowHalf {
721 workflows: Default::default(),
722 workflow_definitions: workflows,
723 workflow_removed_from_map: Default::default(),
724 detect_nondeterministic_futures: false,
725 patch_activation_callback: None,
726 },
727 activity_half: ActivityHalf {
728 activities,
729 ..Default::default()
730 },
731 }
732 }
733
734 pub fn task_queue(&self) -> &str {
736 &self.common.task_queue
737 }
738
739 #[doc(hidden)]
740 pub fn set_detect_nondeterministic_futures(&mut self, enabled: bool) {
743 self.workflow_half.detect_nondeterministic_futures = enabled;
744 }
745
746 pub fn shutdown_handle(&self) -> impl Fn() + use<> {
749 let w = self.common.worker.clone();
750 move || w.initiate_shutdown()
751 }
752
753 pub fn register_activities<AI: ActivityImplementer>(&mut self, instance: AI) -> &mut Self {
755 self.activity_half
756 .activities
757 .register_activities::<AI>(instance);
758 self
759 }
760 pub fn register_activity<AD>(&mut self, instance: Arc<AD::Implementer>) -> &mut Self
762 where
763 AD: ActivityDefinition + ExecutableActivity,
764 AD::Input: Send + Sync,
765 AD::Output: Send + Sync,
766 {
767 self.activity_half
768 .activities
769 .register_activity::<AD>(instance);
770 self
771 }
772
773 pub fn register_workflow<W>(&mut self) -> Result<&mut Self, WorkflowRegistrationError>
775 where
776 W: WorkflowImplementation,
777 <W::Run as WorkflowDefinition>::Input: Send,
778 {
779 self.workflow_half
780 .workflow_definitions
781 .register_workflow::<W>()?;
782 Ok(self)
783 }
784
785 pub fn register_workflow_with_factory<W, F>(
789 &mut self,
790 factory: F,
791 ) -> Result<&mut Self, WorkflowRegistrationError>
792 where
793 W: WorkflowImplementation,
794 <W::Run as WorkflowDefinition>::Input: Send,
795 F: Fn() -> W + Send + Sync + 'static,
796 {
797 self.workflow_half
798 .workflow_definitions
799 .register_workflow_run_with_factory::<W, F>(factory)?;
800 Ok(self)
801 }
802
803 pub async fn run(&mut self) -> Result<(), anyhow::Error> {
806 self.common.worker.validate().await?;
809 let shutdown_token = CancellationToken::new();
810 let (common, wf_half, act_half) = self.split_apart();
811 let (wf_future_tx, wf_future_rx) =
812 unbounded_channel::<WorkflowFutureHandle<TaskHandle<WorkflowResult<Payload>>>>();
813 let (completions_tx, completions_rx) = unbounded_channel();
814
815 let workflow_local_set = tokio::task::LocalSet::new();
820 let executor = WorkflowExecutor::new();
821
822 let wf_future_joiner = async {
823 UnboundedReceiverStream::new(wf_future_rx)
824 .map(Result::<_, anyhow::Error>::Ok)
825 .try_for_each_concurrent(
826 None,
827 |WorkflowFutureHandle {
828 join_handle,
829 run_id,
830 }| {
831 let wf_half = &*wf_half;
832 async move {
833 let result = join_handle.await.map_err(anyhow::Error::new)?;
834 if let Err(e) = result
837 && !matches!(e, WorkflowTermination::Evicted)
838 {
839 return Err(anyhow::Error::new(e));
840 }
841 debug!(run_id=%run_id, "Removing workflow from cache");
842 wf_half.workflows.borrow_mut().remove(&run_id);
843 wf_half.workflow_removed_from_map.notify_one();
844 Ok(())
845 }
846 },
847 )
848 .await
849 .context("Workflow futures encountered an error")
850 };
851 let wf_completion_processor = async {
852 UnboundedReceiverStream::new(completions_rx)
853 .map(Ok)
854 .try_for_each_concurrent(None, |mut completion| async {
855 encode_workflow_completion(&mut completion, &common.data_converter).await;
856 if let Some(ref i) = common.worker_interceptor {
857 i.on_workflow_activation_completion(&completion).await;
858 }
859 common.worker.complete_workflow_activation(completion).await
860 })
861 .map_err(anyhow::Error::from)
862 .await
863 .context("Workflow completions processor encountered an error")
864 };
865 tokio::try_join!(
866 async {
868 workflow_local_set.run_until(async {
869 tokio::try_join!(
870 async {
872 loop {
873 let mut activation =
874 match common.worker.poll_workflow_activation().await {
875 Err(PollError::ShutDown) => {
876 break;
877 }
878 o => o?,
879 };
880 if let Err(err) = decode_payloads(
881 &mut activation,
882 common.data_converter.codec(),
883 &SerializationContextData::Workflow,
884 )
885 .await
886 {
887 let run_id = activation.run_id;
888 error!(run_id, error = %err, "Failed decoding workflow activation");
889 completions_tx
890 .send(WorkflowActivationCompletion::fail(
891 run_id,
892 Failure {
893 message: format!("Failed decoding activation: {err}"),
894 ..Default::default()
895 },
896 Some(
897 WorkflowTaskFailedCause::WorkflowWorkerUnhandledFailure,
898 ),
899 ))
900 .expect("Completion channel intact");
901 continue;
902 }
903 if let Some(ref i) = common.worker_interceptor {
904 i.on_workflow_activation(&activation).await?;
905 }
906 if let Some(wf_fut) = wf_half
907 .workflow_activation_handler(
908 common,
909 shutdown_token.clone(),
910 activation,
911 &completions_tx,
912 &executor,
913 )
914 .await?
915 && wf_future_tx.send(wf_fut).is_err()
916 {
917 panic!(
918 "Receive half of completion processor channel cannot be dropped"
919 );
920 }
921 }
922 shutdown_token.cancel();
924 drop(wf_future_tx);
927 drop(completions_tx);
928 Result::<_, anyhow::Error>::Ok(())
929 },
930 wf_future_joiner,
931 async {
932 tokio::select! {
933 _ = executor.drive() => unreachable!("executor driver cannot finish"),
934 _ = shutdown_token.cancelled() => {}
935 }
936 executor.shutdown().await;
937 Result::<_, anyhow::Error>::Ok(())
938 },
939 )
940 }).await
941 },
942 async {
945 if !act_half.activities.is_empty() {
946 loop {
947 let activity = common.worker.poll_activity_task().await;
948 if matches!(activity, Err(PollError::ShutDown)) {
949 break;
950 }
951 let mut activity = activity?;
952 if let Err(err) = decode_payloads(
953 &mut activity,
954 common.data_converter.codec(),
955 &SerializationContextData::Activity,
956 )
957 .await
958 {
959 error!(error = %err, "Failed decoding activity task");
960 let mut completion = ActivityTaskCompletion {
961 task_token: activity.task_token,
962 result: Some(ActivityExecutionResult::fail(
963 Failure::application_failure(
964 format!("Failed decoding activity task: {err}"),
965 false,
966 ),
967 )),
968 };
969 encode_activity_completion(&mut completion, &common.data_converter)
970 .await;
971 common.worker.complete_activity_task(completion).await?;
972 continue;
973 }
974 match act_half.activity_task_handler(
975 common.worker.clone(),
976 common.client_options.clone(),
977 common.task_queue.clone(),
978 common.data_converter.clone(),
979 common.activity_inbound_interceptors.clone(),
980 activity,
981 ) {
982 Ok(()) => {}
983 Err(ActivityTaskHandlerError::UnregisteredActivity {
984 source,
985 task_token,
986 }) => {
987 let failure = common.data_converter.to_failure(
988 &SerializationContextData::Activity,
989 OutgoingError::Activity(OutgoingActivityError::Application(
990 ApplicationFailure::builder(source)
991 .type_name("NotFoundError".to_owned())
992 .build()
993 .into(),
994 )),
995 );
996 let mut completion = ActivityTaskCompletion {
997 task_token,
998 result: Some(ActivityExecutionResult::fail(failure)),
999 };
1000 encode_activity_completion(&mut completion, &common.data_converter)
1001 .await;
1002 common.worker.complete_activity_task(completion).await?;
1003 }
1004 Err(ActivityTaskHandlerError::Fatal(err)) => return Err(err),
1005 };
1006 }
1007 };
1008 Result::<_, anyhow::Error>::Ok(())
1009 },
1010 wf_completion_processor,
1011 )?;
1012
1013 if let Some(i) = self.common.worker_interceptor.as_ref() {
1014 i.on_shutdown(self);
1015 }
1016 self.common.worker.shutdown().await;
1017 Ok(())
1018 }
1019
1020 pub fn set_worker_interceptor(&mut self, interceptor: impl WorkerInterceptor + 'static) {
1022 self.common.worker_interceptor = Some(Box::new(interceptor));
1023 }
1024
1025 pub fn add_activity_inbound_interceptor(
1028 &mut self,
1029 interceptor: impl ActivityInboundInterceptor,
1030 ) {
1031 self.common
1032 .activity_inbound_interceptors
1033 .push(Arc::new(interceptor));
1034 }
1035
1036 pub fn register_workflow_interceptors(
1041 &mut self,
1042 constructors: Vec<WorkflowInterceptorConstructor>,
1043 ) {
1044 self.common.workflow_interceptor_constructors = constructors;
1045 }
1046
1047 pub fn with_new_core_worker(&mut self, new_core_worker: Arc<CoreWorker>) {
1051 self.common.worker = new_core_worker;
1052 }
1053
1054 pub fn cached_workflows(&self) -> usize {
1057 self.workflow_half.workflows.borrow().len()
1058 }
1059
1060 pub fn worker_instance_key(&self) -> Uuid {
1062 self.common.worker.worker_instance_key()
1063 }
1064
1065 #[doc(hidden)]
1066 pub fn core_worker(&self) -> Arc<CoreWorker> {
1067 self.common.worker.clone()
1068 }
1069
1070 fn split_apart(&mut self) -> (&mut CommonWorker, &mut WorkflowHalf, &mut ActivityHalf) {
1071 (
1072 &mut self.common,
1073 &mut self.workflow_half,
1074 &mut self.activity_half,
1075 )
1076 }
1077}
1078
1079impl WorkflowHalf {
1080 #[allow(clippy::type_complexity)]
1081 async fn workflow_activation_handler(
1082 &self,
1083 common: &CommonWorker,
1084 shutdown_token: CancellationToken,
1085 mut activation: WorkflowActivation,
1086 completions_tx: &UnboundedSender<WorkflowActivationCompletion>,
1087 executor: &WorkflowExecutor,
1088 ) -> Result<Option<WorkflowFutureHandle<TaskHandle<WorkflowResult<Payload>>>>, anyhow::Error>
1089 {
1090 let mut res = None;
1091 let run_id = activation.run_id.clone();
1092
1093 if let Some(sw) = activation.jobs.iter_mut().find_map(|j| match j.variant {
1096 Some(Variant::InitializeWorkflow(ref mut sw)) => Some(sw),
1097 _ => None,
1098 }) {
1099 let workflow_type = sw.workflow_type.clone();
1100 let (wff, activations) = {
1101 if let Some(factory) = self.workflow_definitions.get_workflow(&workflow_type) {
1102 match start_workflow(
1103 factory,
1104 common.worker.get_config().namespace.clone(),
1105 common.task_queue.clone(),
1106 run_id.clone(),
1107 std::mem::take(sw),
1108 completions_tx.clone(),
1109 common.data_converter.clone(),
1110 self.detect_nondeterministic_futures,
1111 self.patch_activation_callback.clone(),
1112 common.workflow_interceptor_constructors.clone(),
1113 ) {
1114 Ok(result) => result,
1115 Err(e) => {
1116 warn!("Failed to create workflow {workflow_type}: {e}");
1117 completions_tx
1118 .send(WorkflowActivationCompletion::fail(
1119 run_id,
1120 format!("Failed to create workflow: {e}").into(),
1121 Some(WorkflowTaskFailedCause::WorkflowWorkerUnhandledFailure),
1122 ))
1123 .expect("Completion channel intact");
1124 return Ok(None);
1125 }
1126 }
1127 } else {
1128 warn!("Workflow type {workflow_type} not found");
1129 completions_tx
1130 .send(WorkflowActivationCompletion::fail(
1131 run_id,
1132 format!("Workflow type {workflow_type} not found").into(),
1133 Some(WorkflowTaskFailedCause::WorkflowWorkerUnhandledFailure),
1134 ))
1135 .expect("Completion channel intact");
1136 return Ok(None);
1137 }
1138 };
1139 let wff = tokio::task::coop::unconstrained(wff);
1142 let jh = executor.spawn(async move {
1144 tokio::select! {
1145 r = wff.fuse() => r,
1146 _ = shutdown_token.cancelled() => {
1149 Err(WorkflowTermination::Evicted)
1150 }
1151 }
1152 });
1153 res = Some(WorkflowFutureHandle {
1154 join_handle: jh,
1155 run_id: run_id.clone(),
1156 });
1157 loop {
1158 if self.workflows.borrow_mut().contains_key(&run_id) {
1163 self.workflow_removed_from_map.notified().await;
1164 } else {
1165 break;
1166 }
1167 }
1168 self.workflows.borrow_mut().insert(
1169 run_id.clone(),
1170 WorkflowData {
1171 activation_chan: activations,
1172 },
1173 );
1174 }
1175
1176 if let Some(dat) = self.workflows.borrow_mut().get_mut(&run_id) {
1179 dat.activation_chan
1180 .send(activation)
1181 .expect("Workflow should exist if we're sending it an activation");
1182 } else {
1183 if activation.jobs.len() == 1
1189 && matches!(
1190 activation.jobs.first().map(|j| &j.variant),
1191 Some(Some(Variant::RemoveFromCache(_)))
1192 )
1193 {
1194 completions_tx
1195 .send(WorkflowActivationCompletion::from_cmds(run_id, vec![]))
1196 .expect("Completion channel intact");
1197 return Ok(None);
1198 }
1199
1200 bail!("Got activation {activation:?} for unknown workflow {run_id}");
1203 };
1204
1205 Ok(res)
1206 }
1207}
1208
1209impl ActivityHalf {
1210 fn activity_task_handler(
1212 &mut self,
1213 worker: Arc<CoreWorker>,
1214 client_options: ClientOptions,
1215 task_queue: String,
1216 data_converter: DataConverter,
1217 activity_inbound_interceptors: Vec<Arc<dyn ActivityInboundInterceptor>>,
1218 activity: ActivityTask,
1219 ) -> Result<(), ActivityTaskHandlerError> {
1220 match activity.variant {
1221 Some(activity_task::Variant::Start(start)) => {
1222 let Some(act_fn) = self.activities.get(&start.activity_type) else {
1223 let activity_type = start.activity_type.clone();
1224 let source =
1225 ActivityNotRegisteredError::new(activity_type, self.activities.names());
1226 return Err(ActivityTaskHandlerError::UnregisteredActivity {
1227 source,
1228 task_token: activity.task_token,
1229 });
1230 };
1231 let span = info_span!(
1232 "RunActivity",
1233 "otel.name" = format!("RunActivity:{}", start.activity_type),
1234 "otel.kind" = "server",
1235 "temporalActivityID" = start.activity_id,
1236 "temporalWorkflowID" = field::Empty,
1237 "temporalRunID" = field::Empty,
1238 );
1239 let ct = CancellationToken::new();
1240 let task_token = activity.task_token;
1241 self.task_tokens_to_cancels
1242 .insert(task_token.clone().into(), ct.clone());
1243
1244 let (ctx, args) = ActivityContext::new(
1245 worker.clone(),
1246 client_options,
1247 ct,
1248 task_queue,
1249 task_token.clone(),
1250 start,
1251 );
1252 let codec_data_converter = data_converter.clone();
1253
1254 tokio::spawn(async move {
1255 let act_fut = async move {
1256 if let Some(info) = &ctx.info().workflow_execution {
1257 Span::current()
1258 .record("temporalWorkflowID", info.workflow_id())
1259 .record("temporalRunID", info.run_id());
1260 }
1261 (act_fn)(args, data_converter, ctx, activity_inbound_interceptors).await
1262 }
1263 .instrument(span);
1264 let result = act_fut.await;
1265 let result = match result {
1266 Ok(output) => {
1267 let pc = codec_data_converter.payload_converter();
1270 let ctx = SerializationContext {
1271 data: &SerializationContextData::Activity,
1272 converter: pc,
1273 };
1274 match output.serialize_payload(&ctx) {
1275 Ok(payload) => ActivityExecutionResult::ok(payload),
1276 Err(err) => {
1277 activity_error_to_core_result(&codec_data_converter, err.into())
1278 }
1279 }
1280 }
1281 Err(err) => activity_error_to_core_result(&codec_data_converter, err),
1282 };
1283 let mut completion = ActivityTaskCompletion {
1284 task_token,
1285 result: Some(result),
1286 };
1287 encode_activity_completion(&mut completion, &codec_data_converter).await;
1288 worker.complete_activity_task(completion).await?;
1289 Ok::<_, anyhow::Error>(())
1290 });
1291 }
1292 Some(activity_task::Variant::Cancel(_)) => {
1293 if let Some(ct) = self
1294 .task_tokens_to_cancels
1295 .get(activity.task_token.as_slice())
1296 {
1297 ct.cancel();
1298 }
1299 }
1300 None => {
1301 return Err(anyhow!("Undefined activity task variant").into());
1302 }
1303 }
1304 Ok(())
1305 }
1306}
1307
1308#[derive(Debug)]
1310pub enum ActExitValue<T> {
1311 WillCompleteAsync,
1313 Normal(T),
1315}
1316
1317impl<T: AsJsonPayloadExt> From<T> for ActExitValue<T> {
1318 fn from(t: T) -> Self {
1319 Self::Normal(t)
1320 }
1321}
1322
1323fn panic_formatter(panic: Box<dyn Any>) -> Box<dyn Display> {
1325 _panic_formatter::<&str>(panic)
1326}
1327fn _panic_formatter<T: 'static + PrintablePanicType>(panic: Box<dyn Any>) -> Box<dyn Display> {
1328 match panic.downcast::<T>() {
1329 Ok(d) => d,
1330 Err(orig) => {
1331 if TypeId::of::<<T as PrintablePanicType>::NextType>()
1332 == TypeId::of::<EndPrintingAttempts>()
1333 {
1334 return Box::new("Couldn't turn panic into a string");
1335 }
1336 _panic_formatter::<T::NextType>(orig)
1337 }
1338 }
1339}
1340trait PrintablePanicType: Display {
1341 type NextType: PrintablePanicType;
1342}
1343
1344impl PrintablePanicType for &str {
1345 type NextType = String;
1346}
1347impl PrintablePanicType for String {
1348 type NextType = EndPrintingAttempts;
1349}
1350struct EndPrintingAttempts {}
1351impl Display for EndPrintingAttempts {
1352 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1353 write!(f, "Will never be printed")
1354 }
1355}
1356impl PrintablePanicType for EndPrintingAttempts {
1357 type NextType = EndPrintingAttempts;
1358}
1359
1360#[cfg(test)]
1361mod tests {
1362 use super::*;
1363 use crate::{activities::ActivityError, workflow_interceptors::WorkflowInterceptor};
1364 use futures_util::future::BoxFuture;
1365 use std::sync::atomic::{AtomicUsize, Ordering};
1366 use temporalio_common::{
1367 data_converters::{
1368 DefaultFailureConverter, PayloadCodec, PayloadConversionError, PayloadConverter,
1369 },
1370 protos::coresdk::{
1371 activity_result::activity_execution_result,
1372 workflow_commands::{CompleteWorkflowExecution, workflow_command},
1373 workflow_completion::workflow_activation_completion,
1374 },
1375 };
1376 use temporalio_macros::{activities, activity_definitions, workflow, workflow_methods};
1377
1378 #[derive(Default)]
1379 struct FailingEncodeCodec {
1380 calls: AtomicUsize,
1381 }
1382
1383 impl PayloadCodec for FailingEncodeCodec {
1384 fn encode(
1385 &self,
1386 _: &SerializationContextData,
1387 _: Vec<Payload>,
1388 ) -> BoxFuture<'static, Result<Vec<Payload>, PayloadConversionError>> {
1389 self.calls.fetch_add(1, Ordering::SeqCst);
1390 async move {
1391 Err(PayloadConversionError::EncodingError(
1392 "codec encode failed".into(),
1393 ))
1394 }
1395 .boxed()
1396 }
1397
1398 fn decode(
1399 &self,
1400 _: &SerializationContextData,
1401 payloads: Vec<Payload>,
1402 ) -> BoxFuture<'static, Result<Vec<Payload>, PayloadConversionError>> {
1403 async move { Ok(payloads) }.boxed()
1404 }
1405 }
1406
1407 struct NoopWorkflowInterceptor;
1408
1409 impl WorkflowInterceptor for NoopWorkflowInterceptor {}
1410
1411 struct MyActivities {}
1412
1413 struct SharedActivities;
1414 #[activity_definitions]
1415 impl SharedActivities {
1416 #[activity(name = "shared-greet")]
1417 fn greet(name: String) -> Result<String, ActivityError> {
1418 unimplemented!()
1419 }
1420 }
1421
1422 #[activities]
1423 impl MyActivities {
1424 #[activity]
1425 async fn my_activity(_ctx: ActivityContext) -> Result<(), ActivityError> {
1426 Ok(())
1427 }
1428
1429 #[activity(definition = shared_activities::Greet)]
1430 async fn greet(_ctx: ActivityContext, name: String) -> Result<String, ActivityError> {
1431 Ok(name)
1432 }
1433
1434 #[activity]
1435 async fn takes_self(
1436 self: Arc<Self>,
1437 _ctx: ActivityContext,
1438 _: String,
1439 ) -> Result<(), ActivityError> {
1440 Ok(())
1441 }
1442 }
1443
1444 #[test]
1445 fn test_activity_registration() {
1446 let act_instance = MyActivities {};
1447 let _ = WorkerOptions::new("task_q").register_activities(act_instance);
1448 }
1449
1450 #[tokio::test]
1451 async fn workflow_completion_codec_error_uses_unencoded_failure() {
1452 let codec = Arc::new(FailingEncodeCodec::default());
1453 let data_converter = DataConverter::new(
1454 PayloadConverter::default(),
1455 DefaultFailureConverter,
1456 codec.clone(),
1457 );
1458 let mut completion = WorkflowActivationCompletion::from_cmd(
1459 "run-id",
1460 workflow_command::Variant::CompleteWorkflowExecution(CompleteWorkflowExecution {
1461 result: Some(Payload::default()),
1462 }),
1463 );
1464
1465 encode_workflow_completion(&mut completion, &data_converter).await;
1466
1467 let Some(workflow_activation_completion::Status::Failed(failed)) = completion.status else {
1468 panic!("expected failed workflow completion")
1469 };
1470 assert_eq!(
1471 failed.failure.unwrap().message,
1472 "Failed encoding completion: Encoding error: codec encode failed"
1473 );
1474 assert_eq!(
1475 failed.force_cause,
1476 WorkflowTaskFailedCause::WorkflowWorkerUnhandledFailure as i32
1477 );
1478 assert_eq!(codec.calls.load(Ordering::SeqCst), 1);
1479 }
1480
1481 #[tokio::test]
1482 async fn activity_completion_codec_error_uses_unencoded_failure() {
1483 let codec = Arc::new(FailingEncodeCodec::default());
1484 let data_converter = DataConverter::new(
1485 PayloadConverter::default(),
1486 DefaultFailureConverter,
1487 codec.clone(),
1488 );
1489 let mut completion = ActivityTaskCompletion {
1490 task_token: vec![],
1491 result: Some(ActivityExecutionResult::ok(Payload::default())),
1492 };
1493
1494 encode_activity_completion(&mut completion, &data_converter).await;
1495
1496 let Some(activity_execution_result::Status::Failed(failed)) =
1497 completion.result.unwrap().status
1498 else {
1499 panic!("expected failed activity completion")
1500 };
1501 let failure = failed.failure.unwrap();
1502 assert_eq!(
1503 failure.message,
1504 "Failed encoding activity completion: Encoding error: codec encode failed"
1505 );
1506 assert!(matches!(
1507 failure.failure_info,
1508 Some(
1509 temporalio_common::protos::temporal::api::failure::v1::failure::FailureInfo::ApplicationFailureInfo(info)
1510 ) if !info.non_retryable
1511 ));
1512 assert_eq!(codec.calls.load(Ordering::SeqCst), 1);
1513 }
1514
1515 #[allow(unused, clippy::diverging_sub_expression)]
1517 fn test_activity_via_workflow_context() {
1518 let wf_ctx: WorkflowContext<MyWorkflow> = unimplemented!();
1519 wf_ctx.execute_activity(
1520 MyActivities::my_activity,
1521 (),
1522 ActivityOptions::start_to_close_timeout(Duration::from_secs(5)),
1523 );
1524 wf_ctx.execute_activity(
1525 SharedActivities::greet,
1526 "Hi".to_owned(),
1527 ActivityOptions::start_to_close_timeout(Duration::from_secs(5)),
1528 );
1529 wf_ctx.execute_activity(
1530 MyActivities::greet,
1531 "Hi".to_owned(),
1532 ActivityOptions::start_to_close_timeout(Duration::from_secs(5)),
1533 );
1534 wf_ctx.execute_activity(
1535 MyActivities::takes_self,
1536 "Hi".to_owned(),
1537 ActivityOptions::start_to_close_timeout(Duration::from_secs(5)),
1538 );
1539 }
1540
1541 #[allow(dead_code, unreachable_code, unused, clippy::diverging_sub_expression)]
1543 async fn test_activity_direct_invocation() {
1544 let ctx: ActivityContext = unimplemented!();
1545 let _result = MyActivities::my_activity.run(ctx).await;
1546 }
1547
1548 #[workflow]
1549 struct MyWorkflow {
1550 counter: u32,
1551 }
1552
1553 #[allow(dead_code)]
1554 #[workflow_methods]
1555 impl MyWorkflow {
1556 #[init]
1557 fn new(_ctx: &WorkflowContextView, _input: String) -> Self {
1558 Self { counter: 0 }
1559 }
1560
1561 #[run]
1562 async fn run(ctx: &mut WorkflowContext<Self>) -> WorkflowResult<String> {
1563 Ok(format!("Counter: {}", ctx.state(|s| s.counter)))
1564 }
1565
1566 #[signal(name = "increment")]
1567 fn increment_counter(&mut self, _ctx: &mut SyncWorkflowContext<Self>, amount: u32) {
1568 self.counter += amount;
1569 }
1570
1571 #[signal]
1572 async fn async_signal(_ctx: &mut WorkflowContext<Self>) {}
1573
1574 #[query]
1575 fn get_counter(&self, _ctx: &WorkflowContextView) -> u32 {
1576 self.counter
1577 }
1578
1579 #[update(name = "double")]
1580 fn double_counter(&mut self, _ctx: &mut SyncWorkflowContext<Self>) -> u32 {
1581 self.counter *= 2;
1582 self.counter
1583 }
1584
1585 #[update]
1586 async fn async_update(_ctx: &mut WorkflowContext<Self>, val: i32) -> i32 {
1587 val * 2
1588 }
1589 }
1590
1591 #[test]
1592 fn test_workflow_registration() {
1593 let _ = WorkerOptions::new("task_q")
1594 .register_workflow::<MyWorkflow>()
1595 .unwrap();
1596 }
1597
1598 #[test]
1599 fn workflow_interceptor_registration_replaces_previous_constructors() {
1600 let mut options = WorkerOptions::new("task_q").build();
1601 options.register_workflow_interceptors(vec![
1602 WorkflowInterceptorConstructor::new(|_| NoopWorkflowInterceptor),
1603 WorkflowInterceptorConstructor::new(|_| NoopWorkflowInterceptor),
1604 ]);
1605 assert_eq!(options.workflow_interceptor_constructors.len(), 2);
1606
1607 options.register_workflow_interceptors(vec![WorkflowInterceptorConstructor::new(|_| {
1608 NoopWorkflowInterceptor
1609 })]);
1610 assert_eq!(options.workflow_interceptor_constructors.len(), 1);
1611 }
1612
1613 #[test]
1614 fn duplicate_workflow_registration_errors() {
1615 let result = WorkerOptions::new("task_q")
1616 .register_workflow::<MyWorkflow>()
1617 .unwrap()
1618 .register_workflow::<MyWorkflow>();
1619
1620 let err = match result {
1621 Ok(_) => panic!("duplicate workflow registration should error"),
1622 Err(err) => err,
1623 };
1624 assert_eq!(
1625 err,
1626 WorkflowRegistrationError::DuplicateWorkflowType {
1627 workflow_type: "MyWorkflow".to_string()
1628 }
1629 );
1630 }
1631
1632 #[test]
1633 fn factory_registration_with_init_errors() {
1634 let result = WorkerOptions::new("task_q")
1635 .register_workflow_with_factory(|| MyWorkflow { counter: 0 });
1636
1637 let err = match result {
1638 Ok(_) => panic!("factory registration with #[init] should error"),
1639 Err(err) => err,
1640 };
1641 assert_eq!(
1642 err,
1643 WorkflowRegistrationError::FactoryRegistrationWithInit {
1644 workflow_type: "MyWorkflow".to_string()
1645 }
1646 );
1647 }
1648
1649 fn default_identity() -> String {
1650 format!(
1651 "{}@{}",
1652 std::process::id(),
1653 gethostname::gethostname().to_string_lossy()
1654 )
1655 }
1656
1657 #[rstest::rstest]
1658 #[case::default_when_none_provided(None, "", Some(default_identity()))]
1659 #[case::connection_identity_preserved(None, "conn-identity", None)]
1660 #[case::worker_override_takes_precedence(
1661 Some("worker-identity"),
1662 "conn-identity",
1663 Some("worker-identity".into())
1664 )]
1665 #[case::worker_override_with_empty_connection(
1666 Some("worker-identity"),
1667 "",
1668 Some("worker-identity".into())
1669 )]
1670 #[test]
1671 fn client_identity_resolution(
1672 #[case] worker_override: Option<&str>,
1673 #[case] connection_identity: &str,
1674 #[case] expected: Option<String>,
1675 ) {
1676 let opts = WorkerOptions::new("task_q")
1677 .task_types(WorkerTaskTypes::activity_only())
1678 .maybe_client_identity_override(worker_override.map(|s| s.to_owned()))
1679 .build();
1680 let config = opts
1681 .to_core_options("ns".into(), connection_identity.into())
1682 .unwrap();
1683 assert_eq!(config.client_identity_override, expected);
1684 }
1685
1686 #[rstest::rstest]
1687 #[case::default_enforces_error_limit(None, false)]
1688 #[case::opt_out_disables_error_limit(Some(true), true)]
1689 #[case::explicit_enable_error_limit(Some(false), false)]
1690 #[test]
1691 fn disable_payload_error_limit_propagates(
1692 #[case] override_value: Option<bool>,
1693 #[case] expected: bool,
1694 ) {
1695 let config = WorkerOptions::new("task_q")
1696 .task_types(WorkerTaskTypes::activity_only())
1697 .maybe_disable_payload_error_limit(override_value)
1698 .build()
1699 .to_core_options("ns".into(), String::new())
1700 .unwrap();
1701 assert_eq!(config.disable_payload_error_limit, expected);
1702 }
1703
1704 #[test]
1705 fn max_eager_activity_reservations_per_workflow_task_propagates() {
1706 let config = WorkerOptions::new("task_q")
1707 .task_types(WorkerTaskTypes::activity_only())
1708 .max_eager_activity_reservations_per_workflow_task(7)
1709 .build()
1710 .to_core_options("ns".into(), String::new())
1711 .unwrap();
1712 assert_eq!(config.max_eager_activity_reservations_per_workflow_task, 7);
1713 }
1714}