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