Skip to main content

temporalio_sdk/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![warn(missing_docs)] // error if there are missing docs
3
4//! This crate defines the Temporal Rust SDK.
5//!
6//! The SDK is built on top of Core and provides a native Rust experience for writing Temporal
7//! Workflows and Activities.
8//!
9//! An example of running an activity worker:
10//! ```no_run
11//! use std::str::FromStr;
12//! use temporalio_client::{Client, ClientOptions, Connection, ConnectionOptions, Url};
13//! use temporalio_common::worker::{WorkerDeploymentOptions, WorkerDeploymentVersion};
14//! use temporalio_macros::activities;
15//! use temporalio_sdk::{
16//!     Runtime, Worker, WorkerOptions,
17//!     activities::{ActivityContext, ActivityError},
18//! };
19//!
20//! struct MyActivities;
21//!
22//! #[activities]
23//! impl MyActivities {
24//!     #[activity]
25//!     pub(crate) async fn echo(
26//!         _ctx: ActivityContext,
27//!         e: String,
28//!     ) -> Result<String, ActivityError> {
29//!         Ok(e)
30//!     }
31//! }
32//!
33//! #[tokio::main]
34//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
35//!     let connection_options =
36//!         ConnectionOptions::new(Url::from_str("http://localhost:7233")?).build();
37//!     let runtime = Runtime::from_current_tokio(Default::default())?;
38//!     let connection = Connection::connect(connection_options).await?;
39//!     let client = Client::new(connection, ClientOptions::new("my_namespace").build())?;
40//!
41//!     let worker_options = WorkerOptions::new("task_queue")
42//!         .deployment_options(
43//!             WorkerDeploymentOptions::new(
44//!                 WorkerDeploymentVersion::builder()
45//!                     .deployment_name("my_deployment")
46//!                     .build_id("my_build_id")
47//!                     .build(),
48//!             )
49//!             .build(),
50//!         )
51//!         .register_activities(MyActivities)
52//!         .build();
53//!
54//!     let mut worker = Worker::new(&runtime, client, worker_options)?;
55//!     worker.run().await?;
56//!
57//!     Ok(())
58//! }
59//! ```
60
61#[macro_use]
62extern crate tracing;
63extern crate self as temporalio_sdk;
64
65pub mod activities;
66pub mod error;
67pub mod interceptors;
68#[cfg(feature = "experimental")]
69/// Experimental APIs for configuring clients and workers with reusable plugins.
70pub mod plugins;
71pub mod runtime;
72#[cfg(feature = "testing")]
73pub mod testing;
74mod workflow_executor;
75mod workflow_future;
76pub mod workflow_interceptors;
77mod workflow_registry;
78/// Workflow history replay APIs.
79pub mod workflow_replayer;
80#[cfg(feature = "wasm-workflows")]
81mod workflow_wasm;
82pub mod workflows;
83
84#[cfg(feature = "experimental")]
85pub use crate::plugins::{
86    ClientAndWorkerPlugin, SimplePlugin, SimplePluginBuilder, SimplePluginOption, WorkerPlugin,
87};
88pub use crate::{
89    error::{
90        ActivityExecutionError, ApplicationFailure, CancelExternalWorkflowError,
91        ChildWorkflowExecutionError, ChildWorkflowStartError, OutgoingActivityError, OutgoingError,
92        OutgoingWorkflowError, RetryState, RuntimeError, TimeoutType, WorkerCreateError,
93        WorkerRunError, WorkerValidationError, WorkflowRegistrationError, WorkflowSignalError,
94    },
95    workflow_registry::WorkflowDefinitions,
96};
97pub use runtime::Runtime;
98pub use temporalio_client::Namespace;
99pub use temporalio_workflow::{
100    ActivityCancellationType, ActivityCloseTimeouts, ActivityOptions, BaseWorkflowContext,
101    CancellableFuture, CancellableFutureWithReason, ChildWorkflowCancellationType,
102    ChildWorkflowOptions, ContinueAsNewOptions, ExternalWorkflowHandle, LocalActivityOptions,
103    MemoValue, ParentClosePolicy, SignalWorkflowOptions, StartChildWorkflowExecutionFailedCause,
104    StartChildWorkflowOutput, StartedChildWorkflow, SyncWorkflowContext, TimerOptions, TimerResult,
105    VersioningIntent, WaitConditionOptions, WorkflowCancellationError, WorkflowCancellationToken,
106    WorkflowContext, WorkflowContextFuture, WorkflowContextKey, WorkflowContextView,
107    WorkflowIdReusePolicy, WorkflowRandomValue, WorkflowResult, WorkflowTermination,
108};
109#[cfg(feature = "experimental")]
110pub use temporalio_workflow::{
111    ContinueAsNewVersioningBehavior, NexusOperationCancellationType, NexusOperationOptions,
112    PatchActivationCallback, PatchActivationInput, StartedNexusOperation,
113};
114#[cfg(feature = "wasm-workflows")]
115pub use workflow_wasm::WasmWorkflowComponent;
116
117use crate::{
118    activities::{
119        ActivityContext, ActivityDefinitions, ActivityImplementer, ExecutableActivity,
120        activity_error_to_core_result,
121    },
122    interceptors::{ActivityInboundInterceptor, Next, RunWorkerInput, WorkerInterceptor},
123    workflow_executor::{TaskHandle, WorkflowExecutor},
124    workflow_future::start_workflow,
125    workflow_interceptors::WorkflowInterceptorConstructor,
126};
127use anyhow::{anyhow, bail};
128use futures_util::{FutureExt, StreamExt, TryStreamExt, future::LocalBoxFuture};
129use std::{
130    any::{Any, TypeId},
131    cell::RefCell,
132    collections::{HashMap, HashSet},
133    fmt::{Debug, Display, Formatter},
134    future::Future,
135    sync::Arc,
136    time::Duration,
137};
138use temporalio_client::{Client, ClientOptions, NamespacedClient};
139#[cfg(feature = "experimental")]
140use temporalio_common::protos::temporal::api::worker::v1::PluginInfo;
141use temporalio_common::{
142    ActivityDefinition, WorkflowDefinition,
143    data_converters::{
144        ActivitySerializationContext, DataConverter, SerializationContext,
145        SerializationContextData, WorkflowSerializationContext,
146    },
147    payload_visitor::{decode_payloads, encode_payloads},
148    protos::{
149        TaskToken,
150        coresdk::{
151            ActivityTaskCompletion,
152            activity_result::ActivityExecutionResult,
153            activity_task::{ActivityTask, activity_task},
154            workflow_activation::{WorkflowActivation, workflow_activation_job::Variant},
155            workflow_completion::WorkflowActivationCompletion,
156        },
157        temporal::api::{
158            common::v1::Payload, enums::v1::WorkflowTaskFailedCause, failure::v1::Failure,
159        },
160    },
161    worker::{WorkerDeploymentOptions, WorkerTaskTypes, build_id_from_current_exe},
162};
163use temporalio_sdk_core::{
164    PollError, Worker as CoreWorker, WorkerConfig, WorkerVersioningStrategy, init_worker,
165};
166use temporalio_workflow::{InternalPatchActivationCallback, workflows::WorkflowImplementation};
167use tokio::sync::{
168    Notify,
169    mpsc::{UnboundedSender, unbounded_channel},
170};
171use tokio_stream::wrappers::UnboundedReceiverStream;
172use tokio_util::sync::CancellationToken;
173use tracing::{Instrument, Span, field};
174use uuid::Uuid;
175
176use crate::runtime::{PollerBehavior, WorkflowErrorType, worker_tuner::WorkerTuner};
177
178/// Contains options for configuring a worker.
179///
180/// The worker polls task types according to its registered workflows and activities. At least one
181/// workflow or activity must be registered.
182#[derive(bon::Builder, Clone)]
183#[builder(start_fn = new, on(String, into), state_mod(vis = "pub"))]
184#[non_exhaustive]
185pub struct WorkerOptions {
186    /// What task queue will this worker poll from? This task queue name will be used for both
187    /// workflow and activity polling.
188    #[builder(start_fn)]
189    pub task_queue: String,
190
191    #[builder(field)]
192    activities: ActivityDefinitions,
193
194    #[builder(field)]
195    workflows: WorkflowDefinitions,
196
197    #[builder(field)]
198    worker_interceptors: Vec<Arc<dyn WorkerInterceptor>>,
199
200    #[builder(field)]
201    activity_inbound_interceptors: Vec<Arc<dyn ActivityInboundInterceptor>>,
202
203    #[builder(field)]
204    workflow_interceptor_constructors: Vec<WorkflowInterceptorConstructor>,
205
206    #[builder(field)]
207    #[cfg(feature = "experimental")]
208    worker_plugins: Vec<Arc<dyn WorkerPlugin>>,
209
210    #[builder(field)]
211    #[cfg(feature = "experimental")]
212    client_plugin_names: HashSet<String>,
213
214    #[cfg(feature = "wasm-workflows")]
215    #[builder(field)]
216    wasm_workflow_components: Vec<WasmWorkflowComponent>,
217
218    /// Set the deployment options for this worker. Defaults to a hash of the currently running
219    /// executable.
220    #[builder(default = def_build_id())]
221    pub deployment_options: WorkerDeploymentOptions,
222    /// A human-readable string that can identify this worker. If set, overrides the identity on
223    /// the client used by this worker. If unset and the client has no identity, defaults to
224    /// `{pid}@{hostname}`.
225    pub client_identity_override: Option<String>,
226    /// If set nonzero, workflows will be cached and sticky task queues will be used, meaning that
227    /// history updates are applied incrementally to suspended instances of workflow execution.
228    /// Workflows are evicted according to a least-recently-used policy once the cache maximum is
229    /// reached. Workflows may also be explicitly evicted at any time, or as a result of errors
230    /// or failures.
231    #[builder(default = 1000)]
232    pub max_cached_workflows: usize,
233    /// Set a [`runtime::worker_tuner::WorkerTuner`] for this worker, which controls how many slots
234    /// are available for the different kinds of tasks.
235    #[builder(into, default)]
236    pub tuner: WorkerTuner,
237    /// Controls how polling for Workflow tasks will happen on this worker's task queue. See also
238    /// [WorkerConfig::nonsticky_to_sticky_poll_ratio]. If using SimpleMaximum, Must be at least 2
239    /// when `max_cached_workflows` > 0, or is an error.
240    ///
241    /// If left unset, the worker uses `SimpleMaximum(5)` and becomes eligible for automatic
242    /// enrollment into poller autoscaling when the namespace advertises support for it.
243    pub workflow_task_poller_behavior: Option<PollerBehavior>,
244    /// Only applies when using [PollerBehavior::SimpleMaximum]
245    ///
246    /// (max workflow task polls * this number) = the number of max pollers that will be allowed for
247    /// the nonsticky queue when sticky tasks are enabled. If both defaults are used, the sticky
248    /// queue will allow 4 max pollers while the nonsticky queue will allow one. The minimum for
249    /// either poller is 1, so if the maximum allowed is 1 and sticky queues are enabled, there will
250    /// be 2 concurrent polls.
251    #[builder(default = 0.2)]
252    pub nonsticky_to_sticky_poll_ratio: f32,
253    /// Controls how polling for Activity tasks will happen on this worker's task queue.
254    ///
255    /// If left unset, the worker uses `SimpleMaximum(5)` and becomes eligible for automatic
256    /// enrollment into poller autoscaling when the namespace advertises support for it.
257    pub activity_task_poller_behavior: Option<PollerBehavior>,
258    /// Controls how polling for Nexus tasks will happen on this worker's task queue.
259    ///
260    /// If left unset, the worker uses `SimpleMaximum(5)` and becomes eligible for automatic
261    /// enrollment into poller autoscaling when the namespace advertises support for it.
262    pub nexus_task_poller_behavior: Option<PollerBehavior>,
263    /// How long a workflow task is allowed to sit on the sticky queue before it is timed out
264    /// and moved to the non-sticky queue where it may be picked up by any worker.
265    #[builder(default = Duration::from_secs(10))]
266    pub sticky_queue_schedule_to_start_timeout: Duration,
267    /// Longest interval for throttling activity heartbeats
268    #[builder(default = Duration::from_secs(60))]
269    pub max_heartbeat_throttle_interval: Duration,
270    /// Default interval for throttling activity heartbeats in case
271    /// `ActivityOptions.heartbeat_timeout` is unset.
272    /// When the timeout *is* set in the `ActivityOptions`, throttling is set to
273    /// `heartbeat_timeout * 0.8`.
274    #[builder(default = Duration::from_secs(30))]
275    pub default_heartbeat_throttle_interval: Duration,
276    /// Sets the maximum number of activities per second the task queue will dispatch, controlled
277    /// server-side. Note that this only takes effect upon an activity poll request. If multiple
278    /// workers on the same queue have different values set, they will thrash with the last poller
279    /// winning.
280    ///
281    /// Setting this to a nonzero value will also disable eager activity execution.
282    pub max_task_queue_activities_per_second: Option<f64>,
283    /// Limits the number of activities per second that this worker will process. The worker will
284    /// not poll for new activities if by doing so it might receive and execute an activity which
285    /// would cause it to exceed this limit. Negative, zero, or NaN values will cause building
286    /// the options to fail.
287    pub max_worker_activities_per_second: Option<f64>,
288    /// Maximum number of activity slots that may be reserved for eager execution when completing
289    /// a workflow task. The default is 3. Setting this to zero disables eager activity execution.
290    #[builder(default = 3)]
291    pub max_eager_activity_reservations_per_workflow_task: usize,
292    /// Any error types listed here will cause any workflow being processed by this worker to fail,
293    /// rather than simply failing the workflow task.
294    #[builder(default)]
295    pub workflow_failure_errors: HashSet<WorkflowErrorType>,
296    /// Like [WorkerConfig::workflow_failure_errors], but specific to certain workflow types (the
297    /// map key).
298    #[builder(default)]
299    pub workflow_types_to_failure_errors: HashMap<String, HashSet<WorkflowErrorType>>,
300    /// If set, the worker will issue cancels for all outstanding activities and nexus operations after
301    /// shutdown has been initiated and this amount of time has elapsed.
302    pub graceful_shutdown_period: Option<Duration>,
303    /// Detect nondeterministic async usage in workflow code. When enabled (the default), workflows
304    /// that use external async operations (tokio timers, IO, spawned threads, raw tokio::sync
305    /// channels, etc.) will have their tasks failed with a descriptive error.
306    #[builder(default = true)]
307    pub detect_nondeterministic_futures: bool,
308    /// If set true, the worker will not proactively fail workflow/activity tasks whose payloads
309    /// exceed the namespace error limits; oversized payloads are sent to server, which enforces the
310    /// limit. Defaults to false.
311    /// NOTE: Experimental
312    #[cfg(feature = "experimental")]
313    #[cfg_attr(
314        docsrs,
315        builder(setters(
316            some_fn(name = disable_payload_error_limit_impl, vis = "pub(crate)"),
317            option_fn(name = maybe_disable_payload_error_limit_impl, vis = "pub(crate)")
318        ))
319    )]
320    #[builder(default = false)]
321    pub disable_payload_error_limit: bool,
322    /// Experimental callback that decides whether the first non-replay call to
323    /// [`SyncWorkflowContext::patched`] for a patch ID should activate that patch.
324    ///
325    /// The callback receives an immutable workflow information snapshot and patch ID. Returning
326    /// `true` records the patch marker; returning `false` leaves the patch inactive for the
327    /// workflow run. For registered WASM workflow components, the callback remains on the worker
328    /// host and is invoked through the workflow component's synchronous host interface.
329    #[cfg(feature = "experimental")]
330    #[cfg_attr(
331        docsrs,
332        builder(setters(
333            some_fn(name = patch_activation_callback_impl, vis = "pub(crate)"),
334            option_fn(name = maybe_patch_activation_callback_impl, vis = "pub(crate)")
335        ))
336    )]
337    pub patch_activation_callback: Option<PatchActivationCallback>,
338}
339
340// Bon does not propagate `doc(cfg)` to generated setters, so these docs-only methods forward to
341// renamed generated implementations.
342#[cfg(all(feature = "experimental", docsrs))]
343impl<S: worker_options_builder::State> WorkerOptionsBuilder<S> {
344    /// Set whether payloads over the namespace error limit are sent to the server.
345    #[doc(cfg(feature = "experimental"))]
346    pub fn disable_payload_error_limit(
347        self,
348        value: bool,
349    ) -> WorkerOptionsBuilder<worker_options_builder::SetDisablePayloadErrorLimit<S>>
350    where
351        S::DisablePayloadErrorLimit: worker_options_builder::IsUnset,
352    {
353        self.disable_payload_error_limit_impl(value)
354    }
355
356    /// Set the payload error limit override from an optional value.
357    #[doc(cfg(feature = "experimental"))]
358    pub fn maybe_disable_payload_error_limit(
359        self,
360        value: Option<bool>,
361    ) -> WorkerOptionsBuilder<worker_options_builder::SetDisablePayloadErrorLimit<S>>
362    where
363        S::DisablePayloadErrorLimit: worker_options_builder::IsUnset,
364    {
365        self.maybe_disable_payload_error_limit_impl(value)
366    }
367
368    /// Set the callback used to decide whether a patch should activate.
369    #[doc(cfg(feature = "experimental"))]
370    pub fn patch_activation_callback(
371        self,
372        value: PatchActivationCallback,
373    ) -> WorkerOptionsBuilder<worker_options_builder::SetPatchActivationCallback<S>>
374    where
375        S::PatchActivationCallback: worker_options_builder::IsUnset,
376    {
377        self.patch_activation_callback_impl(value)
378    }
379
380    /// Set the patch activation callback from an optional value.
381    #[doc(cfg(feature = "experimental"))]
382    pub fn maybe_patch_activation_callback(
383        self,
384        value: Option<PatchActivationCallback>,
385    ) -> WorkerOptionsBuilder<worker_options_builder::SetPatchActivationCallback<S>>
386    where
387        S::PatchActivationCallback: worker_options_builder::IsUnset,
388    {
389        self.maybe_patch_activation_callback_impl(value)
390    }
391}
392
393impl<S: worker_options_builder::State> WorkerOptionsBuilder<S> {
394    pub(crate) fn with_workflows(mut self, workflows: WorkflowDefinitions) -> Self {
395        self.workflows = workflows;
396        self
397    }
398
399    pub(crate) fn with_worker_interceptors(
400        mut self,
401        worker_interceptors: Vec<Arc<dyn WorkerInterceptor>>,
402    ) -> Self {
403        self.worker_interceptors = worker_interceptors;
404        self
405    }
406
407    pub(crate) fn with_workflow_interceptor_constructors(
408        mut self,
409        workflow_interceptor_constructors: Vec<WorkflowInterceptorConstructor>,
410    ) -> Self {
411        self.workflow_interceptor_constructors = workflow_interceptor_constructors;
412        self
413    }
414
415    #[cfg(feature = "experimental")]
416    pub(crate) fn with_worker_plugins(
417        mut self,
418        worker_plugins: Vec<Arc<dyn WorkerPlugin>>,
419    ) -> Self {
420        self.worker_plugins = worker_plugins;
421        self
422    }
423
424    #[cfg(feature = "wasm-workflows")]
425    pub(crate) fn with_wasm_workflow_components(
426        mut self,
427        wasm_workflow_components: Vec<WasmWorkflowComponent>,
428    ) -> Self {
429        self.wasm_workflow_components = wasm_workflow_components;
430        self
431    }
432
433    /// Register a worker plugin.
434    ///
435    /// **Experimental:** This API may change or be removed.
436    #[cfg(feature = "experimental")]
437    pub fn worker_plugin<P: WorkerPlugin>(mut self, plugin: P) -> Self {
438        self.worker_plugins.push(Arc::new(plugin));
439        self
440    }
441
442    /// Append a worker interceptor. Interceptors run in registration order.
443    #[cfg(feature = "experimental")]
444    pub fn worker_interceptor<I: WorkerInterceptor + 'static>(mut self, interceptor: I) -> Self {
445        self.worker_interceptors.push(Arc::new(interceptor));
446        self
447    }
448
449    /// Append an activity inbound interceptor. Interceptors run outermost-first in registration
450    /// order.
451    pub fn activity_inbound_interceptor<I: ActivityInboundInterceptor>(
452        mut self,
453        interceptor: I,
454    ) -> Self {
455        self.activity_inbound_interceptors
456            .push(Arc::new(interceptor));
457        self
458    }
459
460    /// Append a workflow interceptor constructor.
461    pub fn workflow_interceptor(mut self, constructor: WorkflowInterceptorConstructor) -> Self {
462        self.workflow_interceptor_constructors.push(constructor);
463        self
464    }
465
466    /// Registers all activities on an activity implementer.
467    pub fn register_activities<AI: ActivityImplementer>(mut self, instance: AI) -> Self {
468        self.activities.register_activities::<AI>(instance);
469        self
470    }
471    /// Registers a specific activitiy.
472    pub fn register_activity<AD>(mut self, instance: Arc<AD::Implementer>) -> Self
473    where
474        AD: ActivityDefinition + ExecutableActivity,
475        AD::Input: Send + Sync,
476        AD::Output: Send + Sync,
477    {
478        self.activities.register_activity::<AD>(instance);
479        self
480    }
481
482    /// Registers all workflows on a workflow implementer.
483    pub fn register_workflow<W>(mut self) -> Result<Self, WorkflowRegistrationError>
484    where
485        W: WorkflowImplementation,
486        <W::Run as WorkflowDefinition>::Input: Send,
487    {
488        self.workflows.register_workflow::<W>()?;
489        Ok(self)
490    }
491
492    /// Register a workflow with a custom factory for instance creation.
493    ///
494    /// # Warning: Advanced Usage
495    ///
496    /// This method is intended for scenarios requiring injection of un-serializable
497    /// state into workflows.
498    ///
499    /// **This can easily cause nondeterminism**
500    ///
501    /// Only use when you understand the implications and have a specific need that cannot be met
502    /// otherwise.
503    ///
504    /// # Errors
505    ///
506    /// Returns an error if a workflow with the same type is already registered, or if the workflow
507    /// type defines an `#[init]` method. Workflows using factory registration must not have
508    /// `#[init]` to avoid ambiguity about instance creation.
509    pub fn register_workflow_with_factory<W, F>(
510        mut self,
511        factory: F,
512    ) -> Result<Self, WorkflowRegistrationError>
513    where
514        W: WorkflowImplementation,
515        <W::Run as WorkflowDefinition>::Input: Send,
516        F: Fn() -> W + Send + Sync + 'static,
517    {
518        self.workflows
519            .register_workflow_run_with_factory::<W, F>(factory)?;
520        Ok(self)
521    }
522
523    /// Set the ordered constructors used to create workflow interceptors for each workflow instance.
524    ///
525    /// This replaces any previously configured workflow interceptor constructors.
526    pub fn register_workflow_interceptors(
527        mut self,
528        constructors: Vec<WorkflowInterceptorConstructor>,
529    ) -> Self {
530        self.workflow_interceptor_constructors = constructors;
531        self
532    }
533
534    /// Get a mutable reference to the workflow interceptor constructors list.
535    pub fn workflow_interceptor_constructors_mut(
536        &mut self,
537    ) -> &mut Vec<WorkflowInterceptorConstructor> {
538        &mut self.workflow_interceptor_constructors
539    }
540
541    /// Register a prebuilt WASM workflow component that exports one or more workflows.
542    #[cfg(feature = "wasm-workflows")]
543    pub fn register_wasm_workflow(mut self, component: WasmWorkflowComponent) -> Self {
544        self.wasm_workflow_components.push(component);
545        self
546    }
547}
548
549// Needs to exist to avoid https://github.com/elastio/bon/issues/359
550fn def_build_id() -> WorkerDeploymentOptions {
551    WorkerDeploymentOptions::from_build_id(build_id_from_current_exe().to_owned())
552}
553
554impl WorkerOptions {
555    /// Append a worker interceptor. Interceptors run in registration order.
556    #[cfg(feature = "experimental")]
557    pub fn worker_interceptor<I: WorkerInterceptor + 'static>(
558        &mut self,
559        interceptor: I,
560    ) -> &mut Self {
561        self.worker_interceptors.push(Arc::new(interceptor));
562        self
563    }
564
565    /// Append an activity inbound interceptor. Interceptors run outermost-first in registration
566    /// order.
567    pub fn activity_inbound_interceptor<I: ActivityInboundInterceptor>(
568        &mut self,
569        interceptor: I,
570    ) -> &mut Self {
571        self.activity_inbound_interceptors
572            .push(Arc::new(interceptor));
573        self
574    }
575
576    /// Append a workflow interceptor constructor.
577    pub fn workflow_interceptor(
578        &mut self,
579        constructor: WorkflowInterceptorConstructor,
580    ) -> &mut Self {
581        self.workflow_interceptor_constructors.push(constructor);
582        self
583    }
584
585    /// Registers all activities on an activity implementer.
586    pub fn register_activities<AI: ActivityImplementer>(&mut self, instance: AI) -> &mut Self {
587        self.activities.register_activities::<AI>(instance);
588        self
589    }
590    /// Registers a specific activitiy.
591    pub fn register_activity<AD>(&mut self, instance: Arc<AD::Implementer>) -> &mut Self
592    where
593        AD: ActivityDefinition + ExecutableActivity,
594        AD::Input: Send + Sync,
595        AD::Output: Send + Sync,
596    {
597        self.activities.register_activity::<AD>(instance);
598        self
599    }
600    /// Returns all the registered activities by cloning the current set.
601    pub fn activities(&self) -> ActivityDefinitions {
602        self.activities.clone()
603    }
604
605    /// Registers all workflows on a workflow implementer.
606    pub fn register_workflow<W>(&mut self) -> Result<&mut Self, WorkflowRegistrationError>
607    where
608        W: WorkflowImplementation,
609        <W::Run as WorkflowDefinition>::Input: Send,
610    {
611        self.workflows.register_workflow::<W>()?;
612        Ok(self)
613    }
614
615    /// Register a workflow with a custom factory for instance creation.
616    ///
617    /// # Warning: Advanced Usage
618    /// See [WorkerOptionsBuilder::register_workflow_with_factory] for more.
619    pub fn register_workflow_with_factory<W, F>(
620        &mut self,
621        factory: F,
622    ) -> Result<&mut Self, WorkflowRegistrationError>
623    where
624        W: WorkflowImplementation,
625        <W::Run as WorkflowDefinition>::Input: Send,
626        F: Fn() -> W + Send + Sync + 'static,
627    {
628        self.workflows
629            .register_workflow_run_with_factory::<W, F>(factory)?;
630        Ok(self)
631    }
632
633    /// Set the ordered constructors used to create workflow interceptors for each workflow instance.
634    ///
635    /// This replaces any previously configured workflow interceptor constructors.
636    pub fn register_workflow_interceptors(
637        &mut self,
638        constructors: Vec<WorkflowInterceptorConstructor>,
639    ) -> &mut Self {
640        self.workflow_interceptor_constructors = constructors;
641        self
642    }
643
644    /// Register a prebuilt WASM workflow component that exports one or more workflows.
645    #[cfg(feature = "wasm-workflows")]
646    pub fn register_wasm_workflow(&mut self, component: WasmWorkflowComponent) -> &mut Self {
647        self.wasm_workflow_components.push(component);
648        self
649    }
650
651    /// Returns all the registered workflows by cloning the current set.
652    pub fn workflows(&self) -> WorkflowDefinitions {
653        self.workflows.clone()
654    }
655
656    #[doc(hidden)]
657    pub fn to_core_options(
658        &self,
659        namespace: String,
660        connection_identity: String,
661    ) -> Result<WorkerConfig, String> {
662        let workflows_registered = !self.workflows.is_empty();
663        #[cfg(feature = "wasm-workflows")]
664        let workflows_registered =
665            workflows_registered || !self.wasm_workflow_components.is_empty();
666        let activities_registered = !self.activities.is_empty();
667        if !workflows_registered && !activities_registered {
668            return Err("At least one workflow or activity must be registered".to_owned());
669        }
670        #[cfg(feature = "experimental")]
671        let disable_payload_error_limit = self.disable_payload_error_limit;
672        #[cfg(not(feature = "experimental"))]
673        let disable_payload_error_limit = false;
674        #[cfg(feature = "experimental")]
675        let plugin_info = self
676            .client_plugin_names
677            .iter()
678            .map(|name| PluginInfo {
679                name: name.clone(),
680                version: String::new(),
681            })
682            .chain(self.worker_plugins.iter().map(|registration| PluginInfo {
683                name: registration.name().to_owned(),
684                version: String::new(),
685            }))
686            .collect();
687        #[cfg(not(feature = "experimental"))]
688        let plugin_info = HashSet::new();
689
690        let tuner = self.tuner.to_core()?;
691
692        WorkerConfig::builder()
693            .namespace(namespace)
694            .task_queue(self.task_queue.clone())
695            .maybe_client_identity_override(self.client_identity_override.clone().or_else(|| {
696                connection_identity.is_empty().then(|| {
697                    format!(
698                        "{}@{}",
699                        std::process::id(),
700                        gethostname::gethostname().to_string_lossy()
701                    )
702                })
703            }))
704            .max_cached_workflows(self.max_cached_workflows)
705            .tuner(tuner)
706            .maybe_workflow_task_poller_behavior(
707                self.workflow_task_poller_behavior
708                    .map(PollerBehavior::into_core),
709            )
710            .maybe_activity_task_poller_behavior(
711                self.activity_task_poller_behavior
712                    .map(PollerBehavior::into_core),
713            )
714            .maybe_nexus_task_poller_behavior(
715                self.nexus_task_poller_behavior
716                    .map(PollerBehavior::into_core),
717            )
718            .task_types(WorkerTaskTypes {
719                enable_workflows: workflows_registered,
720                enable_local_activities: workflows_registered && activities_registered,
721                enable_remote_activities: activities_registered,
722                enable_nexus: false,
723            })
724            .sticky_queue_schedule_to_start_timeout(self.sticky_queue_schedule_to_start_timeout)
725            .max_heartbeat_throttle_interval(self.max_heartbeat_throttle_interval)
726            .default_heartbeat_throttle_interval(self.default_heartbeat_throttle_interval)
727            .maybe_max_task_queue_activities_per_second(self.max_task_queue_activities_per_second)
728            .maybe_max_worker_activities_per_second(self.max_worker_activities_per_second)
729            .max_eager_activity_reservations_per_workflow_task(
730                self.max_eager_activity_reservations_per_workflow_task,
731            )
732            .maybe_graceful_shutdown_period(self.graceful_shutdown_period)
733            .versioning_strategy(WorkerVersioningStrategy::WorkerDeploymentBased(
734                self.deployment_options.clone(),
735            ))
736            .workflow_failure_errors(
737                self.workflow_failure_errors
738                    .iter()
739                    .cloned()
740                    .map(WorkflowErrorType::into_core)
741                    .collect(),
742            )
743            .workflow_types_to_failure_errors(
744                self.workflow_types_to_failure_errors
745                    .iter()
746                    .map(|(workflow_type, error_types)| {
747                        (
748                            workflow_type.clone(),
749                            error_types
750                                .iter()
751                                .cloned()
752                                .map(WorkflowErrorType::into_core)
753                                .collect(),
754                        )
755                    })
756                    .collect(),
757            )
758            .plugins(plugin_info)
759            .disable_payload_error_limit(disable_payload_error_limit)
760            .build()
761    }
762}
763
764/// A worker that can poll for and respond to workflow tasks by using
765/// [temporalio_macros::workflow], and activity tasks by using activities defined with
766/// [temporalio_macros::activities].
767#[derive(Debug)]
768pub struct Worker {
769    common: CommonWorker,
770    workflow_half: WorkflowHalf,
771    activity_half: ActivityHalf,
772}
773
774#[derive(derive_more::Debug)]
775struct CommonWorker {
776    #[debug(skip)]
777    worker: Arc<CoreWorker>,
778    task_queue: String,
779    #[debug(skip)]
780    worker_interceptors: Vec<Arc<dyn WorkerInterceptor>>,
781    #[debug(skip)]
782    activity_inbound_interceptors: Vec<Arc<dyn ActivityInboundInterceptor>>,
783    #[debug(skip)]
784    workflow_interceptor_constructors: Vec<WorkflowInterceptorConstructor>,
785    client_options: ClientOptions,
786    data_converter: DataConverter,
787}
788
789#[derive(derive_more::Debug)]
790struct WorkflowHalf {
791    /// Maps run id to cached workflow state
792    workflows: RefCell<HashMap<String, WorkflowData>>,
793    workflow_definitions: WorkflowDefinitions,
794    workflow_removed_from_map: Notify,
795    detect_nondeterministic_futures: bool,
796    #[debug(skip)]
797    patch_activation_callback: Option<InternalPatchActivationCallback>,
798}
799#[derive(Debug)]
800struct WorkflowData {
801    /// Channel used to send the workflow activations
802    activation_chan: UnboundedSender<WorkflowActivation>,
803}
804
805struct WorkflowFutureHandle<F: Future> {
806    join_handle: F,
807    run_id: String,
808}
809
810#[derive(Debug, Default)]
811struct ActivityHalf {
812    /// Maps activity type to the function for executing activities of that type
813    activities: ActivityDefinitions,
814    task_tokens_to_cancels: HashMap<TaskToken, CancellationToken>,
815}
816
817#[derive(Debug, thiserror::Error)]
818enum ActivityTaskHandlerError {
819    #[error("{source}")]
820    UnregisteredActivity {
821        source: ActivityNotRegisteredError,
822        task_token: Vec<u8>,
823    },
824    #[error(transparent)]
825    Fatal(#[from] anyhow::Error),
826}
827
828#[derive(Debug, thiserror::Error)]
829enum ActivityNotRegisteredError {
830    #[error(
831        "Activity {activity_type} is not registered on this worker, available activities: {}",
832        .available_activities.join(", ")
833    )]
834    HasAvailable {
835        activity_type: String,
836        available_activities: Vec<String>,
837    },
838    #[error("Activity {activity_type} is not registered on this worker, no available activities.")]
839    NoAvailable { activity_type: String },
840}
841
842impl ActivityNotRegisteredError {
843    fn new(activity_type: String, available_activities: Vec<String>) -> Self {
844        if available_activities.is_empty() {
845            Self::NoAvailable { activity_type }
846        } else {
847            Self::HasAvailable {
848                activity_type,
849                available_activities,
850            }
851        }
852    }
853}
854
855async fn encode_workflow_completion(
856    completion: &mut WorkflowActivationCompletion,
857    data_converter: &DataConverter,
858) {
859    let run_id = completion.run_id.clone();
860    if let Err(err) = encode_payloads(
861        completion,
862        data_converter.codec(),
863        &SerializationContextData::Workflow(WorkflowSerializationContext::new()),
864    )
865    .await
866    {
867        error!(run_id, error = %err, "Failed encoding workflow activation completion");
868        *completion = WorkflowActivationCompletion::fail(
869            run_id,
870            Failure {
871                message: format!("Failed encoding completion: {err}"),
872                ..Default::default()
873            },
874            Some(WorkflowTaskFailedCause::WorkflowWorkerUnhandledFailure),
875        );
876    }
877}
878
879async fn encode_activity_completion(
880    completion: &mut ActivityTaskCompletion,
881    data_converter: &DataConverter,
882) {
883    if let Err(err) = encode_payloads(
884        completion,
885        data_converter.codec(),
886        &SerializationContextData::Activity(ActivitySerializationContext::new()),
887    )
888    .await
889    {
890        error!(error = %err, "Failed encoding activity task completion");
891        completion.result = Some(ActivityExecutionResult::fail(Failure::application_failure(
892            format!("Failed encoding activity completion: {err}"),
893            false,
894        )));
895    }
896}
897
898impl Worker {
899    /// Create a new worker from an existing client, and options.
900    pub fn new(
901        runtime: &Runtime,
902        client: Client,
903        options: WorkerOptions,
904    ) -> Result<Self, WorkerCreateError> {
905        #[cfg(feature = "experimental")]
906        let mut options = options;
907        #[cfg(feature = "experimental")]
908        plugins::apply_worker_plugins(client.options(), &mut options)?;
909        let wc = options
910            .to_core_options(client.namespace(), client.identity())
911            .map_err(|error| WorkerCreateError::Initialization(anyhow!(error)))?;
912        let core = init_worker(runtime.core(), wc, client.connection().clone())
913            .map_err(WorkerCreateError::Initialization)?;
914        Self::new_from_core_options_prepared(Arc::new(core), client.options().clone(), options)
915    }
916
917    // TODO [rust-sdk-branch]: Eliminate this constructor in favor of passing in fake connection
918    #[cfg(feature = "experimental")]
919    #[doc(hidden)]
920    pub fn new_from_core(worker: Arc<CoreWorker>, data_converter: DataConverter) -> Self {
921        let client_options = ClientOptions::new(worker.get_config().namespace.clone())
922            .data_converter(data_converter)
923            .build();
924        Self::new_from_core_definitions(
925            worker,
926            client_options,
927            Default::default(),
928            Default::default(),
929            Default::default(),
930            Default::default(),
931            Default::default(),
932        )
933    }
934
935    // TODO [rust-sdk-branch]: Eliminate this constructor in favor of passing in fake connection
936    #[cfg(feature = "experimental")]
937    #[doc(hidden)]
938    pub fn new_from_core_options(
939        worker: Arc<CoreWorker>,
940        client_options: ClientOptions,
941        options: WorkerOptions,
942    ) -> Result<Self, WorkerCreateError> {
943        #[cfg(feature = "experimental")]
944        let mut options = options;
945        #[cfg(feature = "experimental")]
946        plugins::apply_worker_plugins(&client_options, &mut options)?;
947        Self::new_from_core_options_prepared(worker, client_options, options)
948    }
949
950    fn new_from_core_options_prepared(
951        worker: Arc<CoreWorker>,
952        client_options: ClientOptions,
953        mut options: WorkerOptions,
954    ) -> Result<Self, WorkerCreateError> {
955        let acts = std::mem::take(&mut options.activities);
956        let wfs = std::mem::take(&mut options.workflows);
957        let worker_interceptors = std::mem::take(&mut options.worker_interceptors);
958        let activity_inbound_interceptors =
959            std::mem::take(&mut options.activity_inbound_interceptors);
960        let workflow_interceptor_constructors =
961            std::mem::take(&mut options.workflow_interceptor_constructors);
962        #[cfg(feature = "wasm-workflows")]
963        let wasm_components = std::mem::take(&mut options.wasm_workflow_components);
964        let mut me = Self::new_from_core_definitions(
965            worker,
966            client_options,
967            acts,
968            wfs,
969            worker_interceptors,
970            activity_inbound_interceptors,
971            workflow_interceptor_constructors,
972        );
973        me.workflow_half.detect_nondeterministic_futures = options.detect_nondeterministic_futures;
974        #[cfg(feature = "experimental")]
975        {
976            me.workflow_half.patch_activation_callback = options.patch_activation_callback;
977        }
978        #[cfg(feature = "wasm-workflows")]
979        me.workflow_half
980            .workflow_definitions
981            .register_wasm_workflows(
982                wasm_components,
983                !me.common.workflow_interceptor_constructors.is_empty(),
984            )
985            .map_err(|error| WorkerCreateError::Initialization(anyhow!(error)))?;
986        Ok(me)
987    }
988
989    fn new_from_core_definitions(
990        worker: Arc<CoreWorker>,
991        client_options: ClientOptions,
992        activities: ActivityDefinitions,
993        workflows: WorkflowDefinitions,
994        worker_interceptors: Vec<Arc<dyn WorkerInterceptor>>,
995        activity_inbound_interceptors: Vec<Arc<dyn ActivityInboundInterceptor>>,
996        workflow_interceptor_constructors: Vec<WorkflowInterceptorConstructor>,
997    ) -> Self {
998        let data_converter = client_options.data_converter.clone();
999        Self {
1000            common: CommonWorker {
1001                task_queue: worker.get_config().task_queue.clone(),
1002                worker,
1003                worker_interceptors,
1004                activity_inbound_interceptors,
1005                workflow_interceptor_constructors,
1006                client_options,
1007                data_converter,
1008            },
1009            workflow_half: WorkflowHalf {
1010                workflows: Default::default(),
1011                workflow_definitions: workflows,
1012                workflow_removed_from_map: Default::default(),
1013                detect_nondeterministic_futures: false,
1014                patch_activation_callback: None,
1015            },
1016            activity_half: ActivityHalf {
1017                activities,
1018                ..Default::default()
1019            },
1020        }
1021    }
1022
1023    /// Returns the task queue name this worker polls on
1024    pub fn task_queue(&self) -> &str {
1025        &self.common.task_queue
1026    }
1027
1028    /// Return a handle that can be used to initiate shutdown. This is useful because [Worker::run]
1029    /// takes self mutably, so you may want to obtain a handle for shutting down before running.
1030    pub fn shutdown_handle(&self) -> impl Fn() + use<> {
1031        let w = self.common.worker.clone();
1032        move || w.initiate_shutdown()
1033    }
1034
1035    /// Runs the worker. Eventually resolves after the worker has been explicitly shut down,
1036    /// or may return early with an error in the event of some unresolvable problem.
1037    pub async fn run(&mut self) -> Result<(), WorkerRunError> {
1038        let interceptors = self.common.worker_interceptors.clone();
1039        interceptors::call_run_worker(
1040            &interceptors,
1041            RunWorkerInput::new(self),
1042            Next::new(
1043                |input: RunWorkerInput<'_>| -> LocalBoxFuture<'_, Result<(), _>> {
1044                    Box::pin(async move { input.worker.run_inner().await })
1045                },
1046            ),
1047        )
1048        .await
1049    }
1050
1051    pub(crate) async fn run_inner(&mut self) -> Result<(), WorkerRunError> {
1052        // Perform the namespace check-in so poller behavior (e.g. autoscaling auto-enroll) is
1053        // resolved before any polling begins.
1054        self.common
1055            .worker
1056            .validate()
1057            .await
1058            .map_err(WorkerValidationError::from_core)
1059            .map_err(WorkerRunError::Validation)?;
1060        let shutdown_token = CancellationToken::new();
1061        let (common, wf_half, act_half) = self.split_apart();
1062        let (wf_future_tx, wf_future_rx) =
1063            unbounded_channel::<WorkflowFutureHandle<TaskHandle<WorkflowResult<Payload>>>>();
1064        let (completions_tx, completions_rx) = unbounded_channel();
1065
1066        // Workflows run in a LocalSet because they use Rc<RefCell> for state management.
1067        // This allows them to not require Send/Sync bounds. The WorkflowExecutor replaces
1068        // tokio::task::spawn_local for workflow tasks and provides custom wakers for
1069        // nondeterminism detection.
1070        let workflow_local_set = tokio::task::LocalSet::new();
1071        let executor = WorkflowExecutor::new();
1072
1073        let wf_future_joiner = async {
1074            UnboundedReceiverStream::new(wf_future_rx)
1075                .map(Result::<_, WorkerRunError>::Ok)
1076                .try_for_each_concurrent(
1077                    None,
1078                    |WorkflowFutureHandle {
1079                         join_handle,
1080                         run_id,
1081                     }| {
1082                        let wf_half = &*wf_half;
1083                        async move {
1084                            let result = join_handle.await.map_err(|e| WorkerRunError::Fatal {
1085                                message: "workflow task dropped".into(),
1086                                source: e.into(),
1087                            })?;
1088                            // Eviction is normal workflow lifecycle - workflows loop waiting for
1089                            // eviction after completion to manage cache cleanup
1090                            if let Err(e) = result
1091                                && !matches!(e, WorkflowTermination::Evicted)
1092                            {
1093                                return Err(WorkerRunError::Fatal {
1094                                    message: "workflow execution failed".into(),
1095                                    source: e.into(),
1096                                });
1097                            }
1098                            debug!(run_id=%run_id, "Removing workflow from cache");
1099                            wf_half.workflows.borrow_mut().remove(&run_id);
1100                            wf_half.workflow_removed_from_map.notify_one();
1101                            Ok(())
1102                        }
1103                    },
1104                )
1105                .await
1106        };
1107        let wf_completion_processor = async {
1108            UnboundedReceiverStream::new(completions_rx)
1109                .map(Ok)
1110                .try_for_each_concurrent(None, |mut completion| async {
1111                    encode_workflow_completion(&mut completion, &common.data_converter).await;
1112                    for i in &common.worker_interceptors {
1113                        i.on_workflow_activation_completion(&completion).await;
1114                    }
1115                    common.worker.complete_workflow_activation(completion).await
1116                })
1117                .await
1118                .map_err(|source| WorkerRunError::Fatal {
1119                    message: "workflow completions processor encountered an error".to_owned(),
1120                    source: Box::new(source),
1121                })
1122        };
1123        tokio::try_join!(
1124            // Workflow-related tasks run inside LocalSet (allows !Send futures)
1125            async {
1126                workflow_local_set.run_until(async {
1127                    tokio::try_join!(
1128                        // Workflow polling loop
1129                        async {
1130                            loop {
1131                            let mut activation =
1132                                match common.worker.poll_workflow_activation().await {
1133                                    Err(PollError::ShutDown) => {
1134                                        break;
1135                                    }
1136                                    o => o.map_err(|source| WorkerRunError::Fatal {
1137                                        message: "workflow polling failed".to_owned(),
1138                                        source: Box::new(source),
1139                                    })?,
1140                                };
1141                            if let Err(err) = decode_payloads(
1142                                &mut activation,
1143                                common.data_converter.codec(),
1144                                &SerializationContextData::Workflow(WorkflowSerializationContext::new()),
1145                            )
1146                            .await
1147                            {
1148                                let run_id = activation.run_id;
1149                                error!(run_id, error = %err, "Failed decoding workflow activation");
1150                                completions_tx
1151                                    .send(WorkflowActivationCompletion::fail(
1152                                        run_id,
1153                                        Failure {
1154                                            message: format!("Failed decoding activation: {err}"),
1155                                            ..Default::default()
1156                                        },
1157                                        Some(
1158                                            WorkflowTaskFailedCause::WorkflowWorkerUnhandledFailure,
1159                                        ),
1160                                    ))
1161                                    .expect("Completion channel intact");
1162                                continue;
1163                            }
1164                            for i in &common.worker_interceptors {
1165                                i.on_workflow_activation(&activation).await.map_err(|source| {
1166                                    WorkerRunError::Fatal {
1167                                        message: "workflow activation interceptor failed".to_owned(),
1168                                        source: source.into_boxed_dyn_error(),
1169                                    }
1170                                })?;
1171                            }
1172                            if let Some(wf_fut) = wf_half
1173                                .workflow_activation_handler(
1174                                    common,
1175                                    shutdown_token.clone(),
1176                                    activation,
1177                                    &completions_tx,
1178                                    &executor,
1179                                )
1180                                .await
1181                                .map_err(|source| {
1182                                    WorkerRunError::Fatal {
1183                                        message: "workflow activation processing failed".to_owned(),
1184                                        source: source.into_boxed_dyn_error(),
1185                                    }
1186                                })?
1187                                && wf_future_tx.send(wf_fut).is_err()
1188                            {
1189                                panic!(
1190                                    "Receive half of completion processor channel cannot be dropped"
1191                                );
1192                            }
1193                        }
1194                        // Tell still-alive workflows to evict themselves
1195                        shutdown_token.cancel();
1196                        // It's important to drop these so the future and completion processors will
1197                        // terminate.
1198                        drop(wf_future_tx);
1199                        drop(completions_tx);
1200                        Result::<_, WorkerRunError>::Ok(())
1201                    },
1202                    wf_future_joiner,
1203                    async {
1204                        tokio::select! {
1205                            _ = executor.drive() => unreachable!("executor driver cannot finish"),
1206                            _ = shutdown_token.cancelled() => {}
1207                        }
1208                        executor.shutdown().await;
1209                        Result::<_, WorkerRunError>::Ok(())
1210                    },
1211                )
1212                }).await
1213            },
1214            // Only poll on the activity queue if activity functions have been registered. This
1215            // makes tests which use mocks dramatically more manageable.
1216            async {
1217                if !act_half.activities.is_empty() {
1218                    loop {
1219                        let activity = common.worker.poll_activity_task().await;
1220                        if matches!(activity, Err(PollError::ShutDown)) {
1221                            break;
1222                        }
1223                        let mut activity = activity.map_err(|source| WorkerRunError::Fatal {
1224                            message: "activity polling failed".to_owned(),
1225                            source: Box::new(source),
1226                        })?;
1227                        if let Err(err) =
1228                            decode_payloads(
1229                                &mut activity,
1230                                common.data_converter.codec(),
1231                                &SerializationContextData::Activity(
1232                                    ActivitySerializationContext::new(),
1233                                ),
1234                            )
1235                            .await
1236                        {
1237                            error!(error = %err, "Failed decoding activity task");
1238                            let mut completion = ActivityTaskCompletion {
1239                                task_token: activity.task_token,
1240                                result: Some(ActivityExecutionResult::fail(
1241                                    Failure::application_failure(
1242                                        format!("Failed decoding activity task: {err}"),
1243                                        false,
1244                                    ),
1245                                )),
1246                            };
1247                            encode_activity_completion(&mut completion, &common.data_converter)
1248                                .await;
1249                            common
1250                                .worker
1251                                .complete_activity_task(completion)
1252                                .await
1253                                .map_err(|source| WorkerRunError::Fatal {
1254                                    message: "activity completion failed".to_owned(),
1255                                    source: Box::new(source),
1256                                })?;
1257                            continue;
1258                        }
1259                        match act_half.activity_task_handler(
1260                            common.worker.clone(),
1261                            common.client_options.clone(),
1262                            common.task_queue.clone(),
1263                            common.data_converter.clone(),
1264                            common.activity_inbound_interceptors.clone(),
1265                            activity,
1266                        ) {
1267                            Ok(()) => {}
1268                            Err(ActivityTaskHandlerError::UnregisteredActivity {
1269                                source,
1270                                task_token,
1271                            }) => {
1272                                let failure = common.data_converter.to_failure(
1273                                    &SerializationContextData::Activity(
1274                                        ActivitySerializationContext::new(),
1275                                    ),
1276                                    OutgoingError::Activity(OutgoingActivityError::Application(
1277                                        ApplicationFailure::builder(source)
1278                                            .type_name("NotFoundError".to_owned())
1279                                            .build()
1280                                            .into(),
1281                                    )),
1282                                );
1283                                let mut completion = ActivityTaskCompletion {
1284                                    task_token,
1285                                    result: Some(ActivityExecutionResult::fail(failure)),
1286                                };
1287                                encode_activity_completion(&mut completion, &common.data_converter)
1288                                    .await;
1289                                common
1290                                    .worker
1291                                    .complete_activity_task(completion)
1292                                    .await
1293                                    .map_err(|source| WorkerRunError::Fatal {
1294                                        message: "activity completion failed".to_owned(),
1295                                        source: Box::new(source),
1296                                    })?;
1297                            }
1298                            Err(ActivityTaskHandlerError::Fatal(source)) => {
1299                                return Err(WorkerRunError::Fatal {
1300                                    message: "activity task handling failed".to_owned(),
1301                                    source: source.into_boxed_dyn_error(),
1302                                });
1303                            }
1304                        };
1305                    }
1306                };
1307                Result::<_, WorkerRunError>::Ok(())
1308            },
1309            wf_completion_processor,
1310        )?;
1311
1312        for i in &self.common.worker_interceptors {
1313            i.on_shutdown(self);
1314        }
1315        self.common.worker.shutdown().await;
1316        Ok(())
1317    }
1318
1319    pub(crate) fn worker_interceptors(&self) -> Vec<Arc<dyn WorkerInterceptor>> {
1320        self.common.worker_interceptors.clone()
1321    }
1322
1323    /// Turns this rust worker into a new worker with all the same workflows and activities
1324    /// registered, but with a new underlying core worker. Can be used to swap the worker for
1325    /// a replay worker, change task queues, etc.
1326    pub fn with_new_core_worker(&mut self, new_core_worker: Arc<CoreWorker>) {
1327        self.common.worker = new_core_worker;
1328    }
1329
1330    /// Returns number of currently cached workflows as understood by the SDK. Importantly, this
1331    /// is not the same as understood by core, though they *should* always be in sync.
1332    pub fn cached_workflows(&self) -> usize {
1333        self.workflow_half.workflows.borrow().len()
1334    }
1335
1336    /// Returns the instance key for this worker, used for worker heartbeating.
1337    pub fn worker_instance_key(&self) -> Uuid {
1338        self.common.worker.worker_instance_key()
1339    }
1340
1341    fn split_apart(&mut self) -> (&mut CommonWorker, &mut WorkflowHalf, &mut ActivityHalf) {
1342        (
1343            &mut self.common,
1344            &mut self.workflow_half,
1345            &mut self.activity_half,
1346        )
1347    }
1348}
1349
1350impl WorkflowHalf {
1351    #[allow(clippy::type_complexity)]
1352    async fn workflow_activation_handler(
1353        &self,
1354        common: &CommonWorker,
1355        shutdown_token: CancellationToken,
1356        mut activation: WorkflowActivation,
1357        completions_tx: &UnboundedSender<WorkflowActivationCompletion>,
1358        executor: &WorkflowExecutor,
1359    ) -> Result<Option<WorkflowFutureHandle<TaskHandle<WorkflowResult<Payload>>>>, anyhow::Error>
1360    {
1361        let mut res = None;
1362        let run_id = activation.run_id.clone();
1363
1364        // If the activation is to init a workflow, create a new workflow driver for it,
1365        // using the function associated with that workflow id
1366        if let Some(sw) = activation.jobs.iter_mut().find_map(|j| match j.variant {
1367            Some(Variant::InitializeWorkflow(ref mut sw)) => Some(sw),
1368            _ => None,
1369        }) {
1370            let workflow_type = sw.workflow_type.clone();
1371            let (wff, activations) = {
1372                if let Some(factory) = self.workflow_definitions.get_workflow(&workflow_type) {
1373                    match start_workflow(
1374                        factory,
1375                        common.worker.get_config().namespace.clone(),
1376                        common.task_queue.clone(),
1377                        run_id.clone(),
1378                        std::mem::take(sw),
1379                        completions_tx.clone(),
1380                        common.data_converter.clone(),
1381                        self.detect_nondeterministic_futures,
1382                        self.patch_activation_callback.clone(),
1383                        common.workflow_interceptor_constructors.clone(),
1384                    ) {
1385                        Ok(result) => result,
1386                        Err(e) => {
1387                            warn!("Failed to create workflow {workflow_type}: {e}");
1388                            completions_tx
1389                                .send(WorkflowActivationCompletion::fail(
1390                                    run_id,
1391                                    format!("Failed to create workflow: {e}").into(),
1392                                    Some(WorkflowTaskFailedCause::WorkflowWorkerUnhandledFailure),
1393                                ))
1394                                .expect("Completion channel intact");
1395                            return Ok(None);
1396                        }
1397                    }
1398                } else {
1399                    warn!("Workflow type {workflow_type} not found");
1400                    completions_tx
1401                        .send(WorkflowActivationCompletion::fail(
1402                            run_id,
1403                            format!("Workflow type {workflow_type} not found").into(),
1404                            Some(WorkflowTaskFailedCause::WorkflowWorkerUnhandledFailure),
1405                        ))
1406                        .expect("Completion channel intact");
1407                    return Ok(None);
1408                }
1409            };
1410            // The executor consumes self-wakes synchronously, so cooperative budget exhaustion
1411            // would otherwise re-poll the workflow forever without returning to Tokio.
1412            let wff = tokio::task::coop::unconstrained(wff);
1413            // TODO [rust-sdk-branch]: Deadlock detection
1414            let jh = executor.spawn(async move {
1415                tokio::select! {
1416                    r = wff.fuse() => r,
1417                    // TODO: This probably shouldn't abort early, as it could cause an in-progress
1418                    //  complete to abort. Send synthetic remove activation
1419                    _ = shutdown_token.cancelled() => {
1420                        Err(WorkflowTermination::Evicted)
1421                    }
1422                }
1423            });
1424            res = Some(WorkflowFutureHandle {
1425                join_handle: jh,
1426                run_id: run_id.clone(),
1427            });
1428            loop {
1429                // It's possible that we've got a new initialize workflow action before the last
1430                // future for this run finished evicting, as a result of how futures might be
1431                // interleaved. In that case, just wait until it's not in the map, which should be
1432                // a matter of only a few `poll` calls.
1433                if self.workflows.borrow_mut().contains_key(&run_id) {
1434                    self.workflow_removed_from_map.notified().await;
1435                } else {
1436                    break;
1437                }
1438            }
1439            self.workflows.borrow_mut().insert(
1440                run_id.clone(),
1441                WorkflowData {
1442                    activation_chan: activations,
1443                },
1444            );
1445        }
1446
1447        // The activation is expected to apply to some workflow we know about. Use it to
1448        // unblock things and advance the workflow.
1449        if let Some(dat) = self.workflows.borrow_mut().get_mut(&run_id) {
1450            dat.activation_chan
1451                .send(activation)
1452                .expect("Workflow should exist if we're sending it an activation");
1453        } else {
1454            // When we failed to start a workflow, we never inserted it into the cache. But core
1455            // sends us a `RemoveFromCache` job when we mark the StartWorkflow workflow activation
1456            // as a failure, which we need to complete. Other SDKs add the workflow to the cache
1457            // even when the workflow type is unknown/not found. To circumvent this, we simply mark
1458            // any RemoveFromCache job for workflows that are not in the cache as complete.
1459            if activation.jobs.len() == 1
1460                && matches!(
1461                    activation.jobs.first().map(|j| &j.variant),
1462                    Some(Some(Variant::RemoveFromCache(_)))
1463                )
1464            {
1465                completions_tx
1466                    .send(WorkflowActivationCompletion::from_cmds(run_id, vec![]))
1467                    .expect("Completion channel intact");
1468                return Ok(None);
1469            }
1470
1471            // In all other cases, we want to error as the runtime could be in an inconsistent state
1472            // at this point.
1473            bail!("Got activation {activation:?} for unknown workflow {run_id}");
1474        };
1475
1476        Ok(res)
1477    }
1478}
1479
1480impl ActivityHalf {
1481    /// Spawns off a task to handle the provided activity task
1482    fn activity_task_handler(
1483        &mut self,
1484        worker: Arc<CoreWorker>,
1485        client_options: ClientOptions,
1486        task_queue: String,
1487        data_converter: DataConverter,
1488        activity_inbound_interceptors: Vec<Arc<dyn ActivityInboundInterceptor>>,
1489        activity: ActivityTask,
1490    ) -> Result<(), ActivityTaskHandlerError> {
1491        match activity.variant {
1492            Some(activity_task::Variant::Start(start)) => {
1493                let Some(act_fn) = self.activities.get(&start.activity_type) else {
1494                    let activity_type = start.activity_type.clone();
1495                    let source =
1496                        ActivityNotRegisteredError::new(activity_type, self.activities.names());
1497                    return Err(ActivityTaskHandlerError::UnregisteredActivity {
1498                        source,
1499                        task_token: activity.task_token,
1500                    });
1501                };
1502                let span = info_span!(
1503                    "RunActivity",
1504                    "otel.name" = format!("RunActivity:{}", start.activity_type),
1505                    "otel.kind" = "server",
1506                    "temporalActivityID" = start.activity_id,
1507                    "temporalWorkflowID" = field::Empty,
1508                    "temporalRunID" = field::Empty,
1509                );
1510                let ct = CancellationToken::new();
1511                let task_token = activity.task_token;
1512                self.task_tokens_to_cancels
1513                    .insert(task_token.clone().into(), ct.clone());
1514
1515                let (ctx, args) = ActivityContext::new(
1516                    worker.clone(),
1517                    client_options,
1518                    ct,
1519                    task_queue,
1520                    task_token.clone(),
1521                    start,
1522                );
1523                let codec_data_converter = data_converter.clone();
1524
1525                tokio::spawn(async move {
1526                    let act_fut = async move {
1527                        let span = Span::current();
1528                        if let Some(workflow_id) = &ctx.info().workflow_id {
1529                            span.record("temporalWorkflowID", workflow_id);
1530                        }
1531                        if let Some(workflow_run_id) = &ctx.info().workflow_run_id {
1532                            span.record("temporalRunID", workflow_run_id);
1533                        }
1534                        (act_fn)(args, data_converter, ctx, activity_inbound_interceptors).await
1535                    }
1536                    .instrument(span);
1537                    let result = act_fut.await;
1538                    let result = match result {
1539                        Ok(output) => {
1540                            // Codec application happens at the SDK/Core boundary, so activity
1541                            // implementations work with the payload converter directly.
1542                            let pc = codec_data_converter.payload_converter();
1543                            let context_data = SerializationContextData::Activity(
1544                                ActivitySerializationContext::new(),
1545                            );
1546                            let ctx = SerializationContext::new(&context_data, pc);
1547                            match output.serialize_payload(&ctx) {
1548                                Ok(payload) => ActivityExecutionResult::ok(payload),
1549                                Err(err) => {
1550                                    activity_error_to_core_result(&codec_data_converter, err.into())
1551                                }
1552                            }
1553                        }
1554                        Err(err) => activity_error_to_core_result(&codec_data_converter, err),
1555                    };
1556                    let mut completion = ActivityTaskCompletion {
1557                        task_token,
1558                        result: Some(result),
1559                    };
1560                    encode_activity_completion(&mut completion, &codec_data_converter).await;
1561                    worker.complete_activity_task(completion).await?;
1562                    Ok::<_, anyhow::Error>(())
1563                });
1564            }
1565            Some(activity_task::Variant::Cancel(_)) => {
1566                if let Some(ct) = self
1567                    .task_tokens_to_cancels
1568                    .get(activity.task_token.as_slice())
1569                {
1570                    ct.cancel();
1571                }
1572            }
1573            None => {
1574                return Err(anyhow!("Undefined activity task variant").into());
1575            }
1576        }
1577        Ok(())
1578    }
1579}
1580
1581/// Attempts to turn caught panics into something printable
1582fn panic_formatter(panic: Box<dyn Any>) -> Box<dyn Display> {
1583    _panic_formatter::<&str>(panic)
1584}
1585fn _panic_formatter<T: 'static + PrintablePanicType>(panic: Box<dyn Any>) -> Box<dyn Display> {
1586    match panic.downcast::<T>() {
1587        Ok(d) => d,
1588        Err(orig) => {
1589            if TypeId::of::<<T as PrintablePanicType>::NextType>()
1590                == TypeId::of::<EndPrintingAttempts>()
1591            {
1592                return Box::new("Couldn't turn panic into a string");
1593            }
1594            _panic_formatter::<T::NextType>(orig)
1595        }
1596    }
1597}
1598trait PrintablePanicType: Display {
1599    type NextType: PrintablePanicType;
1600}
1601
1602impl PrintablePanicType for &str {
1603    type NextType = String;
1604}
1605impl PrintablePanicType for String {
1606    type NextType = EndPrintingAttempts;
1607}
1608struct EndPrintingAttempts {}
1609impl Display for EndPrintingAttempts {
1610    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1611        write!(f, "Will never be printed")
1612    }
1613}
1614impl PrintablePanicType for EndPrintingAttempts {
1615    type NextType = EndPrintingAttempts;
1616}
1617
1618#[cfg(test)]
1619mod tests {
1620    use super::*;
1621    use crate::{activities::ActivityError, workflow_interceptors::WorkflowInterceptor};
1622    use futures_util::future::BoxFuture;
1623    use std::sync::atomic::{AtomicUsize, Ordering};
1624    use temporalio_common::{
1625        data_converters::{
1626            DefaultFailureConverter, PayloadCodec, PayloadConversionError, PayloadConverter,
1627        },
1628        protos::coresdk::{
1629            activity_result::activity_execution_result,
1630            workflow_commands::{CompleteWorkflowExecution, workflow_command},
1631            workflow_completion::workflow_activation_completion,
1632        },
1633    };
1634    use temporalio_macros::{activities, activity_definitions, workflow, workflow_methods};
1635
1636    #[derive(Default)]
1637    struct FailingEncodeCodec {
1638        calls: AtomicUsize,
1639    }
1640
1641    impl PayloadCodec for FailingEncodeCodec {
1642        fn encode(
1643            &self,
1644            _: &SerializationContextData,
1645            _: Vec<Payload>,
1646        ) -> BoxFuture<'static, Result<Vec<Payload>, PayloadConversionError>> {
1647            self.calls.fetch_add(1, Ordering::SeqCst);
1648            async move {
1649                Err(PayloadConversionError::EncodingError(
1650                    "codec encode failed".into(),
1651                ))
1652            }
1653            .boxed()
1654        }
1655
1656        fn decode(
1657            &self,
1658            _: &SerializationContextData,
1659            payloads: Vec<Payload>,
1660        ) -> BoxFuture<'static, Result<Vec<Payload>, PayloadConversionError>> {
1661            async move { Ok(payloads) }.boxed()
1662        }
1663    }
1664
1665    struct NoopWorkflowInterceptor;
1666
1667    impl WorkflowInterceptor for NoopWorkflowInterceptor {}
1668
1669    struct MyActivities {}
1670
1671    struct SharedActivities;
1672    #[activity_definitions]
1673    impl SharedActivities {
1674        #[activity(name = "shared-greet")]
1675        fn greet(name: String) -> Result<String, ActivityError> {
1676            unimplemented!()
1677        }
1678    }
1679
1680    #[activities]
1681    impl MyActivities {
1682        #[activity]
1683        async fn my_activity(_ctx: ActivityContext) -> Result<(), ActivityError> {
1684            Ok(())
1685        }
1686
1687        #[activity(definition = shared_activities::Greet)]
1688        async fn greet(_ctx: ActivityContext, name: String) -> Result<String, ActivityError> {
1689            Ok(name)
1690        }
1691
1692        #[activity]
1693        async fn takes_self(
1694            self: Arc<Self>,
1695            _ctx: ActivityContext,
1696            _: String,
1697        ) -> Result<(), ActivityError> {
1698            Ok(())
1699        }
1700    }
1701
1702    #[test]
1703    fn test_activity_registration() {
1704        let act_instance = MyActivities {};
1705        let _ = WorkerOptions::new("task_q").register_activities(act_instance);
1706    }
1707
1708    #[tokio::test]
1709    async fn workflow_completion_codec_error_uses_unencoded_failure() {
1710        let codec = Arc::new(FailingEncodeCodec::default());
1711        let data_converter = DataConverter::new(
1712            PayloadConverter::default(),
1713            DefaultFailureConverter::default(),
1714            codec.clone(),
1715        );
1716        let mut completion = WorkflowActivationCompletion::from_cmd(
1717            "run-id",
1718            workflow_command::Variant::CompleteWorkflowExecution(CompleteWorkflowExecution {
1719                result: Some(Payload::default()),
1720            }),
1721        );
1722
1723        encode_workflow_completion(&mut completion, &data_converter).await;
1724
1725        let Some(workflow_activation_completion::Status::Failed(failed)) = completion.status else {
1726            panic!("expected failed workflow completion")
1727        };
1728        assert_eq!(
1729            failed.failure.unwrap().message,
1730            "Failed encoding completion: Encoding error: codec encode failed"
1731        );
1732        assert_eq!(
1733            failed.force_cause,
1734            WorkflowTaskFailedCause::WorkflowWorkerUnhandledFailure as i32
1735        );
1736        assert_eq!(codec.calls.load(Ordering::SeqCst), 1);
1737    }
1738
1739    #[tokio::test]
1740    async fn activity_completion_codec_error_uses_unencoded_failure() {
1741        let codec = Arc::new(FailingEncodeCodec::default());
1742        let data_converter = DataConverter::new(
1743            PayloadConverter::default(),
1744            DefaultFailureConverter::default(),
1745            codec.clone(),
1746        );
1747        let mut completion = ActivityTaskCompletion {
1748            task_token: vec![],
1749            result: Some(ActivityExecutionResult::ok(Payload::default())),
1750        };
1751
1752        encode_activity_completion(&mut completion, &data_converter).await;
1753
1754        let Some(activity_execution_result::Status::Failed(failed)) =
1755            completion.result.unwrap().status
1756        else {
1757            panic!("expected failed activity completion")
1758        };
1759        let failure = failed.failure.unwrap();
1760        assert_eq!(
1761            failure.message,
1762            "Failed encoding activity completion: Encoding error: codec encode failed"
1763        );
1764        assert!(matches!(
1765            failure.failure_info,
1766            Some(
1767                temporalio_common::protos::temporal::api::failure::v1::failure::FailureInfo::ApplicationFailureInfo(info)
1768            ) if !info.non_retryable
1769        ));
1770        assert_eq!(codec.calls.load(Ordering::SeqCst), 1);
1771    }
1772
1773    // Compile-only test for workflow context invocation
1774    #[allow(unused, clippy::diverging_sub_expression)]
1775    fn test_activity_via_workflow_context() {
1776        let wf_ctx: WorkflowContext<MyWorkflow> = unimplemented!();
1777        wf_ctx.execute_activity(
1778            MyActivities::my_activity,
1779            (),
1780            ActivityOptions::start_to_close_timeout(Duration::from_secs(5)),
1781        );
1782        wf_ctx.execute_activity(
1783            SharedActivities::greet,
1784            "Hi".to_owned(),
1785            ActivityOptions::start_to_close_timeout(Duration::from_secs(5)),
1786        );
1787        wf_ctx.execute_activity(
1788            MyActivities::greet,
1789            "Hi".to_owned(),
1790            ActivityOptions::start_to_close_timeout(Duration::from_secs(5)),
1791        );
1792        wf_ctx.execute_activity(
1793            MyActivities::takes_self,
1794            "Hi".to_owned(),
1795            ActivityOptions::start_to_close_timeout(Duration::from_secs(5)),
1796        );
1797    }
1798
1799    // Compile-only test for direct invocation via .run()
1800    #[allow(dead_code, unreachable_code, unused, clippy::diverging_sub_expression)]
1801    async fn test_activity_direct_invocation() {
1802        let ctx: ActivityContext = unimplemented!();
1803        let _result = MyActivities::my_activity.run(ctx).await;
1804    }
1805
1806    #[workflow]
1807    struct MyWorkflow {
1808        counter: u32,
1809    }
1810
1811    #[allow(dead_code)]
1812    #[workflow_methods]
1813    impl MyWorkflow {
1814        #[init]
1815        fn new(_ctx: &WorkflowContextView, _input: String) -> Self {
1816            Self { counter: 0 }
1817        }
1818
1819        #[run]
1820        async fn run(ctx: &mut WorkflowContext<Self>) -> WorkflowResult<String> {
1821            Ok(format!("Counter: {}", ctx.state(|s| s.counter)))
1822        }
1823
1824        #[signal(name = "increment")]
1825        fn increment_counter(&mut self, _ctx: &mut SyncWorkflowContext<Self>, amount: u32) {
1826            self.counter += amount;
1827        }
1828
1829        #[signal]
1830        async fn async_signal(_ctx: &mut WorkflowContext<Self>) {}
1831
1832        #[query]
1833        fn get_counter(&self, _ctx: &WorkflowContextView) -> u32 {
1834            self.counter
1835        }
1836
1837        #[update(name = "double")]
1838        fn double_counter(&mut self, _ctx: &mut SyncWorkflowContext<Self>) -> u32 {
1839            self.counter *= 2;
1840            self.counter
1841        }
1842
1843        #[update]
1844        async fn async_update(_ctx: &mut WorkflowContext<Self>, val: i32) -> i32 {
1845            val * 2
1846        }
1847    }
1848
1849    #[workflow]
1850    #[derive(Default)]
1851    struct OtherWorkflow;
1852
1853    #[workflow_methods]
1854    impl OtherWorkflow {
1855        #[run]
1856        async fn run(_ctx: &mut WorkflowContext<Self>) -> WorkflowResult<()> {
1857            Ok(())
1858        }
1859    }
1860
1861    #[test]
1862    fn test_workflow_registration() {
1863        let _ = WorkerOptions::new("task_q")
1864            .register_workflow::<MyWorkflow>()
1865            .unwrap();
1866    }
1867
1868    #[rstest::rstest]
1869    #[case::workflow_only(true, false, Ok(WorkerTaskTypes::workflow_only()))]
1870    #[case::activity_only(false, true, Ok(WorkerTaskTypes::activity_only()))]
1871    #[case::workflow_and_activity(
1872        true,
1873        true,
1874        Ok(WorkerTaskTypes {
1875            enable_workflows: true,
1876            enable_local_activities: true,
1877            enable_remote_activities: true,
1878            enable_nexus: false,
1879        })
1880    )]
1881    #[case::empty(
1882        false,
1883        false,
1884        Err("At least one workflow or activity must be registered")
1885    )]
1886    #[test]
1887    fn task_types_are_derived_from_registrations(
1888        #[case] register_workflow: bool,
1889        #[case] register_activities: bool,
1890        #[case] expected: Result<WorkerTaskTypes, &str>,
1891    ) {
1892        let options = if register_workflow {
1893            WorkerOptions::new("task_q")
1894                .register_workflow::<MyWorkflow>()
1895                .unwrap()
1896        } else {
1897            WorkerOptions::new("task_q")
1898        };
1899        let options = if register_activities {
1900            options.register_activities(MyActivities {})
1901        } else {
1902            options
1903        };
1904
1905        let actual = options
1906            .build()
1907            .to_core_options("ns".into(), String::new())
1908            .map(|config| config.task_types);
1909        assert_eq!(
1910            actual.as_ref().map_err(String::as_str),
1911            expected.as_ref().map_err(|err| *err)
1912        );
1913    }
1914
1915    #[test]
1916    fn workflow_interceptor_registration_replaces_previous_constructors() {
1917        let mut options = WorkerOptions::new("task_q").build();
1918        options.register_workflow_interceptors(vec![
1919            WorkflowInterceptorConstructor::new(|_| NoopWorkflowInterceptor),
1920            WorkflowInterceptorConstructor::new(|_| NoopWorkflowInterceptor),
1921        ]);
1922        assert_eq!(options.workflow_interceptor_constructors.len(), 2);
1923
1924        options.register_workflow_interceptors(vec![WorkflowInterceptorConstructor::new(|_| {
1925            NoopWorkflowInterceptor
1926        })]);
1927        assert_eq!(options.workflow_interceptor_constructors.len(), 1);
1928    }
1929
1930    #[test]
1931    fn duplicate_workflow_registration_errors() {
1932        let result = WorkerOptions::new("task_q")
1933            .register_workflow::<MyWorkflow>()
1934            .unwrap()
1935            .register_workflow::<MyWorkflow>();
1936
1937        let err = match result {
1938            Ok(_) => panic!("duplicate workflow registration should error"),
1939            Err(err) => err,
1940        };
1941        assert_eq!(
1942            err,
1943            WorkflowRegistrationError::DuplicateWorkflowType {
1944                workflow_type: "MyWorkflow".to_string()
1945            }
1946        );
1947    }
1948
1949    #[test]
1950    fn factory_registration_with_init_errors() {
1951        let result = WorkerOptions::new("task_q")
1952            .register_workflow_with_factory(|| MyWorkflow { counter: 0 });
1953
1954        let err = match result {
1955            Ok(_) => panic!("factory registration with #[init] should error"),
1956            Err(err) => err,
1957        };
1958        assert_eq!(
1959            err,
1960            WorkflowRegistrationError::FactoryRegistrationWithInit {
1961                workflow_type: "MyWorkflow".to_string()
1962            }
1963        );
1964    }
1965
1966    fn default_identity() -> String {
1967        format!(
1968            "{}@{}",
1969            std::process::id(),
1970            gethostname::gethostname().to_string_lossy()
1971        )
1972    }
1973
1974    #[rstest::rstest]
1975    #[case::default_when_none_provided(None, "", Some(default_identity()))]
1976    #[case::connection_identity_preserved(None, "conn-identity", None)]
1977    #[case::worker_override_takes_precedence(
1978        Some("worker-identity"),
1979        "conn-identity",
1980        Some("worker-identity".into())
1981    )]
1982    #[case::worker_override_with_empty_connection(
1983        Some("worker-identity"),
1984        "",
1985        Some("worker-identity".into())
1986    )]
1987    #[test]
1988    fn client_identity_resolution(
1989        #[case] worker_override: Option<&str>,
1990        #[case] connection_identity: &str,
1991        #[case] expected: Option<String>,
1992    ) {
1993        let opts = WorkerOptions::new("task_q")
1994            .register_activities(MyActivities {})
1995            .maybe_client_identity_override(worker_override.map(|s| s.to_owned()))
1996            .build();
1997        let config = opts
1998            .to_core_options("ns".into(), connection_identity.into())
1999            .unwrap();
2000        assert_eq!(config.client_identity_override, expected);
2001    }
2002
2003    #[test]
2004    fn max_eager_activity_reservations_per_workflow_task_propagates() {
2005        let config = WorkerOptions::new("task_q")
2006            .register_activities(MyActivities {})
2007            .max_eager_activity_reservations_per_workflow_task(7)
2008            .build()
2009            .to_core_options("ns".into(), String::new())
2010            .unwrap();
2011        assert_eq!(config.max_eager_activity_reservations_per_workflow_task, 7);
2012    }
2013
2014    #[cfg(feature = "experimental")]
2015    mod experimental_tests {
2016        use super::*;
2017
2018        #[test]
2019        fn simple_plugin_workflow_function_merges_definitions() {
2020            let plugin = SimplePlugin::builder("simple")
2021                .workflows(|existing: Option<WorkflowDefinitions>| {
2022                    assert!(existing.is_some());
2023                    let mut workflows = WorkflowDefinitions::new();
2024                    workflows.register_workflow::<OtherWorkflow>().unwrap();
2025                    workflows
2026                })
2027                .build();
2028            let client_options = ClientOptions::new("namespace").build();
2029            let mut worker_options = WorkerOptions::new("task_q")
2030                .register_workflow::<MyWorkflow>()
2031                .unwrap()
2032                .worker_plugin(plugin)
2033                .build();
2034
2035            crate::plugins::apply_worker_plugins(&client_options, &mut worker_options).unwrap();
2036
2037            let workflows = format!("{:?}", worker_options.workflows());
2038            assert!(workflows.contains("MyWorkflow"));
2039            assert!(workflows.contains("OtherWorkflow"));
2040        }
2041
2042        #[rstest::rstest]
2043        #[case::default_enforces_error_limit(None, false)]
2044        #[case::opt_out_disables_error_limit(Some(true), true)]
2045        #[case::explicit_enable_error_limit(Some(false), false)]
2046        #[test]
2047        fn disable_payload_error_limit_propagates(
2048            #[case] override_value: Option<bool>,
2049            #[case] expected: bool,
2050        ) {
2051            let config = WorkerOptions::new("task_q")
2052                .register_activities(MyActivities {})
2053                .maybe_disable_payload_error_limit(override_value)
2054                .build()
2055                .to_core_options("ns".into(), String::new())
2056                .unwrap();
2057            assert_eq!(config.disable_payload_error_limit, expected);
2058        }
2059    }
2060}