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