Skip to main content

temporalio_sdk/
lib.rs

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