From fba10c6703241bdfb7cf1175375903ce53d52519 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Mon, 22 Dec 2025 10:31:37 -0800 Subject: [PATCH 01/13] Hook up new options --- crates/sdk-core/src/worker/mod.rs | 2 +- crates/sdk-core/tests/common/mod.rs | 9 +- .../tests/integ_tests/activity_functions.rs | 11 ++ crates/sdk/src/activities.rs | 1 - crates/sdk/src/lib.rs | 156 +++++++++++++++--- 5 files changed, 152 insertions(+), 27 deletions(-) diff --git a/crates/sdk-core/src/worker/mod.rs b/crates/sdk-core/src/worker/mod.rs index 6d1732ec5..3951d4480 100644 --- a/crates/sdk-core/src/worker/mod.rs +++ b/crates/sdk-core/src/worker/mod.rs @@ -130,7 +130,7 @@ pub struct WorkerConfig { pub client_identity_override: Option, /// If set nonzero, workflows will be cached and sticky task queues will be used, meaning that /// history updates are applied incrementally to suspended instances of workflow execution. - /// Workflows are evicted according to a least-recently-used policy one the cache maximum is + /// Workflows are evicted according to a least-recently-used policy once the cache maximum is /// reached. Workflows may also be explicitly evicted at any time, or as a result of errors /// or failures. #[builder(default = 0)] diff --git a/crates/sdk-core/tests/common/mod.rs b/crates/sdk-core/tests/common/mod.rs index 8a6b59e87..ae8373d8f 100644 --- a/crates/sdk-core/tests/common/mod.rs +++ b/crates/sdk-core/tests/common/mod.rs @@ -143,7 +143,7 @@ where I: Stream + Send + 'static, { let core = init_core_replay_stream("replay_worker_test", histories); - let mut worker = Worker::new_from_core(Arc::new(core), "replay_q".to_string()); + let mut worker = Worker::new_from_core(Arc::new(core)); worker.set_worker_interceptor(FailOnNondeterminismInterceptor {}); worker } @@ -479,10 +479,7 @@ pub(crate) struct TestWorker { impl TestWorker { /// Create a new test worker pub(crate) fn new(core_worker: Arc) -> Self { - let inner = Worker::new_from_core( - core_worker.clone(), - core_worker.get_config().task_queue.clone(), - ); + let inner = Worker::new_from_core(core_worker.clone()); Self { inner, core_worker, @@ -947,7 +944,7 @@ pub(crate) fn build_fake_sdk(mock_cfg: MockPollCfg) -> temporalio_sdk::Worker { c.ignore_evicts_on_shutdown = false; }); let core = mock_worker(mock); - let mut worker = temporalio_sdk::Worker::new_from_core(Arc::new(core), "replay_q".to_string()); + let mut worker = temporalio_sdk::Worker::new_from_core(Arc::new(core)); worker.set_worker_interceptor(FailOnNondeterminismInterceptor {}); worker } diff --git a/crates/sdk-core/tests/integ_tests/activity_functions.rs b/crates/sdk-core/tests/integ_tests/activity_functions.rs index 165099d77..a80f7c86a 100644 --- a/crates/sdk-core/tests/integ_tests/activity_functions.rs +++ b/crates/sdk-core/tests/integ_tests/activity_functions.rs @@ -1,5 +1,16 @@ +use temporalio_macros::activities; use temporalio_sdk::activities::{ActivityContext, ActivityError}; pub(crate) async fn echo(_ctx: ActivityContext, e: String) -> Result { Ok(e) } + +struct StdActivities {} + +#[activities] +impl StdActivities { + #[activity] + async fn echo(_ctx: ActivityContext, e: String) -> Result { + Ok(e) + } +} diff --git a/crates/sdk/src/activities.rs b/crates/sdk/src/activities.rs index 9df1ce997..ab7c4db1e 100644 --- a/crates/sdk/src/activities.rs +++ b/crates/sdk/src/activities.rs @@ -1,7 +1,6 @@ //! Functionality related to defining and interacting with activities use crate::{WorkerOptionsBuilder, app_data::AppData, worker_options_builder}; - use futures_util::future::BoxFuture; use prost_types::{Duration, Timestamp}; use std::{ diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index 1eb00d6ab..ec8c1b5ae 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -86,8 +86,11 @@ use std::{ future::Future, panic::AssertUnwindSafe, sync::Arc, + time::Duration, +}; +use temporalio_client::{ + Connection, ConnectionOptions, ConnectionOptionsBuilder, connection_options_builder, }; -use temporalio_client::{ConnectionOptions, ConnectionOptionsBuilder, connection_options_builder}; use temporalio_common::{ ActivityDefinition, data_converters::{GenericPayloadConverter, SerializationContext}, @@ -116,8 +119,12 @@ use temporalio_common::{ failure::v1::{Failure, failure}, }, }, + worker::{WorkerDeploymentOptions, WorkerTaskTypes}, +}; +use temporalio_sdk_core::{ + CoreRuntime, PollError, PollerBehavior, Url, Worker as CoreWorker, WorkerConfig, WorkerTuner, + WorkerVersioningStrategy, init_worker, }; -use temporalio_sdk_core::{PollError, Url, Worker as CoreWorker}; use tokio::{ sync::{ Notify, @@ -131,15 +138,89 @@ use tokio_util::sync::CancellationToken; const VERSION: &str = env!("CARGO_PKG_VERSION"); -// TODO [rust-sdk-branch]: Stubbed while working on macros -#[allow(unused)] +// TODO [rust-sdk-branch]: See about making clone /// Contains options for configuring a worker. #[derive(bon::Builder)] -#[builder(state_mod(vis = "pub"))] +#[builder(on(String, into), state_mod(vis = "pub"))] #[non_exhaustive] pub struct WorkerOptions { #[builder(field)] activities: HashMap<&'static str, ActivityInvocation>, + + /// The Temporal service namespace this worker is bound to + pub namespace: String, + /// What task queue will this worker poll from? This task queue name will be used for both + /// workflow and activity polling. + pub task_queue: String, + /// A human-readable string that can identify this worker. Using something like sdk version + /// and host name is a good default. If set, overrides the identity set (if any) on the client + /// used by this worker. + pub client_identity_override: Option, + /// If set nonzero, workflows will be cached and sticky task queues will be used, meaning that + /// history updates are applied incrementally to suspended instances of workflow execution. + /// Workflows are evicted according to a least-recently-used policy once the cache maximum is + /// reached. Workflows may also be explicitly evicted at any time, or as a result of errors + /// or failures. + #[builder(default = 1000)] + pub max_cached_workflows: usize, + /// Set a [crate::WorkerTuner] for this worker, which controls how many slots are available for + /// the different kinds of tasks. + pub tuner: Arc, + /// Controls how polling for Workflow tasks will happen on this worker's task queue. See also + /// [WorkerConfig::nonsticky_to_sticky_poll_ratio]. If using SimpleMaximum, Must be at least 2 + /// when `max_cached_workflows` > 0, or is an error. + #[builder(default = PollerBehavior::SimpleMaximum(5))] + pub workflow_task_poller_behavior: PollerBehavior, + /// Only applies when using [PollerBehavior::SimpleMaximum] + /// + /// (max workflow task polls * this number) = the number of max pollers that will be allowed for + /// the nonsticky queue when sticky tasks are enabled. If both defaults are used, the sticky + /// queue will allow 4 max pollers while the nonsticky queue will allow one. The minimum for + /// either poller is 1, so if the maximum allowed is 1 and sticky queues are enabled, there will + /// be 2 concurrent polls. + #[builder(default = 0.2)] + pub nonsticky_to_sticky_poll_ratio: f32, + /// Controls how polling for Activity tasks will happen on this worker's task queue. + #[builder(default = PollerBehavior::SimpleMaximum(5))] + pub activity_task_poller_behavior: PollerBehavior, + /// Controls how polling for Nexus tasks will happen on this worker's task queue. + #[builder(default = PollerBehavior::SimpleMaximum(5))] + pub nexus_task_poller_behavior: PollerBehavior, + /// Specifies which task types this worker will poll for. + /// + /// Note: At least one task type must be specified or the worker will fail validation. + #[builder(default = WorkerTaskTypes::all())] + pub task_types: WorkerTaskTypes, + /// How long a workflow task is allowed to sit on the sticky queue before it is timed out + /// and moved to the non-sticky queue where it may be picked up by any worker. + #[builder(default = Duration::from_secs(10))] + pub sticky_queue_schedule_to_start_timeout: Duration, + /// Longest interval for throttling activity heartbeats + #[builder(default = Duration::from_secs(60))] + pub max_heartbeat_throttle_interval: Duration, + /// Default interval for throttling activity heartbeats in case + /// `ActivityOptions.heartbeat_timeout` is unset. + /// When the timeout *is* set in the `ActivityOptions`, throttling is set to + /// `heartbeat_timeout * 0.8`. + #[builder(default = Duration::from_secs(30))] + pub default_heartbeat_throttle_interval: Duration, + /// Sets the maximum number of activities per second the task queue will dispatch, controlled + /// server-side. Note that this only takes effect upon an activity poll request. If multiple + /// workers on the same queue have different values set, they will thrash with the last poller + /// winning. + /// + /// Setting this to a nonzero value will also disable eager activity execution. + pub max_task_queue_activities_per_second: Option, + /// Limits the number of activities per second that this worker will process. The worker will + /// not poll for new activities if by doing so it might receive and execute an activity which + /// would cause it to exceed this limit. Negative, zero, or NaN values will cause building + /// the options to fail. + pub max_worker_activities_per_second: Option, + /// If set, the worker will issue cancels for all outstanding activities and nexus operations after + /// shutdown has been initiated and this amount of time has elapsed. + pub graceful_shutdown_period: Option, + /// Set the deployment options for this worker. + pub deployment_options: WorkerDeploymentOptions, } impl WorkerOptionsBuilder { @@ -233,6 +314,7 @@ struct CommonWorker { worker_interceptor: Option>, } +#[derive(Default)] struct WorkflowHalf { /// Maps run id to cached workflow state workflows: RefCell>, @@ -250,33 +332,43 @@ struct WorkflowFutureHandle, J run_id: String, } +#[derive(Default)] struct ActivityHalf { + /// Maps activity type to the function for executing activities of that type + activities: HashMap<&'static str, ActivityInvocation>, /// Maps activity type to the function for executing activities of that type activity_fns: HashMap, task_tokens_to_cancels: HashMap, } impl Worker { - // /// Create a new worker from an existing connection, and options. - // pub fn new(connection: Connection, options: WorkerOptions) -> Self {} + // TODO [rust-sdk-branch]: Not 100% sure I like passing runtime here + // TODO [rust-sdk-branch]: Don't use anyhow + /// Create a new worker from an existing connection, and options. + pub fn new( + runtime: &CoreRuntime, + connection: Connection, + mut options: WorkerOptions, + ) -> Result { + let acts = std::mem::take(&mut options.activities); + let wc = options.try_into().map_err(|s| anyhow::anyhow!("{s}"))?; + let core = init_worker(runtime, wc, connection)?; + let mut me = Self::new_from_core(Arc::new(core)); + me.activity_half.activities = acts; + Ok(me) + } - /// Create a new Rust SDK worker from a core worker - pub fn new_from_core(worker: Arc, task_queue: impl Into) -> Self { + /// Create a new Rust SDK worker from a Core worker. For internal testing only. + #[doc(hidden)] + pub fn new_from_core(worker: Arc) -> Self { Self { common: CommonWorker { + task_queue: worker.get_config().task_queue.clone(), worker, - task_queue: task_queue.into(), worker_interceptor: None, }, - workflow_half: WorkflowHalf { - workflows: Default::default(), - workflow_fns: Default::default(), - workflow_removed_from_map: Default::default(), - }, - activity_half: ActivityHalf { - activity_fns: Default::default(), - task_tokens_to_cancels: Default::default(), - }, + workflow_half: Default::default(), + activity_half: Default::default(), app_data: Some(Default::default()), } } @@ -1195,6 +1287,32 @@ impl PrintablePanicType for EndPrintingAttempts { type NextType = EndPrintingAttempts; } +impl TryFrom for WorkerConfig { + type Error = String; + fn try_from(o: WorkerOptions) -> Result { + WorkerConfig::builder() + .namespace(o.namespace) + .task_queue(o.task_queue) + .maybe_client_identity_override(o.client_identity_override) + .max_cached_workflows(o.max_cached_workflows) + .tuner(o.tuner) + .workflow_task_poller_behavior(o.workflow_task_poller_behavior) + .activity_task_poller_behavior(o.activity_task_poller_behavior) + .nexus_task_poller_behavior(o.nexus_task_poller_behavior) + .task_types(o.task_types) + .sticky_queue_schedule_to_start_timeout(o.sticky_queue_schedule_to_start_timeout) + .max_heartbeat_throttle_interval(o.max_heartbeat_throttle_interval) + .default_heartbeat_throttle_interval(o.default_heartbeat_throttle_interval) + .maybe_max_task_queue_activities_per_second(o.max_task_queue_activities_per_second) + .maybe_max_worker_activities_per_second(o.max_worker_activities_per_second) + .maybe_graceful_shutdown_period(o.graceful_shutdown_period) + .versioning_strategy(WorkerVersioningStrategy::WorkerDeploymentBased( + o.deployment_options, + )) + .build() + } +} + #[cfg(test)] mod tests { use super::*; From 1cbb1d971435948f447b39a30b2e029360e2333c Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Mon, 22 Dec 2025 16:12:19 -0800 Subject: [PATCH 02/13] Hooked up definitions to execution --- crates/common/src/data_converters.rs | 21 +- crates/macros/src/definitions.rs | 12 +- crates/sdk-core/tests/common/mod.rs | 44 +++-- crates/sdk-core/tests/heavy_tests.rs | 2 +- .../tests/integ_tests/activity_functions.rs | 2 +- .../tests/integ_tests/update_tests.rs | 2 +- .../integ_tests/worker_heartbeat_tests.rs | 2 +- .../tests/integ_tests/worker_tests.rs | 3 +- .../tests/integ_tests/workflow_tests.rs | 2 +- .../integ_tests/workflow_tests/activities.rs | 34 ++-- .../workflow_tests/local_activities.rs | 2 +- crates/sdk/Cargo.toml | 1 + crates/sdk/src/activities.rs | 118 +++++++++-- crates/sdk/src/lib.rs | 187 +++++++++++------- 14 files changed, 304 insertions(+), 128 deletions(-) diff --git a/crates/common/src/data_converters.rs b/crates/common/src/data_converters.rs index 2f1e47c78..70ae62405 100644 --- a/crates/common/src/data_converters.rs +++ b/crates/common/src/data_converters.rs @@ -47,9 +47,28 @@ impl PayloadConverter { // TODO [rust-sdk-branch]: Proto binary, other standard built-ins } +#[derive(Debug)] pub enum PayloadConversionError { WrongEncoding, - EncodingError(Box), + EncodingError(Box), +} + +impl std::fmt::Display for PayloadConversionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PayloadConversionError::WrongEncoding => write!(f, "Wrong encoding"), + PayloadConversionError::EncodingError(err) => write!(f, "Encoding error: {}", err), + } + } +} + +impl std::error::Error for PayloadConversionError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + PayloadConversionError::WrongEncoding => None, + PayloadConversionError::EncodingError(err) => Some(err.as_ref()), + } + } } pub trait FailureConverter { diff --git a/crates/macros/src/definitions.rs b/crates/macros/src/definitions.rs index 330bb6351..b6db4b8ca 100644 --- a/crates/macros/src/definitions.rs +++ b/crates/macros/src/definitions.rs @@ -320,7 +320,7 @@ impl ActivitiesDefinition { let struct_name = method_name_to_pascal_case(&a.method.sig.ident); let struct_ident = format_ident!("{}", struct_name); quote! { - worker_options.register_activity::<#struct_ident>(); + defs.register_activity::<#struct_ident>(); } }) .collect(); @@ -333,7 +333,7 @@ impl ActivitiesDefinition { let struct_name = method_name_to_pascal_case(&a.method.sig.ident); let struct_ident = format_ident!("{}", struct_name); quote! { - worker_options.register_activity_with_instance::<#struct_ident>(self.clone()); + defs.register_activity_with_instance::<#struct_ident>(self.clone()); } }) .collect(); @@ -346,15 +346,15 @@ impl ActivitiesDefinition { quote! { impl ::temporalio_sdk::activities::ActivityImplementer for #impl_type { - fn register_all_static( - worker_options: &mut ::temporalio_sdk::WorkerOptionsBuilder, + fn register_all_static( + defs: &mut ::temporalio_sdk::activities::ActivityDefinitions, ) { #(#static_activities)* } - fn register_all_instance( + fn register_all_instance( self: ::std::sync::Arc, - worker_options: &mut ::temporalio_sdk::WorkerOptionsBuilder, + defs: &mut ::temporalio_sdk::activities::ActivityDefinitions, ) { #register_instance_body } diff --git a/crates/sdk-core/tests/common/mod.rs b/crates/sdk-core/tests/common/mod.rs index ae8373d8f..251eb83c1 100644 --- a/crates/sdk-core/tests/common/mod.rs +++ b/crates/sdk-core/tests/common/mod.rs @@ -48,10 +48,10 @@ use temporalio_common::{ Logger, OtelCollectorOptions, PrometheusExporterOptions, TelemetryOptions, build_otlp_metric_exporter, metrics::CoreMeter, start_prometheus_metric_exporter, }, - worker::WorkerTaskTypes, + worker::{WorkerDeploymentOptions, WorkerDeploymentVersion, WorkerTaskTypes}, }; use temporalio_sdk::{ - IntoActivityFunc, Worker, WorkflowFunction, + IntoActivityFunc, Worker, WorkerOptions, WorkflowFunction, interceptors::{ FailOnNondeterminismInterceptor, InterceptorWithNext, ReturnWorkflowExitValueInterceptor, WorkerInterceptor, @@ -114,6 +114,21 @@ pub(crate) fn integ_worker_config(tq: &str) -> WorkerConfig { .expect("Configuration options construct properly") } +pub(crate) fn integ_sdk_config(tq: &str) -> WorkerOptions { + WorkerOptions::new(tq) + .namespace(env::var(INTEG_NAMESPACE_ENV_VAR).unwrap_or(NAMESPACE.to_string())) + .deployment_options(WorkerDeploymentOptions { + version: WorkerDeploymentVersion { + deployment_name: "".to_owned(), + build_id: "test_build_id".to_owned(), + }, + use_worker_versioning: false, + default_versioning_behavior: None, + }) + .task_types(WorkerTaskTypes::all()) + .build() +} + /// Create a worker replay instance preloaded with provided histories. Returns the worker impl. pub(crate) fn init_core_replay_preloaded(test_name: &str, histories: I) -> CoreWorker where @@ -216,7 +231,9 @@ pub(crate) async fn get_cloud_client() -> Client { pub(crate) struct CoreWfStarter { /// Used for both the task queue and workflow id task_queue_name: String, + // TODO [rust-sdk-branch]: Get rid of worker config pub worker_config: WorkerConfig, + pub sdk_config: WorkerOptions, /// Options to use when starting workflow(s) pub workflow_options: WorkflowOptions, initted_worker: OnceCell, @@ -291,9 +308,11 @@ impl CoreWfStarter { let task_queue = format!("{test_name}_{task_q_salt}"); let mut worker_config = integ_worker_config(&task_queue); worker_config.max_cached_workflows = 1000_usize; + let sdk_config = integ_sdk_config(&task_queue); Self { task_queue_name: task_queue, worker_config, + sdk_config, initted_worker: OnceCell::new(), workflow_options: Default::default(), runtime_override: runtime_override.map(Arc::new), @@ -308,6 +327,7 @@ impl CoreWfStarter { Self { task_queue_name: self.task_queue_name.clone(), worker_config: self.worker_config.clone(), + sdk_config: self.sdk_config.clone(), workflow_options: self.workflow_options.clone(), runtime_override: self.runtime_override.clone(), client_override: self.client_override.clone(), @@ -317,8 +337,9 @@ impl CoreWfStarter { } pub(crate) async fn worker(&mut self) -> TestWorker { - let w = self.get_worker().await; - let mut w = TestWorker::new(w); + let worker = self.get_worker().await; + let sdk = Worker::new_from_core_activities(worker, self.sdk_config.activities()); + let mut w = TestWorker::new(sdk); w.client = Some(self.get_client().await); w @@ -469,7 +490,6 @@ impl CoreWfStarter { /// Provides conveniences for running integ tests with the SDK (against real server or mocks) pub(crate) struct TestWorker { inner: Worker, - pub core_worker: Arc, client: Option, pub started_workflows: Arc>>, /// If set true (default), and a client is available, we will fetch workflow results to @@ -478,11 +498,9 @@ pub(crate) struct TestWorker { } impl TestWorker { /// Create a new test worker - pub(crate) fn new(core_worker: Arc) -> Self { - let inner = Worker::new_from_core(core_worker.clone()); + pub(crate) fn new(sdk: Worker) -> Self { Self { - inner, - core_worker, + inner: sdk, client: None, started_workflows: Arc::new(Mutex::new(vec![])), fetch_results: true, @@ -494,7 +512,7 @@ impl TestWorker { } pub(crate) fn worker_instance_key(&self) -> Uuid { - self.core_worker.worker_instance_key() + self.inner.worker_instance_key() } // TODO: Maybe trait-ify? @@ -629,6 +647,10 @@ impl TestWorker { tokio::try_join!(self.inner.run(), get_results_waiter)?; Ok(()) } + + pub(crate) fn core_worker(&self) -> Arc { + self.inner.core_worker() + } } pub(crate) struct TestWorkerSubmitterHandle { @@ -961,7 +983,7 @@ pub(crate) fn mock_sdk_cfg( let mut mock = build_mock_pollers(poll_cfg); mock.worker_cfg(mutator); let core = mock_worker(mock); - TestWorker::new(Arc::new(core)) + TestWorker::new(temporalio_sdk::Worker::new_from_core(Arc::new(core))) } #[derive(Default)] diff --git a/crates/sdk-core/tests/heavy_tests.rs b/crates/sdk-core/tests/heavy_tests.rs index 0ed12be14..177fdba2e 100644 --- a/crates/sdk-core/tests/heavy_tests.rs +++ b/crates/sdk-core/tests/heavy_tests.rs @@ -305,7 +305,7 @@ async fn evict_while_la_running_no_interference() { ) .await .unwrap(); - let cw = worker.core_worker.clone(); + let cw = worker.core_worker(); let client = client.clone(); subfs.push(async move { // Evict the workflow diff --git a/crates/sdk-core/tests/integ_tests/activity_functions.rs b/crates/sdk-core/tests/integ_tests/activity_functions.rs index a80f7c86a..1769bff49 100644 --- a/crates/sdk-core/tests/integ_tests/activity_functions.rs +++ b/crates/sdk-core/tests/integ_tests/activity_functions.rs @@ -5,7 +5,7 @@ pub(crate) async fn echo(_ctx: ActivityContext, e: String) -> Result WorkflowResult<()> { - ctx.activity(ActivityOptions { - activity_type: "echo_activity".to_string(), - start_to_close_timeout: Some(Duration::from_secs(5)), - input: "hi!".as_json_payload().expect("serializes fine"), - ..Default::default() - }) - .await; - Ok(().into()) +async fn one_activity_wf(ctx: WfContext) -> WorkflowResult { + // TODO [rust-sdk-branch]: activities need to return deserialzied results + let r = ctx + .activity(ActivityOptions { + // TODO [rust-sdk-branch]: shouldn't be "Echo" but "echo"? + activity_type: "std_activities::Echo".to_string(), + start_to_close_timeout: Some(Duration::from_secs(5)), + input: "hi!".as_json_payload().expect("serializes fine"), + ..Default::default() + }) + .await + .unwrap_ok_payload(); + Ok(r.into()) } #[tokio::test] async fn one_activity_only() { let wf_name = "one_activity"; let mut starter = CoreWfStarter::new(wf_name); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; let client = starter.get_client().await; worker.register_wf(wf_name.to_owned(), one_activity_wf); - worker.register_activity("echo_activity", echo); let run_id = worker .submit_wf( @@ -93,7 +99,9 @@ async fn one_activity_only() { .get_workflow_result(Default::default()) .await .unwrap(); - assert_matches!(res, WorkflowExecutionResult::Succeeded(_)); + let r = assert_matches!(res, WorkflowExecutionResult::Succeeded(r) => r); + let p = Payload::from_json_payload(&r[0]).unwrap(); + assert_eq!(String::from_json_payload(&p).unwrap(), "hi!"); } #[tokio::test] @@ -1020,7 +1028,7 @@ async fn it_can_complete_async() { let task_token = &activity_info.task_token; let mut shared = shared_token_ref.lock().await; *shared = Some(task_token.clone()); - Ok::, _>(ActExitValue::WillCompleteAsync) + Err::, _>(ActivityError::WillCompleteAsync) } }, ); diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/local_activities.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/local_activities.rs index 07e66eda0..b2a8df561 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/local_activities.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/local_activities.rs @@ -1104,7 +1104,7 @@ async fn local_act_heartbeat(#[case] shutdown_middle: bool) { wc.max_cached_workflows = 1; wc.max_outstanding_workflow_tasks = Some(1); }); - let core = worker.core_worker.clone(); + let core = worker.core_worker(); let shutdown_barr: &'static Barrier = Box::leak(Box::new(Barrier::new(2))); diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index 5d55226e6..a61a14c0b 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -29,6 +29,7 @@ tokio = { version = "1.47", features = [ tokio-util = { version = "0.7" } tokio-stream = "0.1" tracing = "0.1" +uuid = { version = "1.18", features = ["v4"] } [dependencies.temporalio-sdk-core] path = "../sdk-core" diff --git a/crates/sdk/src/activities.rs b/crates/sdk/src/activities.rs index ab7c4db1e..4a2fd51ba 100644 --- a/crates/sdk/src/activities.rs +++ b/crates/sdk/src/activities.rs @@ -1,17 +1,20 @@ //! Functionality related to defining and interacting with activities -use crate::{WorkerOptionsBuilder, app_data::AppData, worker_options_builder}; -use futures_util::future::BoxFuture; +use crate::app_data::AppData; +use futures_util::{FutureExt, future::BoxFuture}; use prost_types::{Duration, Timestamp}; use std::{ collections::HashMap, + fmt::Debug, sync::Arc, time::{Duration as StdDuration, SystemTime}, }; use temporalio_client::Priority; use temporalio_common::{ ActivityDefinition, - data_converters::{PayloadConversionError, PayloadConverter}, + data_converters::{ + GenericPayloadConverter, PayloadConversionError, PayloadConverter, SerializationContext, + }, protos::{ coresdk::{ActivityHeartbeat, activity_task}, temporal::api::common::v1::{Payload, RetryPolicy, WorkflowExecution}, @@ -211,6 +214,9 @@ pub enum ActivityError { }, /// Return this error to indicate that the activity should not be retried. NonRetryable(anyhow::Error), + /// Return this error to indicate that the activity will be completed outside of this activity + /// definition, by an external client. + WillCompleteAsync, } impl ActivityError { @@ -294,29 +300,26 @@ fn maybe_convert_timestamp(timestamp: &Timestamp) -> Option { }) } -pub(crate) type ActivityInvocation = Box< +pub(crate) type ActivityInvocation = Arc< dyn Fn( - Payload, - PayloadConverter, - ActivityContext, - ) - -> Result>, PayloadConversionError>, + Payload, + PayloadConverter, + ActivityContext, + ) + -> Result>, PayloadConversionError> + + Send + + Sync, >; #[doc(hidden)] pub trait ActivityImplementer { - fn register_all_static( - worker_options: &mut WorkerOptionsBuilder, - ); - fn register_all_instance( - self: Arc, - worker_options: &mut WorkerOptionsBuilder, - ); + fn register_all_static(defs: &mut ActivityDefinitions); + fn register_all_instance(self: Arc, defs: &mut ActivityDefinitions); } #[doc(hidden)] pub trait ExecutableActivity: ActivityDefinition { - type Implementer: ActivityImplementer + 'static; + type Implementer: ActivityImplementer + Send + Sync + 'static; fn execute( receiver: Option>, ctx: ActivityContext, @@ -326,3 +329,84 @@ pub trait ExecutableActivity: ActivityDefinition { #[doc(hidden)] pub trait HasOnlyStaticMethods {} + +/// Contains activity registrations in a form ready for execution by workers. +#[derive(Default, Clone)] +pub struct ActivityDefinitions { + activities: HashMap<&'static str, ActivityInvocation>, +} + +impl ActivityDefinitions { + /// Registers all activities on an activity implementer that don't take a receiver. + pub fn register_activities_static(&mut self) -> &mut Self + where + AI: ActivityImplementer + HasOnlyStaticMethods, + { + AI::register_all_static(self); + self + } + /// Registers all activities on an activity implementer that take a receiver. + pub fn register_activities(&mut self, instance: AI) -> &mut Self { + AI::register_all_static(self); + let arcd = Arc::new(instance); + AI::register_all_instance(arcd, self); + self + } + /// Registers a specific activitiy that does not take a receiver. + pub fn register_activity(&mut self) -> &mut Self { + self.activities.insert( + AD::name(), + Arc::new(move |p, pc, c| { + let deserialized = pc.from_payload(p, &SerializationContext::Activity)?; + let pc2 = pc.clone(); + Ok(AD::execute(None, c, deserialized) + .map(move |v| match v { + Ok(okv) => pc2 + .to_payload(&okv, &SerializationContext::Activity) + .map_err(|_| todo!()), + Err(e) => Err(e), + }) + .boxed()) + }), + ); + self + } + /// Registers a specific activitiy that takes a receiver. + pub fn register_activity_with_instance( + &mut self, + instance: Arc, + ) -> &mut Self { + self.activities.insert( + AD::name(), + Arc::new(move |p, pc, c| { + let deserialized = pc.from_payload(p, &SerializationContext::Activity)?; + let pc2 = pc.clone(); + Ok(AD::execute(Some(instance.clone()), c, deserialized) + .map(move |v| match v { + Ok(okv) => pc2 + .to_payload(&okv, &SerializationContext::Activity) + .map_err(|_| todo!()), + Err(e) => Err(e), + }) + .boxed()) + }), + ); + self + } + + pub(crate) fn is_empty(&self) -> bool { + self.activities.is_empty() + } + + pub(crate) fn get(&self, act_type: &str) -> Option { + self.activities.get(act_type).cloned() + } +} + +impl Debug for ActivityDefinitions { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ActivityDefinitions") + .field("activities", &self.activities.keys()) + .finish() + } +} diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index ec8c1b5ae..c60d33d8e 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -60,6 +60,7 @@ mod workflow_future; pub use temporalio_client::Namespace; use tracing::{Instrument, Span, field}; +use uuid::Uuid; pub use workflow_context::{ ActivityOptions, CancellableFuture, ChildWorkflow, ChildWorkflowOptions, LocalActivityOptions, NexusOperationOptions, PendingChildWorkflow, Signal, SignalData, SignalWorkflowOptions, @@ -68,7 +69,7 @@ pub use workflow_context::{ use crate::{ activities::{ - ActivityContext, ActivityError, ActivityImplementer, ActivityInvocation, + ActivityContext, ActivityDefinitions, ActivityError, ActivityImplementer, ExecutableActivity, HasOnlyStaticMethods, }, interceptors::WorkerInterceptor, @@ -93,7 +94,7 @@ use temporalio_client::{ }; use temporalio_common::{ ActivityDefinition, - data_converters::{GenericPayloadConverter, SerializationContext}, + data_converters::PayloadConverter, protos::{ TaskToken, coresdk::{ @@ -122,8 +123,8 @@ use temporalio_common::{ worker::{WorkerDeploymentOptions, WorkerTaskTypes}, }; use temporalio_sdk_core::{ - CoreRuntime, PollError, PollerBehavior, Url, Worker as CoreWorker, WorkerConfig, WorkerTuner, - WorkerVersioningStrategy, init_worker, + CoreRuntime, PollError, PollerBehavior, TunerBuilder, Url, Worker as CoreWorker, WorkerConfig, + WorkerTuner, WorkerVersioningStrategy, init_worker, }; use tokio::{ sync::{ @@ -138,20 +139,22 @@ use tokio_util::sync::CancellationToken; const VERSION: &str = env!("CARGO_PKG_VERSION"); -// TODO [rust-sdk-branch]: See about making clone /// Contains options for configuring a worker. -#[derive(bon::Builder)] -#[builder(on(String, into), state_mod(vis = "pub"))] +#[derive(bon::Builder, Clone)] +#[builder(start_fn = new, on(String, into), state_mod(vis = "pub"))] #[non_exhaustive] pub struct WorkerOptions { + /// What task queue will this worker poll from? This task queue name will be used for both + /// workflow and activity polling. + #[builder(start_fn)] + pub task_queue: String, + #[builder(field)] - activities: HashMap<&'static str, ActivityInvocation>, + activities: ActivityDefinitions, + // TODO [rust-sdk-branch]: Other SDKs are pulling from client /// The Temporal service namespace this worker is bound to pub namespace: String, - /// What task queue will this worker poll from? This task queue name will be used for both - /// workflow and activity polling. - pub task_queue: String, /// A human-readable string that can identify this worker. Using something like sdk version /// and host name is a good default. If set, overrides the identity set (if any) on the client /// used by this worker. @@ -165,6 +168,7 @@ pub struct WorkerOptions { pub max_cached_workflows: usize, /// Set a [crate::WorkerTuner] for this worker, which controls how many slots are available for /// the different kinds of tasks. + #[builder(default = Arc::new(TunerBuilder::default().build()))] pub tuner: Arc, /// Controls how polling for Workflow tasks will happen on this worker's task queue. See also /// [WorkerConfig::nonsticky_to_sticky_poll_ratio]. If using SimpleMaximum, Must be at least 2 @@ -223,73 +227,71 @@ pub struct WorkerOptions { pub deployment_options: WorkerDeploymentOptions, } +// TODO [rust-sdk-branch]: Traitify this? impl WorkerOptionsBuilder { - /// You shouldn't have to call this directly, instead rely on the `#[activities]` macro. - /// /// Registers all activities on an activity implementer that don't take a receiver. pub fn register_activities_static(&mut self) -> &mut Self where AI: ActivityImplementer + HasOnlyStaticMethods, { - AI::register_all_static(self); + self.activities.register_activities_static::(); self } - /// You shouldn't have to call this directly, instead rely on the `#[activities]` macro. - /// /// Registers all activities on an activity implementer that take a receiver. pub fn register_activities(&mut self, instance: AI) -> &mut Self { - AI::register_all_static(self); - let arcd = Arc::new(instance); - AI::register_all_instance(arcd, self); + self.activities.register_activities::(instance); self } - /// You shouldn't have to call this directly, instead rely on the `#[activities]` macro. - /// /// Registers a specific activitiy that does not take a receiver. pub fn register_activity(&mut self) -> &mut Self { - self.activities.insert( - AD::name(), - Box::new(move |p, pc, c| { - let deserialized = pc.from_payload(p, &SerializationContext::Activity)?; - let pc2 = pc.clone(); - Ok(AD::execute(None, c, deserialized) - .map(move |v| match v { - Ok(okv) => pc2 - .to_payload(&okv, &SerializationContext::Activity) - .map_err(|_| todo!()), - Err(e) => Err(e), - }) - .boxed()) - }), - ); + self.activities.register_activity::(); self } - /// You shouldn't have to call this directly, instead rely on the `#[activities]` macro. - /// /// Registers a specific activitiy that takes a receiver. pub fn register_activity_with_instance( &mut self, instance: Arc, ) -> &mut Self { - self.activities.insert( - AD::name(), - Box::new(move |p, pc, c| { - let deserialized = pc.from_payload(p, &SerializationContext::Activity)?; - let pc2 = pc.clone(); - Ok(AD::execute(Some(instance.clone()), c, deserialized) - .map(move |v| match v { - Ok(okv) => pc2 - .to_payload(&okv, &SerializationContext::Activity) - .map_err(|_| todo!()), - Err(e) => Err(e), - }) - .boxed()) - }), - ); + self.activities + .register_activity_with_instance::(instance); self } } +impl WorkerOptions { + /// Registers all activities on an activity implementer that don't take a receiver. + pub fn register_activities_static(&mut self) -> &mut Self + where + AI: ActivityImplementer + HasOnlyStaticMethods, + { + self.activities.register_activities_static::(); + self + } + /// Registers all activities on an activity implementer that take a receiver. + pub fn register_activities(&mut self, instance: AI) -> &mut Self { + self.activities.register_activities::(instance); + self + } + /// Registers a specific activitiy that does not take a receiver. + pub fn register_activity(&mut self) -> &mut Self { + self.activities.register_activity::(); + self + } + /// Registers a specific activitiy that takes a receiver. + pub fn register_activity_with_instance( + &mut self, + instance: Arc, + ) -> &mut Self { + self.activities + .register_activity_with_instance::(instance); + self + } + /// Returns all the registered activities by cloning the current set. + pub fn activities(&self) -> ActivityDefinitions { + self.activities.clone() + } +} + /// Returns connection options with required fields set to appropriate values for the Rust SDK. pub fn sdk_connection_options( url: impl Into, @@ -335,7 +337,7 @@ struct WorkflowFutureHandle, J #[derive(Default)] struct ActivityHalf { /// Maps activity type to the function for executing activities of that type - activities: HashMap<&'static str, ActivityInvocation>, + activities: ActivityDefinitions, /// Maps activity type to the function for executing activities of that type activity_fns: HashMap, task_tokens_to_cancels: HashMap, @@ -358,9 +360,18 @@ impl Worker { Ok(me) } - /// Create a new Rust SDK worker from a Core worker. For internal testing only. + // TODO [rust-sdk-branch]: Eliminate this constructor in favor of passing in fake connection #[doc(hidden)] pub fn new_from_core(worker: Arc) -> Self { + Self::new_from_core_activities(worker, Default::default()) + } + + // TODO [rust-sdk-branch]: Eliminate this constructor in favor of passing in fake connection + #[doc(hidden)] + pub fn new_from_core_activities( + worker: Arc, + activities: ActivityDefinitions, + ) -> Self { Self { common: CommonWorker { task_queue: worker.get_config().task_queue.clone(), @@ -368,7 +379,10 @@ impl Worker { worker_interceptor: None, }, workflow_half: Default::default(), - activity_half: Default::default(), + activity_half: ActivityHalf { + activities, + ..Default::default() + }, app_data: Some(Default::default()), } } @@ -502,7 +516,7 @@ impl Worker { // Only poll on the activity queue if activity functions have been registered. This // makes tests which use mocks dramatically more manageable. async { - if !act_half.activity_fns.is_empty() { + if !act_half.activity_fns.is_empty() || !act_half.activities.is_empty() { loop { let activity = common.worker.poll_activity_task().await; if matches!(activity, Err(PollError::ShutDown)) { @@ -553,6 +567,16 @@ impl Worker { self.workflow_half.workflows.borrow().len() } + /// Returns the instance key for this worker, used for worker heartbeating. + pub fn worker_instance_key(&self) -> Uuid { + self.common.worker.worker_instance_key() + } + + #[doc(hidden)] + pub fn core_worker(&self) -> Arc { + self.common.worker.clone() + } + fn split_apart( &mut self, ) -> ( @@ -696,16 +720,31 @@ impl ActivityHalf { ) -> Result<(), anyhow::Error> { match activity.variant { Some(activity_task::Variant::Start(start)) => { - let act_fn = self - .activity_fns - .get(&start.activity_type) - .ok_or_else(|| { - anyhow!( - "No function registered for activity type {}", - start.activity_type - ) - })? - .clone(); + let act_fn = if let Some(fun) = self.activities.get(&start.activity_type) { + fun + } else { + let fun = self + .activity_fns + .get(&start.activity_type) + .ok_or_else(|| { + anyhow!( + "No function registered for activity type {}", + start.activity_type + ) + })? + .clone() + .act_func; + Arc::new(move |p, _pc, ac| { + let fun = fun.clone(); + Ok(async move { + fun(ac, p).await.map(|aev| match aev { + ActExitValue::WillCompleteAsync => todo!("get rid of this"), + ActExitValue::Normal(p) => p, + }) + } + .boxed()) + }) + }; let span = info_span!( "RunActivity", "otel.name" = format!("RunActivity:{}", start.activity_type), @@ -727,6 +766,8 @@ impl ActivityHalf { task_token.clone(), start, ); + // TODO [rust-sdk-branch]: Get payload converter from client + let payload_converter = PayloadConverter::serde_json(); tokio::spawn(async move { let act_fut = async move { @@ -735,7 +776,7 @@ impl ActivityHalf { .record("temporalWorkflowID", &info.workflow_id) .record("temporalRunID", &info.run_id); } - (act_fn.act_func)(ctx, arg).await + (act_fn)(arg, payload_converter, ctx)?.await } .instrument(span); let output = AssertUnwindSafe(act_fut).catch_unwind().await; @@ -744,10 +785,7 @@ impl ActivityHalf { format!("Activity function panicked: {}", panic_formatter(e)), true, )), - Ok(Ok(ActExitValue::Normal(p))) => ActivityExecutionResult::ok(p), - Ok(Ok(ActExitValue::WillCompleteAsync)) => { - ActivityExecutionResult::will_complete_async() - } + Ok(Ok(p)) => ActivityExecutionResult::ok(p), Ok(Err(err)) => match err { ActivityError::Retryable { source, @@ -768,6 +806,9 @@ impl ActivityHalf { ActivityError::NonRetryable(nre) => ActivityExecutionResult::fail( Failure::application_failure_from_error(nre, true), ), + ActivityError::WillCompleteAsync => { + ActivityExecutionResult::will_complete_async() + } }, }; worker @@ -1331,6 +1372,6 @@ mod tests { #[test] fn test_activity_registration() { let act_instance = MyActivities {}; - WorkerOptions::builder().register_activities(act_instance); + WorkerOptions::new("task_q").register_activities(act_instance); } } From 4ec08596d2aaf8c6a973359882bd5796e52f1b0c Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Tue, 30 Dec 2025 12:26:05 -0800 Subject: [PATCH 03/13] Convert all existing activities to new format --- crates/common/src/activity_definition.rs | 2 +- .../tests/integ_tests/activity_functions.rs | 2 +- .../integ_tests/workflow_tests/activities.rs | 16 ++++++++-------- crates/sdk/src/workflow_context.rs | 16 ++++++++++++---- crates/sdk/src/workflow_context/options.rs | 4 ++-- 5 files changed, 24 insertions(+), 16 deletions(-) diff --git a/crates/common/src/activity_definition.rs b/crates/common/src/activity_definition.rs index d50b328c1..5e6b40c1c 100644 --- a/crates/common/src/activity_definition.rs +++ b/crates/common/src/activity_definition.rs @@ -9,7 +9,7 @@ use crate::data_converters::{TemporalDeserializable, TemporalSerializable}; /// Implement on a marker struct to define an activity. pub trait ActivityDefinition { /// Type of the input argument to the workflow - type Input: TemporalDeserializable + 'static; + type Input: TemporalDeserializable + TemporalSerializable + 'static; /// Type of the output of the workflow type Output: TemporalSerializable + 'static; diff --git a/crates/sdk-core/tests/integ_tests/activity_functions.rs b/crates/sdk-core/tests/integ_tests/activity_functions.rs index 1769bff49..4aeb0394a 100644 --- a/crates/sdk-core/tests/integ_tests/activity_functions.rs +++ b/crates/sdk-core/tests/integ_tests/activity_functions.rs @@ -10,7 +10,7 @@ pub(crate) struct StdActivities {} #[activities] impl StdActivities { #[activity] - async fn echo(_ctx: ActivityContext, e: String) -> Result { + pub(crate) async fn echo(_ctx: ActivityContext, e: String) -> Result { Ok(e) } } diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/activities.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/activities.rs index f375c23ff..3f13206dc 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/activities.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/activities.rs @@ -3,7 +3,7 @@ use crate::{ ActivationAssertionsInterceptor, CoreWfStarter, INTEG_CLIENT_IDENTITY, build_fake_sdk, eventually, init_core_and_create_wf, mock_sdk, mock_sdk_cfg, }, - integ_tests::activity_functions::{StdActivities, echo}, + integ_tests::activity_functions::{StdActivities, echo, std_activities}, }; use anyhow::anyhow; use assert_matches::assert_matches; @@ -61,13 +61,13 @@ use tokio::{join, sync::Semaphore, time::sleep}; async fn one_activity_wf(ctx: WfContext) -> WorkflowResult { // TODO [rust-sdk-branch]: activities need to return deserialzied results let r = ctx - .activity(ActivityOptions { - // TODO [rust-sdk-branch]: shouldn't be "Echo" but "echo"? - activity_type: "std_activities::Echo".to_string(), - start_to_close_timeout: Some(Duration::from_secs(5)), - input: "hi!".as_json_payload().expect("serializes fine"), - ..Default::default() - }) + .activity::( + "hi!", + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(5)), + ..Default::default() + }, + )? .await .unwrap_ok_payload(); Ok(r.into()) diff --git a/crates/sdk/src/workflow_context.rs b/crates/sdk/src/workflow_context.rs index 4743a41bc..5609cab85 100644 --- a/crates/sdk/src/workflow_context.rs +++ b/crates/sdk/src/workflow_context.rs @@ -29,6 +29,10 @@ use std::{ time::{Duration, SystemTime}, }; use temporalio_common::{ + ActivityDefinition, + data_converters::{ + GenericPayloadConverter, PayloadConversionError, PayloadConverter, SerializationContext, + }, protos::{ coresdk::{ activity_result::{ActivityResolution, activity_resolution}, @@ -213,10 +217,14 @@ impl WfContext { } /// Request to run an activity - pub fn activity( + pub fn activity( &self, + input: AD::Input, mut opts: ActivityOptions, - ) -> impl CancellableFuture { + ) -> Result, PayloadConversionError> { + // TODO [rust-sdk-branch]: Get payload converter properly + let pc = PayloadConverter::serde_json(); + let payload = pc.to_payload(&input, &SerializationContext::Workflow)?; if opts.task_queue.is_none() { opts.task_queue = Some(self.task_queue.clone()); } @@ -224,12 +232,12 @@ impl WfContext { let (cmd, unblocker) = CancellableWFCommandFut::new(CancellableID::Activity(seq)); self.send( CommandCreateRequest { - cmd: opts.into_command(seq), + cmd: opts.into_command(payload, seq), unblocker, } .into(), ); - cmd + Ok(cmd) } /// Request to run a local activity diff --git a/crates/sdk/src/workflow_context/options.rs b/crates/sdk/src/workflow_context/options.rs index 06ac526f1..96cc449ab 100644 --- a/crates/sdk/src/workflow_context/options.rs +++ b/crates/sdk/src/workflow_context/options.rs @@ -76,8 +76,8 @@ pub struct ActivityOptions { pub do_not_eagerly_execute: bool, } -impl IntoWorkflowCommand for ActivityOptions { - fn into_command(self, seq: u32) -> WorkflowCommand { +impl ActivityOptions { + pub(crate) fn into_command(self, input: Payload, seq: u32) -> WorkflowCommand { WorkflowCommand { variant: Some( ScheduleActivity { From e374e87be1137fff5f6bce2480ee8248aa4a8f50 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Tue, 30 Dec 2025 17:39:47 -0800 Subject: [PATCH 04/13] Massive conversion, compiles but needs test fixes --- crates/macros/src/definitions.rs | 51 +- crates/macros/src/lib.rs | 1 + .../tests/common/activity_functions.rs | 45 + crates/sdk-core/tests/common/mod.rs | 21 +- crates/sdk-core/tests/common/workflows.rs | 44 +- crates/sdk-core/tests/heavy_tests.rs | 175 ++- .../tests/heavy_tests/fuzzy_workflow.rs | 48 +- .../tests/integ_tests/activity_functions.rs | 16 - .../tests/integ_tests/heartbeat_tests.rs | 54 +- .../tests/integ_tests/metrics_tests.rs | 112 +- .../tests/integ_tests/polling_tests.rs | 32 +- .../tests/integ_tests/update_tests.rs | 135 +- .../integ_tests/worker_heartbeat_tests.rs | 269 ++-- .../tests/integ_tests/worker_tests.rs | 71 +- .../integ_tests/worker_versioning_tests.rs | 28 +- .../tests/integ_tests/workflow_tests.rs | 99 +- .../integ_tests/workflow_tests/activities.rs | 305 ++-- .../workflow_tests/appdata_propagation.rs | 43 +- .../integ_tests/workflow_tests/determinism.rs | 91 +- .../workflow_tests/local_activities.rs | 1274 +++++++++-------- .../integ_tests/workflow_tests/patches.rs | 76 +- .../integ_tests/workflow_tests/resets.rs | 24 +- crates/sdk-core/tests/main.rs | 1 - crates/sdk-core/tests/manual_tests.rs | 65 +- .../sdk-core/tests/shared_tests/priority.rs | 68 +- crates/sdk/src/lib.rs | 83 +- crates/sdk/src/workflow_context.rs | 66 +- crates/sdk/src/workflow_context/options.rs | 34 +- 28 files changed, 1919 insertions(+), 1412 deletions(-) create mode 100644 crates/sdk-core/tests/common/activity_functions.rs delete mode 100644 crates/sdk-core/tests/integ_tests/activity_functions.rs diff --git a/crates/macros/src/definitions.rs b/crates/macros/src/definitions.rs index b6db4b8ca..f1e9d523e 100644 --- a/crates/macros/src/definitions.rs +++ b/crates/macros/src/definitions.rs @@ -1,8 +1,8 @@ use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; -use quote::{format_ident, quote}; +use quote::{format_ident, quote, quote_spanned}; use syn::{ - FnArg, ImplItem, ItemImpl, ReturnType, Type, TypePath, + Attribute, FnArg, ImplItem, ItemImpl, ReturnType, Type, TypePath, parse::{Parse, ParseStream}, spanned::Spanned, }; @@ -12,8 +12,14 @@ pub(crate) struct ActivitiesDefinition { activities: Vec, } +#[derive(Default)] +struct ActivityAttributes { + name_override: Option, +} + struct ActivityMethod { method: syn::ImplItemFn, + attributes: ActivityAttributes, is_async: bool, is_static: bool, input_type: Option, @@ -48,6 +54,7 @@ impl Parse for ActivitiesDefinition { } fn parse_activity_method(method: &syn::ImplItemFn) -> syn::Result { + let attributes = extract_activity_attributes(method.attrs.as_slice())?; let is_async = method.sig.asyncness.is_some(); // Determine if static (no self receiver) or instance (Arc) @@ -72,6 +79,7 @@ fn parse_activity_method(method: &syn::ImplItemFn) -> syn::Result syn::Result syn::Result { + let mut activity_attributes = ActivityAttributes::default(); + + for attr in attrs { + if attr.path().is_ident("activity") && attr.meta.require_list().is_ok() { + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("name") { + let value = meta.value()?; + let expr: syn::Expr = value.parse()?; + activity_attributes.name_override = Some(expr); + Ok(()) + } else { + Err(meta.error("unsupported activity attribute")) + } + })?; + } + } + + Ok(activity_attributes) +} + fn validate_arc_self_type(ty: &Type) -> syn::Result<()> { let expected: Type = syn::parse_quote!(Arc); @@ -168,10 +197,17 @@ impl ActivitiesDefinition { .activities .iter() .map(|act| { - let visibility = &act.method.vis; + // Default to pub(super) since the method structs need to be visible outside the + // generated module. + let visibility = match &act.method.vis { + syn::Visibility::Inherited => &syn::parse_quote!(pub(super)), + o => o, + }; + let struct_name = method_name_to_pascal_case(&act.method.sig.ident); let struct_ident = format_ident!("{}", struct_name); - quote! { + let span = act.method.span(); + quote_spanned! { span=> #visibility struct #struct_ident; } }) @@ -233,7 +269,12 @@ impl ActivitiesDefinition { .map(|t| quote! { #t }) .unwrap_or(quote! { () }); - let activity_name = format!("{}::{}", module_name, struct_name); + let activity_name = if let Some(ref name_expr) = activity.attributes.name_override { + quote! { #name_expr } + } else { + let default_name = format!("{}::{}", module_name, struct_name); + quote! { #default_name } + }; let receiver_pattern = if activity.is_static { quote! { _receiver } diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index 5f9ad1730..6765e50b2 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -4,6 +4,7 @@ use syn::parse_macro_input; mod definitions; mod fsm_impl; +// TODO [rust-sdk-branch]: Example docstring #[proc_macro_attribute] pub fn activities(_attr: TokenStream, item: TokenStream) -> TokenStream { let def: definitions::ActivitiesDefinition = diff --git a/crates/sdk-core/tests/common/activity_functions.rs b/crates/sdk-core/tests/common/activity_functions.rs new file mode 100644 index 000000000..aafd6ee90 --- /dev/null +++ b/crates/sdk-core/tests/common/activity_functions.rs @@ -0,0 +1,45 @@ +use std::time::Duration; +use temporalio_common::protos::DEFAULT_ACTIVITY_TYPE; +use temporalio_macros::activities; +use temporalio_sdk::activities::{ActivityContext, ActivityError}; +use tokio::time::sleep; + +pub(crate) struct StdActivities {} + +#[activities] +impl StdActivities { + #[activity] + pub(crate) async fn echo(_ctx: ActivityContext, e: String) -> Result { + Ok(e) + } + + /// Activity that does nothing and returns success + #[activity] + pub(crate) async fn no_op(_ctx: ActivityContext, _: ()) -> Result<(), ActivityError> { + Ok(()) + } + + /// Also a no-op, but uses the default name from history construction to work with + /// canned histories + #[activity(name = DEFAULT_ACTIVITY_TYPE)] + pub(crate) async fn default(_ctx: ActivityContext, _: ()) -> Result<(), ActivityError> { + Ok(()) + } + + /// Activity that sleeps for provided duration. Name is overriden to provide compatibility with + /// some canned histories. + #[activity(name = "delay")] + pub(crate) async fn delay( + _ctx: ActivityContext, + duration: Duration, + ) -> Result<(), ActivityError> { + sleep(duration).await; + Ok(()) + } + + /// Always fails + #[activity] + pub(crate) async fn always_fail(_ctx: ActivityContext) -> Result<(), ActivityError> { + Err(anyhow::anyhow!("Oh no I failed!").into()) + } +} diff --git a/crates/sdk-core/tests/common/mod.rs b/crates/sdk-core/tests/common/mod.rs index 251eb83c1..fd6fc78e0 100644 --- a/crates/sdk-core/tests/common/mod.rs +++ b/crates/sdk-core/tests/common/mod.rs @@ -1,6 +1,7 @@ //! Common integration testing utilities //! These utilities are specific to integration tests and depend on the full temporal-client stack. +pub(crate) mod activity_functions; pub(crate) mod fake_grpc_server; pub(crate) mod http_proxy; pub(crate) mod workflows; @@ -51,7 +52,8 @@ use temporalio_common::{ worker::{WorkerDeploymentOptions, WorkerDeploymentVersion, WorkerTaskTypes}, }; use temporalio_sdk::{ - IntoActivityFunc, Worker, WorkerOptions, WorkflowFunction, + Worker, WorkerOptions, WorkflowFunction, + activities::{ActivityImplementer, HasOnlyStaticMethods}, interceptors::{ FailOnNondeterminismInterceptor, InterceptorWithNext, ReturnWorkflowExitValueInterceptor, WorkerInterceptor, @@ -524,12 +526,19 @@ impl TestWorker { self.inner.register_wf(workflow_type, wf_function) } - pub(crate) fn register_activity( + pub(crate) fn register_activities_static(&mut self) -> &mut Self + where + AI: ActivityImplementer + HasOnlyStaticMethods, + { + self.inner.register_activities_static::(); + self + } + pub(crate) fn register_activities( &mut self, - activity_type: impl Into, - act_function: impl IntoActivityFunc, - ) { - self.inner.register_activity(activity_type, act_function) + instance: AI, + ) -> &mut Self { + self.inner.register_activities::(instance); + self } /// Create a handle that can be used to submit workflows. Useful when workflows need to be diff --git a/crates/sdk-core/tests/common/workflows.rs b/crates/sdk-core/tests/common/workflows.rs index f1c42408a..7f50bea37 100644 --- a/crates/sdk-core/tests/common/workflows.rs +++ b/crates/sdk-core/tests/common/workflows.rs @@ -1,31 +1,31 @@ +use crate::common::activity_functions::std_activities; use std::time::Duration; -use temporalio_common::{ - prost_dur, - protos::{coresdk::AsJsonPayloadExt, temporal::api::common::v1::RetryPolicy}, -}; +use temporalio_common::{prost_dur, protos::temporal::api::common::v1::RetryPolicy}; use temporalio_sdk::{ActivityOptions, LocalActivityOptions, WfContext, WorkflowResult}; pub(crate) async fn la_problem_workflow(ctx: WfContext) -> WorkflowResult<()> { - ctx.local_activity(LocalActivityOptions { - activity_type: "delay".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), - retry_policy: RetryPolicy { - initial_interval: Some(prost_dur!(from_micros(15))), - backoff_coefficient: 1_000., - maximum_interval: Some(prost_dur!(from_millis(1500))), - maximum_attempts: 4, - non_retryable_error_types: vec![], + ctx.local_activity::( + Duration::from_secs(15), + LocalActivityOptions { + retry_policy: RetryPolicy { + initial_interval: Some(prost_dur!(from_micros(15))), + backoff_coefficient: 1_000., + maximum_interval: Some(prost_dur!(from_millis(1500))), + maximum_attempts: 4, + non_retryable_error_types: vec![], + }, + timer_backoff_threshold: Some(Duration::from_secs(1)), + ..Default::default() }, - timer_backoff_threshold: Some(Duration::from_secs(1)), - ..Default::default() - }) + )? .await; - ctx.activity(ActivityOptions { - activity_type: "delay".to_string(), - start_to_close_timeout: Some(Duration::from_secs(20)), - input: "hi!".as_json_payload().expect("serializes fine"), - ..Default::default() - }) + ctx.activity::( + Duration::from_secs(15), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(20)), + ..Default::default() + }, + )? .await; Ok(().into()) } diff --git a/crates/sdk-core/tests/heavy_tests.rs b/crates/sdk-core/tests/heavy_tests.rs index 177fdba2e..b31125656 100644 --- a/crates/sdk-core/tests/heavy_tests.rs +++ b/crates/sdk-core/tests/heavy_tests.rs @@ -7,7 +7,10 @@ mod fuzzy_workflow; use crate::common::get_integ_runtime_options; use common::{ - CoreWfStarter, init_integ_telem, prom_metrics, rand_6_chars, workflows::la_problem_workflow, + CoreWfStarter, + activity_functions::{StdActivities, std_activities}, + init_integ_telem, prom_metrics, rand_6_chars, + workflows::la_problem_workflow, }; use futures_util::{ StreamExt, @@ -24,6 +27,7 @@ use std::{ use temporalio_client::{ GetWorkflowResultOptions, WfClientExt, WorkflowClientTrait, WorkflowOptions, }; +use temporalio_macros::activities; use temporalio_common::{ protos::{ @@ -32,7 +36,10 @@ use temporalio_common::{ }, worker::WorkerTaskTypes, }; -use temporalio_sdk::{ActivityOptions, WfContext, WorkflowResult, activities::ActivityContext}; +use temporalio_sdk::{ + ActivityOptions, WfContext, WorkflowResult, + activities::{ActivityContext, ActivityError}, +}; use temporalio_sdk_core::{CoreRuntime, PollerBehavior, ResourceBasedTuner, ResourceSlotOptions}; #[tokio::test] @@ -44,6 +51,9 @@ async fn activity_load() { starter.worker_config.max_cached_workflows = CONCURRENCY; starter.worker_config.activity_task_poller_behavior = PollerBehavior::SimpleMaximum(10); starter.worker_config.max_outstanding_activities = Some(CONCURRENCY); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; let activity_id = "act-1"; @@ -52,21 +62,26 @@ async fn activity_load() { let wf_fn = move |ctx: WfContext| { let task_queue = task_queue.clone(); - let payload = "yo".as_json_payload().unwrap(); + let input_str = "yo".to_string(); async move { - let activity = ActivityOptions { - activity_id: Some(activity_id.to_string()), - activity_type: "test_activity".to_string(), - input: payload.clone(), - task_queue, - schedule_to_start_timeout: Some(activity_timeout), - start_to_close_timeout: Some(activity_timeout), - schedule_to_close_timeout: Some(activity_timeout), - heartbeat_timeout: Some(activity_timeout), - cancellation_type: ActivityCancellationType::TryCancel, - ..Default::default() - }; - let res = ctx.activity(activity).await.unwrap_ok_payload(); + let res = ctx + .activity::( + input_str.clone(), + ActivityOptions { + activity_id: Some(activity_id.to_string()), + task_queue, + schedule_to_start_timeout: Some(activity_timeout), + start_to_close_timeout: Some(activity_timeout), + schedule_to_close_timeout: Some(activity_timeout), + heartbeat_timeout: Some(activity_timeout), + cancellation_type: ActivityCancellationType::TryCancel, + ..Default::default() + }, + ) + .unwrap() + .await + .unwrap_ok_payload(); + let payload = input_str.as_json_payload().unwrap(); assert_eq!(res.data, payload.data); Ok(().into()) } @@ -75,10 +90,6 @@ async fn activity_load() { let starting = Instant::now(); let wf_type = "activity_load"; worker.register_wf(wf_type.to_owned(), wf_fn); - worker.register_activity( - "test_activity", - |_ctx: ActivityContext, echo: String| async move { Ok(echo) }, - ); join_all((0..CONCURRENCY).map(|i| { let worker = &worker; let wf_id = format!("activity_load_{i}"); @@ -103,6 +114,25 @@ async fn activity_load() { dbg!(running.elapsed()); } +struct ChunkyActivities {} +#[activities] +impl ChunkyActivities { + #[activity] + async fn chunky_echo(_ctx: ActivityContext, echo: String) -> Result { + tokio::task::spawn_blocking(move || { + // Allocate a gig and then do some CPU stuff on it + let mut mem = vec![0_u8; 1000 * 1024 * 1024]; + for _ in 1..10 { + for i in 0..mem.len() { + mem[i] &= mem[mem.len() - 1 - i] + } + } + Ok(echo) + }) + .await? + } +} + #[tokio::test] async fn chunky_activities_resource_based() { const WORKFLOWS: usize = 100; @@ -123,22 +153,30 @@ async fn chunky_activities_resource_based() { )) .with_activity_slots_options(ResourceSlotOptions::new(5, 1000, Duration::from_millis(50))); starter.worker_config.tuner = Some(Arc::new(tuner)); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; let activity_id = "act-1"; let activity_timeout = Duration::from_secs(30); let wf_fn = move |ctx: WfContext| { - let payload = "yo".as_json_payload().unwrap(); + let input_str = "yo".to_string(); async move { - let activity = ActivityOptions { - activity_id: Some(activity_id.to_string()), - activity_type: "test_activity".to_string(), - input: payload.clone(), - start_to_close_timeout: Some(activity_timeout), - ..Default::default() - }; - let res = ctx.activity(activity).await.unwrap_ok_payload(); + let res = ctx + .activity::( + input_str.clone(), + ActivityOptions { + activity_id: Some(activity_id.to_string()), + start_to_close_timeout: Some(activity_timeout), + ..Default::default() + }, + ) + .unwrap() + .await + .unwrap_ok_payload(); + let payload = input_str.as_json_payload().unwrap(); assert_eq!(res.data, payload.data); Ok(().into()) } @@ -147,22 +185,6 @@ async fn chunky_activities_resource_based() { let starting = Instant::now(); let wf_type = "chunky_activity_wf"; worker.register_wf(wf_type.to_owned(), wf_fn); - worker.register_activity( - "test_activity", - |_ctx: ActivityContext, echo: String| async move { - tokio::task::spawn_blocking(move || { - // Allocate a gig and then do some CPU stuff on it - let mut mem = vec![0_u8; 1000 * 1024 * 1024]; - for _ in 1..10 { - for i in 0..mem.len() { - mem[i] &= mem[mem.len() - 1 - i] - } - } - Ok(echo) - }) - .await? - }, - ); join_all((0..WORKFLOWS).map(|i| { let worker = &worker; let wf_id = format!("chunk_activity_{i}"); @@ -206,6 +228,9 @@ async fn workflow_load() { starter.worker_config.max_cached_workflows = 200; starter.worker_config.activity_task_poller_behavior = PollerBehavior::SimpleMaximum(10); starter.worker_config.max_outstanding_activities = Some(100); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let sigchan = ctx.make_signal_channel(SIGNAME).map(Ok); @@ -213,12 +238,14 @@ async fn workflow_load() { let real_stuff = async move { for _ in 0..5 { - ctx.activity(ActivityOptions { - activity_type: "echo_activity".to_string(), - start_to_close_timeout: Some(Duration::from_secs(5)), - input: "hi!".as_json_payload().expect("serializes fine"), - ..Default::default() - }) + ctx.activity::( + "hi!".to_string(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(5)), + ..Default::default() + }, + ) + .unwrap() .await; ctx.timer(Duration::from_secs(1)).await; } @@ -230,10 +257,6 @@ async fn workflow_load() { Ok(().into()) }); - worker.register_activity( - "echo_activity", - |_ctx: ActivityContext, echo_me: String| async move { Ok(echo_me) }, - ); let client = starter.get_client().await; let mut workflow_handles = vec![]; @@ -284,13 +307,12 @@ async fn evict_while_la_running_no_interference() { // Though it doesn't make sense to set wft higher than cached workflows, leaving this commented // introduces more instability that can be useful in the test. // starter.max_wft(20); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), la_problem_workflow); - worker.register_activity("delay", |_: ActivityContext, _: String| async { - tokio::time::sleep(Duration::from_secs(15)).await; - Ok(()) - }); let client = starter.get_client().await; let subfs = FuturesUnordered::new(); @@ -381,6 +403,18 @@ async fn can_paginate_long_history() { worker.run_until_done().await.unwrap(); } +struct JitteryActivities {} +#[activities] +impl JitteryActivities { + #[activity] + async fn jittery_echo(_ctx: ActivityContext, echo: String) -> Result { + // Add some jitter to completions + let rand_millis = rand::rng().random_range(0..500); + tokio::time::sleep(Duration::from_millis(rand_millis)).await; + Ok(echo) + } +} + #[tokio::test] async fn poller_autoscaling_basic_loadtest() { const SIGNAME: &str = "signame"; @@ -400,6 +434,9 @@ async fn poller_autoscaling_basic_loadtest() { maximum: 200, initial: 5, }; + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; let shutdown_handle = worker.inner_mut().shutdown_handle(); worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { @@ -408,12 +445,14 @@ async fn poller_autoscaling_basic_loadtest() { let real_stuff = async move { for _ in 0..5 { - ctx.activity(ActivityOptions { - activity_type: "echo".to_string(), - start_to_close_timeout: Some(Duration::from_secs(5)), - input: "hi!".as_json_payload().expect("serializes fine"), - ..Default::default() - }) + ctx.activity::( + "hi!".to_string(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(5)), + ..Default::default() + }, + ) + .unwrap() .await; } }; @@ -424,12 +463,6 @@ async fn poller_autoscaling_basic_loadtest() { Ok(().into()) }); - worker.register_activity("echo", |_: ActivityContext, echo: String| async move { - // Add some jitter to completions - let rand_millis = rand::rng().random_range(0..500); - tokio::time::sleep(Duration::from_millis(rand_millis)).await; - Ok(echo) - }); let client = starter.get_client().await; let mut workflow_handles = vec![]; diff --git a/crates/sdk-core/tests/heavy_tests/fuzzy_workflow.rs b/crates/sdk-core/tests/heavy_tests/fuzzy_workflow.rs index 61b916536..3f98b5c40 100644 --- a/crates/sdk-core/tests/heavy_tests/fuzzy_workflow.rs +++ b/crates/sdk-core/tests/heavy_tests/fuzzy_workflow.rs @@ -1,13 +1,13 @@ -use crate::common::CoreWfStarter; +use crate::common::{ + CoreWfStarter, + activity_functions::{StdActivities, std_activities}, +}; use futures_util::{FutureExt, StreamExt, sink, stream::FuturesUnordered}; use rand::{Rng, SeedableRng, prelude::Distribution, rngs::SmallRng}; use std::{future, time::Duration}; use temporalio_client::{WfClientExt, WorkflowClientTrait, WorkflowOptions}; use temporalio_common::protos::coresdk::{AsJsonPayloadExt, FromJsonPayloadExt, IntoPayloadsExt}; -use temporalio_sdk::{ - ActivityOptions, LocalActivityOptions, WfContext, WorkflowResult, - activities::{ActivityContext, ActivityError}, -}; +use temporalio_sdk::{ActivityOptions, LocalActivityOptions, WfContext, WorkflowResult}; use tokio_util::sync::CancellationToken; const FUZZY_SIG: &str = "fuzzy_sig"; @@ -31,10 +31,6 @@ impl Distribution for FuzzyWfActionSampler { } } -async fn echo(_ctx: ActivityContext, echo_me: String) -> Result { - Ok(echo_me) -} - async fn fuzzy_wf_def(ctx: WfContext) -> WorkflowResult<()> { let sigchan = ctx .make_signal_channel(FUZZY_SIG) @@ -46,21 +42,25 @@ async fn fuzzy_wf_def(ctx: WfContext) -> WorkflowResult<()> { .take_until(done.cancelled()) .for_each_concurrent(None, |action| match action { FuzzyWfAction::DoAct => ctx - .activity(ActivityOptions { - activity_type: "echo_activity".to_string(), - start_to_close_timeout: Some(Duration::from_secs(5)), - input: "hi!".as_json_payload().expect("serializes fine"), - ..Default::default() - }) + .activity::( + "hi!".to_string(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(5)), + ..Default::default() + }, + ) + .unwrap() .map(|_| ()) .boxed(), FuzzyWfAction::DoLocalAct => ctx - .local_activity(LocalActivityOptions { - activity_type: "echo_activity".to_string(), - start_to_close_timeout: Some(Duration::from_secs(5)), - input: "hi!".as_json_payload().expect("serializes fine"), - ..Default::default() - }) + .local_activity::( + "hi!".to_string(), + LocalActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(5)), + ..Default::default() + }, + ) + .unwrap() .map(|_| ()) .boxed(), FuzzyWfAction::Shutdown => { @@ -83,7 +83,11 @@ async fn fuzzy_workflow() { starter.worker_config.max_outstanding_activities = Some(25); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), fuzzy_wf_def); - worker.register_activity("echo_activity", echo); + + starter + .sdk_config + .register_activities_static::(); + let client = starter.get_client().await; let mut workflow_handles = vec![]; diff --git a/crates/sdk-core/tests/integ_tests/activity_functions.rs b/crates/sdk-core/tests/integ_tests/activity_functions.rs deleted file mode 100644 index 4aeb0394a..000000000 --- a/crates/sdk-core/tests/integ_tests/activity_functions.rs +++ /dev/null @@ -1,16 +0,0 @@ -use temporalio_macros::activities; -use temporalio_sdk::activities::{ActivityContext, ActivityError}; - -pub(crate) async fn echo(_ctx: ActivityContext, e: String) -> Result { - Ok(e) -} - -pub(crate) struct StdActivities {} - -#[activities] -impl StdActivities { - #[activity] - pub(crate) async fn echo(_ctx: ActivityContext, e: String) -> Result { - Ok(e) - } -} diff --git a/crates/sdk-core/tests/integ_tests/heartbeat_tests.rs b/crates/sdk-core/tests/integ_tests/heartbeat_tests.rs index 9785cfda0..3486ca811 100644 --- a/crates/sdk-core/tests/integ_tests/heartbeat_tests.rs +++ b/crates/sdk-core/tests/integ_tests/heartbeat_tests.rs @@ -7,7 +7,7 @@ use temporalio_common::{ protos::{ DEFAULT_ACTIVITY_TYPE, coresdk::{ - ActivityHeartbeat, ActivityTaskCompletion, AsJsonPayloadExt, IntoCompletion, + ActivityHeartbeat, ActivityTaskCompletion, IntoCompletion, activity_result::{ self, ActivityExecutionResult, ActivityResolution, activity_resolution as act_res, }, @@ -25,7 +25,11 @@ use temporalio_common::{ test_utils::schedule_activity_cmd, }, }; -use temporalio_sdk::{ActivityOptions, WfContext, activities::ActivityContext}; +use temporalio_macros::activities; +use temporalio_sdk::{ + ActivityOptions, WfContext, + activities::{ActivityContext, ActivityError}, +}; use temporalio_sdk_core::test_help::{WorkerTestHelpers, drain_pollers_and_shutdown}; use tokio::time::sleep; @@ -179,32 +183,44 @@ async fn many_act_fails_with_heartbeats() { drain_pollers_and_shutdown(&core).await; } +pub(crate) struct SlowEchoActivities {} +#[activities] +impl SlowEchoActivities { + #[activity] + async fn echo_activity( + _ctx: ActivityContext, + echo_me: String, + ) -> Result { + sleep(Duration::from_secs(4)).await; + Ok(echo_me) + } +} + #[tokio::test] async fn activity_doesnt_heartbeat_hits_timeout_then_completes() { let wf_name = "activity_doesnt_heartbeat_hits_timeout_then_completes"; let mut starter = CoreWfStarter::new(wf_name); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; let client = starter.get_client().await; - worker.register_activity( - "echo_activity", - |_ctx: ActivityContext, echo_me: String| async move { - sleep(Duration::from_secs(4)).await; - Ok(echo_me) - }, - ); + worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let res = ctx - .activity(ActivityOptions { - activity_type: "echo_activity".to_string(), - input: "hi!".as_json_payload().expect("serializes fine"), - start_to_close_timeout: Some(Duration::from_secs(10)), - heartbeat_timeout: Some(Duration::from_secs(2)), - retry_policy: Some(RetryPolicy { - maximum_attempts: 1, + .activity::( + "hi!".to_string(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(10)), + heartbeat_timeout: Some(Duration::from_secs(2)), + retry_policy: Some(RetryPolicy { + maximum_attempts: 1, + ..Default::default() + }), ..Default::default() - }), - ..Default::default() - }) + }, + ) + .unwrap() .await; assert_eq!(res.timed_out(), Some(TimeoutType::Heartbeat)); Ok(().into()) diff --git a/crates/sdk-core/tests/integ_tests/metrics_tests.rs b/crates/sdk-core/tests/integ_tests/metrics_tests.rs index 1874e2d38..75872727b 100644 --- a/crates/sdk-core/tests/integ_tests/metrics_tests.rs +++ b/crates/sdk-core/tests/integ_tests/metrics_tests.rs @@ -23,7 +23,7 @@ use temporalio_common::{ prost_dur, protos::{ coresdk::{ - ActivityTaskCompletion, AsJsonPayloadExt, + ActivityTaskCompletion, activity_result::ActivityExecutionResult, nexus::{NexusTaskCompletion, nexus_task, nexus_task_completion}, workflow_activation::{WorkflowActivationJob, workflow_activation_job}, @@ -57,6 +57,7 @@ use temporalio_common::{ }, worker::WorkerTaskTypes, }; +use temporalio_macros::activities; use temporalio_sdk::{ ActivityOptions, CancellableFuture, LocalActivityOptions, NexusOperationOptions, WfContext, activities::{ActivityContext, ActivityError}, @@ -764,6 +765,22 @@ async fn docker_metrics_with_prometheus( } } +struct PassFailActivities {} +#[activities] +impl PassFailActivities { + #[activity(name = "pass_fail_act")] + async fn pass_fail_act(ctx: ActivityContext, i: String) -> Result { + match i.as_str() { + "pass" => Ok("pass".to_string()), + "cancel" => { + ctx.cancelled().await; + Err(ActivityError::cancelled()) + } + _ => Err(anyhow!("fail").into()), + } + } +} + #[tokio::test] async fn activity_metrics() { let (telemopts, addr, _aborter) = prom_metrics(None); @@ -771,69 +788,66 @@ async fn activity_metrics() { let wf_name = "activity_metrics"; let mut starter = CoreWfStarter::new_with_runtime(wf_name, rt); starter.worker_config.graceful_shutdown_period = Some(Duration::from_secs(1)); + starter + .sdk_config + .register_activities_static::(); let task_queue = starter.get_task_queue().to_owned(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_string(), |ctx: WfContext| async move { - let normal_act_pass = ctx.activity(ActivityOptions { - activity_type: "pass_fail_act".to_string(), - input: "pass".as_json_payload().expect("serializes fine"), - start_to_close_timeout: Some(Duration::from_secs(1)), - ..Default::default() - }); - let normal_act_fail = ctx.activity(ActivityOptions { - activity_type: "pass_fail_act".to_string(), - input: "fail".as_json_payload().expect("serializes fine"), - start_to_close_timeout: Some(Duration::from_secs(1)), - retry_policy: Some(RetryPolicy { - maximum_attempts: 1, - ..Default::default() - }), - ..Default::default() - }); + let normal_act_pass = ctx + .activity::( + "pass".to_string(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(1)), + ..Default::default() + }, + ) + .unwrap(); + let normal_act_fail = ctx + .activity::( + "fail".to_string(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(1)), + retry_policy: Some(RetryPolicy { + maximum_attempts: 1, + ..Default::default() + }), + ..Default::default() + }, + ) + .unwrap(); join!(normal_act_pass, normal_act_fail); - let local_act_pass = ctx.local_activity(LocalActivityOptions { - activity_type: "pass_fail_act".to_string(), - input: "pass".as_json_payload().expect("serializes fine"), - ..Default::default() - }); - let local_act_fail = ctx.local_activity(LocalActivityOptions { - activity_type: "pass_fail_act".to_string(), - input: "fail".as_json_payload().expect("serializes fine"), - retry_policy: RetryPolicy { - maximum_attempts: 1, + let local_act_pass = ctx.local_activity::( + "pass".to_string(), + LocalActivityOptions::default(), + )?; + let local_act_fail = ctx.local_activity::( + "fail".to_string(), + LocalActivityOptions { + retry_policy: RetryPolicy { + maximum_attempts: 1, + ..Default::default() + }, ..Default::default() }, - ..Default::default() - }); - let local_act_cancel = ctx.local_activity(LocalActivityOptions { - activity_type: "pass_fail_act".to_string(), - input: "cancel".as_json_payload().expect("serializes fine"), - retry_policy: RetryPolicy { - maximum_attempts: 1, + )?; + let local_act_cancel = ctx.local_activity::( + "cancel".to_string(), + LocalActivityOptions { + retry_policy: RetryPolicy { + maximum_attempts: 1, + ..Default::default() + }, ..Default::default() }, - ..Default::default() - }); + )?; join!(local_act_pass, local_act_fail); // TODO: Currently takes a WFT b/c of https://github.com/temporalio/sdk-core/issues/856 local_act_cancel.cancel(&ctx); local_act_cancel.await; Ok(().into()) }); - worker.register_activity( - "pass_fail_act", - |ctx: ActivityContext, i: String| async move { - match i.as_str() { - "pass" => Ok("pass"), - "cancel" => { - ctx.cancelled().await; - Err(ActivityError::cancelled()) - } - _ => Err(anyhow!("fail").into()), - } - }, - ); worker .submit_wf( diff --git a/crates/sdk-core/tests/integ_tests/polling_tests.rs b/crates/sdk-core/tests/integ_tests/polling_tests.rs index 9c0b8d701..6c6859417 100644 --- a/crates/sdk-core/tests/integ_tests/polling_tests.rs +++ b/crates/sdk-core/tests/integ_tests/polling_tests.rs @@ -1,9 +1,8 @@ -use crate::{ - common::{ - CoreWfStarter, INTEG_CLIENT_NAME, INTEG_CLIENT_VERSION, get_integ_client, - init_core_and_create_wf, init_integ_telem, integ_dev_server_config, integ_worker_config, - }, - integ_tests::activity_functions::echo, +use crate::common::{ + CoreWfStarter, INTEG_CLIENT_NAME, INTEG_CLIENT_VERSION, + activity_functions::{StdActivities, std_activities}, + get_integ_client, init_core_and_create_wf, init_integ_telem, integ_dev_server_config, + integ_worker_config, }; use assert_matches::assert_matches; use futures_util::{FutureExt, StreamExt, future::join_all}; @@ -22,7 +21,7 @@ use temporalio_common::{ prost_dur, protos::{ coresdk::{ - AsJsonPayloadExt, IntoCompletion, + IntoCompletion, activity_task::activity_task as act_task, workflow_activation::{FireTimer, WorkflowActivationJob, workflow_activation_job}, workflow_commands::{ActivityCancellationType, RequestCancelActivity, StartTimer}, @@ -261,20 +260,25 @@ async fn small_workflow_slots_and_pollers(#[values(false, true)] use_autoscaling starter.worker_config.max_outstanding_local_activities = Some(1_usize); starter.worker_config.activity_task_poller_behavior = PollerBehavior::SimpleMaximum(1); starter.worker_config.max_outstanding_activities = Some(1_usize); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; + worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { for _ in 0..3 { - ctx.activity(ActivityOptions { - activity_type: "echo_activity".to_string(), - start_to_close_timeout: Some(Duration::from_secs(5)), - input: "hi!".as_json_payload().expect("serializes fine"), - ..Default::default() - }) + ctx.activity::( + "hi!".to_string(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(5)), + ..Default::default() + }, + ) + .unwrap() .await; } Ok(().into()) }); - worker.register_activity("echo_activity", echo); worker .submit_wf( starter.get_task_queue(), diff --git a/crates/sdk-core/tests/integ_tests/update_tests.rs b/crates/sdk-core/tests/integ_tests/update_tests.rs index 2344d7e41..c7be7fb37 100644 --- a/crates/sdk-core/tests/integ_tests/update_tests.rs +++ b/crates/sdk-core/tests/integ_tests/update_tests.rs @@ -1,5 +1,7 @@ use crate::common::{ - CoreWfStarter, WorkflowHandleExt, init_core_and_create_wf, init_core_replay_preloaded, + CoreWfStarter, WorkflowHandleExt, + activity_functions::{StdActivities, std_activities}, + init_core_and_create_wf, init_core_replay_preloaded, }; use anyhow::anyhow; use assert_matches::assert_matches; @@ -36,8 +38,10 @@ use temporalio_common::{ }, worker::WorkerTaskTypes, }; +use temporalio_macros::activities; use temporalio_sdk::{ - ActivityOptions, LocalActivityOptions, UpdateContext, WfContext, activities::ActivityContext, + ActivityOptions, LocalActivityOptions, UpdateContext, WfContext, + activities::{ActivityContext, ActivityError}, }; use temporalio_sdk_core::{ Worker, @@ -638,19 +642,22 @@ async fn update_with_local_acts() { let mut starter = CoreWfStarter::new(wf_name); // Short task timeout to get activities to heartbeat without taking ages starter.workflow_options.task_timeout = Some(Duration::from_secs(1)); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; let client = starter.get_client().await; + worker.register_wf(wf_name.to_owned(), move |ctx: WfContext| async move { ctx.update_handler( "update", |_: &_, _: ()| Ok(()), move |ctx: UpdateContext, _: ()| async move { ctx.wf_ctx - .local_activity(LocalActivityOptions { - activity_type: "echo_activity".to_string(), - input: "hi!".as_json_payload().expect("serializes fine"), - ..Default::default() - }) + .local_activity::( + Duration::from_secs(3), + LocalActivityOptions::default(), + )? .await; Ok("hi") }, @@ -659,14 +666,6 @@ async fn update_with_local_acts() { sig.next().await; Ok(().into()) }); - worker.register_activity( - "echo_activity", - |_ctx: ActivityContext, echo_me: String| async move { - // Sleep so we'll heartbeat - tokio::time::sleep(Duration::from_secs(3)).await; - Ok(echo_me) - }, - ); let handle = starter.start_with_worker(wf_name, &mut worker).await; let wf_id = starter.get_task_queue().to_string(); @@ -955,27 +954,46 @@ async fn task_failure_after_update() { .unwrap(); } +static BARR: LazyLock = LazyLock::new(|| Barrier::new(2)); +static ACT_RAN: AtomicBool = AtomicBool::new(false); +struct BlockingActivities {} +#[activities] +impl BlockingActivities { + #[activity] + async fn blocks(_ctx: ActivityContext, echo_me: String) -> Result { + BARR.wait().await; + if !ACT_RAN.fetch_or(true, Ordering::Relaxed) { + // On first run fail the task so we'll get retried on the new worker + return Err(anyhow!("Fail first time").into()); + } + Ok(echo_me) + } +} + #[tokio::test] async fn worker_restarted_in_middle_of_update() { let wf_name = "worker_restarted_in_middle_of_update"; let mut starter = CoreWfStarter::new(wf_name); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; let client = starter.get_client().await; - static BARR: LazyLock = LazyLock::new(|| Barrier::new(2)); - static ACT_RAN: AtomicBool = AtomicBool::new(false); worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { ctx.update_handler( "update", |_: &_, _: ()| Ok(()), move |ctx: UpdateContext, _: ()| async move { ctx.wf_ctx - .activity(ActivityOptions { - activity_type: "blocks".to_string(), - input: "hi!".as_json_payload().expect("serializes fine"), - start_to_close_timeout: Some(Duration::from_secs(2)), - ..Default::default() - }) + .activity::( + "hi!".to_string(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(2)), + ..Default::default() + }, + ) + .unwrap() .await; Ok(()) }, @@ -984,17 +1002,6 @@ async fn worker_restarted_in_middle_of_update() { sig.next().await; Ok(().into()) }); - worker.register_activity( - "blocks", - |_ctx: ActivityContext, echo_me: String| async move { - BARR.wait().await; - if !ACT_RAN.fetch_or(true, Ordering::Relaxed) { - // On first run fail the task so we'll get retried on the new worker - return Err(anyhow!("Fail first time").into()); - } - Ok(echo_me) - }, - ); let handle = starter.start_with_worker(wf_name, &mut worker).await; @@ -1056,8 +1063,13 @@ async fn worker_restarted_in_middle_of_update() { #[tokio::test] async fn update_after_empty_wft() { + use crate::common::activity_functions::{StdActivities, std_activities}; + let wf_name = "update_after_empty_wft"; let mut starter = CoreWfStarter::new(wf_name); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; let client = starter.get_client().await; @@ -1071,12 +1083,14 @@ async fn update_after_empty_wft() { return Ok(()); } ctx.wf_ctx - .activity(ActivityOptions { - activity_type: "echo".to_string(), - input: "hi!".as_json_payload().expect("serializes fine"), - start_to_close_timeout: Some(Duration::from_secs(2)), - ..Default::default() - }) + .activity::( + "hi!".to_string(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(2)), + ..Default::default() + }, + ) + .unwrap() .await; Ok(()) }, @@ -1085,12 +1099,14 @@ async fn update_after_empty_wft() { let sig_handle = async { sig.next().await; ACT_STARTED.store(true, Ordering::Release); - ctx.activity(ActivityOptions { - activity_type: "echo".to_string(), - input: "hi!".as_json_payload().expect("serializes fine"), - start_to_close_timeout: Some(Duration::from_secs(2)), - ..Default::default() - }) + ctx.activity::( + "hi!".to_string(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(2)), + ..Default::default() + }, + ) + .unwrap() .await; ACT_STARTED.store(false, Ordering::Release); }; @@ -1099,10 +1115,6 @@ async fn update_after_empty_wft() { }); Ok(().into()) }); - worker.register_activity( - "echo", - |_ctx: ActivityContext, echo_me: String| async move { Ok(echo_me) }, - ); let handle = starter.start_with_worker(wf_name, &mut worker).await; @@ -1145,8 +1157,13 @@ async fn update_after_empty_wft() { #[tokio::test] async fn update_lost_on_activity_mismatch() { + use crate::common::activity_functions::{StdActivities, std_activities}; + let wf_name = "update_lost_on_activity_mismatch"; let mut starter = CoreWfStarter::new(wf_name); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; let client = starter.get_client().await; @@ -1167,21 +1184,19 @@ async fn update_lost_on_activity_mismatch() { for _ in 1..=3 { let cr = can_run.clone(); ctx.wait_condition(|| cr.load(Ordering::Relaxed) > 0).await; - ctx.activity(ActivityOptions { - activity_type: "echo".to_string(), - input: "hi!".as_json_payload().expect("serializes fine"), - start_to_close_timeout: Some(Duration::from_secs(2)), - ..Default::default() - }) + ctx.activity::( + "hi!".to_string(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(2)), + ..Default::default() + }, + ) + .unwrap() .await; can_run.fetch_sub(1, Ordering::Release); } Ok(().into()) }); - worker.register_activity( - "echo", - |_ctx: ActivityContext, echo_me: String| async move { Ok(echo_me) }, - ); let core_worker = worker.core_worker(); let handle = starter.start_with_worker(wf_name, &mut worker).await; diff --git a/crates/sdk-core/tests/integ_tests/worker_heartbeat_tests.rs b/crates/sdk-core/tests/integ_tests/worker_heartbeat_tests.rs index 813afc9dc..00d11aae5 100644 --- a/crates/sdk-core/tests/integ_tests/worker_heartbeat_tests.rs +++ b/crates/sdk-core/tests/integ_tests/worker_heartbeat_tests.rs @@ -1,4 +1,8 @@ -use crate::common::{ANY_PORT, CoreWfStarter, eventually, get_integ_telem_options}; +use crate::common::{ + ANY_PORT, CoreWfStarter, + activity_functions::{StdActivities, std_activities}, + eventually, get_integ_telem_options, +}; use anyhow::anyhow; use crossbeam_utils::atomic::AtomicCell; use futures_util::StreamExt; @@ -31,7 +35,11 @@ use temporalio_common::{ build_otlp_metric_exporter, start_prometheus_metric_exporter, }, }; -use temporalio_sdk::{ActivityOptions, WfContext, activities::ActivityContext}; +use temporalio_macros::activities; +use temporalio_sdk::{ + ActivityOptions, WfContext, + activities::{ActivityContext, ActivityError}, +}; use temporalio_sdk_core::{ CoreRuntime, PollerBehavior, ResourceBasedTuner, ResourceSlotOptions, RuntimeOptions, }; @@ -86,6 +94,24 @@ async fn list_worker_heartbeats(client: &Client, query: impl Into) -> Ve .collect() } +struct NotifyActivities { + acts_started: Arc, + acts_done: Arc, +} +#[activities] +impl NotifyActivities { + #[activity] + async fn pass_fail_act( + self: Arc, + _ctx: ActivityContext, + i: String, + ) -> Result { + self.acts_started.notify_one(); + self.acts_done.notified().await; + Ok(i) + } +} + // Tests that rely on Prometheus running in a docker container need to start // with `docker_` and set the `DOCKER_PROMETHEUS_RUNNING` env variable to run #[rstest::rstest] @@ -142,35 +168,28 @@ async fn docker_worker_heartbeat_basic(#[values("otel", "prom", "no_metrics")] b ] .into_iter() .collect(); + let acts_started = Arc::new(Notify::new()); + let acts_done = Arc::new(Notify::new()); + starter.sdk_config.register_activities(NotifyActivities { + acts_started: acts_started.clone(), + acts_done: acts_done.clone(), + }); let mut worker = starter.worker().await; let worker_instance_key = worker.worker_instance_key(); worker.register_wf(wf_name.to_string(), |ctx: WfContext| async move { - ctx.activity(ActivityOptions { - activity_type: "pass_fail_act".to_string(), - input: "pass".as_json_payload().expect("serializes fine"), - start_to_close_timeout: Some(Duration::from_secs(5)), - ..Default::default() - }) + ctx.activity::( + "pass".to_string(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(5)), + ..Default::default() + }, + ) + .unwrap() .await; Ok(().into()) }); - let acts_started = Arc::new(Notify::new()); - let acts_done = Arc::new(Notify::new()); - - let acts_started_act = acts_started.clone(); - let acts_done_act = acts_done.clone(); - worker.register_activity("pass_fail_act", move |_ctx: ActivityContext, i: String| { - let acts_started = acts_started_act.clone(); - let acts_done = acts_done_act.clone(); - async move { - acts_started.notify_one(); - acts_done.notified().await; - Ok(i) - } - }); - starter .start_with_worker(wf_name.clone(), &mut worker) .await; @@ -297,24 +316,25 @@ async fn docker_worker_heartbeat_tuner() { starter.worker_config.max_outstanding_activities = None; starter.worker_config.max_outstanding_nexus_tasks = None; starter.worker_config.tuner = Some(Arc::new(tuner)); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; let worker_instance_key = worker.worker_instance_key(); // Run a workflow worker.register_wf(wf_name.to_string(), |ctx: WfContext| async move { - ctx.activity(ActivityOptions { - activity_type: "pass_fail_act".to_string(), - input: "pass".as_json_payload().expect("serializes fine"), - start_to_close_timeout: Some(Duration::from_secs(1)), - ..Default::default() - }) + ctx.activity::( + "pass".to_string(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(1)), + ..Default::default() + }, + ) + .unwrap() .await; Ok(().into()) }); - worker.register_activity( - "pass_fail_act", - |_ctx: ActivityContext, i: String| async move { Ok(i) }, - ); starter.start_with_worker(wf_name, &mut worker).await; worker.run_until_done().await.unwrap(); @@ -535,12 +555,42 @@ fn after_shutdown_checks( ); } +static HISTORY_WF1_ACTIVITY_STARTED: Notify = Notify::const_new(); +static HISTORY_WF1_ACTIVITY_FINISH: Notify = Notify::const_new(); +static HISTORY_WF2_ACTIVITY_STARTED: Notify = Notify::const_new(); +static HISTORY_WF2_ACTIVITY_FINISH: Notify = Notify::const_new(); +struct StickyCacheActivities; +#[activities] +impl StickyCacheActivities { + #[activity] + async fn sticky_cache_history_act( + _ctx: ActivityContext, + marker: String, + ) -> Result { + match marker.as_str() { + "wf1" => { + HISTORY_WF1_ACTIVITY_STARTED.notify_one(); + HISTORY_WF1_ACTIVITY_FINISH.notified().await; + } + "wf2" => { + HISTORY_WF2_ACTIVITY_STARTED.notify_one(); + HISTORY_WF2_ACTIVITY_FINISH.notified().await; + } + _ => {} + } + Ok(marker) + } +} + #[tokio::test] async fn worker_heartbeat_sticky_cache_miss() { let wf_name = "worker_heartbeat_cache_miss"; let mut starter = new_no_metrics_starter(wf_name); starter.worker_config.max_cached_workflows = 1_usize; starter.worker_config.max_outstanding_workflow_tasks = Some(2_usize); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; worker.fetch_results = false; @@ -551,11 +601,6 @@ async fn worker_heartbeat_sticky_cache_miss() { let client = starter.get_client().await; let client_for_orchestrator = client.clone(); - static HISTORY_WF1_ACTIVITY_STARTED: Notify = Notify::const_new(); - static HISTORY_WF1_ACTIVITY_FINISH: Notify = Notify::const_new(); - static HISTORY_WF2_ACTIVITY_STARTED: Notify = Notify::const_new(); - static HISTORY_WF2_ACTIVITY_FINISH: Notify = Notify::const_new(); - worker.register_wf(wf_name.to_string(), |ctx: WfContext| async move { let wf_marker = ctx .get_args() @@ -563,33 +608,18 @@ async fn worker_heartbeat_sticky_cache_miss() { .and_then(|p| String::from_json_payload(p).ok()) .unwrap_or_else(|| "wf1".to_string()); - ctx.activity(ActivityOptions { - activity_type: "sticky_cache_history_act".to_string(), - input: wf_marker.clone().as_json_payload().expect("serialize"), - start_to_close_timeout: Some(Duration::from_secs(5)), - ..Default::default() - }) + ctx.activity::( + wf_marker.clone(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(5)), + ..Default::default() + }, + ) + .unwrap() .await; Ok(().into()) }); - worker.register_activity( - "sticky_cache_history_act", - |_ctx: ActivityContext, marker: String| async move { - match marker.as_str() { - "wf1" => { - HISTORY_WF1_ACTIVITY_STARTED.notify_one(); - HISTORY_WF1_ACTIVITY_FINISH.notified().await; - } - "wf2" => { - HISTORY_WF2_ACTIVITY_STARTED.notify_one(); - HISTORY_WF2_ACTIVITY_FINISH.notified().await; - } - _ => {} - } - Ok(marker) - }, - ); let wf1_id = format!("{wf_name}_wf1"); let wf2_id = format!("{wf_name}_wf2"); @@ -659,6 +689,9 @@ async fn worker_heartbeat_multiple_workers() { let mut starter = new_no_metrics_starter(wf_name); starter.worker_config.max_outstanding_workflow_tasks = Some(5_usize); starter.worker_config.max_cached_workflows = 5_usize; + starter + .sdk_config + .register_activities_static::(); let client = starter.get_client().await; let starting_hb_len = list_worker_heartbeats(&client, String::new()).await.len(); @@ -667,20 +700,12 @@ async fn worker_heartbeat_multiple_workers() { worker_a.register_wf(wf_name.to_string(), |_ctx: WfContext| async move { Ok(().into()) }); - worker_a.register_activity( - "failing_act", - |_ctx: ActivityContext, _: String| async move { Ok(()) }, - ); let mut starter_b = starter.clone_no_worker(); let mut worker_b = starter_b.worker().await; worker_b.register_wf(wf_name.to_string(), |_ctx: WfContext| async move { Ok(().into()) }); - worker_b.register_activity( - "failing_act", - |_ctx: ActivityContext, _: String| async move { Ok(()) }, - ); let worker_a_key = worker_a.worker_instance_key().to_string(); let worker_b_key = worker_b.worker_instance_key().to_string(); @@ -753,6 +778,24 @@ async fn worker_heartbeat_multiple_workers() { assert_eq!(describe_worker_b, filtered_b[0]); } +static ACT_COUNT: AtomicU64 = AtomicU64::new(0); +static WF_COUNT: AtomicU64 = AtomicU64::new(0); +static ACT_FAIL: Notify = Notify::const_new(); +static WF_FAIL: Notify = Notify::const_new(); +struct FailingActivities; +#[activities] +impl FailingActivities { + #[activity] + async fn failing_act(_ctx: ActivityContext, _: String) -> Result<(), ActivityError> { + if ACT_COUNT.load(Ordering::Relaxed) == 3 { + return Ok(()); + } + ACT_COUNT.fetch_add(1, Ordering::Relaxed); + ACT_FAIL.notify_one(); + Err(anyhow!("Expected error").into()) + } +} + #[tokio::test] async fn worker_heartbeat_failure_metrics() { const WORKFLOW_CONTINUE_SIGNAL: &str = "workflow-continue"; @@ -760,27 +803,29 @@ async fn worker_heartbeat_failure_metrics() { let wf_name = "worker_heartbeat_failure_metrics"; let mut starter = new_no_metrics_starter(wf_name); starter.worker_config.max_outstanding_activities = Some(5_usize); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; let worker_instance_key = worker.worker_instance_key(); - static ACT_COUNT: AtomicU64 = AtomicU64::new(0); - static WF_COUNT: AtomicU64 = AtomicU64::new(0); - static ACT_FAIL: Notify = Notify::const_new(); - static WF_FAIL: Notify = Notify::const_new(); + worker.register_wf(wf_name.to_string(), |ctx: WfContext| async move { let _ = ctx - .activity(ActivityOptions { - activity_type: "failing_act".to_string(), - input: "boom".as_json_payload().expect("serialize"), - start_to_close_timeout: Some(Duration::from_secs(5)), - retry_policy: Some(RetryPolicy { - initial_interval: Some(prost_dur!(from_millis(10))), - backoff_coefficient: 1.0, - maximum_attempts: 4, + .activity::( + "boom".to_string(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(5)), + retry_policy: Some(RetryPolicy { + initial_interval: Some(prost_dur!(from_millis(10))), + backoff_coefficient: 1.0, + maximum_attempts: 4, + ..Default::default() + }), ..Default::default() - }), - ..Default::default() - }) + }, + ) + .unwrap() .await; if WF_COUNT.load(Ordering::Relaxed) == 0 { @@ -796,18 +841,6 @@ async fn worker_heartbeat_failure_metrics() { Ok(().into()) }); - worker.register_activity( - "failing_act", - |_ctx: ActivityContext, _: String| async move { - if ACT_COUNT.load(Ordering::Relaxed) == 3 { - return Ok(()); - } - ACT_COUNT.fetch_add(1, Ordering::Relaxed); - ACT_FAIL.notify_one(); - Err(anyhow!("Expected error").into()) - }, - ); - let worker_key = worker_instance_key.to_string(); starter.workflow_options.retry_policy = Some(RetryPolicy { maximum_attempts: 2, @@ -944,25 +977,25 @@ async fn worker_heartbeat_no_runtime_heartbeat() { .unwrap(); let rt = CoreRuntime::new_assume_tokio(runtimeopts).unwrap(); let mut starter = CoreWfStarter::new_with_runtime(wf_name, rt); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; let worker_instance_key = worker.worker_instance_key(); worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { - ctx.activity(ActivityOptions { - activity_type: "pass_fail_act".to_string(), - input: "pass".as_json_payload().expect("serializes fine"), - start_to_close_timeout: Some(Duration::from_secs(1)), - ..Default::default() - }) + ctx.activity::( + "pass".to_string(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(1)), + ..Default::default() + }, + ) + .unwrap() .await; Ok(().into()) }); - worker.register_activity( - "pass_fail_act", - |_ctx: ActivityContext, i: String| async move { Ok(i) }, - ); - starter .start_with_worker(wf_name.to_owned(), &mut worker) .await; @@ -1006,25 +1039,25 @@ async fn worker_heartbeat_skip_client_worker_set_check() { let rt = CoreRuntime::new_assume_tokio(runtimeopts).unwrap(); let mut starter = CoreWfStarter::new_with_runtime(wf_name, rt); starter.worker_config.skip_client_worker_set_check = true; + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; let worker_instance_key = worker.worker_instance_key(); worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { - ctx.activity(ActivityOptions { - activity_type: "pass_fail_act".to_string(), - input: "pass".as_json_payload().expect("serializes fine"), - start_to_close_timeout: Some(Duration::from_secs(1)), - ..Default::default() - }) + ctx.activity::( + "pass".to_string(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(1)), + ..Default::default() + }, + ) + .unwrap() .await; Ok(().into()) }); - worker.register_activity( - "pass_fail_act", - |_ctx: ActivityContext, i: String| async move { Ok(i) }, - ); - starter .start_with_worker(wf_name.to_owned(), &mut worker) .await; diff --git a/crates/sdk-core/tests/integ_tests/worker_tests.rs b/crates/sdk-core/tests/integ_tests/worker_tests.rs index ba25b07c3..dd3e07660 100644 --- a/crates/sdk-core/tests/integ_tests/worker_tests.rs +++ b/crates/sdk-core/tests/integ_tests/worker_tests.rs @@ -1,7 +1,9 @@ use crate::{ common::{ - CoreWfStarter, fake_grpc_server::fake_server, get_integ_runtime_options, - get_integ_server_options, get_integ_telem_options, mock_sdk_cfg, + CoreWfStarter, + activity_functions::{StdActivities, std_activities}, + fake_grpc_server::fake_server, + get_integ_runtime_options, get_integ_server_options, get_integ_telem_options, mock_sdk_cfg, }, shared_tests, }; @@ -31,15 +33,17 @@ use temporalio_common::{ }, temporal::api::{ command::v1::command::Attributes, - common::v1::WorkerVersionStamp, + common::v1::{Payload, WorkerVersionStamp}, enums::v1::{ - EventType, WorkflowTaskFailedCause, WorkflowTaskFailedCause::GrpcMessageTooLarge, + EventType, + WorkflowTaskFailedCause::{self, GrpcMessageTooLarge}, }, failure::v1::Failure as InnerFailure, history::v1::{ - ActivityTaskScheduledEventAttributes, history_event, - history_event::Attributes::{ - self as EventAttributes, WorkflowTaskFailedEventAttributes, + ActivityTaskScheduledEventAttributes, + history_event::{ + self, + Attributes::{self as EventAttributes, WorkflowTaskFailedEventAttributes}, }, }, workflowservice::v1::{ @@ -363,15 +367,17 @@ async fn activity_tasks_from_completion_reserve_slots() { worker.register_wf(DEFAULT_WORKFLOW_TYPE, move |ctx: WfContext| { let complete_token = workflow_complete_token.clone(); async move { - ctx.activity(ActivityOptions { - activity_type: "act1".to_string(), - ..Default::default() - }) + ctx.activity_untyped( + "act1".to_string(), + Payload::default(), + ActivityOptions::default(), + ) .await; - ctx.activity(ActivityOptions { - activity_type: "act2".to_string(), - ..Default::default() - }) + ctx.activity_untyped( + "act2".to_string(), + Payload::default(), + ActivityOptions::default(), + ) .await; complete_token.cancel(); Ok(().into()) @@ -710,6 +716,9 @@ async fn test_custom_slot_supplier_simple() { starter.worker_config.max_outstanding_local_activities = None; starter.worker_config.max_outstanding_activities = None; starter.worker_config.max_outstanding_nexus_tasks = None; + starter + .sdk_config + .register_activities_static::(); let mut tb = TunerBuilder::default(); tb.workflow_slot_supplier(wf_supplier.clone()); @@ -719,26 +728,26 @@ async fn test_custom_slot_supplier_simple() { let mut worker = starter.worker().await; - worker.register_activity( - "SlotSupplierActivity", - |_: temporalio_sdk::activities::ActivityContext, _: ()| async move { Ok(()) }, - ); worker.register_wf( "SlotSupplierWorkflow".to_owned(), |ctx: WfContext| async move { let _result = ctx - .activity(ActivityOptions { - activity_type: "SlotSupplierActivity".to_string(), - start_to_close_timeout: Some(Duration::from_secs(10)), - ..Default::default() - }) + .activity::( + (), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(10)), + ..Default::default() + }, + )? .await; let _result = ctx - .local_activity(LocalActivityOptions { - activity_type: "SlotSupplierActivity".to_string(), - start_to_close_timeout: Some(Duration::from_secs(10)), - ..Default::default() - }) + .local_activity::( + (), + LocalActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(10)), + ..Default::default() + }, + )? .await; Ok(().into()) }, @@ -820,7 +829,7 @@ async fn test_custom_slot_supplier_simple() { slot_type: "activity", activity_type: Some(act_type), .. - } if act_type == "SlotSupplierActivity")) + } if act_type.contains("NoOp"))) ); assert!( local_activity_events @@ -829,7 +838,7 @@ async fn test_custom_slot_supplier_simple() { slot_type: "local_activity", activity_type: Some(act_type), .. - } if act_type == "SlotSupplierActivity")) + } if act_type.contains("NoOp"))) ); assert!(wf_events.iter().any(|e| matches!( e, diff --git a/crates/sdk-core/tests/integ_tests/worker_versioning_tests.rs b/crates/sdk-core/tests/integ_tests/worker_versioning_tests.rs index 8d33dc564..1decff1a4 100644 --- a/crates/sdk-core/tests/integ_tests/worker_versioning_tests.rs +++ b/crates/sdk-core/tests/integ_tests/worker_versioning_tests.rs @@ -1,13 +1,14 @@ -use crate::{ - common::{CoreWfStarter, eventually}, - integ_tests::activity_functions::echo, +use crate::common::{ + CoreWfStarter, + activity_functions::{StdActivities, std_activities}, + eventually, }; use std::time::Duration; use temporalio_client::{NamespacedClient, WorkflowOptions, WorkflowService}; use temporalio_common::{ protos::{ coresdk::{ - AsJsonPayloadExt, workflow_commands::CompleteWorkflowExecution, workflow_completion, + workflow_commands::CompleteWorkflowExecution, workflow_completion, workflow_completion::WorkflowActivationCompletion, }, temporal::api::{ @@ -157,19 +158,24 @@ async fn activity_has_deployment_stamp() { use_worker_versioning: true, default_versioning_behavior: VersioningBehavior::AutoUpgrade.into(), }); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; let client = starter.get_client().await; + worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { - ctx.activity(ActivityOptions { - activity_type: "echo_activity".to_string(), - start_to_close_timeout: Some(Duration::from_secs(5)), - input: "hi!".as_json_payload().expect("serializes fine"), - ..Default::default() - }) + ctx.activity::( + "hi!".to_string(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(5)), + ..Default::default() + }, + ) + .unwrap() .await; Ok(().into()) }); - worker.register_activity("echo_activity", echo); let submitter = worker.get_submitter_handle(); let shutdown_handle = worker.inner_mut().shutdown_handle(); diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests.rs b/crates/sdk-core/tests/integ_tests/workflow_tests.rs index d9cf243fa..41a899734 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests.rs @@ -20,10 +20,12 @@ mod upsert_search_attrs; use crate::{ common::{ - CoreWfStarter, get_integ_runtime_options, history_from_proto_binary, - init_core_and_create_wf, init_core_replay_preloaded, mock_sdk_cfg, prom_metrics, + CoreWfStarter, + activity_functions::{StdActivities, std_activities}, + get_integ_runtime_options, history_from_proto_binary, init_core_and_create_wf, + init_core_replay_preloaded, mock_sdk_cfg, prom_metrics, }, - integ_tests::{activity_functions::echo, metrics_tests}, + integ_tests::metrics_tests, }; use assert_matches::assert_matches; use std::{ @@ -39,7 +41,7 @@ use temporalio_common::{ protos::{ DEFAULT_WORKFLOW_TYPE, canned_histories, coresdk::{ - ActivityTaskCompletion, AsJsonPayloadExt, IntoCompletion, + ActivityTaskCompletion, IntoCompletion, activity_result::ActivityExecutionResult, workflow_activation::{WorkflowActivationJob, workflow_activation_job}, workflow_commands::{ @@ -473,20 +475,24 @@ async fn slow_completes_with_small_cache() { starter.worker_config.max_outstanding_workflow_tasks = Some(5_usize); starter.worker_config.max_cached_workflows = 5_usize; let mut worker = starter.worker().await; + + worker.register_activities_static::(); + worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { for _ in 0..3 { - ctx.activity(ActivityOptions { - activity_type: "echo_activity".to_string(), - start_to_close_timeout: Some(Duration::from_secs(5)), - input: "hi!".as_json_payload().expect("serializes fine"), - ..Default::default() - }) + ctx.activity::( + "hi!".to_string(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(5)), + ..Default::default() + }, + ) + .unwrap() .await; ctx.timer(Duration::from_secs(1)).await; } Ok(().into()) }); - worker.register_activity("echo_activity", echo); for i in 0..20 { worker .submit_wf( @@ -799,13 +805,18 @@ async fn nondeterminism_errors_fail_workflow_when_configured_to( // Restart the worker with a new, incompatible wf definition which will cause nondeterminism let mut starter = starter.clone_no_worker(); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), move |ctx: WfContext| async move { - ctx.activity(ActivityOptions { - activity_type: "echo_activity".to_string(), - start_to_close_timeout: Some(Duration::from_secs(5)), - ..Default::default() - }) + ctx.activity::( + "hi".to_owned(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(5)), + ..Default::default() + }, + )? .await; Ok(().into()) }); @@ -843,49 +854,53 @@ async fn history_out_of_order_on_restart() { static HIT_SLEEP: Notify = Notify::const_new(); + worker.register_activities_static::(); + worker2.register_activities_static::(); worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { - ctx.local_activity(LocalActivityOptions { - activity_type: "echo".to_owned(), - input: "hi".as_json_payload().unwrap(), - start_to_close_timeout: Some(Duration::from_secs(5)), - ..Default::default() - }) + ctx.local_activity::( + "hi".to_string(), + LocalActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(5)), + ..Default::default() + }, + )? .await; - ctx.activity(ActivityOptions { - activity_type: "echo".to_owned(), - input: "hi".as_json_payload().unwrap(), - start_to_close_timeout: Some(Duration::from_secs(5)), - ..Default::default() - }) + ctx.activity::( + "hi".to_string(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(5)), + ..Default::default() + }, + )? .await; // Interrupt this sleep on first go HIT_SLEEP.notify_one(); ctx.timer(Duration::from_secs(5)).await; Ok(().into()) }); - worker.register_activity("echo", echo); worker2.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { - ctx.local_activity(LocalActivityOptions { - activity_type: "echo".to_owned(), - input: "hi".as_json_payload().unwrap(), - start_to_close_timeout: Some(Duration::from_secs(5)), - ..Default::default() - }) + ctx.local_activity::( + "hi".to_string(), + LocalActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(5)), + ..Default::default() + }, + )? .await; // Timer is added after restarting workflow ctx.timer(Duration::from_secs(1)).await; - ctx.activity(ActivityOptions { - activity_type: "echo".to_owned(), - input: "hi".as_json_payload().unwrap(), - start_to_close_timeout: Some(Duration::from_secs(5)), - ..Default::default() - }) + ctx.activity::( + "hi".to_string(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(5)), + ..Default::default() + }, + )? .await; ctx.timer(Duration::from_secs(2)).await; Ok(().into()) }); - worker2.register_activity("echo", echo); worker .submit_wf( wf_name.to_owned(), diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/activities.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/activities.rs index 3f13206dc..470c8337d 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/activities.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/activities.rs @@ -1,15 +1,16 @@ -use crate::{ - common::{ - ActivationAssertionsInterceptor, CoreWfStarter, INTEG_CLIENT_IDENTITY, build_fake_sdk, - eventually, init_core_and_create_wf, mock_sdk, mock_sdk_cfg, - }, - integ_tests::activity_functions::{StdActivities, echo, std_activities}, +use crate::common::{ + ActivationAssertionsInterceptor, CoreWfStarter, INTEG_CLIENT_IDENTITY, + activity_functions::{StdActivities, std_activities}, + build_fake_sdk, eventually, init_core_and_create_wf, mock_sdk, mock_sdk_cfg, }; use anyhow::anyhow; use assert_matches::assert_matches; use futures_util::future::join_all; use std::{ - sync::atomic::{AtomicBool, Ordering}, + sync::{ + Arc, + atomic::{AtomicBool, Ordering}, + }, time::Duration, }; use temporalio_client::{ @@ -44,9 +45,9 @@ use temporalio_common::{ test_utils::schedule_activity_cmd, }, }; +use temporalio_macros::activities; use temporalio_sdk::{ - ActExitValue, ActivityOptions, CancellableFuture, WfContext, WfExitValue, WorkflowFunction, - WorkflowResult, + ActivityOptions, CancellableFuture, WfContext, WfExitValue, WorkflowFunction, WorkflowResult, activities::{ActivityContext, ActivityError}, }; use temporalio_sdk_core::{ @@ -58,11 +59,23 @@ use temporalio_sdk_core::{ }; use tokio::{join, sync::Semaphore, time::sleep}; +pub(crate) struct SleepyActivities {} + +#[activities] +impl SleepyActivities { + /// Activity that echoes input after sleeping for 2 seconds + #[activity] + async fn sleepy_echo(_ctx: ActivityContext, echo_me: String) -> Result { + sleep(Duration::from_secs(2)).await; + Ok(echo_me) + } +} + async fn one_activity_wf(ctx: WfContext) -> WorkflowResult { // TODO [rust-sdk-branch]: activities need to return deserialzied results let r = ctx .activity::( - "hi!", + "hi!".to_string(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(5)), ..Default::default() @@ -899,27 +912,26 @@ async fn activity_heartbeat_not_flushed_on_success() { async fn one_activity_abandon_cancelled_before_started() { let wf_name = "one_activity_abandon_cancelled_before_started"; let mut starter = CoreWfStarter::new(wf_name); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; let client = starter.get_client().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { - let act_fut = ctx.activity(ActivityOptions { - activity_type: "echo_activity".to_string(), - start_to_close_timeout: Some(Duration::from_secs(5)), - input: "hi!".as_json_payload().expect("serializes fine"), - cancellation_type: ActivityCancellationType::Abandon, - ..Default::default() - }); + let act_fut = ctx + .activity::( + "hi!".to_string(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(5)), + cancellation_type: ActivityCancellationType::Abandon, + ..Default::default() + }, + ) + .unwrap(); act_fut.cancel(&ctx); act_fut.await; Ok(().into()) }); - worker.register_activity( - "echo_activity", - |_ctx: ActivityContext, echo_me: String| async move { - sleep(Duration::from_secs(2)).await; - Ok(echo_me) - }, - ); let run_id = worker .submit_wf( @@ -943,29 +955,28 @@ async fn one_activity_abandon_cancelled_before_started() { async fn one_activity_abandon_cancelled_after_complete() { let wf_name = "one_activity_abandon_cancelled_after_complete"; let mut starter = CoreWfStarter::new(wf_name); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; let client = starter.get_client().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { - let act_fut = ctx.activity(ActivityOptions { - activity_type: "echo_activity".to_string(), - start_to_close_timeout: Some(Duration::from_secs(5)), - input: "hi!".as_json_payload().expect("serializes fine"), - cancellation_type: ActivityCancellationType::Abandon, - ..Default::default() - }); + let act_fut = ctx + .activity::( + "hi!".to_string(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(5)), + cancellation_type: ActivityCancellationType::Abandon, + ..Default::default() + }, + ) + .unwrap(); ctx.timer(Duration::from_secs(1)).await; act_fut.cancel(&ctx); ctx.timer(Duration::from_secs(3)).await; act_fut.await; Ok(().into()) }); - worker.register_activity( - "echo_activity", - |_ctx: ActivityContext, echo_me: String| async move { - sleep(Duration::from_secs(2)).await; - Ok(echo_me) - }, - ); let run_id = worker .submit_wf( @@ -985,25 +996,49 @@ async fn one_activity_abandon_cancelled_after_complete() { assert_matches!(res, WorkflowExecutionResult::Succeeded(_)); } +struct AsyncActivities { + shared_token: Arc>>>, +} +#[activities] +impl AsyncActivities { + #[activity] + async fn complete_async_activity( + self: Arc, + ctx: ActivityContext, + _: String, + ) -> Result<(), ActivityError> { + // set the `activity_task_token` + let activity_info = ctx.get_info(); + let task_token = &activity_info.task_token; + let mut shared = self.shared_token.lock().await; + *shared = Some(task_token.clone()); + Err(ActivityError::WillCompleteAsync) + } +} + #[tokio::test] async fn it_can_complete_async() { - use std::sync::Arc; - use tokio::sync::Mutex; - let wf_name = "it_can_complete_async".to_owned(); let mut starter = CoreWfStarter::new(&wf_name); + let async_response = "agence"; + let shared_token = Arc::new(tokio::sync::Mutex::new(None)); + starter.sdk_config.register_activities(AsyncActivities { + shared_token: shared_token.clone(), + }); + let mut worker = starter.worker().await; let client = starter.get_client().await; - let async_response = "agence"; - let shared_token: Arc>>> = Arc::new(Mutex::new(None)); + worker.register_wf(wf_name.clone(), move |ctx: WfContext| async move { let activity_resolution = ctx - .activity(ActivityOptions { - activity_type: "complete_async_activity".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), - start_to_close_timeout: Some(Duration::from_secs(30)), - ..Default::default() - }) + .activity::( + "hi".to_string(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(30)), + ..Default::default() + }, + ) + .unwrap() .await; let res = match activity_resolution.status { @@ -1017,22 +1052,6 @@ async fn it_can_complete_async() { Ok(().into()) }); - let shared_token_ref = shared_token.clone(); - worker.register_activity( - "complete_async_activity", - move |ctx: ActivityContext, _: String| { - let shared_token_ref = shared_token_ref.clone(); - async move { - // set the `activity_task_token` - let activity_info = ctx.get_info(); - let task_token = &activity_info.task_token; - let mut shared = shared_token_ref.lock().await; - *shared = Some(task_token.clone()); - Err::, _>(ActivityError::WillCompleteAsync) - } - }, - ); - let shared_token_ref2 = shared_token.clone(); tokio::spawn(async move { loop { @@ -1065,39 +1084,50 @@ async fn it_can_complete_async() { worker.run_until_done().await.unwrap(); } +static ACTS_STARTED: Semaphore = Semaphore::const_new(0); +static ACTS_DONE: Semaphore = Semaphore::const_new(0); +struct SleeperActivities {} +#[activities] +impl SleeperActivities { + #[activity] + async fn sleeper(ctx: ActivityContext, _: String) -> Result<(), ActivityError> { + ACTS_STARTED.add_permits(1); + // just wait to be cancelled + ctx.cancelled().await; + ACTS_DONE.add_permits(1); + Err(ActivityError::cancelled()) + } +} + #[tokio::test] async fn graceful_shutdown() { let wf_name = "graceful_shutdown"; let mut starter = CoreWfStarter::new(wf_name); starter.worker_config.graceful_shutdown_period = Some(Duration::from_millis(500)); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; let client = starter.get_client().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let act_futs = (1..=10).map(|_| { - ctx.activity(ActivityOptions { - activity_type: "sleeper".to_string(), - start_to_close_timeout: Some(Duration::from_secs(5)), - retry_policy: Some(RetryPolicy { - maximum_attempts: 1, + ctx.activity::( + "hi".to_string(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(5)), + retry_policy: Some(RetryPolicy { + maximum_attempts: 1, + ..Default::default() + }), + cancellation_type: ActivityCancellationType::WaitCancellationCompleted, ..Default::default() - }), - cancellation_type: ActivityCancellationType::WaitCancellationCompleted, - input: "hi".as_json_payload().unwrap(), - ..Default::default() - }) + }, + ) + .unwrap() }); join_all(act_futs).await; Ok(().into()) }); - static ACTS_STARTED: Semaphore = Semaphore::const_new(0); - static ACTS_DONE: Semaphore = Semaphore::const_new(0); - worker.register_activity("sleeper", |ctx: ActivityContext, _: String| async move { - ACTS_STARTED.add_permits(1); - // just wait to be cancelled - ctx.cancelled().await; - ACTS_DONE.add_permits(1); - Result::<(), _>::Err(ActivityError::cancelled()) - }); worker .submit_wf( @@ -1128,38 +1158,49 @@ async fn graceful_shutdown() { join!(shutdowner, runner); } +static WAS_CANCELLED: AtomicBool = AtomicBool::new(false); +struct CancellableEchoActivities {} +#[activities] +impl CancellableEchoActivities { + #[activity] + async fn cancellable_echo( + ctx: ActivityContext, + echo_me: String, + ) -> Result { + // Doesn't heartbeat + ctx.cancelled().await; + WAS_CANCELLED.store(true, Ordering::Relaxed); + Ok(echo_me) + } +} + #[tokio::test] async fn activity_can_be_cancelled_by_local_timeout() { let wf_name = "activity_can_be_cancelled_by_local_timeout"; let mut starter = CoreWfStarter::new(wf_name); starter.worker_config.local_timeout_buffer_for_activities = Duration::from_secs(0); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let res = ctx - .activity(ActivityOptions { - activity_type: "echo_activity".to_string(), - start_to_close_timeout: Some(Duration::from_secs(1)), - input: "hi!".as_json_payload().expect("serializes fine"), - retry_policy: Some(RetryPolicy { - maximum_attempts: 1, + .activity::( + "hi!".to_string(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(1)), + retry_policy: Some(RetryPolicy { + maximum_attempts: 1, + ..Default::default() + }), ..Default::default() - }), - ..Default::default() - }) + }, + ) + .unwrap() .await; assert!(res.timed_out().is_some()); Ok(().into()) }); - static WAS_CANCELLED: AtomicBool = AtomicBool::new(false); - worker.register_activity( - "echo_activity", - |ctx: ActivityContext, echo_me: String| async move { - // Doesn't heartbeat - ctx.cancelled().await; - WAS_CANCELLED.store(true, Ordering::Relaxed); - Ok(echo_me) - }, - ); starter.start_with_worker(wf_name, &mut worker).await; worker.run_until_done().await.unwrap(); @@ -1184,21 +1225,26 @@ async fn long_activity_timeout_repro() { initial: 5, }; starter.worker_config.local_timeout_buffer_for_activities = Duration::from_secs(0); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let mut iter = 1; loop { let res = ctx - .activity(ActivityOptions { - activity_type: "echo_activity".to_string(), - start_to_close_timeout: Some(Duration::from_secs(1)), - input: "hi!".as_json_payload().expect("serializes fine"), - retry_policy: Some(RetryPolicy { - maximum_attempts: 1, + .activity::( + "hi!".to_string(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(1)), + retry_policy: Some(RetryPolicy { + maximum_attempts: 1, + ..Default::default() + }), ..Default::default() - }), - ..Default::default() - }) + }, + ) + .unwrap() .await; assert!(res.completed_ok()); ctx.timer(Duration::from_secs(60 * 3)).await; @@ -1208,7 +1254,6 @@ async fn long_activity_timeout_repro() { } } }); - worker.register_activity("echo_activity", echo); starter.start_with_worker(wf_name, &mut worker).await; worker.run_until_done().await.unwrap(); @@ -1245,11 +1290,14 @@ async fn pass_activity_summary_to_metadata() { let mut worker = mock_sdk_cfg(mock_cfg, |_| {}); worker.register_wf(wf_type, |ctx: WfContext| async move { - ctx.activity(ActivityOptions { - activity_type: DEFAULT_ACTIVITY_TYPE.to_string(), - summary: Some("activity summary".to_string()), - ..Default::default() - }) + ctx.activity_untyped( + DEFAULT_ACTIVITY_TYPE.to_string(), + Payload::default(), + ActivityOptions { + summary: Some("activity summary".to_string()), + ..Default::default() + }, + ) .await; Ok(().into()) }); @@ -1294,12 +1342,15 @@ async fn abandoned_activities_ignore_start_and_complete(hist_batches: &'static [ let mut worker = mock_sdk(MockPollCfg::from_resp_batches(wfid, t, hist_batches, mock)); worker.register_wf(wf_type.to_owned(), |ctx: WfContext| async move { - let act_fut = ctx.activity(ActivityOptions { - activity_type: DEFAULT_ACTIVITY_TYPE.to_string(), - start_to_close_timeout: Some(Duration::from_secs(5)), - cancellation_type: ActivityCancellationType::Abandon, - ..Default::default() - }); + let act_fut = ctx.activity_untyped( + DEFAULT_ACTIVITY_TYPE.to_string(), + Payload::default(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(5)), + cancellation_type: ActivityCancellationType::Abandon, + ..Default::default() + }, + ); ctx.timer(Duration::from_secs(1)).await; act_fut.cancel(&ctx); ctx.timer(Duration::from_secs(3)).await; @@ -1316,7 +1367,11 @@ async fn abandoned_activities_ignore_start_and_complete(hist_batches: &'static [ #[tokio::test] async fn immediate_activity_cancelation() { let func = WorkflowFunction::new(|ctx: WfContext| async move { - let cancel_activity_future = ctx.activity(ActivityOptions::default()); + let cancel_activity_future = ctx.activity_untyped( + DEFAULT_ACTIVITY_TYPE.to_string(), + Payload::default(), + ActivityOptions::default(), + ); // Immediately cancel the activity cancel_activity_future.cancel(&ctx); cancel_activity_future.await; diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/appdata_propagation.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/appdata_propagation.rs index aef3b0016..53d96332f 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/appdata_propagation.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/appdata_propagation.rs @@ -2,8 +2,11 @@ use crate::common::CoreWfStarter; use assert_matches::assert_matches; use std::time::Duration; use temporalio_client::{WfClientExt, WorkflowExecutionResult, WorkflowOptions}; -use temporalio_common::protos::coresdk::AsJsonPayloadExt; -use temporalio_sdk::{ActivityOptions, WfContext, WorkflowResult, activities::ActivityContext}; +use temporalio_macros::activities; +use temporalio_sdk::{ + ActivityOptions, WfContext, WorkflowResult, + activities::{ActivityContext, ActivityError}, +}; const TEST_APPDATA_MESSAGE: &str = "custom app data, yay"; @@ -12,20 +15,36 @@ struct Data { } pub(crate) async fn appdata_activity_wf(ctx: WfContext) -> WorkflowResult<()> { - ctx.activity(ActivityOptions { - activity_type: "echo_activity".to_string(), - start_to_close_timeout: Some(Duration::from_secs(5)), - input: "hi!".as_json_payload().expect("serializes fine"), - ..Default::default() - }) + ctx.activity::( + "hi!".to_string(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(5)), + ..Default::default() + }, + ) + .unwrap() .await; Ok(().into()) } +struct AppdataActivities {} +#[activities] +impl AppdataActivities { + #[activity] + async fn echo(ctx: ActivityContext, echo_me: String) -> Result { + let data = ctx.app_data::().expect("appdata exists. qed"); + assert_eq!(data.message, TEST_APPDATA_MESSAGE.to_owned()); + Ok(echo_me) + } +} + #[tokio::test] async fn appdata_access_in_activities_and_workflows() { let wf_name = "appdata_activity"; let mut starter = CoreWfStarter::new(wf_name); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; worker.inner_mut().insert_app_data(Data { message: TEST_APPDATA_MESSAGE.to_owned(), @@ -33,14 +52,6 @@ async fn appdata_access_in_activities_and_workflows() { let client = starter.get_client().await; worker.register_wf(wf_name.to_owned(), appdata_activity_wf); - worker.register_activity( - "echo_activity", - |ctx: ActivityContext, echo_me: String| async move { - let data = ctx.app_data::().expect("appdata exists. qed"); - assert_eq!(data.message, TEST_APPDATA_MESSAGE.to_owned()); - Ok(echo_me) - }, - ); let run_id = worker .submit_wf( diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/determinism.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/determinism.rs index 2d6676c7c..1c4f99103 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/determinism.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/determinism.rs @@ -1,4 +1,6 @@ -use crate::common::{CoreWfStarter, WorkflowHandleExt, mock_sdk, mock_sdk_cfg}; +use crate::common::{ + CoreWfStarter, WorkflowHandleExt, activity_functions::std_activities, mock_sdk, mock_sdk_cfg, +}; use std::{ sync::atomic::{AtomicBool, AtomicUsize, Ordering}, time::Duration, @@ -7,8 +9,8 @@ use temporalio_client::WorkflowOptions; use temporalio_common::{ protos::{ DEFAULT_ACTIVITY_TYPE, TestHistoryBuilder, canned_histories, - coresdk::AsJsonPayloadExt, temporal::api::{ + common::v1::Payload, enums::v1::{EventType, WorkflowTaskFailedCause}, failure::v1::Failure, }, @@ -17,7 +19,6 @@ use temporalio_common::{ }; use temporalio_sdk::{ ActivityOptions, ChildWorkflowOptions, LocalActivityOptions, WfContext, WorkflowResult, - activities::ActivityContext, }; use temporalio_sdk_core::{ replay::DEFAULT_WORKFLOW_TYPE, @@ -40,10 +41,11 @@ pub(crate) async fn timer_wf_nondeterministic(ctx: WfContext) -> WorkflowResult< } 2 => { // On the second attempt we should cause a nondeterminism error - ctx.activity(ActivityOptions { - activity_type: "whatever".to_string(), - ..Default::default() - }) + ctx.activity_untyped( + "whatever".to_string(), + Payload::default(), + ActivityOptions::default(), + ) .await; } _ => panic!("Ran too many times"), @@ -67,8 +69,13 @@ async fn test_determinism_error_then_recovers() { #[tokio::test] async fn task_fail_causes_replay_unset_too_soon() { + use crate::common::activity_functions::StdActivities; + let wf_name = "task_fail_causes_replay_unset_too_soon"; let mut starter = CoreWfStarter::new(wf_name); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; static DID_FAIL: AtomicBool = AtomicBool::new(false); @@ -76,12 +83,14 @@ async fn task_fail_causes_replay_unset_too_soon() { if DID_FAIL.load(Ordering::Relaxed) { assert!(ctx.is_replaying()); } - ctx.activity(ActivityOptions { - activity_type: "echo".to_string(), - input: "hi!".as_json_payload().expect("serializes fine"), - start_to_close_timeout: Some(Duration::from_secs(2)), - ..Default::default() - }) + ctx.activity::( + "hi!".to_string(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(2)), + ..Default::default() + }, + ) + .unwrap() .await; if !DID_FAIL.load(Ordering::Relaxed) { DID_FAIL.store(true, Ordering::Relaxed); @@ -89,10 +98,6 @@ async fn task_fail_causes_replay_unset_too_soon() { } Ok(().into()) }); - worker.register_activity( - "echo", - |_ctx: ActivityContext, echo_me: String| async move { Ok(echo_me) }, - ); let handle = starter.start_with_worker(wf_name, &mut worker).await; @@ -248,32 +253,40 @@ async fn activity_id_or_type_change_is_nondeterministic( worker.register_wf(wf_type.to_owned(), move |ctx: WfContext| async move { if local_act { - ctx.local_activity(if id_change { - LocalActivityOptions { - activity_id: Some("I'm bad and wrong!".to_string()), - activity_type: DEFAULT_ACTIVITY_TYPE.to_string(), - ..Default::default() - } + if id_change { + ctx.local_activity_untyped( + DEFAULT_ACTIVITY_TYPE.to_string(), + Payload::default(), + LocalActivityOptions { + activity_id: Some("I'm bad and wrong!".to_string()), + ..Default::default() + }, + ) + .await; } else { - LocalActivityOptions { - activity_type: "not the default act type".to_string(), - ..Default::default() - } - }) - .await; - } else { - ctx.activity(if id_change { + ctx.local_activity_untyped( + "not the default act type".to_string(), + Payload::default(), + Default::default(), + ) + .await; + } + } else if id_change { + ctx.activity_untyped( + DEFAULT_ACTIVITY_TYPE.to_string(), + Payload::default(), ActivityOptions { activity_id: Some("I'm bad and wrong!".to_string()), - activity_type: DEFAULT_ACTIVITY_TYPE.to_string(), - ..Default::default() - } - } else { - ActivityOptions { - activity_type: "not the default act type".to_string(), ..Default::default() - } - }) + }, + ) + .await; + } else { + ctx.activity_untyped( + "not the default act type".to_string(), + Payload::default(), + ActivityOptions::default(), + ) .await; } Ok(().into()) diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/local_activities.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/local_activities.rs index b2a8df561..bd0b15c72 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/local_activities.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/local_activities.rs @@ -1,7 +1,9 @@ use crate::common::{ - ActivationAssertionsInterceptor, CoreWfStarter, WorkflowHandleExt, build_fake_sdk, - history_from_proto_binary, init_core_replay_preloaded, mock_sdk, mock_sdk_cfg, - replay_sdk_worker, workflows::la_problem_workflow, + ActivationAssertionsInterceptor, CoreWfStarter, WorkflowHandleExt, + activity_functions::{StdActivities, std_activities}, + build_fake_sdk, history_from_proto_binary, init_core_replay_preloaded, mock_sdk, mock_sdk_cfg, + replay_sdk_worker, + workflows::la_problem_workflow, }; use anyhow::anyhow; use crossbeam_queue::SegQueue; @@ -17,32 +19,37 @@ use std::{ time::{Duration, Instant, SystemTime}, }; use temporalio_client::{WfClientExt, WorkflowClientTrait, WorkflowOptions}; -use temporalio_common::protos::{ - DEFAULT_ACTIVITY_TYPE, canned_histories, - coresdk::{ - ActivityTaskCompletion, AsJsonPayloadExt, FromJsonPayloadExt, IntoPayloadsExt, - activity_result::ActivityExecutionResult, - workflow_activation::{WorkflowActivationJob, workflow_activation_job}, - workflow_commands::{ - ActivityCancellationType, ScheduleLocalActivity, workflow_command::Variant, +use temporalio_common::{ + ActivityDefinition, + protos::{ + DEFAULT_ACTIVITY_TYPE, canned_histories, + coresdk::{ + ActivityTaskCompletion, AsJsonPayloadExt, FromJsonPayloadExt, IntoPayloadsExt, + activity_result::ActivityExecutionResult, + workflow_activation::{WorkflowActivationJob, workflow_activation_job}, + workflow_commands::{ + ActivityCancellationType, ScheduleLocalActivity, workflow_command::Variant, + }, + workflow_completion::{ + self, WorkflowActivationCompletion, workflow_activation_completion, + }, }, - workflow_completion, - workflow_completion::{WorkflowActivationCompletion, workflow_activation_completion}, - }, - temporal::api::{ - command::v1::{RecordMarkerCommandAttributes, command}, - common::v1::RetryPolicy, - enums::v1::{ - CommandType, EventType, TimeoutType, UpdateWorkflowExecutionLifecycleStage, - WorkflowTaskFailedCause, + temporal::api::{ + command::v1::{RecordMarkerCommandAttributes, command}, + common::v1::RetryPolicy, + enums::v1::{ + CommandType, EventType, TimeoutType, UpdateWorkflowExecutionLifecycleStage, + WorkflowTaskFailedCause, + }, + failure::v1::{Failure, failure::FailureInfo}, + history::v1::history_event::Attributes::MarkerRecordedEventAttributes, + query::v1::WorkflowQuery, + update::v1::WaitPolicy, }, - failure::v1::{Failure, failure::FailureInfo}, - history::v1::history_event::Attributes::MarkerRecordedEventAttributes, - query::v1::WorkflowQuery, - update::v1::WaitPolicy, + test_utils::{query_ok, schedule_local_activity_cmd, start_timer_cmd}, }, - test_utils::{query_ok, schedule_local_activity_cmd, start_timer_cmd}, }; +use temporalio_macros::activities; use temporalio_sdk::{ ActivityOptions, CancellableFuture, LocalActivityOptions, UpdateContext, WfContext, WorkflowFunction, WorkflowResult, @@ -63,12 +70,8 @@ use tokio_util::sync::CancellationToken; pub(crate) async fn one_local_activity_wf(ctx: WfContext) -> WorkflowResult<()> { let initial_workflow_time = ctx.workflow_time().expect("Workflow time should be set"); - ctx.local_activity(LocalActivityOptions { - activity_type: "echo_activity".to_string(), - input: "hi!".as_json_payload().expect("serializes fine"), - ..Default::default() - }) - .await; + ctx.local_activity::("hi!".to_string(), LocalActivityOptions::default())? + .await; // Verify LA execution advances the clock assert!(initial_workflow_time < ctx.workflow_time().unwrap()); Ok(().into()) @@ -78,9 +81,11 @@ pub(crate) async fn one_local_activity_wf(ctx: WfContext) -> WorkflowResult<()> async fn one_local_activity() { let wf_name = "one_local_activity"; let mut starter = CoreWfStarter::new(wf_name); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), one_local_activity_wf); - worker.register_activity("echo_activity", echo); let handle = starter.start_with_worker(wf_name, &mut worker).await; worker.run_until_done().await.unwrap(); @@ -91,11 +96,10 @@ async fn one_local_activity() { } pub(crate) async fn local_act_concurrent_with_timer_wf(ctx: WfContext) -> WorkflowResult<()> { - let la = ctx.local_activity(LocalActivityOptions { - activity_type: "echo_activity".to_string(), - input: "hi!".as_json_payload().expect("serializes fine"), - ..Default::default() - }); + let la = ctx.local_activity::( + "hi!".to_string(), + LocalActivityOptions::default(), + )?; let timer = ctx.timer(Duration::from_secs(1)); tokio::join!(la, timer); Ok(().into()) @@ -105,52 +109,60 @@ pub(crate) async fn local_act_concurrent_with_timer_wf(ctx: WfContext) -> Workfl async fn local_act_concurrent_with_timer() { let wf_name = "local_act_concurrent_with_timer"; let mut starter = CoreWfStarter::new(wf_name); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), local_act_concurrent_with_timer_wf); - worker.register_activity("echo_activity", echo); starter.start_with_worker(wf_name, &mut worker).await; worker.run_until_done().await.unwrap(); } -pub(crate) async fn local_act_then_timer_then_wait(ctx: WfContext) -> WorkflowResult<()> { - let la = ctx.local_activity(LocalActivityOptions { - activity_type: "echo_activity".to_string(), - input: "hi!".as_json_payload().expect("serializes fine"), - ..Default::default() - }); - ctx.timer(Duration::from_secs(1)).await; - let res = la.await; - assert!(res.completed_ok()); - Ok(().into()) -} - #[tokio::test] async fn local_act_then_timer_then_wait_result() { let wf_name = "local_act_then_timer_then_wait_result"; let mut starter = CoreWfStarter::new(wf_name); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; - worker.register_wf(wf_name.to_owned(), local_act_then_timer_then_wait); - worker.register_activity("echo_activity", echo); + worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { + let la = ctx.local_activity::( + "hi!".to_string(), + LocalActivityOptions::default(), + )?; + ctx.timer(Duration::from_secs(1)).await; + let res = la.await; + assert!(res.completed_ok()); + Ok(().into()) + }); starter.start_with_worker(wf_name, &mut worker).await; worker.run_until_done().await.unwrap(); } +pub(crate) async fn local_act_then_timer_then_wait(ctx: WfContext) -> WorkflowResult<()> { + let la = ctx.local_activity::( + Duration::from_secs(4), + LocalActivityOptions::default(), + )?; + ctx.timer(Duration::from_secs(1)).await; + let res = la.await; + assert!(res.completed_ok()); + Ok(().into()) +} + #[tokio::test] async fn long_running_local_act_with_timer() { let wf_name = "long_running_local_act_with_timer"; let mut starter = CoreWfStarter::new(wf_name); starter.workflow_options.task_timeout = Some(Duration::from_secs(1)); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), local_act_then_timer_then_wait); - worker.register_activity( - "echo_activity", - |_ctx: ActivityContext, str: String| async { - tokio::time::sleep(Duration::from_secs(4)).await; - Ok(str) - }, - ); starter.start_with_worker(wf_name, &mut worker).await; worker.run_until_done().await.unwrap(); @@ -159,13 +171,8 @@ async fn long_running_local_act_with_timer() { pub(crate) async fn local_act_fanout_wf(ctx: WfContext) -> WorkflowResult<()> { let las: Vec<_> = (1..=50) .map(|i| { - ctx.local_activity(LocalActivityOptions { - activity_type: "echo_activity".to_string(), - input: format!("Hi {i}") - .as_json_payload() - .expect("serializes fine"), - ..Default::default() - }) + ctx.local_activity::(format!("Hi {i}"), Default::default()) + .expect("serializes fine") }) .collect(); ctx.timer(Duration::from_secs(1)).await; @@ -178,9 +185,11 @@ async fn local_act_fanout() { let wf_name = "local_act_fanout"; let mut starter = CoreWfStarter::new(wf_name); starter.worker_config.max_outstanding_local_activities = Some(1_usize); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), local_act_fanout_wf); - worker.register_activity("echo_activity", echo); starter.start_with_worker(wf_name, &mut worker).await; worker.run_until_done().await.unwrap(); @@ -190,30 +199,31 @@ async fn local_act_fanout() { async fn local_act_retry_timer_backoff() { let wf_name = "local_act_retry_timer_backoff"; let mut starter = CoreWfStarter::new(wf_name); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let res = ctx - .local_activity(LocalActivityOptions { - activity_type: "echo".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), - retry_policy: RetryPolicy { - initial_interval: Some(prost_dur!(from_micros(15))), - // We want two local backoffs that are short. Third backoff will use timer - backoff_coefficient: 1_000., - maximum_interval: Some(prost_dur!(from_millis(1500))), - maximum_attempts: 4, - non_retryable_error_types: vec![], + .local_activity::( + (), + LocalActivityOptions { + retry_policy: RetryPolicy { + initial_interval: Some(prost_dur!(from_micros(15))), + // We want two local backoffs that are short. Third backoff will use timer + backoff_coefficient: 1_000., + maximum_interval: Some(prost_dur!(from_millis(1500))), + maximum_attempts: 4, + non_retryable_error_types: vec![], + }, + timer_backoff_threshold: Some(Duration::from_secs(1)), + ..Default::default() }, - timer_backoff_threshold: Some(Duration::from_secs(1)), - ..Default::default() - }) + )? .await; assert!(res.failed()); Ok(().into()) }); - worker.register_activity("echo", |_: ActivityContext, _: String| async { - Result::<(), _>::Err(anyhow!("Oh no I failed!").into()) - }); let run_id = worker .submit_wf( @@ -233,6 +243,24 @@ async fn local_act_retry_timer_backoff() { .unwrap(); } +struct EchoWithManualCancel { + manual_cancel: CancellationToken, +} +#[activities] +impl EchoWithManualCancel { + #[activity] + async fn echo(self: Arc, ctx: ActivityContext, _: String) -> Result<(), ActivityError> { + tokio::select! { + _ = tokio::time::sleep(Duration::from_secs(10)) => {} + _ = ctx.cancelled() => { + return Err(ActivityError::cancelled()) + } + _ = self.manual_cancel.cancelled() => {} + } + Ok(()) + } +} + #[rstest::rstest] #[case::wait(ActivityCancellationType::WaitCancellationCompleted)] #[case::try_cancel(ActivityCancellationType::TryCancel)] @@ -240,39 +268,29 @@ async fn local_act_retry_timer_backoff() { #[tokio::test] async fn cancel_immediate(#[case] cancel_type: ActivityCancellationType) { let wf_name = format!("cancel_immediate_{cancel_type:?}"); + // If we don't use this, we'd hang on shutdown for abandon cancel modes. + let manual_cancel = CancellationToken::new(); let mut starter = CoreWfStarter::new(&wf_name); + starter + .sdk_config + .register_activities(EchoWithManualCancel { + manual_cancel: manual_cancel.clone(), + }); let mut worker = starter.worker().await; worker.register_wf(&wf_name, move |ctx: WfContext| async move { - let la = ctx.local_activity(LocalActivityOptions { - activity_type: "echo".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), - cancel_type, - ..Default::default() - }); + let la = ctx.local_activity::( + "hi".to_string(), + LocalActivityOptions { + cancel_type, + ..Default::default() + }, + )?; la.cancel(&ctx); let resolution = la.await; assert!(resolution.cancelled()); Ok(().into()) }); - // If we don't use this, we'd hang on shutdown for abandon cancel modes. - let manual_cancel = CancellationToken::new(); - let manual_cancel_act = manual_cancel.clone(); - - worker.register_activity("echo", move |ctx: ActivityContext, _: String| { - let manual_cancel_act = manual_cancel_act.clone(); - async move { - tokio::select! { - _ = tokio::time::sleep(Duration::from_secs(10)) => {}, - _ = ctx.cancelled() => { - return Err(ActivityError::cancelled()) - } - _ = manual_cancel_act.cancelled() => {} - } - Ok(()) - } - }); - starter.start_with_worker(wf_name, &mut worker).await; worker .run_until_done_intercepted(Some(LACancellerInterceptor { @@ -309,6 +327,35 @@ impl WorkerInterceptor for LACancellerInterceptor { } } +struct EchoWithManualCancelAndBackoff { + manual_cancel: CancellationToken, + cancel_on_backoff: Option, +} +#[activities] +impl EchoWithManualCancelAndBackoff { + #[activity] + async fn echo(self: Arc, ctx: ActivityContext, _: String) -> Result<(), ActivityError> { + if self.cancel_on_backoff.is_some() { + if ctx.is_cancelled() { + return Err(ActivityError::cancelled()); + } + // Just fail constantly so we get stuck on the backoff timer + return Err(anyhow!("Oh no I failed!").into()); + } else { + tokio::select! { + _ = tokio::time::sleep(Duration::from_secs(100)) => {} + _ = ctx.cancelled() => { + return Err(ActivityError::cancelled()) + } + _ = self.manual_cancel.cancelled() => { + return Ok(()) + } + } + } + Err(anyhow!("Oh no I failed!").into()) + } +} + #[rstest::rstest] #[case::while_running(None)] #[case::while_backing_off(Some(Duration::from_millis(1500)))] @@ -324,25 +371,38 @@ async fn cancel_after_act_starts( cancel_type: ActivityCancellationType, ) { let wf_name = format!("cancel_after_act_starts_{cancel_on_backoff:?}_{cancel_type:?}"); + // If we don't use this, we'd hang on shutdown for abandon cancel modes. + let manual_cancel = CancellationToken::new(); let mut starter = CoreWfStarter::new(&wf_name); starter.workflow_options.task_timeout = Some(Duration::from_secs(1)); + starter + .sdk_config + .register_activities(EchoWithManualCancelAndBackoff { + manual_cancel: manual_cancel.clone(), + cancel_on_backoff: if cancel_on_backoff.is_some() { + Some(CancellationToken::new()) + } else { + None + }, + }); let mut worker = starter.worker().await; let bo_dur = cancel_on_backoff.unwrap_or_else(|| Duration::from_secs(1)); worker.register_wf(&wf_name, move |ctx: WfContext| async move { - let la = ctx.local_activity(LocalActivityOptions { - activity_type: "echo".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), - retry_policy: RetryPolicy { - initial_interval: Some(bo_dur.try_into().unwrap()), - backoff_coefficient: 1., - maximum_interval: Some(bo_dur.try_into().unwrap()), - // Retry forever until cancelled + let la = ctx.local_activity::( + "hi".to_string(), + LocalActivityOptions { + retry_policy: RetryPolicy { + initial_interval: Some(bo_dur.try_into().unwrap()), + backoff_coefficient: 1., + maximum_interval: Some(bo_dur.try_into().unwrap()), + // Retry forever until cancelled + ..Default::default() + }, + timer_backoff_threshold: Some(Duration::from_secs(1)), + cancel_type, ..Default::default() }, - timer_backoff_threshold: Some(Duration::from_secs(1)), - cancel_type, - ..Default::default() - }); + )?; ctx.timer(Duration::from_secs(1)).await; // Note that this cancel can't go through for *two* WF tasks, because we do a full heartbeat // before the timer (LA hasn't resolved), and then the timer fired event won't appear in @@ -357,34 +417,6 @@ async fn cancel_after_act_starts( Ok(().into()) }); - // If we don't use this, we'd hang on shutdown for abandon cancel modes. - let manual_cancel = CancellationToken::new(); - let manual_cancel_act = manual_cancel.clone(); - - worker.register_activity("echo", move |ctx: ActivityContext, _: String| { - let manual_cancel_act = manual_cancel_act.clone(); - async move { - if cancel_on_backoff.is_some() { - if ctx.is_cancelled() { - return Err(ActivityError::cancelled()); - } - // Just fail constantly so we get stuck on the backoff timer - return Err(anyhow!("Oh no I failed!").into()); - } else { - tokio::select! { - _ = tokio::time::sleep(Duration::from_secs(100)) => {}, - _ = ctx.cancelled() => { - return Err(ActivityError::cancelled()) - } - _ = manual_cancel_act.cancelled() => { - return Ok(()) - } - } - } - Err(anyhow!("Oh no I failed!").into()) - } - }); - starter.start_with_worker(&wf_name, &mut worker).await; worker .run_until_done_intercepted(Some(LACancellerInterceptor { @@ -398,6 +430,21 @@ async fn cancel_after_act_starts( starter.shutdown().await; } +struct LongRunningWithCancellation; +#[activities] +impl LongRunningWithCancellation { + #[activity] + async fn go(ctx: ActivityContext) -> Result<(), ActivityError> { + tokio::select! { + _ = tokio::time::sleep(Duration::from_secs(100)) => {} + _ = ctx.cancelled() => { + return Err(ActivityError::cancelled()) + } + } + Ok(()) + } +} + #[rstest::rstest] #[case::schedule(true)] #[case::start(false)] @@ -408,6 +455,9 @@ async fn x_to_close_timeout(#[case] is_schedule: bool) { if is_schedule { "schedule" } else { "start" } ); let mut starter = CoreWfStarter::new(&wf_name); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; let (sched, start) = if is_schedule { (Some(Duration::from_secs(2)), None) @@ -422,39 +472,43 @@ async fn x_to_close_timeout(#[case] is_schedule: bool) { worker.register_wf(wf_name.to_owned(), move |ctx: WfContext| async move { let res = ctx - .local_activity(LocalActivityOptions { - activity_type: "echo".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), - retry_policy: RetryPolicy { - initial_interval: Some(prost_dur!(from_micros(15))), - backoff_coefficient: 1_000., - maximum_interval: Some(prost_dur!(from_millis(1500))), - maximum_attempts: 4, - non_retryable_error_types: vec![], + .local_activity::( + (), + LocalActivityOptions { + retry_policy: RetryPolicy { + initial_interval: Some(prost_dur!(from_micros(15))), + backoff_coefficient: 1_000., + maximum_interval: Some(prost_dur!(from_millis(1500))), + maximum_attempts: 4, + non_retryable_error_types: vec![], + }, + timer_backoff_threshold: Some(Duration::from_secs(1)), + schedule_to_close_timeout: sched, + start_to_close_timeout: start, + ..Default::default() }, - timer_backoff_threshold: Some(Duration::from_secs(1)), - schedule_to_close_timeout: sched, - start_to_close_timeout: start, - ..Default::default() - }) + )? .await; assert_eq!(res.timed_out(), Some(timeout_type)); Ok(().into()) }); - worker.register_activity("echo", |ctx: ActivityContext, _: String| async move { - tokio::select! { - _ = tokio::time::sleep(Duration::from_secs(100)) => {}, - _ = ctx.cancelled() => { - return Err(ActivityError::cancelled()) - } - }; - Ok(()) - }); starter.start_with_worker(wf_name, &mut worker).await; worker.run_until_done().await.unwrap(); } +struct FailWithAtomicCounter { + counter: Arc, +} +#[activities] +impl FailWithAtomicCounter { + #[activity] + async fn go(self: Arc, _: ActivityContext, _: String) -> Result<(), ActivityError> { + self.counter.fetch_add(1, Ordering::Relaxed); + Err(anyhow!("Oh no I failed!").into()) + } +} + #[rstest::rstest] #[case::cached(true)] #[case::not_cached(false)] @@ -471,28 +525,28 @@ async fn schedule_to_close_timeout_across_timer_backoff(#[case] cached: bool) { let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let res = ctx - .local_activity(LocalActivityOptions { - activity_type: "echo".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), - retry_policy: RetryPolicy { - initial_interval: Some(prost_dur!(from_millis(15))), - backoff_coefficient: 1_000., - maximum_interval: Some(prost_dur!(from_millis(1000))), - maximum_attempts: 40, - non_retryable_error_types: vec![], + .local_activity::( + "hi".to_string(), + LocalActivityOptions { + retry_policy: RetryPolicy { + initial_interval: Some(prost_dur!(from_millis(15))), + backoff_coefficient: 1_000., + maximum_interval: Some(prost_dur!(from_millis(1000))), + maximum_attempts: 40, + non_retryable_error_types: vec![], + }, + timer_backoff_threshold: Some(Duration::from_millis(500)), + schedule_to_close_timeout: Some(Duration::from_secs(2)), + ..Default::default() }, - timer_backoff_threshold: Some(Duration::from_millis(500)), - schedule_to_close_timeout: Some(Duration::from_secs(2)), - ..Default::default() - }) + )? .await; assert_eq!(res.timed_out(), Some(TimeoutType::ScheduleToClose)); Ok(().into()) }); - let num_attempts: &'static _ = Box::leak(Box::new(AtomicU8::new(0))); - worker.register_activity("echo", move |_: ActivityContext, _: String| async { - num_attempts.fetch_add(1, Ordering::Relaxed); - Result::<(), _>::Err(anyhow!("Oh no I failed!").into()) + let num_attempts = Arc::new(AtomicU8::new(0)); + worker.register_activities(FailWithAtomicCounter { + counter: num_attempts.clone(), }); starter.start_with_worker(wf_name, &mut worker).await; @@ -508,15 +562,11 @@ async fn eviction_wont_make_local_act_get_dropped(#[values(true, false)] short_w let wf_name = format!("eviction_wont_make_local_act_get_dropped_{short_wft_timeout}"); let mut starter = CoreWfStarter::new(&wf_name); starter.worker_config.max_cached_workflows = 0_usize; + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), local_act_then_timer_then_wait); - worker.register_activity( - "echo_activity", - |_ctx: ActivityContext, str: String| async { - tokio::time::sleep(Duration::from_secs(4)).await; - Ok(str) - }, - ); let opts = if short_wft_timeout { WorkflowOptions { @@ -537,42 +587,44 @@ async fn eviction_wont_make_local_act_get_dropped(#[values(true, false)] short_w async fn timer_backoff_concurrent_with_non_timer_backoff() { let wf_name = "timer_backoff_concurrent_with_non_timer_backoff"; let mut starter = CoreWfStarter::new(wf_name); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { - let r1 = ctx.local_activity(LocalActivityOptions { - activity_type: "echo".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), - retry_policy: RetryPolicy { - initial_interval: Some(prost_dur!(from_micros(15))), - backoff_coefficient: 1_000., - maximum_interval: Some(prost_dur!(from_millis(1500))), - maximum_attempts: 4, - non_retryable_error_types: vec![], + let r1 = ctx.local_activity::( + (), + LocalActivityOptions { + retry_policy: RetryPolicy { + initial_interval: Some(prost_dur!(from_micros(15))), + backoff_coefficient: 1_000., + maximum_interval: Some(prost_dur!(from_millis(1500))), + maximum_attempts: 4, + non_retryable_error_types: vec![], + }, + timer_backoff_threshold: Some(Duration::from_secs(1)), + ..Default::default() }, - timer_backoff_threshold: Some(Duration::from_secs(1)), - ..Default::default() - }); - let r2 = ctx.local_activity(LocalActivityOptions { - activity_type: "echo".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), - retry_policy: RetryPolicy { - initial_interval: Some(prost_dur!(from_millis(15))), - backoff_coefficient: 10., - maximum_interval: Some(prost_dur!(from_millis(1500))), - maximum_attempts: 4, - non_retryable_error_types: vec![], + )?; + let r2 = ctx.local_activity::( + (), + LocalActivityOptions { + retry_policy: RetryPolicy { + initial_interval: Some(prost_dur!(from_millis(15))), + backoff_coefficient: 10., + maximum_interval: Some(prost_dur!(from_millis(1500))), + maximum_attempts: 4, + non_retryable_error_types: vec![], + }, + timer_backoff_threshold: Some(Duration::from_secs(10)), + ..Default::default() }, - timer_backoff_threshold: Some(Duration::from_secs(10)), - ..Default::default() - }); + )?; let (r1, r2) = tokio::join!(r1, r2); assert!(r1.failed()); assert!(r2.failed()); Ok(().into()) }); - worker.register_activity("echo", |_: ActivityContext, _: String| async { - Result::<(), _>::Err(anyhow!("Oh no I failed!").into()) - }); starter.start_with_worker(wf_name, &mut worker).await; worker.run_until_done().await.unwrap(); @@ -582,23 +634,27 @@ async fn timer_backoff_concurrent_with_non_timer_backoff() { async fn repro_nondeterminism_with_timer_bug() { let wf_name = "repro_nondeterminism_with_timer_bug"; let mut starter = CoreWfStarter::new(wf_name); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let t1 = ctx.timer(Duration::from_secs(30)); - let r1 = ctx.local_activity(LocalActivityOptions { - activity_type: "delay".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), - retry_policy: RetryPolicy { - initial_interval: Some(prost_dur!(from_micros(15))), - backoff_coefficient: 1_000., - maximum_interval: Some(prost_dur!(from_millis(1500))), - maximum_attempts: 4, - non_retryable_error_types: vec![], + let r1 = ctx.local_activity::( + Duration::from_secs(2), + LocalActivityOptions { + retry_policy: RetryPolicy { + initial_interval: Some(prost_dur!(from_micros(15))), + backoff_coefficient: 1_000., + maximum_interval: Some(prost_dur!(from_millis(1500))), + maximum_attempts: 4, + non_retryable_error_types: vec![], + }, + timer_backoff_threshold: Some(Duration::from_secs(1)), + ..Default::default() }, - timer_backoff_threshold: Some(Duration::from_secs(1)), - ..Default::default() - }); + )?; tokio::pin!(t1); tokio::select! { _ = &mut t1 => {}, @@ -609,10 +665,6 @@ async fn repro_nondeterminism_with_timer_bug() { ctx.timer(Duration::from_secs(1)).await; Ok(().into()) }); - worker.register_activity("delay", |_: ActivityContext, _: String| async { - tokio::time::sleep(Duration::from_secs(2)).await; - Ok(()) - }); let run_id = worker .submit_wf( @@ -653,10 +705,7 @@ async fn weird_la_nondeterminism_repro(#[values(true, false)] fix_hist: bool) { "evict_while_la_running_no_interference", la_problem_workflow, ); - worker.register_activity("delay", |_: ActivityContext, _: String| async { - tokio::time::sleep(Duration::from_secs(15)).await; - Ok(()) - }); + worker.register_activities_static::(); worker.run().await.unwrap(); } @@ -677,10 +726,7 @@ async fn second_weird_la_nondeterminism_repro() { "evict_while_la_running_no_interference", la_problem_workflow, ); - worker.register_activity("delay", |_: ActivityContext, _: String| async { - tokio::time::sleep(Duration::from_secs(15)).await; - Ok(()) - }); + worker.register_activities_static::(); worker.run().await.unwrap(); } @@ -699,13 +745,23 @@ async fn third_weird_la_nondeterminism_repro() { "evict_while_la_running_no_interference", la_problem_workflow, ); - worker.register_activity("delay", |_: ActivityContext, _: String| async { - tokio::time::sleep(Duration::from_secs(15)).await; - Ok(()) - }); + worker.register_activities_static::(); worker.run().await.unwrap(); } +struct DelayWithCancellation; +#[activities] +impl DelayWithCancellation { + #[activity] + async fn delay(ctx: ActivityContext, dur: Duration) -> Result<(), ActivityError> { + tokio::select! { + _ = tokio::time::sleep(dur) => {} + _ = ctx.cancelled() => {} + } + Ok(()) + } +} + /// This test demonstrates why it's important to send LA resolutions last within a job. /// If we were to (during replay) scan ahead, see the marker, and resolve the LA before the /// activity cancellation, that would be wrong because, during execution, the LA resolution is @@ -722,27 +778,34 @@ async fn third_weird_la_nondeterminism_repro() { async fn la_resolve_same_time_as_other_cancel() { let wf_name = "la_resolve_same_time_as_other_cancel"; let mut starter = CoreWfStarter::new(wf_name); + starter + .sdk_config + .register_activities_static::(); // The activity won't get a chance to receive the cancel so make sure we still exit fast starter.worker_config.graceful_shutdown_period = Some(Duration::from_millis(100)); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { - let normal_act = ctx.activity(ActivityOptions { - activity_type: "delay".to_string(), - input: 9000.as_json_payload().expect("serializes fine"), - cancellation_type: ActivityCancellationType::TryCancel, - start_to_close_timeout: Some(Duration::from_secs(9000)), - ..Default::default() - }); + let normal_act = ctx + .activity::( + Duration::from_secs(9), + ActivityOptions { + cancellation_type: ActivityCancellationType::TryCancel, + start_to_close_timeout: Some(Duration::from_secs(9000)), + ..Default::default() + }, + ) + .unwrap(); // Make new task ctx.timer(Duration::from_millis(1)).await; // Start LA and cancel the activity at the same time - let local_act = ctx.local_activity(LocalActivityOptions { - activity_type: "delay".to_string(), - input: 100.as_json_payload().expect("serializes fine"), - ..Default::default() - }); + let local_act = ctx.local_activity::( + Duration::from_millis(100), + LocalActivityOptions { + ..Default::default() + }, + )?; normal_act.cancel(&ctx); // Race them, starting a timer if LA completes first tokio::select! { @@ -754,13 +817,6 @@ async fn la_resolve_same_time_as_other_cancel() { } Ok(().into()) }); - worker.register_activity("delay", |ctx: ActivityContext, wait_time: u64| async move { - tokio::select! { - _ = tokio::time::sleep(Duration::from_millis(wait_time)) => {} - _ = ctx.cancelled() => {} - } - Ok(()) - }); let run_id = worker .submit_wf( @@ -794,6 +850,9 @@ async fn long_local_activity_with_update( let wf_name = format!("{}-{}", ctx.name, ctx.case.unwrap()); let mut starter = CoreWfStarter::new(&wf_name); starter.workflow_options.task_timeout = Some(Duration::from_secs(1)); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; let client = starter.get_client().await; @@ -816,19 +875,14 @@ async fn long_local_activity_with_update( } }, ); - ctx.local_activity(LocalActivityOptions { - activity_type: "delay".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), - ..Default::default() - }) + ctx.local_activity::( + Duration::from_secs(6), + LocalActivityOptions::default(), + )? .await; update_counter.load(Ordering::Relaxed); Ok(().into()) }); - worker.register_activity("delay", |_: ActivityContext, _: String| async { - tokio::time::sleep(Duration::from_secs(6)).await; - Ok(()) - }); let handle = starter .start_with_worker(wf_name.clone(), &mut worker) @@ -887,6 +941,9 @@ async fn local_activity_with_heartbeat_only_causes_one_wakeup() { let wf_name = "local_activity_with_heartbeat_only_causes_one_wakeup"; let mut starter = CoreWfStarter::new(wf_name); starter.workflow_options.task_timeout = Some(Duration::from_secs(1)); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), move |ctx: WfContext| async move { @@ -894,11 +951,11 @@ async fn local_activity_with_heartbeat_only_causes_one_wakeup() { let la_resolved = AtomicBool::new(false); tokio::join!( async { - ctx.local_activity(LocalActivityOptions { - activity_type: "delay".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), - ..Default::default() - }) + ctx.local_activity::( + Duration::from_secs(6), + LocalActivityOptions::default(), + ) + .unwrap() .await; la_resolved.store(true, Ordering::Relaxed); }, @@ -912,10 +969,6 @@ async fn local_activity_with_heartbeat_only_causes_one_wakeup() { ); Ok(().into()) }); - worker.register_activity("delay", |_: ActivityContext, _: String| async { - tokio::time::sleep(Duration::from_secs(6)).await; - Ok(()) - }); let handle = starter.start_with_worker(wf_name, &mut worker).await; worker.run_until_done().await.unwrap(); @@ -932,12 +985,13 @@ async fn local_activity_with_heartbeat_only_causes_one_wakeup() { } pub(crate) async fn local_activity_with_summary_wf(ctx: WfContext) -> WorkflowResult<()> { - ctx.local_activity(LocalActivityOptions { - activity_type: "echo_activity".to_string(), - input: "hi!".as_json_payload().expect("serializes fine"), - summary: Some("Echo summary".to_string()), - ..Default::default() - }) + ctx.local_activity::( + "hi".to_string(), + LocalActivityOptions { + summary: Some("Echo summary".to_string()), + ..Default::default() + }, + )? .await; Ok(().into()) } @@ -946,9 +1000,11 @@ pub(crate) async fn local_activity_with_summary_wf(ctx: WfContext) -> WorkflowRe async fn local_activity_with_summary() { let wf_name = "local_activity_with_summary"; let mut starter = CoreWfStarter::new(wf_name); + starter + .sdk_config + .register_activities_static::(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), local_activity_with_summary_wf); - worker.register_activity("echo_activity", echo); let handle = starter.start_with_worker(wf_name, &mut worker).await; worker.run_until_done().await.unwrap(); @@ -981,10 +1037,6 @@ async fn local_activity_with_summary() { ); } -async fn echo(_ctx: ActivityContext, e: String) -> Result { - Ok(e) -} - /// This test verifies that when replaying we are able to resolve local activities whose data we /// don't see until after the workflow issues the command #[rstest::rstest] @@ -1021,17 +1073,13 @@ async fn local_act_two_wfts_before_marker(#[case] replay: bool, #[case] cached: worker.register_wf( DEFAULT_WORKFLOW_TYPE.to_owned(), |ctx: WfContext| async move { - let la = ctx.local_activity(LocalActivityOptions { - activity_type: DEFAULT_ACTIVITY_TYPE.to_string(), - input: "hi".as_json_payload().expect("serializes fine"), - ..Default::default() - }); + let la = ctx.local_activity::((), Default::default())?; ctx.timer(Duration::from_secs(1)).await; la.await; Ok(().into()) }, ); - worker.register_activity(DEFAULT_ACTIVITY_TYPE, echo); + worker.register_activities_static::(); worker .submit_wf( wf_id.to_owned(), @@ -1064,7 +1112,7 @@ async fn local_act_many_concurrent() { let mut worker = mock_sdk(mh); worker.register_wf(DEFAULT_WORKFLOW_TYPE.to_owned(), local_act_fanout_wf); - worker.register_activity("echo_activity", echo); + worker.register_activities_static::(); worker .submit_wf( wf_id.to_owned(), @@ -1077,6 +1125,28 @@ async fn local_act_many_concurrent() { worker.run_until_done().await.unwrap(); } +struct EchoWithConditionalBarrier { + shutdown_middle: bool, + shutdown_barr: &'static Barrier, + wft_timeout: Duration, +} +#[activities] +impl EchoWithConditionalBarrier { + #[activity] + async fn echo( + self: Arc, + _: ActivityContext, + str: String, + ) -> Result { + if self.shutdown_middle { + self.shutdown_barr.wait().await; + } + // Take slightly more than two workflow tasks + tokio::time::sleep(self.wft_timeout.mul_f32(2.2)).await; + Ok(str) + } +} + /// Verifies that local activities which take more than a workflow task timeout will cause /// us to issue additional (empty) WFT completions with the force flag on, thus preventing timeout /// of WFT while the local activity continues to execute. @@ -1111,26 +1181,19 @@ async fn local_act_heartbeat(#[case] shutdown_middle: bool) { worker.register_wf( DEFAULT_WORKFLOW_TYPE.to_owned(), |ctx: WfContext| async move { - ctx.local_activity(LocalActivityOptions { - activity_type: "echo".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), - ..Default::default() - }) + ctx.local_activity::( + "hi".to_string(), + LocalActivityOptions::default(), + )? .await; Ok(().into()) }, ); - worker.register_activity( - "echo", - move |_ctx: ActivityContext, str: String| async move { - if shutdown_middle { - shutdown_barr.wait().await; - } - // Take slightly more than two workflow tasks - tokio::time::sleep(wft_timeout.mul_f32(2.2)).await; - Ok(str) - }, - ); + worker.register_activities(EchoWithConditionalBarrier { + shutdown_middle, + shutdown_barr, + wft_timeout, + }); worker .submit_wf( wf_id.to_owned(), @@ -1152,6 +1215,23 @@ async fn local_act_heartbeat(#[case] shutdown_middle: bool) { runres.unwrap(); } +struct EventuallyPassingActivity { + attempts: Arc, + eventually_pass: bool, +} +#[activities] +impl EventuallyPassingActivity { + #[activity] + async fn echo(self: Arc, _: ActivityContext, _: String) -> Result<(), ActivityError> { + // Succeed on 3rd attempt (which is ==2 since fetch_add returns prev val) + if 2 == self.attempts.fetch_add(1, Ordering::Relaxed) && self.eventually_pass { + Ok(()) + } else { + Err(anyhow!("Oh no I failed!").into()) + } + } +} + #[rstest::rstest] #[case::retry_then_pass(true)] #[case::retry_until_fail(false)] @@ -1170,18 +1250,19 @@ async fn local_act_fail_and_retry(#[case] eventually_pass: bool) { DEFAULT_WORKFLOW_TYPE.to_owned(), move |ctx: WfContext| async move { let la_res = ctx - .local_activity(LocalActivityOptions { - activity_type: "echo".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), - retry_policy: RetryPolicy { - initial_interval: Some(prost_dur!(from_millis(50))), - backoff_coefficient: 1.2, - maximum_interval: None, - maximum_attempts: 5, - non_retryable_error_types: vec![], + .local_activity::( + "hi".to_string(), + LocalActivityOptions { + retry_policy: RetryPolicy { + initial_interval: Some(prost_dur!(from_millis(50))), + backoff_coefficient: 1.2, + maximum_interval: None, + maximum_attempts: 5, + non_retryable_error_types: vec![], + }, + ..Default::default() }, - ..Default::default() - }) + )? .await; if eventually_pass { assert!(la_res.completed_ok()) @@ -1191,14 +1272,10 @@ async fn local_act_fail_and_retry(#[case] eventually_pass: bool) { Ok(().into()) }, ); - let attempts: &'static _ = Box::leak(Box::new(AtomicUsize::new(0))); - worker.register_activity("echo", move |_ctx: ActivityContext, _: String| async move { - // Succeed on 3rd attempt (which is ==2 since fetch_add returns prev val) - if 2 == attempts.fetch_add(1, Ordering::Relaxed) && eventually_pass { - Ok(()) - } else { - Err(anyhow!("Oh no I failed!").into()) - } + let attempts = Arc::new(AtomicUsize::new(0)); + worker.register_activities(EventuallyPassingActivity { + attempts: attempts.clone(), + eventually_pass, }); worker .submit_wf( @@ -1219,18 +1296,22 @@ async fn local_act_retry_long_backoff_uses_timer() { let mut t = TestHistoryBuilder::default(); t.add_by_type(EventType::WorkflowExecutionStarted); t.add_full_wf_task(); - t.add_local_activity_fail_marker( + t.add_local_activity_marker( 1, "1", - Failure::application_failure("la failed".to_string(), false), + None, + Some(Failure::application_failure("la failed".to_string(), false)), + |m| m.activity_type = std_activities::AlwaysFail::name().to_owned(), ); let timer_started_event_id = t.add_by_type(EventType::TimerStarted); t.add_timer_fired(timer_started_event_id, "1".to_string()); t.add_full_wf_task(); - t.add_local_activity_fail_marker( + t.add_local_activity_marker( 2, "2", - Failure::application_failure("la failed".to_string(), false), + None, + Some(Failure::application_failure("la failed".to_string(), false)), + |m| m.activity_type = std_activities::AlwaysFail::name().to_owned(), ); let timer_started_event_id = t.add_by_type(EventType::TimerStarted); t.add_timer_fired(timer_started_event_id, "2".to_string()); @@ -1251,19 +1332,20 @@ async fn local_act_retry_long_backoff_uses_timer() { DEFAULT_WORKFLOW_TYPE.to_owned(), |ctx: WfContext| async move { let la_res = ctx - .local_activity(LocalActivityOptions { - activity_type: DEFAULT_ACTIVITY_TYPE.to_string(), - input: "hi".as_json_payload().expect("serializes fine"), - retry_policy: RetryPolicy { - initial_interval: Some(prost_dur!(from_millis(65))), - // This will make the second backoff 65 seconds, plenty to use timer - backoff_coefficient: 1_000., - maximum_interval: Some(prost_dur!(from_secs(600))), - maximum_attempts: 3, - non_retryable_error_types: vec![], + .local_activity::( + (), + LocalActivityOptions { + retry_policy: RetryPolicy { + initial_interval: Some(prost_dur!(from_millis(65))), + // This will make the second backoff 65 seconds, plenty to use timer + backoff_coefficient: 1_000., + maximum_interval: Some(prost_dur!(from_secs(600))), + maximum_attempts: 3, + non_retryable_error_types: vec![], + }, + ..Default::default() }, - ..Default::default() - }) + )? .await; assert!(la_res.failed()); // Extra timer just to have an extra workflow task which we can return full history for @@ -1271,12 +1353,7 @@ async fn local_act_retry_long_backoff_uses_timer() { Ok(().into()) }, ); - worker.register_activity( - DEFAULT_ACTIVITY_TYPE, - move |_ctx: ActivityContext, _: String| async move { - Result::<(), _>::Err(anyhow!("Oh no I failed!").into()) - }, - ); + worker.register_activities_static::(); worker .submit_wf( wf_id.to_owned(), @@ -1294,7 +1371,9 @@ async fn local_act_null_result() { let mut t = TestHistoryBuilder::default(); t.add_by_type(EventType::WorkflowExecutionStarted); t.add_full_wf_task(); - t.add_local_activity_marker(1, "1", None, None, |_| {}); + t.add_local_activity_marker(1, "1", None, None, |m| { + m.activity_type = std_activities::NoOp::name().to_owned() + }); t.add_workflow_execution_completed(); let wf_id = "fakeid"; @@ -1305,18 +1384,12 @@ async fn local_act_null_result() { worker.register_wf( DEFAULT_WORKFLOW_TYPE.to_owned(), |ctx: WfContext| async move { - ctx.local_activity(LocalActivityOptions { - activity_type: "nullres".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), - ..Default::default() - }) - .await; + ctx.local_activity::((), LocalActivityOptions::default())? + .await; Ok(().into()) }, ); - worker.register_activity("nullres", |_ctx: ActivityContext, _: String| async { - Ok(()) - }); + worker.register_activities_static::(); worker .submit_wf( wf_id.to_owned(), @@ -1350,19 +1423,13 @@ async fn local_act_command_immediately_follows_la_marker() { worker.register_wf( DEFAULT_WORKFLOW_TYPE.to_owned(), |ctx: WfContext| async move { - ctx.local_activity(LocalActivityOptions { - activity_type: "nullres".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), - ..Default::default() - }) - .await; + ctx.local_activity::((), LocalActivityOptions::default())? + .await; ctx.timer(Duration::from_secs(1)).await; Ok(().into()) }, ); - worker.register_activity("nullres", |_ctx: ActivityContext, _: String| async { - Ok(()) - }); + worker.register_activities_static::(); worker .submit_wf( wf_id.to_owned(), @@ -1646,13 +1713,14 @@ async fn test_schedule_to_start_timeout() { DEFAULT_WORKFLOW_TYPE.to_owned(), |ctx: WfContext| async move { let la_res = ctx - .local_activity(LocalActivityOptions { - activity_type: "echo".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), - // Impossibly small timeout so we timeout in the queue - schedule_to_start_timeout: prost_dur!(from_nanos(1)), - ..Default::default() - }) + .local_activity::( + "hi".to_string(), + LocalActivityOptions { + // Impossibly small timeout so we timeout in the queue + schedule_to_start_timeout: prost_dur!(from_nanos(1)), + ..Default::default() + }, + )? .await; assert_eq!(la_res.timed_out(), Some(TimeoutType::ScheduleToStart)); let rfail = la_res.unwrap_failure(); @@ -1667,9 +1735,7 @@ async fn test_schedule_to_start_timeout() { Ok(().into()) }, ); - worker.register_activity("echo", move |_ctx: ActivityContext, _: String| async move { - Ok(()) - }); + worker.register_activities_static::(); worker .submit_wf( wf_id.to_owned(), @@ -1733,20 +1799,21 @@ async fn test_schedule_to_start_timeout_not_based_on_original_time( DEFAULT_WORKFLOW_TYPE.to_owned(), move |ctx: WfContext| async move { let la_res = ctx - .local_activity(LocalActivityOptions { - activity_type: "echo".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), - retry_policy: RetryPolicy { - initial_interval: Some(prost_dur!(from_millis(50))), - backoff_coefficient: 1.2, - maximum_interval: None, - maximum_attempts: 5, - non_retryable_error_types: vec![], + .local_activity::( + "hi".to_string(), + LocalActivityOptions { + retry_policy: RetryPolicy { + initial_interval: Some(prost_dur!(from_millis(50))), + backoff_coefficient: 1.2, + maximum_interval: None, + maximum_attempts: 5, + non_retryable_error_types: vec![], + }, + schedule_to_start_timeout: Some(Duration::from_secs(60)), + schedule_to_close_timeout, + ..Default::default() }, - schedule_to_start_timeout: Some(Duration::from_secs(60)), - schedule_to_close_timeout, - ..Default::default() - }) + )? .await; if is_sched_to_start { assert!(la_res.completed_ok()); @@ -1756,9 +1823,7 @@ async fn test_schedule_to_start_timeout_not_based_on_original_time( Ok(().into()) }, ); - worker.register_activity("echo", move |_ctx: ActivityContext, _: String| async move { - Ok(()) - }); + worker.register_activities_static::(); worker .submit_wf( wf_id.to_owned(), @@ -1771,6 +1836,29 @@ async fn test_schedule_to_start_timeout_not_based_on_original_time( worker.run_until_done().await.unwrap(); } +struct ActivityWithRetriesAndCancellation { + attempts: Arc, + cancels: Arc, + la_completes: bool, +} +#[activities] +impl ActivityWithRetriesAndCancellation { + #[activity(name = DEFAULT_ACTIVITY_TYPE)] + async fn go(self: Arc, ctx: ActivityContext) -> Result<(), ActivityError> { + // Timeout the first 4 attempts, or all of them if we intend to fail + if self.attempts.fetch_add(1, Ordering::AcqRel) < 4 || !self.la_completes { + select! { + _ = tokio::time::sleep(Duration::from_millis(100)) => (), + _ = ctx.cancelled() => { + self.cancels.fetch_add(1, Ordering::AcqRel); + return Err(ActivityError::cancelled()); + } + } + } + Ok(()) + } +} + #[rstest::rstest] #[tokio::test] async fn start_to_close_timeout_allows_retries(#[values(true, false)] la_completes: bool) { @@ -1805,19 +1893,20 @@ async fn start_to_close_timeout_allows_retries(#[values(true, false)] la_complet DEFAULT_WORKFLOW_TYPE.to_owned(), move |ctx: WfContext| async move { let la_res = ctx - .local_activity(LocalActivityOptions { - activity_type: DEFAULT_ACTIVITY_TYPE.to_string(), - input: "hi".as_json_payload().expect("serializes fine"), - retry_policy: RetryPolicy { - initial_interval: Some(prost_dur!(from_millis(20))), - backoff_coefficient: 1.0, - maximum_interval: None, - maximum_attempts: 5, - non_retryable_error_types: vec![], + .local_activity::( + (), + LocalActivityOptions { + retry_policy: RetryPolicy { + initial_interval: Some(prost_dur!(from_millis(20))), + backoff_coefficient: 1.0, + maximum_interval: None, + maximum_attempts: 5, + non_retryable_error_types: vec![], + }, + start_to_close_timeout: Some(prost_dur!(from_millis(25))), + ..Default::default() }, - start_to_close_timeout: Some(prost_dur!(from_millis(25))), - ..Default::default() - }) + )? .await; if la_completes { assert!(la_res.completed_ok()); @@ -1827,24 +1916,13 @@ async fn start_to_close_timeout_allows_retries(#[values(true, false)] la_complet Ok(().into()) }, ); - let attempts: &'static _ = Box::leak(Box::new(AtomicUsize::new(0))); - let cancels: &'static _ = Box::leak(Box::new(AtomicUsize::new(0))); - worker.register_activity( - DEFAULT_ACTIVITY_TYPE, - move |ctx: ActivityContext, _: String| async move { - // Timeout the first 4 attempts, or all of them if we intend to fail - if attempts.fetch_add(1, Ordering::AcqRel) < 4 || !la_completes { - select! { - _ = tokio::time::sleep(Duration::from_millis(100)) => (), - _ = ctx.cancelled() => { - cancels.fetch_add(1, Ordering::AcqRel); - return Err(ActivityError::cancelled()); - } - } - } - Ok(()) - }, - ); + let attempts = Arc::new(AtomicUsize::new(0)); + let cancels = Arc::new(AtomicUsize::new(0)); + worker.register_activities(ActivityWithRetriesAndCancellation { + attempts: attempts.clone(), + cancels: cancels.clone(), + la_completes, + }); worker .submit_wf( wf_id.to_owned(), @@ -1861,6 +1939,19 @@ async fn start_to_close_timeout_allows_retries(#[values(true, false)] la_complet assert_eq!(cancels.load(Ordering::Acquire), num_cancels); } +struct ActivityThatExpectsCancellation; +#[activities] +impl ActivityThatExpectsCancellation { + #[activity] + async fn go(ctx: ActivityContext) -> Result<(), ActivityError> { + let res = tokio::time::timeout(Duration::from_millis(500), ctx.cancelled()).await; + if res.is_err() { + panic!("Activity must be cancelled!!!!"); + } + Err(ActivityError::cancelled()) + } +} + #[tokio::test] async fn wft_failure_cancels_running_las() { let mut t = TestHistoryBuilder::default(); @@ -1879,11 +1970,8 @@ async fn wft_failure_cancels_running_las() { worker.register_wf( DEFAULT_WORKFLOW_TYPE.to_owned(), |ctx: WfContext| async move { - let la_handle = ctx.local_activity(LocalActivityOptions { - activity_type: DEFAULT_ACTIVITY_TYPE.to_string(), - input: "hi".as_json_payload().expect("serializes fine"), - ..Default::default() - }); + let la_handle = ctx + .local_activity::((), Default::default())?; tokio::join!( async { ctx.timer(Duration::from_secs(1)).await; @@ -1894,16 +1982,7 @@ async fn wft_failure_cancels_running_las() { Ok(().into()) }, ); - worker.register_activity( - DEFAULT_ACTIVITY_TYPE, - move |ctx: ActivityContext, _: String| async move { - let res = tokio::time::timeout(Duration::from_millis(500), ctx.cancelled()).await; - if res.is_err() { - panic!("Activity must be cancelled!!!!"); - } - Result::<(), _>::Err(ActivityError::cancelled()) - }, - ); + worker.register_activities_static::(); worker .submit_wf( wf_id.to_owned(), @@ -1946,18 +2025,17 @@ async fn resolved_las_not_recorded_if_wft_fails_many_times() { worker.register_wf( DEFAULT_WORKFLOW_TYPE.to_owned(), WorkflowFunction::new::<_, _, ()>(|ctx: WfContext| async move { - ctx.local_activity(LocalActivityOptions { - activity_type: "echo".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), - ..Default::default() - }) + ctx.local_activity::( + "hi".to_string(), + LocalActivityOptions { + ..Default::default() + }, + )? .await; panic!() }), ); - worker.register_activity("echo", move |_: ActivityContext, _: String| async move { - Ok(()) - }); + worker.register_activities_static::(); worker .submit_wf( wf_id.to_owned(), @@ -2000,25 +2078,24 @@ async fn local_act_records_nonfirst_attempts_ok() { worker.register_wf( DEFAULT_WORKFLOW_TYPE.to_owned(), |ctx: WfContext| async move { - ctx.local_activity(LocalActivityOptions { - activity_type: "echo".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), - retry_policy: RetryPolicy { - initial_interval: Some(prost_dur!(from_millis(10))), - backoff_coefficient: 1.0, - maximum_interval: None, - maximum_attempts: 0, - non_retryable_error_types: vec![], + ctx.local_activity::( + (), + LocalActivityOptions { + retry_policy: RetryPolicy { + initial_interval: Some(prost_dur!(from_millis(10))), + backoff_coefficient: 1.0, + maximum_interval: None, + maximum_attempts: 0, + non_retryable_error_types: vec![], + }, + ..Default::default() }, - ..Default::default() - }) + )? .await; Ok(().into()) }, ); - worker.register_activity("echo", move |_ctx: ActivityContext, _: String| async move { - Result::<(), _>::Err(anyhow!("I fail").into()) - }); + worker.register_activities_static::(); worker .submit_wf( wf_id.to_owned(), @@ -2304,6 +2381,28 @@ async fn local_activity_after_wf_complete_is_discarded() { core.drain_pollers_and_shutdown().await; } +struct ActivityWithExplicitBackoff { + attempts: Arc, +} +#[activities] +impl ActivityWithExplicitBackoff { + #[activity] + async fn go(self: Arc, _: ActivityContext) -> Result<(), ActivityError> { + // Succeed on 3rd attempt (which is ==2 since fetch_add returns prev val) + let last_attempt = self.attempts.fetch_add(1, Ordering::Relaxed); + if 0 == last_attempt { + Err(ActivityError::Retryable { + source: anyhow!("Explicit backoff error"), + explicit_delay: Some(Duration::from_millis(300)), + }) + } else if 2 == last_attempt { + Ok(()) + } else { + Err(anyhow!("Oh no I failed!").into()) + } + } +} + #[tokio::test] async fn local_act_retry_explicit_delay() { let mut t = TestHistoryBuilder::default(); @@ -2319,36 +2418,26 @@ async fn local_act_retry_explicit_delay() { DEFAULT_WORKFLOW_TYPE.to_owned(), move |ctx: WfContext| async move { let la_res = ctx - .local_activity(LocalActivityOptions { - activity_type: "echo".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), - retry_policy: RetryPolicy { - initial_interval: Some(prost_dur!(from_millis(50))), - backoff_coefficient: 1.0, - maximum_attempts: 5, + .local_activity::( + (), + LocalActivityOptions { + retry_policy: RetryPolicy { + initial_interval: Some(prost_dur!(from_millis(50))), + backoff_coefficient: 1.0, + maximum_attempts: 5, + ..Default::default() + }, ..Default::default() }, - ..Default::default() - }) + )? .await; assert!(la_res.completed_ok()); Ok(().into()) }, ); - let attempts: &'static _ = Box::leak(Box::new(AtomicUsize::new(0))); - worker.register_activity("echo", move |_ctx: ActivityContext, _: String| async move { - // Succeed on 3rd attempt (which is ==2 since fetch_add returns prev val) - let last_attempt = attempts.fetch_add(1, Ordering::Relaxed); - if 0 == last_attempt { - Err(ActivityError::Retryable { - source: anyhow!("Explicit backoff error"), - explicit_delay: Some(Duration::from_millis(300)), - }) - } else if 2 == last_attempt { - Ok(()) - } else { - Err(anyhow!("Oh no I failed!").into()) - } + let attempts = Arc::new(AtomicUsize::new(0)); + worker.register_activities(ActivityWithExplicitBackoff { + attempts: attempts.clone(), }); worker .submit_wf( @@ -2368,19 +2457,44 @@ async fn local_act_retry_explicit_delay() { } async fn la_wf(ctx: WfContext) -> WorkflowResult<()> { - ctx.local_activity(LocalActivityOptions { - activity_type: DEFAULT_ACTIVITY_TYPE.to_string(), - input: ().as_json_payload().unwrap(), - retry_policy: RetryPolicy { - maximum_attempts: 1, + ctx.local_activity_untyped( + DEFAULT_ACTIVITY_TYPE.to_string(), + ().as_json_payload().expect("serializes fine"), + LocalActivityOptions { + retry_policy: RetryPolicy { + maximum_attempts: 1, + ..Default::default() + }, ..Default::default() }, - ..Default::default() - }) + ) .await; Ok(().into()) } +struct ActivityWithReplayCheck { + replay: bool, + completes_ok: bool, +} +#[activities] +impl ActivityWithReplayCheck { + #[activity(name = DEFAULT_ACTIVITY_TYPE)] + async fn echo( + self: Arc, + _: ActivityContext, + _: (), + ) -> Result<&'static str, ActivityError> { + if self.replay { + panic!("Should not be invoked on replay"); + } + if self.completes_ok { + Ok("hi") + } else { + Err(anyhow!("Oh no I failed!").into()) + } + } +} + #[rstest] #[case::incremental(false, true)] #[case::replay(true, true)] @@ -2448,54 +2562,54 @@ async fn one_la_success(#[case] replay: bool, #[case] completes_ok: bool) { let mut worker = build_fake_sdk(mock_cfg); worker.register_wf(DEFAULT_WORKFLOW_TYPE, la_wf); - worker.register_activity( - DEFAULT_ACTIVITY_TYPE, - move |_ctx: ActivityContext, _: ()| async move { - if replay { - panic!("Should not be invoked on replay"); - } - if completes_ok { - Ok("hi") - } else { - Err(anyhow!("Oh no I failed!").into()) - } - }, - ); + worker.register_activities(ActivityWithReplayCheck { + replay, + completes_ok, + }); worker.run().await.unwrap(); } async fn two_la_wf(ctx: WfContext) -> WorkflowResult<()> { - ctx.local_activity(LocalActivityOptions { - activity_type: DEFAULT_ACTIVITY_TYPE.to_string(), - input: ().as_json_payload().unwrap(), - ..Default::default() - }) + ctx.local_activity_untyped( + DEFAULT_ACTIVITY_TYPE.to_string(), + ().as_json_payload().expect("serializes fine"), + LocalActivityOptions::default(), + ) .await; - ctx.local_activity(LocalActivityOptions { - activity_type: DEFAULT_ACTIVITY_TYPE.to_string(), - input: ().as_json_payload().unwrap(), - ..Default::default() - }) + ctx.local_activity_untyped( + DEFAULT_ACTIVITY_TYPE.to_string(), + ().as_json_payload().expect("serializes fine"), + LocalActivityOptions::default(), + ) .await; Ok(().into()) } async fn two_la_wf_parallel(ctx: WfContext) -> WorkflowResult<()> { tokio::join!( - ctx.local_activity(LocalActivityOptions { - activity_type: DEFAULT_ACTIVITY_TYPE.to_string(), - input: ().as_json_payload().unwrap(), - ..Default::default() - }), - ctx.local_activity(LocalActivityOptions { - activity_type: DEFAULT_ACTIVITY_TYPE.to_string(), - input: ().as_json_payload().unwrap(), - ..Default::default() - }) + ctx.local_activity_untyped( + DEFAULT_ACTIVITY_TYPE.to_string(), + ().as_json_payload().expect("serializes fine"), + LocalActivityOptions::default(), + ), + ctx.local_activity_untyped( + DEFAULT_ACTIVITY_TYPE.to_string(), + ().as_json_payload().expect("serializes fine"), + LocalActivityOptions::default(), + ) ); Ok(().into()) } +struct ResolvedActivity; +#[activities] +impl ResolvedActivity { + #[activity(name = DEFAULT_ACTIVITY_TYPE)] + async fn echo(_: ActivityContext, _: ()) -> Result<&'static str, ActivityError> { + Ok("Resolved") + } +} + #[rstest] #[tokio::test] async fn two_sequential_las( @@ -2585,26 +2699,23 @@ async fn two_sequential_las( } else { worker.register_wf(DEFAULT_WORKFLOW_TYPE, two_la_wf); } - worker.register_activity( - DEFAULT_ACTIVITY_TYPE, - move |_ctx: ActivityContext, _: ()| async move { Ok("Resolved") }, - ); + worker.register_activities_static::(); worker.run().await.unwrap(); } async fn la_timer_la(ctx: WfContext) -> WorkflowResult<()> { - ctx.local_activity(LocalActivityOptions { - activity_type: DEFAULT_ACTIVITY_TYPE.to_string(), - input: ().as_json_payload().unwrap(), - ..Default::default() - }) + ctx.local_activity_untyped( + DEFAULT_ACTIVITY_TYPE.to_string(), + ().as_json_payload().expect("serializes fine"), + LocalActivityOptions::default(), + ) .await; ctx.timer(Duration::from_secs(5)).await; - ctx.local_activity(LocalActivityOptions { - activity_type: DEFAULT_ACTIVITY_TYPE.to_string(), - input: ().as_json_payload().unwrap(), - ..Default::default() - }) + ctx.local_activity_untyped( + DEFAULT_ACTIVITY_TYPE.to_string(), + ().as_json_payload().expect("serializes fine"), + LocalActivityOptions::default(), + ) .await; Ok(().into()) } @@ -2680,10 +2791,7 @@ async fn las_separated_by_timer(#[case] replay: bool) { let mut worker = build_fake_sdk(mock_cfg); worker.set_worker_interceptor(aai); worker.register_wf(DEFAULT_WORKFLOW_TYPE, la_timer_la); - worker.register_activity( - DEFAULT_ACTIVITY_TYPE, - move |_ctx: ActivityContext, _: ()| async move { Ok("Resolved") }, - ); + worker.register_activities_static::(); worker.run().await.unwrap(); } @@ -2715,10 +2823,7 @@ async fn one_la_heartbeating_wft_failure_still_executes() { let mut worker = build_fake_sdk(mock_cfg); worker.register_wf(DEFAULT_WORKFLOW_TYPE, la_wf); - worker.register_activity( - DEFAULT_ACTIVITY_TYPE, - move |_ctx: ActivityContext, _: ()| async move { Ok("Resolved") }, - ); + worker.register_activities_static::(); worker.run().await.unwrap(); } @@ -2752,10 +2857,14 @@ async fn immediate_cancel( let mut worker = build_fake_sdk(mock_cfg); worker.register_wf(DEFAULT_WORKFLOW_TYPE, move |ctx: WfContext| async move { - let la = ctx.local_activity(LocalActivityOptions { - cancel_type, - ..Default::default() - }); + let la = ctx.local_activity_untyped( + DEFAULT_ACTIVITY_TYPE.to_string(), + ().as_json_payload().expect("serializes fine"), + LocalActivityOptions { + cancel_type, + ..Default::default() + }, + ); la.cancel(&ctx); la.await; Ok(().into()) @@ -2764,6 +2873,22 @@ async fn immediate_cancel( worker.run().await.unwrap(); } +struct ActivityWithConditionalCancelWait { + cancel_type: ActivityCancellationType, + allow_cancel_barr: CancellationToken, +} +#[activities] +impl ActivityWithConditionalCancelWait { + #[activity(name = DEFAULT_ACTIVITY_TYPE)] + async fn echo(self: Arc, ctx: ActivityContext, _: ()) -> Result<(), ActivityError> { + if self.cancel_type == ActivityCancellationType::WaitCancellationCompleted { + ctx.cancelled().await; + } + self.allow_cancel_barr.cancelled().await; + Err(ActivityError::cancelled()) + } +} + #[rstest] #[case::incremental(false)] #[case::replay(true)] @@ -2849,12 +2974,13 @@ async fn cancel_after_act_starts_canned( let mut worker = build_fake_sdk(mock_cfg); worker.register_wf(DEFAULT_WORKFLOW_TYPE, move |ctx: WfContext| async move { - let la = ctx.local_activity(LocalActivityOptions { - cancel_type, - input: ().as_json_payload().unwrap(), - activity_type: DEFAULT_ACTIVITY_TYPE.to_string(), - ..Default::default() - }); + let la = ctx.local_activity::( + (), + LocalActivityOptions { + cancel_type, + ..Default::default() + }, + )?; ctx.timer(Duration::from_secs(1)).await; la.cancel(&ctx); // This extra timer is here to ensure the presence of another WF task doesn't mess up @@ -2873,15 +2999,9 @@ async fn cancel_after_act_starts_canned( ); Ok(().into()) }); - worker.register_activity(DEFAULT_ACTIVITY_TYPE, move |ctx: ActivityContext, _: ()| { - let allow_cancel_barr_clone = allow_cancel_barr_clone.clone(); - async move { - if cancel_type == ActivityCancellationType::WaitCancellationCompleted { - ctx.cancelled().await; - } - allow_cancel_barr_clone.cancelled().await; - Result::<(), _>::Err(ActivityError::cancelled()) - } + worker.register_activities(ActivityWithConditionalCancelWait { + cancel_type, + allow_cancel_barr: allow_cancel_barr_clone, }); worker.run().await.unwrap(); } diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/patches.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/patches.rs index 18e6d78b4..a9b8dfb79 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/patches.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/patches.rs @@ -21,7 +21,7 @@ use temporalio_common::protos::{ RecordMarkerCommandAttributes, ScheduleActivityTaskCommandAttributes, UpsertWorkflowSearchAttributesCommandAttributes, command::Attributes, }, - common::v1::ActivityType, + common::v1::{ActivityType, Payload}, enums::v1::{CommandType, EventType, IndexedValueType}, history::v1::{ ActivityTaskCompletedEventAttributes, ActivityTaskScheduledEventAttributes, @@ -313,26 +313,38 @@ fn patch_marker_single_activity( } async fn v1(ctx: &mut WfContext) { - ctx.activity(ActivityOptions { - activity_id: Some("no_change".to_owned()), - ..Default::default() - }) + ctx.activity_untyped( + "".to_string(), + Payload::default(), + ActivityOptions { + activity_id: Some("no_change".to_owned()), + ..Default::default() + }, + ) .await; } async fn v2(ctx: &mut WfContext) -> bool { if ctx.patched(MY_PATCH_ID) { - ctx.activity(ActivityOptions { - activity_id: Some("had_change".to_owned()), - ..Default::default() - }) + ctx.activity_untyped( + "".to_string(), + Payload::default(), + ActivityOptions { + activity_id: Some("had_change".to_owned()), + ..Default::default() + }, + ) .await; true } else { - ctx.activity(ActivityOptions { - activity_id: Some("no_change".to_owned()), - ..Default::default() - }) + ctx.activity_untyped( + "".to_string(), + Payload::default(), + ActivityOptions { + activity_id: Some("no_change".to_owned()), + ..Default::default() + }, + ) .await; false } @@ -340,18 +352,26 @@ async fn v2(ctx: &mut WfContext) -> bool { async fn v3(ctx: &mut WfContext) { ctx.deprecate_patch(MY_PATCH_ID); - ctx.activity(ActivityOptions { - activity_id: Some("had_change".to_owned()), - ..Default::default() - }) + ctx.activity_untyped( + "".to_string(), + Payload::default(), + ActivityOptions { + activity_id: Some("had_change".to_owned()), + ..Default::default() + }, + ) .await; } async fn v4(ctx: &mut WfContext) { - ctx.activity(ActivityOptions { - activity_id: Some("had_change".to_owned()), - ..Default::default() - }) + ctx.activity_untyped( + "".to_string(), + Payload::default(), + ActivityOptions { + activity_id: Some("had_change".to_owned()), + ..Default::default() + }, + ) .await; } @@ -642,13 +662,23 @@ async fn same_change_multiple_spots(#[case] have_marker_in_hist: bool, #[case] r let mut worker = build_fake_sdk(mock_cfg); worker.register_wf(DEFAULT_WORKFLOW_TYPE, move |ctx: WfContext| async move { if ctx.patched(MY_PATCH_ID) { - ctx.activity(ActivityOptions::default()).await; + ctx.activity_untyped( + "".to_string(), + Payload::default(), + ActivityOptions::default(), + ) + .await; } else { ctx.timer(ONE_SECOND).await; } ctx.timer(ONE_SECOND).await; if ctx.patched(MY_PATCH_ID) { - ctx.activity(ActivityOptions::default()).await; + ctx.activity_untyped( + "".to_string(), + Payload::default(), + ActivityOptions::default(), + ) + .await; } else { ctx.timer(ONE_SECOND).await; } diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/resets.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/resets.rs index 034a060b6..4e4242bcd 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/resets.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/resets.rs @@ -1,6 +1,6 @@ -use crate::{ - common::{CoreWfStarter, NAMESPACE}, - integ_tests::activity_functions::echo, +use crate::common::{ + CoreWfStarter, NAMESPACE, + activity_functions::{StdActivities, std_activities}, }; use futures_util::StreamExt; use std::{ @@ -11,11 +11,8 @@ use std::{ time::Duration, }; use temporalio_client::{WfClientExt, WorkflowClientTrait, WorkflowOptions, WorkflowService}; -use temporalio_common::protos::{ - coresdk::AsJsonPayloadExt, - temporal::api::{ - common::v1::WorkflowExecution, workflowservice::v1::ResetWorkflowExecutionRequest, - }, +use temporalio_common::protos::temporal::api::{ + common::v1::WorkflowExecution, workflowservice::v1::ResetWorkflowExecutionRequest, }; use temporalio_common::worker::WorkerTaskTypes; @@ -153,11 +150,10 @@ async fn reset_randomseed() { if RAND_SEED.load(Ordering::Relaxed) == ctx.random_seed() { ctx.timer(Duration::from_millis(100)).await; } else { - ctx.local_activity(LocalActivityOptions { - activity_type: "echo".to_string(), - input: "hi!".as_json_payload().expect("serializes fine"), - ..Default::default() - }) + ctx.local_activity::( + "hi!".to_string(), + LocalActivityOptions::default(), + )? .await; } // Wait for the post-task-fail signal @@ -172,7 +168,7 @@ async fn reset_randomseed() { Ok(().into()) } }); - worker.register_activity("echo", echo); + worker.register_activities_static::(); let run_id = worker .submit_wf( diff --git a/crates/sdk-core/tests/main.rs b/crates/sdk-core/tests/main.rs index d9c0d5b56..0abe176ff 100644 --- a/crates/sdk-core/tests/main.rs +++ b/crates/sdk-core/tests/main.rs @@ -12,7 +12,6 @@ mod shared_tests; #[cfg(test)] mod integ_tests { - mod activity_functions; mod client_tests; mod ephemeral_server_tests; mod heartbeat_tests; diff --git a/crates/sdk-core/tests/manual_tests.rs b/crates/sdk-core/tests/manual_tests.rs index 7c15450b2..b7291fa5e 100644 --- a/crates/sdk-core/tests/manual_tests.rs +++ b/crates/sdk-core/tests/manual_tests.rs @@ -22,14 +22,27 @@ use std::{ use temporalio_client::{ GetWorkflowResultOptions, WfClientExt, WorkflowClientTrait, WorkflowOptions, }; -use temporalio_common::{ - protos::coresdk::AsJsonPayloadExt, telemetry::PrometheusExporterOptions, - worker::WorkerTaskTypes, +use temporalio_common::{telemetry::PrometheusExporterOptions, worker::WorkerTaskTypes}; +use temporalio_macros::activities; +use temporalio_sdk::{ + ActivityOptions, WfContext, + activities::{ActivityContext, ActivityError}, }; -use temporalio_sdk::{ActivityOptions, WfContext, activities::ActivityContext}; use temporalio_sdk_core::{CoreRuntime, PollerBehavior}; use tracing::info; +struct JitteryEchoActivities {} +#[activities] +impl JitteryEchoActivities { + #[activity] + async fn echo(_ctx: ActivityContext, echo: String) -> Result { + // Add some jitter to completions + let rand_millis = rand::rng().random_range(0..500); + tokio::time::sleep(Duration::from_millis(rand_millis)).await; + Ok(echo) + } +} + #[tokio::test] async fn poller_load_spiky() { const SIGNAME: &str = "signame"; @@ -62,18 +75,22 @@ async fn poller_load_spiky() { }; let mut worker = starter.worker().await; let submitter = worker.get_submitter_handle(); + + worker.register_activities_static::(); worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let sigchan = ctx.make_signal_channel(SIGNAME).map(Ok); let drained_fut = sigchan.forward(sink::drain()); let real_stuff = async move { for _ in 0..5 { - ctx.activity(ActivityOptions { - activity_type: "echo".to_string(), - start_to_close_timeout: Some(Duration::from_secs(5)), - input: "hi!".as_json_payload().expect("serializes fine"), - ..Default::default() - }) + ctx.activity::( + "hi!".to_string(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(5)), + ..Default::default() + }, + ) + .unwrap() .await; } }; @@ -84,12 +101,6 @@ async fn poller_load_spiky() { Ok(().into()) }); - worker.register_activity("echo", |_: ActivityContext, echo: String| async move { - // Add some jitter to completions - let rand_millis = rand::rng().random_range(0..500); - tokio::time::sleep(Duration::from_millis(rand_millis)).await; - Ok(echo) - }); let client = starter.get_client().await; info!("Prom bound to {:?}", addr); @@ -305,18 +316,22 @@ async fn poller_load_spike_then_sustained() { }; let mut worker = starter.worker().await; let submitter = worker.get_submitter_handle(); + + worker.register_activities_static::(); worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let sigchan = ctx.make_signal_channel(SIGNAME).map(Ok); let drained_fut = sigchan.forward(sink::drain()); let real_stuff = async move { for _ in 0..5 { - ctx.activity(ActivityOptions { - activity_type: "echo".to_string(), - start_to_close_timeout: Some(Duration::from_secs(5)), - input: "hi!".as_json_payload().expect("serializes fine"), - ..Default::default() - }) + ctx.activity::( + "hi!".to_string(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(5)), + ..Default::default() + }, + ) + .unwrap() .await; } }; @@ -327,12 +342,6 @@ async fn poller_load_spike_then_sustained() { Ok(().into()) }); - worker.register_activity("echo", |_: ActivityContext, echo: String| async move { - // Add some jitter to completions - let rand_millis = rand::rng().random_range(0..500); - tokio::time::sleep(Duration::from_millis(rand_millis)).await; - Ok(echo) - }); let client = starter.get_client().await; info!("Prom bound to {:?}", addr); diff --git a/crates/sdk-core/tests/shared_tests/priority.rs b/crates/sdk-core/tests/shared_tests/priority.rs index 6e4253e83..540974c7e 100644 --- a/crates/sdk-core/tests/shared_tests/priority.rs +++ b/crates/sdk-core/tests/shared_tests/priority.rs @@ -3,14 +3,30 @@ use std::time::Duration; use temporalio_client::{ GetWorkflowResultOptions, Priority, WfClientExt, WorkflowClientTrait, WorkflowOptions, }; -use temporalio_common::protos::{ - coresdk::AsJsonPayloadExt, - temporal::api::{common, history::v1::history_event::Attributes}, -}; +use temporalio_common::protos::temporal::api::{common, history::v1::history_event::Attributes}; +use temporalio_macros::activities; use temporalio_sdk::{ - ActivityOptions, ChildWorkflowOptions, WfContext, activities::ActivityContext, + ActivityOptions, ChildWorkflowOptions, WfContext, + activities::{ActivityContext, ActivityError}, }; +struct PriorityActivities {} +#[activities] +impl PriorityActivities { + #[activity] + async fn echo(ctx: ActivityContext, echo_me: String) -> Result { + assert_eq!( + ctx.get_info().priority, + Priority { + priority_key: 5, + fairness_key: "fair-act".to_string(), + fairness_weight: 1.1 + } + ); + Ok(echo_me) + } +} + pub(crate) async fn priority_values_sent_to_server() { let mut starter = if let Some(wfs) = CoreWfStarter::new_cloud_or_local("priority_values_sent_to_server", ">=1.29.0-139.2").await @@ -26,7 +42,7 @@ pub(crate) async fn priority_values_sent_to_server() { }); let mut worker = starter.worker().await; let child_type = "child-wf"; - + worker.register_activities_static::(); worker.register_wf(starter.get_task_queue(), move |ctx: WfContext| async move { let child = ctx.child_workflow(ChildWorkflowOptions { workflow_id: format!("{}-child", ctx.task_queue()), @@ -47,19 +63,22 @@ pub(crate) async fn priority_values_sent_to_server() { .await .into_started() .expect("Child should start OK"); - let activity = ctx.activity(ActivityOptions { - activity_type: "echo".to_owned(), - input: "hello".as_json_payload().unwrap(), - start_to_close_timeout: Some(Duration::from_secs(5)), - priority: Some(Priority { - priority_key: 5, - fairness_key: "fair-act".to_string(), - fairness_weight: 1.1, - }), - // Currently no priority info attached to eagerly run activities - do_not_eagerly_execute: true, - ..Default::default() - }); + let activity = ctx + .activity::( + "hello".to_string(), + ActivityOptions { + start_to_close_timeout: Some(Duration::from_secs(5)), + priority: Some(Priority { + priority_key: 5, + fairness_key: "fair-act".to_string(), + fairness_weight: 1.1, + }), + // Currently no priority info attached to eagerly run activities + do_not_eagerly_execute: true, + ..Default::default() + }, + ) + .unwrap(); started.result().await; activity.await.unwrap_ok_payload(); Ok(().into()) @@ -75,17 +94,6 @@ pub(crate) async fn priority_values_sent_to_server() { ); Ok(().into()) }); - worker.register_activity("echo", |ctx: ActivityContext, echo_me: String| async move { - assert_eq!( - ctx.get_info().priority, - Priority { - priority_key: 5, - fairness_key: "fair-act".to_string(), - fairness_weight: 1.1 - } - ); - Ok(echo_me) - }); starter .start_with_worker(starter.get_task_queue(), &mut worker) diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index c60d33d8e..38a4c4168 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -338,8 +338,6 @@ struct WorkflowFutureHandle, J struct ActivityHalf { /// Maps activity type to the function for executing activities of that type activities: ActivityDefinitions, - /// Maps activity type to the function for executing activities of that type - activity_fns: HashMap, task_tokens_to_cancels: HashMap, } @@ -412,19 +410,37 @@ impl Worker { .insert(workflow_type.into(), wf_function.into()); } - /// Register an Activity function to invoke when the Worker is asked to run an activity of - /// `activity_type` - pub fn register_activity( + /// Registers all activities on an activity implementer that don't take a receiver. + pub fn register_activities_static(&mut self) -> &mut Self + where + AI: ActivityImplementer + HasOnlyStaticMethods, + { + self.activity_half + .activities + .register_activities_static::(); + self + } + /// Registers all activities on an activity implementer that take a receiver. + pub fn register_activities(&mut self, instance: AI) -> &mut Self { + self.activity_half + .activities + .register_activities::(instance); + self + } + /// Registers a specific activitiy that does not take a receiver. + pub fn register_activity(&mut self) -> &mut Self { + self.activity_half.activities.register_activity::(); + self + } + /// Registers a specific activitiy that takes a receiver. + pub fn register_activity_with_instance( &mut self, - activity_type: impl Into, - act_function: impl IntoActivityFunc, - ) { - self.activity_half.activity_fns.insert( - activity_type.into(), - ActivityFunction { - act_func: act_function.into_activity_fn(), - }, - ); + instance: Arc, + ) -> &mut Self { + self.activity_half + .activities + .register_activity_with_instance::(instance); + self } /// Insert Custom App Context for Workflows and Activities @@ -516,7 +532,7 @@ impl Worker { // Only poll on the activity queue if activity functions have been registered. This // makes tests which use mocks dramatically more manageable. async { - if !act_half.activity_fns.is_empty() || !act_half.activities.is_empty() { + if !act_half.activities.is_empty() { loop { let activity = common.worker.poll_activity_task().await; if matches!(activity, Err(PollError::ShutDown)) { @@ -720,31 +736,12 @@ impl ActivityHalf { ) -> Result<(), anyhow::Error> { match activity.variant { Some(activity_task::Variant::Start(start)) => { - let act_fn = if let Some(fun) = self.activities.get(&start.activity_type) { - fun - } else { - let fun = self - .activity_fns - .get(&start.activity_type) - .ok_or_else(|| { - anyhow!( - "No function registered for activity type {}", - start.activity_type - ) - })? - .clone() - .act_func; - Arc::new(move |p, _pc, ac| { - let fun = fun.clone(); - Ok(async move { - fun(ac, p).await.map(|aev| match aev { - ActExitValue::WillCompleteAsync => todo!("get rid of this"), - ActExitValue::Normal(p) => p, - }) - } - .boxed()) - }) - }; + let act_fn = self.activities.get(&start.activity_type).ok_or_else(|| { + anyhow!( + "No function registered for activity type {}", + start.activity_type + ) + })?; let span = info_span!( "RunActivity", "otel.name" = format!("RunActivity:{}", start.activity_type), @@ -1166,12 +1163,6 @@ type BoxActFn = Arc< + Sync, >; -/// Container for user-defined activity functions -#[derive(Clone)] -pub struct ActivityFunction { - act_func: BoxActFn, -} - /// Closures / functions which can be turned into activity functions implement this trait pub trait IntoActivityFunc { /// Consume the closure or fn pointer and turned it into a boxed activity function diff --git a/crates/sdk/src/workflow_context.rs b/crates/sdk/src/workflow_context.rs index 5609cab85..3111558bc 100644 --- a/crates/sdk/src/workflow_context.rs +++ b/crates/sdk/src/workflow_context.rs @@ -220,44 +220,75 @@ impl WfContext { pub fn activity( &self, input: AD::Input, - mut opts: ActivityOptions, + opts: ActivityOptions, ) -> Result, PayloadConversionError> { // TODO [rust-sdk-branch]: Get payload converter properly let pc = PayloadConverter::serde_json(); let payload = pc.to_payload(&input, &SerializationContext::Workflow)?; + Ok(self.activity_untyped(AD::name().to_string(), payload, opts)) + } + + /// Request to run an activity with an explicit name and payload + pub fn activity_untyped( + &self, + activity_type: String, + input: Payload, + mut opts: ActivityOptions, + ) -> impl CancellableFuture { + let seq = self.seq_nums.write().next_activity_seq(); + let (cmd, unblocker) = CancellableWFCommandFut::new(CancellableID::Activity(seq)); if opts.task_queue.is_none() { opts.task_queue = Some(self.task_queue.clone()); } - let seq = self.seq_nums.write().next_activity_seq(); - let (cmd, unblocker) = CancellableWFCommandFut::new(CancellableID::Activity(seq)); self.send( CommandCreateRequest { - cmd: opts.into_command(payload, seq), + cmd: opts.into_command(activity_type, input, seq), unblocker, } .into(), ); - Ok(cmd) + cmd } /// Request to run a local activity - pub fn local_activity( + pub fn local_activity( &self, + input: AD::Input, + opts: LocalActivityOptions, + ) -> Result + '_, PayloadConversionError> { + // TODO [rust-sdk-branch]: Get payload converter properly + let pc = PayloadConverter::serde_json(); + let payload = pc.to_payload(&input, &SerializationContext::Workflow)?; + Ok(LATimerBackoffFut::new( + AD::name().to_string(), + payload, + opts, + self, + )) + } + + /// Request to run a local activity with an explicit name and payload + pub fn local_activity_untyped( + &self, + activity_type: String, + input: Payload, opts: LocalActivityOptions, ) -> impl CancellableFuture + '_ { - LATimerBackoffFut::new(opts, self) + LATimerBackoffFut::new(activity_type, input, opts, self) } /// Request to run a local activity with no implementation of timer-backoff based retrying. fn local_activity_no_timer_retry( &self, + activity_type: String, + input: Payload, opts: LocalActivityOptions, ) -> impl CancellableFuture { let seq = self.seq_nums.write().next_activity_seq(); let (cmd, unblocker) = CancellableWFCommandFut::new(CancellableID::LocalActivity(seq)); self.send( CommandCreateRequest { - cmd: opts.into_command(seq), + cmd: opts.into_command(activity_type, input, seq), unblocker, } .into(), @@ -676,6 +707,8 @@ where struct LATimerBackoffFut<'a> { la_opts: LocalActivityOptions, + activity_type: String, + input: Payload, current_fut: Pin + Send + Unpin + 'a>>, timer_fut: Option + Send + Unpin + 'a>>>, ctx: &'a WfContext, @@ -684,10 +717,17 @@ struct LATimerBackoffFut<'a> { did_cancel: AtomicBool, } impl<'a> LATimerBackoffFut<'a> { - pub(crate) fn new(opts: LocalActivityOptions, ctx: &'a WfContext) -> Self { + pub(crate) fn new( + activity_type: String, + input: Payload, + opts: LocalActivityOptions, + ctx: &'a WfContext, + ) -> Self { Self { la_opts: opts.clone(), - current_fut: Box::pin(ctx.local_activity_no_timer_retry(opts)), + activity_type: activity_type.clone(), + input: input.clone(), + current_fut: Box::pin(ctx.local_activity_no_timer_retry(activity_type, input, opts)), timer_fut: None, ctx, next_attempt: 1, @@ -712,7 +752,11 @@ impl Future for LATimerBackoffFut<'_> { opts.attempt = Some(self.next_attempt); opts.original_schedule_time .clone_from(&self.next_sched_time); - self.current_fut = Box::pin(self.ctx.local_activity_no_timer_retry(opts)); + self.current_fut = Box::pin(self.ctx.local_activity_no_timer_retry( + self.activity_type.clone(), + self.input.clone(), + opts, + )); Poll::Pending } else { Poll::Ready(ActivityResolution { diff --git a/crates/sdk/src/workflow_context/options.rs b/crates/sdk/src/workflow_context/options.rs index 96cc449ab..4218c5690 100644 --- a/crates/sdk/src/workflow_context/options.rs +++ b/crates/sdk/src/workflow_context/options.rs @@ -33,10 +33,6 @@ pub struct ActivityOptions { /// /// If `None` use the context's sequence number pub activity_id: Option, - /// Type of activity to schedule - pub activity_type: String, - /// Input to the activity - pub input: Payload, /// Task queue to schedule the activity in /// /// If `None`, use the same task queue as the parent workflow. @@ -77,7 +73,12 @@ pub struct ActivityOptions { } impl ActivityOptions { - pub(crate) fn into_command(self, input: Payload, seq: u32) -> WorkflowCommand { + pub(crate) fn into_command( + self, + activity_type: String, + input: Payload, + seq: u32, + ) -> WorkflowCommand { WorkflowCommand { variant: Some( ScheduleActivity { @@ -86,7 +87,7 @@ impl ActivityOptions { None => seq.to_string(), Some(aid) => aid, }, - activity_type: self.activity_type, + activity_type, task_queue: self.task_queue.unwrap_or_default(), schedule_to_close_timeout: self .schedule_to_close_timeout @@ -99,7 +100,8 @@ impl ActivityOptions { .and_then(|d| d.try_into().ok()), heartbeat_timeout: self.heartbeat_timeout.and_then(|d| d.try_into().ok()), cancellation_type: self.cancellation_type as i32, - arguments: vec![self.input], + // TODO [rust-sdk-branch]: Handle multi-args + arguments: vec![input], retry_policy: self.retry_policy, priority: self.priority.map(Into::into), do_not_eagerly_execute: self.do_not_eagerly_execute, @@ -124,11 +126,6 @@ pub struct LocalActivityOptions { /// /// If `None` use the context's sequence number pub activity_id: Option, - /// Type of activity to schedule - pub activity_type: String, - /// Input to the activity - // TODO: Make optional - pub input: Payload, /// Retry policy pub retry_policy: RetryPolicy, /// Override attempt number rather than using 1. @@ -160,8 +157,13 @@ pub struct LocalActivityOptions { pub summary: Option, } -impl IntoWorkflowCommand for LocalActivityOptions { - fn into_command(mut self, seq: u32) -> WorkflowCommand { +impl LocalActivityOptions { + pub(crate) fn into_command( + mut self, + activity_type: String, + input: Payload, + seq: u32, + ) -> WorkflowCommand { // Allow tests to avoid extra verbosity when they don't care about timeouts // TODO: Builderize LA options self.schedule_to_close_timeout @@ -177,8 +179,8 @@ impl IntoWorkflowCommand for LocalActivityOptions { None => seq.to_string(), Some(aid) => aid, }, - activity_type: self.activity_type, - arguments: vec![self.input], + activity_type, + arguments: vec![input], retry_policy: Some(self.retry_policy), local_retry_threshold: self .timer_backoff_threshold From e8e41158eff3ce72ac94ddc9e2b6455fa6838d73 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Thu, 1 Jan 2026 19:05:11 -0800 Subject: [PATCH 05/13] Cleanup - but doctests can't pass because of module-in-function issue --- crates/macros/src/lib.rs | 11 +-- .../tests/integ_tests/heartbeat_tests.rs | 31 +++----- .../tests/integ_tests/update_tests.rs | 4 -- crates/sdk/Cargo.toml | 8 +-- crates/sdk/src/lib.rs | 71 ++++++++++++------- rustfmt.toml | 3 +- 6 files changed, 67 insertions(+), 61 deletions(-) diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index 6765e50b2..883576aa9 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -24,9 +24,9 @@ pub fn activity(_attr: TokenStream, item: TokenStream) -> TokenStream { /// /// An example state machine definition of a card reader for unlocking a door: /// ``` -/// use temporalio_macros::fsm; /// use std::convert::Infallible; /// use temporalio_common::fsm_trait::{StateMachine, TransitionResult}; +/// use temporalio_macros::fsm; /// /// fsm! { /// name CardReader; command Commands; error Infallible; shared_state SharedState; @@ -40,7 +40,7 @@ pub fn activity(_attr: TokenStream, item: TokenStream) -> TokenStream { /// /// #[derive(Clone)] /// pub struct SharedState { -/// last_id: Option +/// last_id: Option, /// } /// /// #[derive(Debug, Clone, Eq, PartialEq, Hash)] @@ -72,8 +72,11 @@ pub fn activity(_attr: TokenStream, item: TokenStream) -> TokenStream { /// } /// /// impl Locked { -/// fn on_card_readable(&self, shared_dat: &mut SharedState, data: CardData) -/// -> CardReaderTransition { +/// fn on_card_readable( +/// &self, +/// shared_dat: &mut SharedState, +/// data: CardData, +/// ) -> CardReaderTransition { /// match &shared_dat.last_id { /// // Arbitrarily deny the same person entering twice in a row /// Some(d) if d == &data => TransitionResult::ok(vec![], Locked {}.into()), diff --git a/crates/sdk-core/tests/integ_tests/heartbeat_tests.rs b/crates/sdk-core/tests/integ_tests/heartbeat_tests.rs index 3486ca811..bfd5a7c1f 100644 --- a/crates/sdk-core/tests/integ_tests/heartbeat_tests.rs +++ b/crates/sdk-core/tests/integ_tests/heartbeat_tests.rs @@ -1,4 +1,8 @@ -use crate::common::{CoreWfStarter, init_core_and_create_wf}; +use crate::common::{ + CoreWfStarter, + activity_functions::{StdActivities, std_activities}, + init_core_and_create_wf, +}; use assert_matches::assert_matches; use std::time::Duration; use temporalio_client::{WfClientExt, WorkflowOptions}; @@ -25,11 +29,7 @@ use temporalio_common::{ test_utils::schedule_activity_cmd, }, }; -use temporalio_macros::activities; -use temporalio_sdk::{ - ActivityOptions, WfContext, - activities::{ActivityContext, ActivityError}, -}; +use temporalio_sdk::{ActivityOptions, WfContext}; use temporalio_sdk_core::test_help::{WorkerTestHelpers, drain_pollers_and_shutdown}; use tokio::time::sleep; @@ -183,33 +183,20 @@ async fn many_act_fails_with_heartbeats() { drain_pollers_and_shutdown(&core).await; } -pub(crate) struct SlowEchoActivities {} -#[activities] -impl SlowEchoActivities { - #[activity] - async fn echo_activity( - _ctx: ActivityContext, - echo_me: String, - ) -> Result { - sleep(Duration::from_secs(4)).await; - Ok(echo_me) - } -} - #[tokio::test] async fn activity_doesnt_heartbeat_hits_timeout_then_completes() { let wf_name = "activity_doesnt_heartbeat_hits_timeout_then_completes"; let mut starter = CoreWfStarter::new(wf_name); starter .sdk_config - .register_activities_static::(); + .register_activities_static::(); let mut worker = starter.worker().await; let client = starter.get_client().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let res = ctx - .activity::( - "hi!".to_string(), + .activity::( + Duration::from_secs(4), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(10)), heartbeat_timeout: Some(Duration::from_secs(2)), diff --git a/crates/sdk-core/tests/integ_tests/update_tests.rs b/crates/sdk-core/tests/integ_tests/update_tests.rs index c7be7fb37..fc8dada12 100644 --- a/crates/sdk-core/tests/integ_tests/update_tests.rs +++ b/crates/sdk-core/tests/integ_tests/update_tests.rs @@ -1063,8 +1063,6 @@ async fn worker_restarted_in_middle_of_update() { #[tokio::test] async fn update_after_empty_wft() { - use crate::common::activity_functions::{StdActivities, std_activities}; - let wf_name = "update_after_empty_wft"; let mut starter = CoreWfStarter::new(wf_name); starter @@ -1157,8 +1155,6 @@ async fn update_after_empty_wft() { #[tokio::test] async fn update_lost_on_activity_mismatch() { - use crate::common::activity_functions::{StdActivities, std_activities}; - let wf_name = "update_lost_on_activity_mismatch"; let mut starter = CoreWfStarter::new(wf_name); starter diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index a61a14c0b..768fe5c95 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -44,13 +44,13 @@ version = "0.1" path = "../client" version = "0.1" -[dev-dependencies] -futures = "0.3" - -[dev-dependencies.temporalio-macros] +[dependencies.temporalio-macros] path = "../macros" version = "0.1" +[dev-dependencies] +futures = "0.3" + [features] default = [] antithesis_assertions = ["temporalio-sdk-core/antithesis_assertions"] diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index 38a4c4168..eb377f2a3 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -8,40 +8,58 @@ //! //! An example of running an activity worker: //! ```no_run -//! use std::{str::FromStr, sync::Arc}; -//! use temporalio_client::{ConnectionOptions, ClientOptions, Connection, Client}; -//! use temporalio_sdk::{activities::ActivityContext, Worker}; -//! use temporalio_sdk_core::{init_worker, Url, CoreRuntime, RuntimeOptions, WorkerConfig, WorkerVersioningStrategy }; +//! use std::str::FromStr; +//! use temporalio_client::{Connection, ConnectionOptions}; //! use temporalio_common::{ -//! worker::WorkerTaskTypes, -//! telemetry::TelemetryOptions +//! telemetry::TelemetryOptions, +//! worker::{WorkerDeploymentOptions, WorkerDeploymentVersion, WorkerTaskTypes}, //! }; +//! use temporalio_macros::activities; +//! use temporalio_sdk::{ +//! Worker, WorkerOptions, +//! activities::{ActivityContext, ActivityError}, +//! }; +//! use temporalio_sdk_core::{CoreRuntime, RuntimeOptions, Url}; +//! +//! #[activities] +//! impl MyActivities { +//! #[activity] +//! pub(crate) async fn echo( +//! _ctx: ActivityContext, +//! e: String, +//! ) -> Result { +//! Ok(e) +//! } +//! } //! //! #[tokio::main] //! async fn main() -> Result<(), Box> { -//! let connection_options = ConnectionOptions::new(Url::from_str("http://localhost:7233")?).build(); +//! let connection_options = +//! ConnectionOptions::new(Url::from_str("http://localhost:7233")?).build(); //! let telemetry_options = TelemetryOptions::builder().build(); -//! let runtime_options = RuntimeOptions::builder().telemetry_options(telemetry_options).build().unwrap(); +//! let runtime_options = RuntimeOptions::builder() +//! .telemetry_options(telemetry_options) +//! .build() +//! .unwrap(); //! let runtime = CoreRuntime::new_assume_tokio(runtime_options)?; //! //! let connection = Connection::connect(connection_options).await?; //! -//! let worker_config = WorkerConfig::builder() +//! let worker_options = WorkerOptions::new("task_queue") //! .namespace("default") -//! .task_queue("task_queue") //! .task_types(WorkerTaskTypes::activity_only()) -//! .versioning_strategy(WorkerVersioningStrategy::None { build_id: "rust-sdk".to_owned() }) -//! .build() -//! .unwrap(); -//! -//! let core_worker = init_worker(&runtime, worker_config, connection)?; -//! -//! let mut worker = Worker::new_from_core(Arc::new(core_worker), "task_queue"); -//! worker.register_activity( -//! "echo_activity", -//! |_ctx: ActivityContext, echo_me: String| async move { Ok(echo_me) }, -//! ); +//! .deployment_options(WorkerDeploymentOptions { +//! version: WorkerDeploymentVersion { +//! deployment_name: "my_deployment".to_owned(), +//! build_id: "my_build_id".to_owned(), +//! }, +//! use_worker_versioning: false, +//! default_versioning_behavior: None, +//! }) +//! .register_activities_static::() +//! .build(); //! +//! let worker = Worker::new(&runtime, connection, worker_options)?; //! worker.run().await?; //! //! Ok(()) @@ -59,8 +77,6 @@ mod workflow_context; mod workflow_future; pub use temporalio_client::Namespace; -use tracing::{Instrument, Span, field}; -use uuid::Uuid; pub use workflow_context::{ ActivityOptions, CancellableFuture, ChildWorkflow, ChildWorkflowOptions, LocalActivityOptions, NexusOperationOptions, PendingChildWorkflow, Signal, SignalData, SignalWorkflowOptions, @@ -136,6 +152,8 @@ use tokio::{ }; use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_util::sync::CancellationToken; +use tracing::{Instrument, Span, field}; +use uuid::Uuid; const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -155,6 +173,9 @@ pub struct WorkerOptions { // TODO [rust-sdk-branch]: Other SDKs are pulling from client /// The Temporal service namespace this worker is bound to pub namespace: String, + // TODO [rust-sdk-branch]: Provide defaults? + /// Set the deployment options for this worker. + pub deployment_options: WorkerDeploymentOptions, /// A human-readable string that can identify this worker. Using something like sdk version /// and host name is a good default. If set, overrides the identity set (if any) on the client /// used by this worker. @@ -223,8 +244,6 @@ pub struct WorkerOptions { /// If set, the worker will issue cancels for all outstanding activities and nexus operations after /// shutdown has been initiated and this amount of time has elapsed. pub graceful_shutdown_period: Option, - /// Set the deployment options for this worker. - pub deployment_options: WorkerDeploymentOptions, } // TODO [rust-sdk-branch]: Traitify this? @@ -302,7 +321,7 @@ pub fn sdk_connection_options( } /// A worker that can poll for and respond to workflow tasks by using [WorkflowFunction]s, -/// and activity tasks by using [ActivityFunction]s +/// and activity tasks by using activities defined with [temporalio_macros::activities]. pub struct Worker { common: CommonWorker, workflow_half: WorkflowHalf, diff --git a/rustfmt.toml b/rustfmt.toml index d3db3454a..c6b99ae57 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1 +1,2 @@ -imports_granularity="Crate" \ No newline at end of file +imports_granularity="Crate" +format_code_in_doc_comments=true From 88451e9af88849e50e4f4912be782c3565440b7e Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Fri, 2 Jan 2026 11:38:46 -0800 Subject: [PATCH 06/13] Remove core config from integ starter & anyhow from activity error --- crates/sdk-core/src/worker/tuner.rs | 17 ++++++ crates/sdk-core/tests/common/mod.rs | 29 +++++++--- crates/sdk-core/tests/heavy_tests.rs | 46 +++++++-------- .../tests/heavy_tests/fuzzy_workflow.rs | 8 +-- .../tests/integ_tests/metrics_tests.rs | 29 ++++------ .../tests/integ_tests/polling_tests.rs | 12 ++-- .../tests/integ_tests/update_tests.rs | 10 ++-- .../integ_tests/worker_heartbeat_tests.rs | 54 +++++++++--------- .../tests/integ_tests/worker_tests.rs | 18 ++---- .../integ_tests/worker_versioning_tests.rs | 32 +++++------ .../tests/integ_tests/workflow_tests.rs | 57 +++++++++++-------- .../integ_tests/workflow_tests/activities.rs | 12 ++-- .../workflow_tests/cancel_external.rs | 2 +- .../integ_tests/workflow_tests/cancel_wf.rs | 2 +- .../workflow_tests/child_workflows.rs | 8 +-- .../workflow_tests/continue_as_new.rs | 12 ++-- .../integ_tests/workflow_tests/determinism.rs | 2 +- .../tests/integ_tests/workflow_tests/eager.rs | 4 +- .../workflow_tests/local_activities.rs | 12 ++-- .../workflow_tests/modify_wf_properties.rs | 2 +- .../tests/integ_tests/workflow_tests/nexus.rs | 12 ++-- .../integ_tests/workflow_tests/patches.rs | 14 ++--- .../integ_tests/workflow_tests/resets.rs | 4 +- .../integ_tests/workflow_tests/signals.rs | 8 +-- .../integ_tests/workflow_tests/stickyness.rs | 21 ++++--- .../integ_tests/workflow_tests/timers.rs | 8 +-- .../workflow_tests/upsert_search_attrs.rs | 2 +- crates/sdk-core/tests/manual_tests.rs | 28 ++++----- crates/sdk-core/tests/shared_tests/mod.rs | 2 +- crates/sdk/src/activities.rs | 9 ++- crates/sdk/src/lib.rs | 28 +++++++-- 31 files changed, 267 insertions(+), 237 deletions(-) diff --git a/crates/sdk-core/src/worker/tuner.rs b/crates/sdk-core/src/worker/tuner.rs index a721976ac..dbe4122b8 100644 --- a/crates/sdk-core/src/worker/tuner.rs +++ b/crates/sdk-core/src/worker/tuner.rs @@ -26,6 +26,23 @@ pub struct TunerHolder { nexus_supplier: Arc + Send + Sync>, } +impl TunerHolder { + /// Create a tuner with fixed size slot suppliers for all slot kinds. + pub fn fixed_size( + workflow_slots: usize, + activity_slots: usize, + local_activity_slots: usize, + nexus_slots: usize, + ) -> Self { + Self { + wft_supplier: Arc::new(FixedSizeSlotSupplier::new(workflow_slots)), + act_supplier: Arc::new(FixedSizeSlotSupplier::new(activity_slots)), + la_supplier: Arc::new(FixedSizeSlotSupplier::new(local_activity_slots)), + nexus_supplier: Arc::new(FixedSizeSlotSupplier::new(nexus_slots)), + } + } +} + /// Can be used to construct a [TunerHolder] without needing to manually construct each /// [SlotSupplier]. Useful for lang bridges to allow more easily passing through user options. #[derive(Clone, Debug, bon::Builder)] diff --git a/crates/sdk-core/tests/common/mod.rs b/crates/sdk-core/tests/common/mod.rs index fd6fc78e0..3627c658a 100644 --- a/crates/sdk-core/tests/common/mod.rs +++ b/crates/sdk-core/tests/common/mod.rs @@ -233,8 +233,6 @@ pub(crate) async fn get_cloud_client() -> Client { pub(crate) struct CoreWfStarter { /// Used for both the task queue and workflow id task_queue_name: String, - // TODO [rust-sdk-branch]: Get rid of worker config - pub worker_config: WorkerConfig, pub sdk_config: WorkerOptions, /// Options to use when starting workflow(s) pub workflow_options: WorkflowOptions, @@ -242,6 +240,9 @@ pub(crate) struct CoreWfStarter { runtime_override: Option>, client_override: Option, min_local_server_version: Option, + /// Run when initializing, allows for altering the config used to init the core worker + #[allow(clippy::type_complexity)] // It's not tho + core_config_mutator: Option>, } struct InitializedWorker { worker: Arc, @@ -308,18 +309,16 @@ impl CoreWfStarter { ) -> Self { let task_q_salt = rand_6_chars(); let task_queue = format!("{test_name}_{task_q_salt}"); - let mut worker_config = integ_worker_config(&task_queue); - worker_config.max_cached_workflows = 1000_usize; let sdk_config = integ_sdk_config(&task_queue); Self { task_queue_name: task_queue, - worker_config, sdk_config, initted_worker: OnceCell::new(), workflow_options: Default::default(), runtime_override: runtime_override.map(Arc::new), client_override, min_local_server_version: None, + core_config_mutator: None, } } @@ -328,13 +327,13 @@ impl CoreWfStarter { pub(crate) fn clone_no_worker(&self) -> Self { Self { task_queue_name: self.task_queue_name.clone(), - worker_config: self.worker_config.clone(), sdk_config: self.sdk_config.clone(), workflow_options: self.workflow_options.clone(), runtime_override: self.runtime_override.clone(), client_override: self.client_override.clone(), min_local_server_version: self.min_local_server_version.clone(), initted_worker: Default::default(), + core_config_mutator: self.core_config_mutator.clone(), } } @@ -347,6 +346,10 @@ impl CoreWfStarter { w } + pub(crate) fn set_core_cfg_mutator(&mut self, mutator: impl Fn(&mut WorkerConfig) + 'static) { + self.core_config_mutator = Some(Arc::new(mutator)) + } + pub(crate) async fn shutdown(&mut self) { self.get_worker().await.shutdown().await; } @@ -464,7 +467,6 @@ impl CoreWfStarter { } else { init_integ_telem().unwrap() }; - let cfg = self.worker_config.clone(); let (connection, client) = if let Some(client) = self.client_override.take() { // Extract the connection from the client to pass to init_worker let connection = client.connection().clone(); @@ -475,11 +477,20 @@ impl CoreWfStarter { opts.metrics_meter = rt.telemetry().get_temporal_metric_meter(); let connection = Connection::connect(opts).await.expect("Must connect"); let client_opts = - temporalio_client::ClientOptions::new(cfg.namespace.clone()).build(); + temporalio_client::ClientOptions::new(self.sdk_config.namespace.clone()) + .build(); let client = Client::new(connection.clone(), client_opts); (connection, client) }; - let worker = init_worker(rt, cfg, connection).expect("Worker inits cleanly"); + let worker = init_worker( + rt, + self.sdk_config + .clone() + .try_into() + .expect("sdk config converts to core config"), + connection, + ) + .expect("Worker inits cleanly"); InitializedWorker { worker: Arc::new(worker), client, diff --git a/crates/sdk-core/tests/heavy_tests.rs b/crates/sdk-core/tests/heavy_tests.rs index b31125656..5ac45d962 100644 --- a/crates/sdk-core/tests/heavy_tests.rs +++ b/crates/sdk-core/tests/heavy_tests.rs @@ -40,17 +40,19 @@ use temporalio_sdk::{ ActivityOptions, WfContext, WorkflowResult, activities::{ActivityContext, ActivityError}, }; -use temporalio_sdk_core::{CoreRuntime, PollerBehavior, ResourceBasedTuner, ResourceSlotOptions}; +use temporalio_sdk_core::{ + CoreRuntime, PollerBehavior, ResourceBasedTuner, ResourceSlotOptions, TunerHolder, +}; #[tokio::test] async fn activity_load() { const CONCURRENCY: usize = 512; let mut starter = CoreWfStarter::new("activity_load"); - starter.worker_config.max_outstanding_workflow_tasks = Some(CONCURRENCY); - starter.worker_config.max_cached_workflows = CONCURRENCY; - starter.worker_config.activity_task_poller_behavior = PollerBehavior::SimpleMaximum(10); - starter.worker_config.max_outstanding_activities = Some(CONCURRENCY); + starter.sdk_config.max_cached_workflows = CONCURRENCY; + starter.sdk_config.activity_task_poller_behavior = PollerBehavior::SimpleMaximum(10); + starter.sdk_config.tuner = + Arc::new(TunerHolder::fixed_size(CONCURRENCY, CONCURRENCY, 100, 100)); starter .sdk_config .register_activities_static::(); @@ -138,12 +140,8 @@ async fn chunky_activities_resource_based() { const WORKFLOWS: usize = 100; let mut starter = CoreWfStarter::new("chunky_activities_resource_based"); - starter.worker_config.max_outstanding_workflow_tasks = None; - starter.worker_config.max_outstanding_local_activities = None; - starter.worker_config.max_outstanding_activities = None; - starter.worker_config.max_outstanding_nexus_tasks = None; - starter.worker_config.workflow_task_poller_behavior = PollerBehavior::SimpleMaximum(10_usize); - starter.worker_config.activity_task_poller_behavior = PollerBehavior::SimpleMaximum(10_usize); + starter.sdk_config.workflow_task_poller_behavior = PollerBehavior::SimpleMaximum(10_usize); + starter.sdk_config.activity_task_poller_behavior = PollerBehavior::SimpleMaximum(10_usize); let mut tuner = ResourceBasedTuner::new(0.7, 0.7); tuner .with_workflow_slots_options(ResourceSlotOptions::new( @@ -152,7 +150,7 @@ async fn chunky_activities_resource_based() { Duration::from_millis(0), )) .with_activity_slots_options(ResourceSlotOptions::new(5, 1000, Duration::from_millis(50))); - starter.worker_config.tuner = Some(Arc::new(tuner)); + starter.sdk_config.tuner = Arc::new(tuner); starter .sdk_config .register_activities_static::(); @@ -224,10 +222,9 @@ async fn workflow_load() { init_integ_telem(); let rt = CoreRuntime::new_assume_tokio(get_integ_runtime_options(telemopts)).unwrap(); let mut starter = CoreWfStarter::new_with_runtime("workflow_load", rt); - starter.worker_config.max_outstanding_workflow_tasks = Some(5); - starter.worker_config.max_cached_workflows = 200; - starter.worker_config.activity_task_poller_behavior = PollerBehavior::SimpleMaximum(10); - starter.worker_config.max_outstanding_activities = Some(100); + starter.sdk_config.max_cached_workflows = 200; + starter.sdk_config.activity_task_poller_behavior = PollerBehavior::SimpleMaximum(10); + starter.sdk_config.tuner = Arc::new(TunerHolder::fixed_size(5, 100, 100, 100)); starter .sdk_config .register_activities_static::(); @@ -302,11 +299,11 @@ async fn workflow_load() { async fn evict_while_la_running_no_interference() { let wf_name = "evict_while_la_running_no_interference"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.max_outstanding_local_activities = Some(20); - starter.worker_config.max_cached_workflows = 20; + starter.sdk_config.max_cached_workflows = 20; // Though it doesn't make sense to set wft higher than cached workflows, leaving this commented // introduces more instability that can be useful in the test. // starter.max_wft(20); + starter.sdk_config.tuner = Arc::new(TunerHolder::fixed_size(100, 10, 20, 1)); starter .sdk_config .register_activities_static::(); @@ -367,9 +364,9 @@ pub async fn many_parallel_timers_longhist(ctx: WfContext) -> WorkflowResult<()> async fn can_paginate_long_history() { let wf_name = "can_paginate_long_history"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); // Do not use sticky queues so we are forced to paginate once history gets long - starter.worker_config.max_cached_workflows = 0; + starter.sdk_config.max_cached_workflows = 0; let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), many_parallel_timers_longhist); @@ -421,15 +418,14 @@ async fn poller_autoscaling_basic_loadtest() { let num_workflows = 100; let wf_name = "poller_load"; let mut starter = CoreWfStarter::new("poller_load"); - starter.worker_config.max_cached_workflows = 5000; - starter.worker_config.max_outstanding_workflow_tasks = Some(1000); - starter.worker_config.max_outstanding_activities = Some(1000); - starter.worker_config.workflow_task_poller_behavior = PollerBehavior::Autoscaling { + starter.sdk_config.max_cached_workflows = 5000; + starter.sdk_config.tuner = Arc::new(TunerHolder::fixed_size(1000, 1000, 100, 1)); + starter.sdk_config.workflow_task_poller_behavior = PollerBehavior::Autoscaling { minimum: 1, maximum: 200, initial: 5, }; - starter.worker_config.activity_task_poller_behavior = PollerBehavior::Autoscaling { + starter.sdk_config.activity_task_poller_behavior = PollerBehavior::Autoscaling { minimum: 1, maximum: 200, initial: 5, diff --git a/crates/sdk-core/tests/heavy_tests/fuzzy_workflow.rs b/crates/sdk-core/tests/heavy_tests/fuzzy_workflow.rs index 3f98b5c40..726a75cdf 100644 --- a/crates/sdk-core/tests/heavy_tests/fuzzy_workflow.rs +++ b/crates/sdk-core/tests/heavy_tests/fuzzy_workflow.rs @@ -4,10 +4,11 @@ use crate::common::{ }; use futures_util::{FutureExt, StreamExt, sink, stream::FuturesUnordered}; use rand::{Rng, SeedableRng, prelude::Distribution, rngs::SmallRng}; -use std::{future, time::Duration}; +use std::{future, sync::Arc, time::Duration}; use temporalio_client::{WfClientExt, WorkflowClientTrait, WorkflowOptions}; use temporalio_common::protos::coresdk::{AsJsonPayloadExt, FromJsonPayloadExt, IntoPayloadsExt}; use temporalio_sdk::{ActivityOptions, LocalActivityOptions, WfContext, WorkflowResult}; +use temporalio_sdk_core::TunerHolder; use tokio_util::sync::CancellationToken; const FUZZY_SIG: &str = "fuzzy_sig"; @@ -78,9 +79,8 @@ async fn fuzzy_workflow() { let num_workflows = 200; let wf_name = "fuzzy_wf"; let mut starter = CoreWfStarter::new("fuzzy_workflow"); - starter.worker_config.max_outstanding_workflow_tasks = Some(25); - starter.worker_config.max_cached_workflows = 25; - starter.worker_config.max_outstanding_activities = Some(25); + starter.sdk_config.max_cached_workflows = 25; + starter.sdk_config.tuner = Arc::new(TunerHolder::fixed_size(25, 25, 100, 100)); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), fuzzy_wf_def); diff --git a/crates/sdk-core/tests/integ_tests/metrics_tests.rs b/crates/sdk-core/tests/integ_tests/metrics_tests.rs index 75872727b..f8d3fc6b8 100644 --- a/crates/sdk-core/tests/integ_tests/metrics_tests.rs +++ b/crates/sdk-core/tests/integ_tests/metrics_tests.rs @@ -402,7 +402,7 @@ async fn query_of_closed_workflow_doesnt_tick_terminal_metric( let mut starter = CoreWfStarter::new_with_runtime("query_of_closed_workflow_doesnt_tick_terminal_metric", rt); // Disable cache to ensure replay happens completely - starter.worker_config.max_cached_workflows = 0_usize; + starter.sdk_config.max_cached_workflows = 0_usize; let worker = starter.get_worker().await; let run_id = starter.start_wf().await; let task = worker.poll_workflow_activation().await.unwrap(); @@ -787,7 +787,7 @@ async fn activity_metrics() { let rt = CoreRuntime::new_assume_tokio(get_integ_runtime_options(telemopts)).unwrap(); let wf_name = "activity_metrics"; let mut starter = CoreWfStarter::new_with_runtime(wf_name, rt); - starter.worker_config.graceful_shutdown_period = Some(Duration::from_secs(1)); + starter.sdk_config.graceful_shutdown_period = Some(Duration::from_secs(1)); starter .sdk_config .register_activities_static::(); @@ -919,7 +919,7 @@ async fn nexus_metrics() { let rt = CoreRuntime::new_assume_tokio(get_integ_runtime_options(telemopts)).unwrap(); let wf_name = "nexus_metrics"; let mut starter = CoreWfStarter::new_with_runtime(wf_name, rt); - starter.worker_config.task_types = WorkerTaskTypes { + starter.sdk_config.task_types = WorkerTaskTypes { enable_workflows: true, enable_local_activities: false, enable_remote_activities: false, @@ -1101,7 +1101,7 @@ async fn evict_on_complete_does_not_count_as_forced_eviction() { let rt = CoreRuntime::new_assume_tokio(get_integ_runtime_options(telemopts)).unwrap(); let wf_name = "evict_on_complete_does_not_count_as_forced_eviction"; let mut starter = CoreWfStarter::new_with_runtime(wf_name, rt); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut worker = starter.worker().await; worker.register_wf( @@ -1184,17 +1184,13 @@ async fn metrics_available_from_custom_slot_supplier() { let rt = CoreRuntime::new_assume_tokio(get_integ_runtime_options(telemopts)).unwrap(); let mut starter = CoreWfStarter::new_with_runtime("metrics_available_from_custom_slot_supplier", rt); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); - starter.worker_config.max_outstanding_workflow_tasks = None; - starter.worker_config.max_outstanding_local_activities = None; - starter.worker_config.max_outstanding_activities = None; - starter.worker_config.max_outstanding_nexus_tasks = None; + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut tb = TunerBuilder::default(); tb.workflow_slot_supplier(Arc::new(MetricRecordingSlotSupplier:: { inner: FixedSizeSlotSupplier::new(5), metrics: OnceLock::new(), })); - starter.worker_config.tuner = Some(Arc::new(tb.build())); + starter.sdk_config.tuner = Arc::new(tb.build()); let mut worker = starter.worker().await; worker.register_wf( @@ -1348,8 +1344,8 @@ async fn sticky_queue_label_strategy( let wf_name = format!("sticky_queue_label_strategy_{strategy:?}"); let mut starter = CoreWfStarter::new_with_runtime(&wf_name, rt); // Enable sticky queues by setting a reasonable cache size - starter.worker_config.max_cached_workflows = 10_usize; - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.max_cached_workflows = 10_usize; + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let task_queue = starter.get_task_queue().to_owned(); let mut worker = starter.worker().await; @@ -1425,15 +1421,10 @@ async fn resource_based_tuner_metrics() { let rt = CoreRuntime::new_assume_tokio(get_integ_runtime_options(telemopts)).unwrap(); let wf_name = "resource_based_tuner_metrics"; let mut starter = CoreWfStarter::new_with_runtime(wf_name, rt); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); - starter.worker_config.max_outstanding_workflow_tasks = None; - starter.worker_config.max_outstanding_local_activities = None; - starter.worker_config.max_outstanding_activities = None; - starter.worker_config.max_outstanding_nexus_tasks = None; - + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); // Create a resource-based tuner with reasonable thresholds let tuner = ResourceBasedTuner::new(0.8, 0.8); - starter.worker_config.tuner = Some(Arc::new(tuner)); + starter.sdk_config.tuner = Arc::new(tuner); let mut worker = starter.worker().await; diff --git a/crates/sdk-core/tests/integ_tests/polling_tests.rs b/crates/sdk-core/tests/integ_tests/polling_tests.rs index 6c6859417..6b05fb5b7 100644 --- a/crates/sdk-core/tests/integ_tests/polling_tests.rs +++ b/crates/sdk-core/tests/integ_tests/polling_tests.rs @@ -34,7 +34,7 @@ use temporalio_common::{ }; use temporalio_sdk::{ActivityOptions, WfContext}; use temporalio_sdk_core::{ - CoreRuntime, PollerBehavior, RuntimeOptions, + CoreRuntime, PollerBehavior, RuntimeOptions, TunerHolder, ephemeral_server::{TemporalDevServerConfig, default_cached_download}, init_worker, test_help::{NAMESPACE, WorkerTestHelpers, drain_pollers_and_shutdown}, @@ -248,18 +248,16 @@ async fn small_workflow_slots_and_pollers(#[values(false, true)] use_autoscaling let wf_name = "only_one_workflow_slot_and_two_pollers"; let mut starter = CoreWfStarter::new(wf_name); if use_autoscaling { - starter.worker_config.workflow_task_poller_behavior = PollerBehavior::Autoscaling { + starter.sdk_config.workflow_task_poller_behavior = PollerBehavior::Autoscaling { minimum: 1, maximum: 5, initial: 1, }; } else { - starter.worker_config.workflow_task_poller_behavior = PollerBehavior::SimpleMaximum(2); + starter.sdk_config.workflow_task_poller_behavior = PollerBehavior::SimpleMaximum(2); } - starter.worker_config.max_outstanding_workflow_tasks = Some(2_usize); - starter.worker_config.max_outstanding_local_activities = Some(1_usize); - starter.worker_config.activity_task_poller_behavior = PollerBehavior::SimpleMaximum(1); - starter.worker_config.max_outstanding_activities = Some(1_usize); + starter.sdk_config.activity_task_poller_behavior = PollerBehavior::SimpleMaximum(1); + starter.sdk_config.tuner = Arc::new(TunerHolder::fixed_size(2, 1, 1, 1)); starter .sdk_config .register_activities_static::(); diff --git a/crates/sdk-core/tests/integ_tests/update_tests.rs b/crates/sdk-core/tests/integ_tests/update_tests.rs index fc8dada12..e394725cb 100644 --- a/crates/sdk-core/tests/integ_tests/update_tests.rs +++ b/crates/sdk-core/tests/integ_tests/update_tests.rs @@ -712,7 +712,7 @@ async fn update_with_local_acts() { async fn update_rejection_sdk() { let wf_name = "update_rejection_sdk"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut worker = starter.worker().await; let client = starter.get_client().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { @@ -756,7 +756,7 @@ async fn update_rejection_sdk() { async fn update_fail_sdk() { let wf_name = "update_fail_sdk"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut worker = starter.worker().await; let client = starter.get_client().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { @@ -800,7 +800,7 @@ async fn update_fail_sdk() { async fn update_timer_sequence() { let wf_name = "update_timer_sequence"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut worker = starter.worker().await; let client = starter.get_client().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { @@ -848,7 +848,7 @@ async fn update_timer_sequence() { async fn task_failure_during_validation() { let wf_name = "task_failure_during_validation"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); starter.workflow_options.task_timeout = Some(Duration::from_secs(1)); let mut worker = starter.worker().await; let client = starter.get_client().await; @@ -909,7 +909,7 @@ async fn task_failure_during_validation() { async fn task_failure_after_update() { let wf_name = "task_failure_after_update"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); starter.workflow_options.task_timeout = Some(Duration::from_secs(1)); let mut worker = starter.worker().await; let client = starter.get_client().await; diff --git a/crates/sdk-core/tests/integ_tests/worker_heartbeat_tests.rs b/crates/sdk-core/tests/integ_tests/worker_heartbeat_tests.rs index 00d11aae5..429f1a685 100644 --- a/crates/sdk-core/tests/integ_tests/worker_heartbeat_tests.rs +++ b/crates/sdk-core/tests/integ_tests/worker_heartbeat_tests.rs @@ -42,6 +42,7 @@ use temporalio_sdk::{ }; use temporalio_sdk_core::{ CoreRuntime, PollerBehavior, ResourceBasedTuner, ResourceSlotOptions, RuntimeOptions, + TunerHolder, }; use tokio::{sync::Notify, time::sleep}; use tonic::IntoRequest; @@ -153,21 +154,22 @@ async fn docker_worker_heartbeat_basic(#[values("otel", "prom", "no_metrics")] b } let wf_name = format!("worker_heartbeat_basic_{backing}"); let mut starter = CoreWfStarter::new_with_runtime(&wf_name, rt); - starter.worker_config.max_outstanding_workflow_tasks = Some(5_usize); - starter.worker_config.max_cached_workflows = 5_usize; - starter.worker_config.max_outstanding_activities = Some(5_usize); - starter.worker_config.plugins = vec![ - PluginInfo { - name: "plugin1".to_string(), - version: "1".to_string(), - }, - PluginInfo { - name: "plugin2".to_string(), - version: "2".to_string(), - }, - ] - .into_iter() - .collect(); + starter.sdk_config.max_cached_workflows = 5_usize; + starter.sdk_config.tuner = Arc::new(TunerHolder::fixed_size(5, 5, 1, 1)); + starter.set_core_cfg_mutator(|c| { + c.plugins = vec![ + PluginInfo { + name: "plugin1".to_string(), + version: "1".to_string(), + }, + PluginInfo { + name: "plugin2".to_string(), + version: "2".to_string(), + }, + ] + .into_iter() + .collect(); + }); let acts_started = Arc::new(Notify::new()); let acts_done = Arc::new(Notify::new()); starter.sdk_config.register_activities(NotifyActivities { @@ -301,21 +303,17 @@ async fn docker_worker_heartbeat_tuner() { tuner .with_workflow_slots_options(ResourceSlotOptions::new(2, 10, Duration::from_millis(0))) .with_activity_slots_options(ResourceSlotOptions::new(5, 10, Duration::from_millis(50))); - starter.worker_config.workflow_task_poller_behavior = PollerBehavior::Autoscaling { + starter.sdk_config.workflow_task_poller_behavior = PollerBehavior::Autoscaling { minimum: 1, maximum: 200, initial: 5, }; - starter.worker_config.nexus_task_poller_behavior = PollerBehavior::Autoscaling { + starter.sdk_config.nexus_task_poller_behavior = PollerBehavior::Autoscaling { minimum: 1, maximum: 200, initial: 5, }; - starter.worker_config.max_outstanding_workflow_tasks = None; - starter.worker_config.max_outstanding_local_activities = None; - starter.worker_config.max_outstanding_activities = None; - starter.worker_config.max_outstanding_nexus_tasks = None; - starter.worker_config.tuner = Some(Arc::new(tuner)); + starter.sdk_config.tuner = Arc::new(tuner); starter .sdk_config .register_activities_static::(); @@ -586,8 +584,8 @@ impl StickyCacheActivities { async fn worker_heartbeat_sticky_cache_miss() { let wf_name = "worker_heartbeat_cache_miss"; let mut starter = new_no_metrics_starter(wf_name); - starter.worker_config.max_cached_workflows = 1_usize; - starter.worker_config.max_outstanding_workflow_tasks = Some(2_usize); + starter.sdk_config.max_cached_workflows = 1_usize; + starter.sdk_config.tuner = Arc::new(TunerHolder::fixed_size(2, 10, 10, 10)); starter .sdk_config .register_activities_static::(); @@ -687,8 +685,8 @@ async fn worker_heartbeat_sticky_cache_miss() { async fn worker_heartbeat_multiple_workers() { let wf_name = "worker_heartbeat_multi_workers"; let mut starter = new_no_metrics_starter(wf_name); - starter.worker_config.max_outstanding_workflow_tasks = Some(5_usize); - starter.worker_config.max_cached_workflows = 5_usize; + starter.sdk_config.max_cached_workflows = 5_usize; + starter.sdk_config.tuner = Arc::new(TunerHolder::fixed_size(5, 10, 10, 10)); starter .sdk_config .register_activities_static::(); @@ -802,7 +800,7 @@ async fn worker_heartbeat_failure_metrics() { let wf_name = "worker_heartbeat_failure_metrics"; let mut starter = new_no_metrics_starter(wf_name); - starter.worker_config.max_outstanding_activities = Some(5_usize); + starter.sdk_config.tuner = Arc::new(TunerHolder::fixed_size(10, 5, 10, 10)); starter .sdk_config .register_activities_static::(); @@ -1038,7 +1036,7 @@ async fn worker_heartbeat_skip_client_worker_set_check() { .unwrap(); let rt = CoreRuntime::new_assume_tokio(runtimeopts).unwrap(); let mut starter = CoreWfStarter::new_with_runtime(wf_name, rt); - starter.worker_config.skip_client_worker_set_check = true; + starter.set_core_cfg_mutator(|m| m.skip_client_worker_set_check = true); starter .sdk_config .register_activities_static::(); diff --git a/crates/sdk-core/tests/integ_tests/worker_tests.rs b/crates/sdk-core/tests/integ_tests/worker_tests.rs index dd3e07660..587b4df43 100644 --- a/crates/sdk-core/tests/integ_tests/worker_tests.rs +++ b/crates/sdk-core/tests/integ_tests/worker_tests.rs @@ -174,17 +174,13 @@ async fn worker_handles_unknown_workflow_types_gracefully() { async fn resource_based_few_pollers_guarantees_non_sticky_poll() { let wf_name = "resource_based_few_pollers_guarantees_non_sticky_poll"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.max_outstanding_workflow_tasks = None; - starter.worker_config.max_outstanding_local_activities = None; - starter.worker_config.max_outstanding_activities = None; - starter.worker_config.max_outstanding_nexus_tasks = None; - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); // 3 pollers so the minimum slots of 2 can both be handed out to a sticky poller - starter.worker_config.workflow_task_poller_behavior = PollerBehavior::SimpleMaximum(3_usize); + starter.sdk_config.workflow_task_poller_behavior = PollerBehavior::SimpleMaximum(3_usize); // Set the limits to zero so it's essentially unwilling to hand out slots let mut tuner = ResourceBasedTuner::new(0.0, 0.0); tuner.with_workflow_slots_options(ResourceSlotOptions::new(2, 10, Duration::from_millis(0))); - starter.worker_config.tuner = Some(Arc::new(tuner)); + starter.sdk_config.tuner = Arc::new(tuner); let mut worker = starter.worker().await; // Workflow doesn't actually need to do anything. We just need to see that we don't get stuck @@ -215,7 +211,7 @@ async fn oversize_grpc_message() { let (telemopts, addr, _aborter) = prom_metrics(None); let runtime = CoreRuntime::new_assume_tokio(get_integ_runtime_options(telemopts)).unwrap(); let mut starter = CoreWfStarter::new_with_runtime(wf_name, runtime); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut core = starter.worker().await; static OVERSIZE_GRPC_MESSAGE_RUN: AtomicBool = AtomicBool::new(false); @@ -712,10 +708,6 @@ async fn test_custom_slot_supplier_simple() { )); let mut starter = CoreWfStarter::new("test_custom_slot_supplier_simple"); - starter.worker_config.max_outstanding_workflow_tasks = None; - starter.worker_config.max_outstanding_local_activities = None; - starter.worker_config.max_outstanding_activities = None; - starter.worker_config.max_outstanding_nexus_tasks = None; starter .sdk_config .register_activities_static::(); @@ -724,7 +716,7 @@ async fn test_custom_slot_supplier_simple() { tb.workflow_slot_supplier(wf_supplier.clone()); tb.activity_slot_supplier(activity_supplier.clone()); tb.local_activity_slot_supplier(local_activity_supplier.clone()); - starter.worker_config.tuner = Some(Arc::new(tb.build())); + starter.sdk_config.tuner = Arc::new(tb.build()); let mut worker = starter.worker().await; diff --git a/crates/sdk-core/tests/integ_tests/worker_versioning_tests.rs b/crates/sdk-core/tests/integ_tests/worker_versioning_tests.rs index 1decff1a4..aff6f331d 100644 --- a/crates/sdk-core/tests/integ_tests/worker_versioning_tests.rs +++ b/crates/sdk-core/tests/integ_tests/worker_versioning_tests.rs @@ -22,7 +22,7 @@ use temporalio_common::{ worker::{WorkerDeploymentOptions, WorkerDeploymentVersion, WorkerTaskTypes}, }; use temporalio_sdk::{ActivityOptions, WfContext}; -use temporalio_sdk_core::{WorkerVersioningStrategy, test_help::WorkerTestHelpers}; +use temporalio_sdk_core::test_help::WorkerTestHelpers; use tokio::join; use tonic::IntoRequest; @@ -36,13 +36,12 @@ async fn sets_deployment_info_on_task_responses(#[values(true, false)] use_defau deployment_name: deploy_name.clone(), build_id: "1.0".to_string(), }; - starter.worker_config.versioning_strategy = - WorkerVersioningStrategy::WorkerDeploymentBased(WorkerDeploymentOptions { - version: version.clone(), - use_worker_versioning: true, - default_versioning_behavior: VersioningBehavior::AutoUpgrade.into(), - }); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.deployment_options = WorkerDeploymentOptions { + version: version.clone(), + use_worker_versioning: true, + default_versioning_behavior: VersioningBehavior::AutoUpgrade.into(), + }; + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let core = starter.get_worker().await; let client = starter.get_client().await; @@ -149,15 +148,14 @@ async fn activity_has_deployment_stamp() { let wf_name = "activity_has_deployment_stamp"; let mut starter = CoreWfStarter::new(wf_name); let deploy_name = format!("deployment-{}", starter.get_task_queue()); - starter.worker_config.versioning_strategy = - WorkerVersioningStrategy::WorkerDeploymentBased(WorkerDeploymentOptions { - version: WorkerDeploymentVersion { - deployment_name: deploy_name.clone(), - build_id: "1.0".to_string(), - }, - use_worker_versioning: true, - default_versioning_behavior: VersioningBehavior::AutoUpgrade.into(), - }); + starter.sdk_config.deployment_options = WorkerDeploymentOptions { + version: WorkerDeploymentVersion { + deployment_name: deploy_name.clone(), + build_id: "1.0".to_string(), + }, + use_worker_versioning: true, + default_versioning_behavior: VersioningBehavior::AutoUpgrade.into(), + }; starter .sdk_config .register_activities_static::(); diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests.rs b/crates/sdk-core/tests/integ_tests/workflow_tests.rs index 41a899734..bba31a498 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests.rs @@ -65,7 +65,7 @@ use temporalio_sdk::{ ActivityOptions, LocalActivityOptions, TimerOptions, WfContext, interceptors::WorkerInterceptor, }; use temporalio_sdk_core::{ - CoreRuntime, PollError, PollerBehavior, WorkerVersioningStrategy, WorkflowErrorType, + CoreRuntime, PollError, PollerBehavior, TunerHolder, WorkflowErrorType, replay::HistoryForReplay, test_help::{MockPollCfg, WorkerTestHelpers, drain_pollers_and_shutdown}, }; @@ -77,7 +77,7 @@ use tokio::{join, sync::Notify, time::sleep}; async fn parallel_workflows_same_queue() { let wf_name = "parallel_workflows_same_queue"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut core = starter.worker().await; core.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { @@ -364,9 +364,9 @@ async fn wft_timeout_doesnt_create_unsolvable_autocomplete() { let signal_at_complete = "at-complete"; let mut wf_starter = CoreWfStarter::new("wft_timeout_doesnt_create_unsolvable_autocomplete"); // Test needs eviction on and a short timeout - wf_starter.worker_config.max_cached_workflows = 0_usize; - wf_starter.worker_config.max_outstanding_workflow_tasks = Some(1_usize); - wf_starter.worker_config.workflow_task_poller_behavior = PollerBehavior::SimpleMaximum(1_usize); + wf_starter.sdk_config.max_cached_workflows = 0_usize; + wf_starter.sdk_config.tuner = Arc::new(TunerHolder::fixed_size(1, 1, 1, 1)); + wf_starter.sdk_config.workflow_task_poller_behavior = PollerBehavior::SimpleMaximum(1_usize); wf_starter.workflow_options.task_timeout = Some(Duration::from_secs(1)); let core = wf_starter.get_worker().await; let client = wf_starter.get_client().await; @@ -472,8 +472,8 @@ async fn wft_timeout_doesnt_create_unsolvable_autocomplete() { async fn slow_completes_with_small_cache() { let wf_name = "slow_completes_with_small_cache"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.max_outstanding_workflow_tasks = Some(5_usize); - starter.worker_config.max_cached_workflows = 5_usize; + starter.sdk_config.tuner = Arc::new(TunerHolder::fixed_size(5, 10, 1, 1)); + starter.sdk_config.max_cached_workflows = 5_usize; let mut worker = starter.worker().await; worker.register_activities_static::(); @@ -525,22 +525,26 @@ async fn slow_completes_with_small_cache() { async fn deployment_version_correct_in_wf_info(#[values(true, false)] use_only_build_id: bool) { let wf_type = "deployment_version_correct_in_wf_info"; let mut starter = CoreWfStarter::new(wf_type); - let version_strat = if use_only_build_id { - WorkerVersioningStrategy::None { - build_id: "1.0".to_owned(), + starter.sdk_config.deployment_options = if use_only_build_id { + WorkerDeploymentOptions { + version: WorkerDeploymentVersion { + deployment_name: "".to_string(), + build_id: "1.0".to_string(), + }, + use_worker_versioning: false, + default_versioning_behavior: None, } } else { - WorkerVersioningStrategy::WorkerDeploymentBased(WorkerDeploymentOptions { + WorkerDeploymentOptions { version: WorkerDeploymentVersion { deployment_name: "deployment-1".to_string(), build_id: "1.0".to_string(), }, use_worker_versioning: false, default_versioning_behavior: None, - }) + } }; - starter.worker_config.versioning_strategy = version_strat; - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let core = starter.get_worker().await; starter.start_wf().await; let client = starter.get_client().await; @@ -630,21 +634,25 @@ async fn deployment_version_correct_in_wf_info(#[values(true, false)] use_only_b .unwrap(); let mut starter = starter.clone_no_worker(); - let version_strat = if use_only_build_id { - WorkerVersioningStrategy::None { - build_id: "2.0".to_owned(), + starter.sdk_config.deployment_options = if use_only_build_id { + WorkerDeploymentOptions { + version: WorkerDeploymentVersion { + deployment_name: "".to_string(), + build_id: "2.0".to_string(), + }, + use_worker_versioning: false, + default_versioning_behavior: None, } } else { - WorkerVersioningStrategy::WorkerDeploymentBased(WorkerDeploymentOptions { + WorkerDeploymentOptions { version: WorkerDeploymentVersion { deployment_name: "deployment-1".to_string(), build_id: "2.0".to_string(), }, use_worker_versioning: false, default_versioning_behavior: None, - }) + } }; - starter.worker_config.versioning_strategy = version_strat; let core = starter.get_worker().await; @@ -763,12 +771,12 @@ async fn nondeterminism_errors_fail_workflow_when_configured_to( let rt = CoreRuntime::new_assume_tokio(get_integ_runtime_options(telemopts)).unwrap(); let wf_name = "nondeterminism_errors_fail_workflow_when_configured_to"; let mut starter = CoreWfStarter::new_with_runtime(wf_name, rt); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let typeset = HashSet::from([WorkflowErrorType::Nondeterminism]); if whole_worker { - starter.worker_config.workflow_failure_errors = typeset; + starter.sdk_config.workflow_failure_errors = typeset; } else { - starter.worker_config.workflow_types_to_failure_errors = + starter.sdk_config.workflow_types_to_failure_errors = HashMap::from([(wf_name.to_owned(), typeset)]); } let wf_id = starter.get_task_queue().to_owned(); @@ -846,8 +854,7 @@ async fn nondeterminism_errors_fail_workflow_when_configured_to( async fn history_out_of_order_on_restart() { let wf_name = "history_out_of_order_on_restart"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.workflow_failure_errors = - HashSet::from([WorkflowErrorType::Nondeterminism]); + starter.sdk_config.workflow_failure_errors = HashSet::from([WorkflowErrorType::Nondeterminism]); let mut worker = starter.worker().await; let mut starter2 = starter.clone_no_worker(); let mut worker2 = starter2.worker().await; diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/activities.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/activities.rs index 470c8337d..d6de5fa5b 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/activities.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/activities.rs @@ -1103,7 +1103,7 @@ impl SleeperActivities { async fn graceful_shutdown() { let wf_name = "graceful_shutdown"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.graceful_shutdown_period = Some(Duration::from_millis(500)); + starter.sdk_config.graceful_shutdown_period = Some(Duration::from_millis(500)); starter .sdk_config .register_activities_static::(); @@ -1178,7 +1178,8 @@ impl CancellableEchoActivities { async fn activity_can_be_cancelled_by_local_timeout() { let wf_name = "activity_can_be_cancelled_by_local_timeout"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.local_timeout_buffer_for_activities = Duration::from_secs(0); + starter + .set_core_cfg_mutator(|m| m.local_timeout_buffer_for_activities = Duration::from_secs(0)); starter .sdk_config .register_activities_static::(); @@ -1214,17 +1215,18 @@ async fn activity_can_be_cancelled_by_local_timeout() { async fn long_activity_timeout_repro() { let wf_name = "long_activity_timeout_repro"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.workflow_task_poller_behavior = PollerBehavior::Autoscaling { + starter.sdk_config.workflow_task_poller_behavior = PollerBehavior::Autoscaling { minimum: 1, maximum: 10, initial: 5, }; - starter.worker_config.activity_task_poller_behavior = PollerBehavior::Autoscaling { + starter.sdk_config.activity_task_poller_behavior = PollerBehavior::Autoscaling { minimum: 1, maximum: 10, initial: 5, }; - starter.worker_config.local_timeout_buffer_for_activities = Duration::from_secs(0); + starter + .set_core_cfg_mutator(|m| m.local_timeout_buffer_for_activities = Duration::from_secs(0)); starter .sdk_config .register_activities_static::(); diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/cancel_external.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/cancel_external.rs index d695bbecb..5f7b0dbee 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/cancel_external.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/cancel_external.rs @@ -42,7 +42,7 @@ async fn cancel_receiver(ctx: WfContext) -> WorkflowResult { #[tokio::test] async fn sends_cancel_to_other_wf() { let mut starter = CoreWfStarter::new("sends_cancel_to_other_wf"); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut worker = starter.worker().await; worker.register_wf("sender", cancel_sender); worker.register_wf("receiver", cancel_receiver); diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/cancel_wf.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/cancel_wf.rs index 580d80995..bf5f5a73a 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/cancel_wf.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/cancel_wf.rs @@ -35,7 +35,7 @@ async fn cancelled_wf(ctx: WfContext) -> WorkflowResult<()> { async fn cancel_during_timer() { let wf_name = "cancel_during_timer"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut worker = starter.worker().await; let client = starter.get_client().await; worker.register_wf(wf_name.to_string(), cancelled_wf); diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/child_workflows.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/child_workflows.rs index 6078fc4c7..d82f5a5a4 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/child_workflows.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/child_workflows.rs @@ -85,7 +85,7 @@ async fn happy_parent(ctx: WfContext) -> WorkflowResult<()> { #[tokio::test] async fn child_workflow_happy_path() { let mut starter = CoreWfStarter::new("child-workflows"); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut worker = starter.worker().await; worker.register_wf(PARENT_WF_TYPE.to_string(), happy_parent); @@ -106,7 +106,7 @@ async fn child_workflow_happy_path() { #[tokio::test] async fn abandoned_child_bug_repro() { let mut starter = CoreWfStarter::new("child-workflow-abandon-bug"); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut worker = starter.worker().await; let barr: &'static Barrier = Box::leak(Box::new(Barrier::new(2))); @@ -177,7 +177,7 @@ async fn abandoned_child_bug_repro() { #[tokio::test] async fn abandoned_child_resolves_post_cancel() { let mut starter = CoreWfStarter::new("child-workflow-resolves-post-cancel"); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut worker = starter.worker().await; let barr: &'static Barrier = Box::leak(Box::new(Barrier::new(2))); @@ -244,7 +244,7 @@ async fn abandoned_child_resolves_post_cancel() { async fn cancelled_child_gets_reason() { let wf_name = "cancelled-child-gets-reason"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_string(), move |ctx: WfContext| async move { diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/continue_as_new.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/continue_as_new.rs index fef4bb2c5..c9ea06b17 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/continue_as_new.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/continue_as_new.rs @@ -1,5 +1,5 @@ use crate::common::{CoreWfStarter, build_fake_sdk}; -use std::time::Duration; +use std::{sync::Arc, time::Duration}; use temporalio_client::WorkflowOptions; use temporalio_common::{ protos::{ @@ -10,7 +10,7 @@ use temporalio_common::{ worker::WorkerTaskTypes, }; use temporalio_sdk::{WfContext, WfExitValue, WorkflowResult}; -use temporalio_sdk_core::test_help::MockPollCfg; +use temporalio_sdk_core::{TunerHolder, test_help::MockPollCfg}; async fn continue_as_new_wf(ctx: WfContext) -> WorkflowResult<()> { let run_ct = ctx.get_args()[0].data[0]; @@ -29,7 +29,7 @@ async fn continue_as_new_wf(ctx: WfContext) -> WorkflowResult<()> { async fn continue_as_new_happy_path() { let wf_name = "continue_as_new_happy_path"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_string(), continue_as_new_wf); @@ -49,9 +49,9 @@ async fn continue_as_new_happy_path() { async fn continue_as_new_multiple_concurrent() { let wf_name = "continue_as_new_multiple_concurrent"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); - starter.worker_config.max_cached_workflows = 5_usize; - starter.worker_config.max_outstanding_workflow_tasks = Some(5_usize); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.max_cached_workflows = 5_usize; + starter.sdk_config.tuner = Arc::new(TunerHolder::fixed_size(5, 1, 1, 1)); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_string(), continue_as_new_wf); diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/determinism.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/determinism.rs index 1c4f99103..354cbf306 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/determinism.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/determinism.rs @@ -57,7 +57,7 @@ pub(crate) async fn timer_wf_nondeterministic(ctx: WfContext) -> WorkflowResult< async fn test_determinism_error_then_recovers() { let wf_name = "test_determinism_error_then_recovers"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), timer_wf_nondeterministic); diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/eager.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/eager.rs index f89730a12..4082f6605 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/eager.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/eager.rs @@ -15,7 +15,7 @@ async fn eager_wf_start() { starter.workflow_options.enable_eager_workflow_start = true; // hang the test if eager task dispatch failed starter.workflow_options.task_timeout = Some(Duration::from_secs(1500)); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), eager_wf); starter.eager_start_with_worker(wf_name, &mut worker).await; @@ -29,7 +29,7 @@ async fn eager_wf_start_different_clients() { starter.workflow_options.enable_eager_workflow_start = true; // hang the test if wf task needs retry starter.workflow_options.task_timeout = Some(Duration::from_secs(1500)); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), eager_wf); diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/local_activities.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/local_activities.rs index bd0b15c72..0ac369031 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/local_activities.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/local_activities.rs @@ -57,7 +57,7 @@ use temporalio_sdk::{ interceptors::{FailOnNondeterminismInterceptor, WorkerInterceptor}, }; use temporalio_sdk_core::{ - PollError, prost_dur, + PollError, TunerHolder, prost_dur, replay::{DEFAULT_WORKFLOW_TYPE, HistoryForReplay, TestHistoryBuilder, default_wes_attribs}, test_help::{ LEGACY_QUERY_ID, MockPollCfg, ResponseType, WorkerExt, WorkerTestHelpers, @@ -184,7 +184,7 @@ pub(crate) async fn local_act_fanout_wf(ctx: WfContext) -> WorkflowResult<()> { async fn local_act_fanout() { let wf_name = "local_act_fanout"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.max_outstanding_local_activities = Some(1_usize); + starter.sdk_config.tuner = Arc::new(TunerHolder::fixed_size(5, 1, 1, 1)); starter .sdk_config .register_activities_static::(); @@ -520,7 +520,7 @@ async fn schedule_to_close_timeout_across_timer_backoff(#[case] cached: bool) { ); let mut starter = CoreWfStarter::new(&wf_name); if !cached { - starter.worker_config.max_cached_workflows = 0_usize; + starter.sdk_config.max_cached_workflows = 0_usize; } let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { @@ -561,7 +561,7 @@ async fn schedule_to_close_timeout_across_timer_backoff(#[case] cached: bool) { async fn eviction_wont_make_local_act_get_dropped(#[values(true, false)] short_wft_timeout: bool) { let wf_name = format!("eviction_wont_make_local_act_get_dropped_{short_wft_timeout}"); let mut starter = CoreWfStarter::new(&wf_name); - starter.worker_config.max_cached_workflows = 0_usize; + starter.sdk_config.max_cached_workflows = 0_usize; starter .sdk_config .register_activities_static::(); @@ -782,7 +782,7 @@ async fn la_resolve_same_time_as_other_cancel() { .sdk_config .register_activities_static::(); // The activity won't get a chance to receive the cancel so make sure we still exit fast - starter.worker_config.graceful_shutdown_period = Some(Duration::from_millis(100)); + starter.sdk_config.graceful_shutdown_period = Some(Duration::from_millis(100)); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { @@ -2392,7 +2392,7 @@ impl ActivityWithExplicitBackoff { let last_attempt = self.attempts.fetch_add(1, Ordering::Relaxed); if 0 == last_attempt { Err(ActivityError::Retryable { - source: anyhow!("Explicit backoff error"), + source: anyhow!("Explicit backoff error").into_boxed_dyn_error(), explicit_delay: Some(Duration::from_millis(300)), }) } else if 2 == last_attempt { diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/modify_wf_properties.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/modify_wf_properties.rs index 4af1162e8..3e2b801b6 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/modify_wf_properties.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/modify_wf_properties.rs @@ -32,7 +32,7 @@ async fn sends_modify_wf_props() { let wf_name = "can_upsert_memo"; let wf_id = Uuid::new_v4(); let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut worker = starter.worker().await; worker.register_wf(wf_name, memo_upserter); diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/nexus.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/nexus.rs index 708123408..5cdbd0231 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/nexus.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/nexus.rs @@ -58,7 +58,7 @@ async fn nexus_basic( ) { let wf_name = "nexus_basic"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.task_types = WorkerTaskTypes { + starter.sdk_config.task_types = WorkerTaskTypes { enable_workflows: true, enable_local_activities: false, enable_remote_activities: false, @@ -208,7 +208,7 @@ async fn nexus_async( ) { let wf_name = "nexus_async"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.task_types = WorkerTaskTypes { + starter.sdk_config.task_types = WorkerTaskTypes { enable_workflows: true, enable_local_activities: false, enable_remote_activities: false, @@ -440,7 +440,7 @@ async fn nexus_async( async fn nexus_cancel_before_start() { let wf_name = "nexus_cancel_before_start"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.task_types = WorkerTaskTypes { + starter.sdk_config.task_types = WorkerTaskTypes { enable_workflows: true, enable_local_activities: false, enable_remote_activities: false, @@ -487,14 +487,14 @@ async fn nexus_cancel_before_start() { async fn nexus_must_complete_task_to_shutdown(#[values(true, false)] use_grace_period: bool) { let wf_name = "nexus_must_complete_task_to_shutdown"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.task_types = WorkerTaskTypes { + starter.sdk_config.task_types = WorkerTaskTypes { enable_workflows: true, enable_local_activities: false, enable_remote_activities: false, enable_nexus: true, }; if use_grace_period { - starter.worker_config.graceful_shutdown_period = Some(Duration::from_millis(500)); + starter.sdk_config.graceful_shutdown_period = Some(Duration::from_millis(500)); } let mut worker = starter.worker().await; let core_worker = starter.get_worker().await; @@ -590,7 +590,7 @@ async fn nexus_cancellation_types( ) { let wf_name = "nexus_cancellation_types"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.task_types = WorkerTaskTypes { + starter.sdk_config.task_types = WorkerTaskTypes { enable_workflows: true, enable_local_activities: false, enable_remote_activities: false, diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/patches.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/patches.rs index a9b8dfb79..1de1bdc63 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/patches.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/patches.rs @@ -57,7 +57,7 @@ pub(crate) async fn changes_wf(ctx: WfContext) -> WorkflowResult<()> { async fn writes_change_markers() { let wf_name = "writes_change_markers"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), changes_wf); @@ -91,7 +91,7 @@ pub(crate) async fn no_change_then_change_wf(ctx: WfContext) -> WorkflowResult<( async fn can_add_change_markers() { let wf_name = "can_add_change_markers"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), no_change_then_change_wf); @@ -115,7 +115,7 @@ pub(crate) async fn replay_with_change_marker_wf(ctx: WfContext) -> WorkflowResu async fn replaying_with_patch_marker() { let wf_name = "replaying_with_patch_marker"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), replay_with_change_marker_wf); @@ -131,8 +131,8 @@ async fn patched_on_second_workflow_task_is_deterministic() { let wf_name = "timer_patched_timer"; let mut starter = CoreWfStarter::new(wf_name); // Disable caching to force replay from beginning - starter.worker_config.max_cached_workflows = 0_usize; - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.max_cached_workflows = 0_usize; + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut worker = starter.worker().await; // Include a task failure as well to make sure that works static FAIL_ONCE: AtomicBool = AtomicBool::new(true); @@ -155,7 +155,7 @@ async fn patched_on_second_workflow_task_is_deterministic() { async fn can_remove_deprecated_patch_near_other_patch() { let wf_name = "can_add_change_markers"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut worker = starter.worker().await; let did_die = Arc::new(AtomicBool::new(false)); worker.register_wf(wf_name.to_owned(), move |ctx: WfContext| { @@ -186,7 +186,7 @@ async fn can_remove_deprecated_patch_near_other_patch() { async fn deprecated_patch_removal() { let wf_name = "deprecated_patch_removal"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut worker = starter.worker().await; let client = starter.get_client().await; let wf_id = starter.get_task_queue().to_string(); diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/resets.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/resets.rs index 4e4242bcd..a2697b708 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/resets.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/resets.rs @@ -26,7 +26,7 @@ const POST_RESET_SIG: &str = "post-reset"; async fn reset_workflow() { let wf_name = "reset_me_wf"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut worker = starter.worker().await; worker.fetch_results = false; let notify = Arc::new(Notify::new()); @@ -111,7 +111,7 @@ async fn reset_workflow() { async fn reset_randomseed() { let wf_name = "reset_randomseed"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.task_types = WorkerTaskTypes { + starter.sdk_config.task_types = WorkerTaskTypes { enable_workflows: true, enable_local_activities: true, enable_remote_activities: false, diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/signals.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/signals.rs index fabe71fbc..49fc43049 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/signals.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/signals.rs @@ -48,7 +48,7 @@ async fn signal_sender(ctx: WfContext) -> WorkflowResult<()> { async fn sends_signal_to_missing_wf() { let wf_name = "sends_signal_to_missing_wf"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), signal_sender); @@ -87,7 +87,7 @@ async fn signal_with_create_wf_receiver(ctx: WfContext) -> WorkflowResult<()> { #[tokio::test] async fn sends_signal_to_other_wf() { let mut starter = CoreWfStarter::new("sends_signal_to_other_wf"); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut worker = starter.worker().await; worker.register_wf("sender", signal_sender); worker.register_wf("receiver", signal_receiver); @@ -116,7 +116,7 @@ async fn sends_signal_to_other_wf() { #[tokio::test] async fn sends_signal_with_create_wf() { let mut starter = CoreWfStarter::new("sends_signal_with_create_wf"); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut worker = starter.worker().await; worker.register_wf("receiver_signal", signal_with_create_wf_receiver); @@ -161,7 +161,7 @@ async fn signals_child(ctx: WfContext) -> WorkflowResult<()> { #[tokio::test] async fn sends_signal_to_child() { let mut starter = CoreWfStarter::new("sends_signal_to_child"); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut worker = starter.worker().await; worker.register_wf("child_signaler", signals_child); worker.register_wf("child_receiver", signal_receiver); diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/stickyness.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/stickyness.rs index dd7975571..4eec1bd5c 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/stickyness.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/stickyness.rs @@ -1,20 +1,23 @@ use crate::{common::CoreWfStarter, integ_tests::workflow_tests::timers::timer_wf}; use std::{ - sync::atomic::{AtomicBool, AtomicUsize, Ordering}, + sync::{ + Arc, + atomic::{AtomicBool, AtomicUsize, Ordering}, + }, time::Duration, }; use temporalio_client::WorkflowOptions; use temporalio_common::worker::WorkerTaskTypes; use temporalio_sdk::{WfContext, WorkflowResult}; -use temporalio_sdk_core::PollerBehavior; +use temporalio_sdk_core::{PollerBehavior, TunerHolder}; use tokio::sync::Barrier; #[tokio::test] async fn timer_workflow_not_sticky() { let wf_name = "timer_wf_not_sticky"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); - starter.worker_config.max_cached_workflows = 0_usize; + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.max_cached_workflows = 0_usize; let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), timer_wf); @@ -41,7 +44,7 @@ async fn timer_workflow_timeout_on_sticky() { // on a not-sticky queue let wf_name = "timer_workflow_timeout_on_sticky"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); starter.workflow_options.task_timeout = Some(Duration::from_secs(2)); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), timer_timeout_wf); @@ -56,10 +59,10 @@ async fn timer_workflow_timeout_on_sticky() { async fn cache_miss_ok() { let wf_name = "cache_miss_ok"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); - starter.worker_config.max_outstanding_workflow_tasks = Some(2_usize); - starter.worker_config.max_cached_workflows = 0_usize; - starter.worker_config.workflow_task_poller_behavior = PollerBehavior::SimpleMaximum(1_usize); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.tuner = Arc::new(TunerHolder::fixed_size(2, 1, 1, 1)); + starter.sdk_config.max_cached_workflows = 0_usize; + starter.sdk_config.workflow_task_poller_behavior = PollerBehavior::SimpleMaximum(1_usize); let mut worker = starter.worker().await; let barr: &'static Barrier = Box::leak(Box::new(Barrier::new(2))); diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/timers.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/timers.rs index 5c0cbab57..a5ec217a5 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/timers.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/timers.rs @@ -28,7 +28,7 @@ pub(crate) async fn timer_wf(command_sink: WfContext) -> WorkflowResult<()> { async fn timer_workflow_workflow_driver() { let wf_name = "timer_wf_new"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), timer_wf); @@ -40,7 +40,7 @@ async fn timer_workflow_workflow_driver() { async fn timer_workflow_manual() { let mut starter = init_core_and_create_wf("timer_workflow").await; let core = starter.get_worker().await; - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let task = core.poll_workflow_activation().await.unwrap(); core.complete_workflow_activation(WorkflowActivationCompletion::from_cmds( task.run_id, @@ -64,7 +64,7 @@ async fn timer_workflow_manual() { async fn timer_cancel_workflow() { let mut starter = init_core_and_create_wf("timer_cancel_workflow").await; let core = starter.get_worker().await; - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let task = core.poll_workflow_activation().await.unwrap(); core.complete_workflow_activation(WorkflowActivationCompletion::from_cmds( task.run_id, @@ -123,7 +123,7 @@ async fn parallel_timer_wf(command_sink: WfContext) -> WorkflowResult<()> { async fn parallel_timers() { let wf_name = "parallel_timers"; let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), parallel_timer_wf); diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/upsert_search_attrs.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/upsert_search_attrs.rs index 81ebdc7a7..df068fe2b 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/upsert_search_attrs.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/upsert_search_attrs.rs @@ -47,7 +47,7 @@ async fn sends_upsert() { let wf_name = "sends_upsert_search_attrs"; let wf_id = Uuid::new_v4(); let mut starter = CoreWfStarter::new(wf_name); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut worker = starter.worker().await; worker.register_wf(wf_name, search_attr_updater); diff --git a/crates/sdk-core/tests/manual_tests.rs b/crates/sdk-core/tests/manual_tests.rs index b7291fa5e..443822ee0 100644 --- a/crates/sdk-core/tests/manual_tests.rs +++ b/crates/sdk-core/tests/manual_tests.rs @@ -17,6 +17,7 @@ use rand::{Rng, SeedableRng}; use std::{ mem, net::SocketAddr, + sync::Arc, time::{Duration, Instant}, }; use temporalio_client::{ @@ -28,7 +29,7 @@ use temporalio_sdk::{ ActivityOptions, WfContext, activities::{ActivityContext, ActivityError}, }; -use temporalio_sdk_core::{CoreRuntime, PollerBehavior}; +use temporalio_sdk_core::{CoreRuntime, PollerBehavior, TunerHolder}; use tracing::info; struct JitteryEchoActivities {} @@ -60,15 +61,14 @@ async fn poller_load_spiky() { }; let rt = CoreRuntime::new_assume_tokio(get_integ_runtime_options(telemopts)).unwrap(); let mut starter = CoreWfStarter::new_with_runtime("poller_load", rt); - starter.worker_config.max_cached_workflows = 5000; - starter.worker_config.max_outstanding_workflow_tasks = Some(1000); - starter.worker_config.max_outstanding_activities = Some(1000); - starter.worker_config.workflow_task_poller_behavior = PollerBehavior::Autoscaling { + starter.sdk_config.max_cached_workflows = 5000; + starter.sdk_config.tuner = Arc::new(TunerHolder::fixed_size(1000, 1000, 100, 100)); + starter.sdk_config.workflow_task_poller_behavior = PollerBehavior::Autoscaling { minimum: 1, maximum: 200, initial: 5, }; - starter.worker_config.activity_task_poller_behavior = PollerBehavior::Autoscaling { + starter.sdk_config.activity_task_poller_behavior = PollerBehavior::Autoscaling { minimum: 1, maximum: 200, initial: 5, @@ -214,14 +214,14 @@ async fn poller_load_sustained() { }; let rt = CoreRuntime::new_assume_tokio(get_integ_runtime_options(telemopts)).unwrap(); let mut starter = CoreWfStarter::new_with_runtime("poller_load", rt); - starter.worker_config.max_cached_workflows = 5000; - starter.worker_config.max_outstanding_workflow_tasks = Some(1000); - starter.worker_config.workflow_task_poller_behavior = PollerBehavior::Autoscaling { + starter.sdk_config.max_cached_workflows = 5000; + starter.sdk_config.tuner = Arc::new(TunerHolder::fixed_size(1000, 100, 100, 100)); + starter.sdk_config.workflow_task_poller_behavior = PollerBehavior::Autoscaling { minimum: 1, maximum: 200, initial: 5, }; - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let sigchan = ctx.make_signal_channel(SIGNAME).map(Ok); @@ -302,14 +302,14 @@ async fn poller_load_spike_then_sustained() { }; let rt = CoreRuntime::new_assume_tokio(get_integ_runtime_options(telemopts)).unwrap(); let mut starter = CoreWfStarter::new_with_runtime("poller_load", rt); - starter.worker_config.max_cached_workflows = 5000; - starter.worker_config.max_outstanding_workflow_tasks = Some(1000); - starter.worker_config.workflow_task_poller_behavior = PollerBehavior::Autoscaling { + starter.sdk_config.max_cached_workflows = 5000; + starter.sdk_config.tuner = Arc::new(TunerHolder::fixed_size(1000, 100, 100, 100)); + starter.sdk_config.workflow_task_poller_behavior = PollerBehavior::Autoscaling { minimum: 1, maximum: 200, initial: 5, }; - starter.worker_config.activity_task_poller_behavior = PollerBehavior::Autoscaling { + starter.sdk_config.activity_task_poller_behavior = PollerBehavior::Autoscaling { minimum: 1, maximum: 200, initial: 5, diff --git a/crates/sdk-core/tests/shared_tests/mod.rs b/crates/sdk-core/tests/shared_tests/mod.rs index e89e47d8b..772317dda 100644 --- a/crates/sdk-core/tests/shared_tests/mod.rs +++ b/crates/sdk-core/tests/shared_tests/mod.rs @@ -20,7 +20,7 @@ pub(crate) async fn grpc_message_too_large() { let mut starter = CoreWfStarter::new_cloud_or_local(wf_name, "") .await .unwrap(); - starter.worker_config.task_types = WorkerTaskTypes::workflow_only(); + starter.sdk_config.task_types = WorkerTaskTypes::workflow_only(); let mut core = starter.worker().await; static OVERSIZE_GRPC_MESSAGE_RUN: AtomicBool = AtomicBool::new(false); diff --git a/crates/sdk/src/activities.rs b/crates/sdk/src/activities.rs index 4a2fd51ba..534a07492 100644 --- a/crates/sdk/src/activities.rs +++ b/crates/sdk/src/activities.rs @@ -194,8 +194,7 @@ pub struct ActivityInfo { pub priority: Priority, } -// TODO [rust-sdk-branch]: Remove anyhow from public interfaces -/// Returned as errors from activity functions +/// Returned as errors from activity functions. #[derive(Debug)] pub enum ActivityError { /// This error can be returned from activities to allow the explicit configuration of certain @@ -203,7 +202,7 @@ pub enum ActivityError { /// into. Retryable { /// The underlying error - source: anyhow::Error, + source: Box, /// If specified, the next retry (if there is one) will occur after this delay explicit_delay: Option, }, @@ -213,7 +212,7 @@ pub enum ActivityError { details: Option, }, /// Return this error to indicate that the activity should not be retried. - NonRetryable(anyhow::Error), + NonRetryable(Box), /// Return this error to indicate that the activity will be completed outside of this activity /// definition, by an external client. WillCompleteAsync, @@ -232,7 +231,7 @@ where { fn from(source: E) -> Self { Self::Retryable { - source: source.into(), + source: source.into().into_boxed_dyn_error(), explicit_delay: None, } } diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index eb377f2a3..438484816 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -98,7 +98,7 @@ use serde::Serialize; use std::{ any::{Any, TypeId}, cell::RefCell, - collections::HashMap, + collections::{HashMap, HashSet}, fmt::{Debug, Display, Formatter}, future::Future, panic::AssertUnwindSafe, @@ -140,7 +140,7 @@ use temporalio_common::{ }; use temporalio_sdk_core::{ CoreRuntime, PollError, PollerBehavior, TunerBuilder, Url, Worker as CoreWorker, WorkerConfig, - WorkerTuner, WorkerVersioningStrategy, init_worker, + WorkerTuner, WorkerVersioningStrategy, WorkflowErrorType, init_worker, }; use tokio::{ sync::{ @@ -241,6 +241,14 @@ pub struct WorkerOptions { /// would cause it to exceed this limit. Negative, zero, or NaN values will cause building /// the options to fail. pub max_worker_activities_per_second: Option, + /// Any error types listed here will cause any workflow being processed by this worker to fail, + /// rather than simply failing the workflow task. + #[builder(default)] + pub workflow_failure_errors: HashSet, + /// Like [WorkerConfig::workflow_failure_errors], but specific to certain workflow types (the + /// map key). + #[builder(default)] + pub workflow_types_to_failure_errors: HashMap>, /// If set, the worker will issue cancels for all outstanding activities and nexus operations after /// shutdown has been initiated and this amount of time has elapsed. pub graceful_shutdown_period: Option, @@ -807,7 +815,10 @@ impl ActivityHalf { source, explicit_delay, } => ActivityExecutionResult::fail({ - let mut f = Failure::application_failure_from_error(source, false); + let mut f = Failure::application_failure_from_error( + anyhow::Error::from_boxed(source), + false, + ); if let Some(d) = explicit_delay && let Some(failure::FailureInfo::ApplicationFailureInfo(fi)) = f.failure_info.as_mut() @@ -820,7 +831,10 @@ impl ActivityHalf { ActivityExecutionResult::cancel_from_details(details) } ActivityError::NonRetryable(nre) => ActivityExecutionResult::fail( - Failure::application_failure_from_error(nre, true), + Failure::application_failure_from_error( + anyhow::Error::from_boxed(nre), + true, + ), ), ActivityError::WillCompleteAsync => { ActivityExecutionResult::will_complete_async() @@ -1210,7 +1224,9 @@ where } ActExitValue::Normal(x) => match x.as_json_payload() { Ok(v) => Ok(ActExitValue::Normal(v)), - Err(e) => Err(ActivityError::NonRetryable(e)), + Err(e) => { + Err(ActivityError::NonRetryable(e.into_boxed_dyn_error())) + } }, } }) @@ -1360,6 +1376,8 @@ impl TryFrom for WorkerConfig { .versioning_strategy(WorkerVersioningStrategy::WorkerDeploymentBased( o.deployment_options, )) + .workflow_failure_errors(o.workflow_failure_errors) + .workflow_types_to_failure_errors(o.workflow_types_to_failure_errors) .build() } } From 853562f5c23da98b5e9369ad4b5a0959db9b855c Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Fri, 2 Jan 2026 13:08:40 -0800 Subject: [PATCH 07/13] Fix todos in payload conversion errors --- crates/sdk/src/activities.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/sdk/src/activities.rs b/crates/sdk/src/activities.rs index 534a07492..42b68add0 100644 --- a/crates/sdk/src/activities.rs +++ b/crates/sdk/src/activities.rs @@ -362,7 +362,7 @@ impl ActivityDefinitions { .map(move |v| match v { Ok(okv) => pc2 .to_payload(&okv, &SerializationContext::Activity) - .map_err(|_| todo!()), + .map_err(|e| e.into()), Err(e) => Err(e), }) .boxed()) @@ -384,7 +384,7 @@ impl ActivityDefinitions { .map(move |v| match v { Ok(okv) => pc2 .to_payload(&okv, &SerializationContext::Activity) - .map_err(|_| todo!()), + .map_err(|e| e.into()), Err(e) => Err(e), }) .boxed()) From 2f355b97b905d6ebfc38ade2bf863bf4bfdf06b6 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Fri, 2 Jan 2026 17:38:57 -0800 Subject: [PATCH 08/13] Use client for worker init --- crates/sdk-core/tests/common/mod.rs | 13 ++--- crates/sdk/src/lib.rs | 77 ++++++++++++++--------------- 2 files changed, 44 insertions(+), 46 deletions(-) diff --git a/crates/sdk-core/tests/common/mod.rs b/crates/sdk-core/tests/common/mod.rs index 3627c658a..93e67387e 100644 --- a/crates/sdk-core/tests/common/mod.rs +++ b/crates/sdk-core/tests/common/mod.rs @@ -100,9 +100,13 @@ pub(crate) async fn init_core_and_create_wf(test_name: &str) -> CoreWfStarter { starter } +pub(crate) fn integ_namespace() -> String { + env::var(INTEG_NAMESPACE_ENV_VAR).unwrap_or(NAMESPACE.to_string()) +} + pub(crate) fn integ_worker_config(tq: &str) -> WorkerConfig { WorkerConfig::builder() - .namespace(env::var(INTEG_NAMESPACE_ENV_VAR).unwrap_or(NAMESPACE.to_string())) + .namespace(integ_namespace()) .task_queue(tq) .max_outstanding_activities(100_usize) .max_outstanding_local_activities(100_usize) @@ -118,7 +122,6 @@ pub(crate) fn integ_worker_config(tq: &str) -> WorkerConfig { pub(crate) fn integ_sdk_config(tq: &str) -> WorkerOptions { WorkerOptions::new(tq) - .namespace(env::var(INTEG_NAMESPACE_ENV_VAR).unwrap_or(NAMESPACE.to_string())) .deployment_options(WorkerDeploymentOptions { version: WorkerDeploymentVersion { deployment_name: "".to_owned(), @@ -477,16 +480,14 @@ impl CoreWfStarter { opts.metrics_meter = rt.telemetry().get_temporal_metric_meter(); let connection = Connection::connect(opts).await.expect("Must connect"); let client_opts = - temporalio_client::ClientOptions::new(self.sdk_config.namespace.clone()) - .build(); + temporalio_client::ClientOptions::new(integ_namespace()).build(); let client = Client::new(connection.clone(), client_opts); (connection, client) }; let worker = init_worker( rt, self.sdk_config - .clone() - .try_into() + .to_core_options(client.namespace()) .expect("sdk config converts to core config"), connection, ) diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index 438484816..20b9bdb96 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -9,7 +9,7 @@ //! An example of running an activity worker: //! ```no_run //! use std::str::FromStr; -//! use temporalio_client::{Connection, ConnectionOptions}; +//! use temporalio_client::{Client, ClientOptions, Connection, ConnectionOptions}; //! use temporalio_common::{ //! telemetry::TelemetryOptions, //! worker::{WorkerDeploymentOptions, WorkerDeploymentVersion, WorkerTaskTypes}, @@ -44,9 +44,9 @@ //! let runtime = CoreRuntime::new_assume_tokio(runtime_options)?; //! //! let connection = Connection::connect(connection_options).await?; +//! let client = Client::new(connection, ClientOptions::new("my_namespace").build()); //! //! let worker_options = WorkerOptions::new("task_queue") -//! .namespace("default") //! .task_types(WorkerTaskTypes::activity_only()) //! .deployment_options(WorkerDeploymentOptions { //! version: WorkerDeploymentVersion { @@ -59,7 +59,7 @@ //! .register_activities_static::() //! .build(); //! -//! let worker = Worker::new(&runtime, connection, worker_options)?; +//! let worker = Worker::new(&runtime, client, worker_options)?; //! worker.run().await?; //! //! Ok(()) @@ -106,7 +106,8 @@ use std::{ time::Duration, }; use temporalio_client::{ - Connection, ConnectionOptions, ConnectionOptionsBuilder, connection_options_builder, + Client, ConnectionOptions, ConnectionOptionsBuilder, NamespacedClient, + connection_options_builder, }; use temporalio_common::{ ActivityDefinition, @@ -170,9 +171,6 @@ pub struct WorkerOptions { #[builder(field)] activities: ActivityDefinitions, - // TODO [rust-sdk-branch]: Other SDKs are pulling from client - /// The Temporal service namespace this worker is bound to - pub namespace: String, // TODO [rust-sdk-branch]: Provide defaults? /// Set the deployment options for this worker. pub deployment_options: WorkerDeploymentOptions, @@ -317,6 +315,32 @@ impl WorkerOptions { pub fn activities(&self) -> ActivityDefinitions { self.activities.clone() } + + #[doc(hidden)] + pub fn to_core_options(&self, namespace: String) -> Result { + WorkerConfig::builder() + .namespace(namespace) + .task_queue(self.task_queue.clone()) + .maybe_client_identity_override(self.client_identity_override.clone()) + .max_cached_workflows(self.max_cached_workflows) + .tuner(self.tuner.clone()) + .workflow_task_poller_behavior(self.workflow_task_poller_behavior) + .activity_task_poller_behavior(self.activity_task_poller_behavior) + .nexus_task_poller_behavior(self.nexus_task_poller_behavior) + .task_types(self.task_types) + .sticky_queue_schedule_to_start_timeout(self.sticky_queue_schedule_to_start_timeout) + .max_heartbeat_throttle_interval(self.max_heartbeat_throttle_interval) + .default_heartbeat_throttle_interval(self.default_heartbeat_throttle_interval) + .maybe_max_task_queue_activities_per_second(self.max_task_queue_activities_per_second) + .maybe_max_worker_activities_per_second(self.max_worker_activities_per_second) + .maybe_graceful_shutdown_period(self.graceful_shutdown_period) + .versioning_strategy(WorkerVersioningStrategy::WorkerDeploymentBased( + self.deployment_options.clone(), + )) + .workflow_failure_errors(self.workflow_failure_errors.clone()) + .workflow_types_to_failure_errors(self.workflow_types_to_failure_errors.clone()) + .build() + } } /// Returns connection options with required fields set to appropriate values for the Rust SDK. @@ -370,16 +394,17 @@ struct ActivityHalf { impl Worker { // TODO [rust-sdk-branch]: Not 100% sure I like passing runtime here - // TODO [rust-sdk-branch]: Don't use anyhow /// Create a new worker from an existing connection, and options. pub fn new( runtime: &CoreRuntime, - connection: Connection, + client: Client, mut options: WorkerOptions, - ) -> Result { + ) -> Result> { let acts = std::mem::take(&mut options.activities); - let wc = options.try_into().map_err(|s| anyhow::anyhow!("{s}"))?; - let core = init_worker(runtime, wc, connection)?; + let wc = options + .to_core_options(client.namespace()) + .map_err(|s| anyhow::anyhow!("{s}"))?; + let core = init_worker(runtime, wc, client.connection().clone())?; let mut me = Self::new_from_core(Arc::new(core)); me.activity_half.activities = acts; Ok(me) @@ -1354,34 +1379,6 @@ impl PrintablePanicType for EndPrintingAttempts { type NextType = EndPrintingAttempts; } -impl TryFrom for WorkerConfig { - type Error = String; - fn try_from(o: WorkerOptions) -> Result { - WorkerConfig::builder() - .namespace(o.namespace) - .task_queue(o.task_queue) - .maybe_client_identity_override(o.client_identity_override) - .max_cached_workflows(o.max_cached_workflows) - .tuner(o.tuner) - .workflow_task_poller_behavior(o.workflow_task_poller_behavior) - .activity_task_poller_behavior(o.activity_task_poller_behavior) - .nexus_task_poller_behavior(o.nexus_task_poller_behavior) - .task_types(o.task_types) - .sticky_queue_schedule_to_start_timeout(o.sticky_queue_schedule_to_start_timeout) - .max_heartbeat_throttle_interval(o.max_heartbeat_throttle_interval) - .default_heartbeat_throttle_interval(o.default_heartbeat_throttle_interval) - .maybe_max_task_queue_activities_per_second(o.max_task_queue_activities_per_second) - .maybe_max_worker_activities_per_second(o.max_worker_activities_per_second) - .maybe_graceful_shutdown_period(o.graceful_shutdown_period) - .versioning_strategy(WorkerVersioningStrategy::WorkerDeploymentBased( - o.deployment_options, - )) - .workflow_failure_errors(o.workflow_failure_errors) - .workflow_types_to_failure_errors(o.workflow_types_to_failure_errors) - .build() - } -} - #[cfg(test)] mod tests { use super::*; From ffecc002333a54dd8baec32e5bd0b7165ca6b86e Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Fri, 2 Jan 2026 18:56:24 -0800 Subject: [PATCH 09/13] Add default build id --- crates/common/Cargo.toml | 1 + crates/common/src/worker.rs | 50 ++++++++++++++++++- .../tests/integ_tests/worker_tests.rs | 10 +++- crates/sdk/src/lib.rs | 7 +-- 4 files changed, 63 insertions(+), 5 deletions(-) diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index ad7d86f1e..22825c79a 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -26,6 +26,7 @@ anyhow = "1.0" async-trait = "0.1" base64 = "0.22" bon = { workspace = true } +crc32fast = "1" dirs = { version = "6.0", optional = true } derive_more = { workspace = true } erased-serde = "0.4" diff --git a/crates/common/src/worker.rs b/crates/common/src/worker.rs index 9370f0b2d..796abcecf 100644 --- a/crates/common/src/worker.rs +++ b/crates/common/src/worker.rs @@ -2,7 +2,12 @@ //! with workers. use crate::protos::{coresdk, temporal, temporal::api::enums::v1::VersioningBehavior}; -use std::str::FromStr; +use std::{ + fs::File, + io::{self, BufReader, Read}, + str::FromStr, + sync::OnceLock, +}; /// Specifies which task types a worker will poll for. /// @@ -84,6 +89,19 @@ pub struct WorkerDeploymentOptions { pub default_versioning_behavior: Option, } +impl WorkerDeploymentOptions { + pub fn from_build_id(build_id: String) -> Self { + Self { + version: WorkerDeploymentVersion { + deployment_name: "".to_owned(), + build_id, + }, + use_worker_versioning: false, + default_versioning_behavior: None, + } + } +} + #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct WorkerDeploymentVersion { /// Name of the deployment @@ -138,3 +156,33 @@ impl From for WorkerDepl } } } + +static CACHED_BUILD_ID: OnceLock = OnceLock::new(); + +/// Build ID derived from hashing the on-disk bytes of the current executable. +/// Deterministic across machines for the same binary. Cached per-process. +pub fn build_id_from_current_exe() -> &'static str { + CACHED_BUILD_ID + .get_or_init(|| compute_crc32_exe_id().unwrap_or_else(|_| "undetermined".to_owned())) +} + +fn compute_crc32_exe_id() -> io::Result { + let exe_path = std::env::current_exe()?; + let file = File::open(exe_path)?; + let mut reader = BufReader::new(file); + + let mut hasher = crc32fast::Hasher::new(); + let mut buf = [0u8; 128 * 1024]; + + loop { + let n = reader.read(&mut buf)?; + if n == 0 { + break; + } + hasher.update(&buf[..n]); + } + + let crc = hasher.finalize(); + + Ok(format!("{:08x}", crc)) +} diff --git a/crates/sdk-core/tests/integ_tests/worker_tests.rs b/crates/sdk-core/tests/integ_tests/worker_tests.rs index 587b4df43..760e5ba01 100644 --- a/crates/sdk-core/tests/integ_tests/worker_tests.rs +++ b/crates/sdk-core/tests/integ_tests/worker_tests.rs @@ -55,7 +55,8 @@ use temporalio_common::{ worker::WorkerTaskTypes, }; use temporalio_sdk::{ - ActivityOptions, LocalActivityOptions, WfContext, interceptors::WorkerInterceptor, + ActivityOptions, LocalActivityOptions, WfContext, WorkerOptions, + interceptors::WorkerInterceptor, }; use temporalio_sdk_core::{ ActivitySlotKind, CoreRuntime, LocalActivitySlotKind, PollError, PollerBehavior, @@ -917,3 +918,10 @@ async fn shutdown_worker_not_retried() { drain_pollers_and_shutdown(&worker).await; assert_eq!(shutdown_call_count.load(Ordering::Relaxed), 1); } + +#[test] +fn test_default_build_id() { + let o = WorkerOptions::new("task_queue").build(); + assert!(!o.deployment_options.version.build_id.is_empty()); + assert_ne!(o.deployment_options.version.build_id, "undetermined"); +} diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index 20b9bdb96..aa2d61a41 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -137,7 +137,7 @@ use temporalio_common::{ failure::v1::{Failure, failure}, }, }, - worker::{WorkerDeploymentOptions, WorkerTaskTypes}, + worker::{WorkerDeploymentOptions, WorkerTaskTypes, build_id_from_current_exe}, }; use temporalio_sdk_core::{ CoreRuntime, PollError, PollerBehavior, TunerBuilder, Url, Worker as CoreWorker, WorkerConfig, @@ -171,8 +171,9 @@ pub struct WorkerOptions { #[builder(field)] activities: ActivityDefinitions, - // TODO [rust-sdk-branch]: Provide defaults? - /// Set the deployment options for this worker. + /// Set the deployment options for this worker. Defaults to a hash of the currently running + /// executable. + #[builder(default = WorkerDeploymentOptions::from_build_id(build_id_from_current_exe().to_owned()))] pub deployment_options: WorkerDeploymentOptions, /// A human-readable string that can identify this worker. Using something like sdk version /// and host name is a good default. If set, overrides the identity set (if any) on the client From 0459c6281812196ef636467bc1f69901e53affd1 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Sat, 3 Jan 2026 12:05:07 -0800 Subject: [PATCH 10/13] Remove unused sdk client-version header setting --- crates/client/src/lib.rs | 2 ++ crates/sdk/src/lib.rs | 18 ++---------------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index c4999908f..970cab024 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -170,6 +170,8 @@ pub struct ConnectionOptions { #[builder(default = "temporal-rust".to_owned())] #[cfg_attr(feature = "core-based-sdk", builder(setters(vis = "pub")))] client_name: String, + // TODO [rust-sdk-branch]: SDK should set this to its version. Doing that probably easiest + // after adding proper client interceptors. /// The version of the SDK being implemented on top of core. Is set as `client-version` header /// in all RPC calls. The server decides if the client is supported based on this. #[builder(default = VERSION.to_owned())] diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index aa2d61a41..02aad9e32 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -105,10 +105,7 @@ use std::{ sync::Arc, time::Duration, }; -use temporalio_client::{ - Client, ConnectionOptions, ConnectionOptionsBuilder, NamespacedClient, - connection_options_builder, -}; +use temporalio_client::{Client, NamespacedClient}; use temporalio_common::{ ActivityDefinition, data_converters::PayloadConverter, @@ -140,7 +137,7 @@ use temporalio_common::{ worker::{WorkerDeploymentOptions, WorkerTaskTypes, build_id_from_current_exe}, }; use temporalio_sdk_core::{ - CoreRuntime, PollError, PollerBehavior, TunerBuilder, Url, Worker as CoreWorker, WorkerConfig, + CoreRuntime, PollError, PollerBehavior, TunerBuilder, Worker as CoreWorker, WorkerConfig, WorkerTuner, WorkerVersioningStrategy, WorkflowErrorType, init_worker, }; use tokio::{ @@ -156,8 +153,6 @@ use tokio_util::sync::CancellationToken; use tracing::{Instrument, Span, field}; use uuid::Uuid; -const VERSION: &str = env!("CARGO_PKG_VERSION"); - /// Contains options for configuring a worker. #[derive(bon::Builder, Clone)] #[builder(start_fn = new, on(String, into), state_mod(vis = "pub"))] @@ -344,15 +339,6 @@ impl WorkerOptions { } } -/// Returns connection options with required fields set to appropriate values for the Rust SDK. -pub fn sdk_connection_options( - url: impl Into, -) -> ConnectionOptionsBuilder { - ConnectionOptions::new(url) - .client_name("temporal-rust".to_string()) - .client_version(VERSION.to_string()) -} - /// A worker that can poll for and respond to workflow tasks by using [WorkflowFunction]s, /// and activity tasks by using activities defined with [temporalio_macros::activities]. pub struct Worker { From f2da163e24102fba79681dcd4620adf9f5a6966b Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Mon, 5 Jan 2026 15:50:07 -0800 Subject: [PATCH 11/13] Change activity invocation to new style --- crates/macros/src/definitions.rs | 214 +++++- crates/sdk-core/tests/common/workflows.rs | 8 +- crates/sdk-core/tests/heavy_tests.rs | 83 +-- .../tests/heavy_tests/fuzzy_workflow.rs | 11 +- .../tests/integ_tests/heartbeat_tests.rs | 9 +- .../tests/integ_tests/metrics_tests.rs | 48 +- .../tests/integ_tests/polling_tests.rs | 6 +- .../tests/integ_tests/update_tests.rs | 49 +- .../integ_tests/worker_heartbeat_tests.rs | 135 ++-- .../tests/integ_tests/worker_tests.rs | 14 +- .../integ_tests/worker_versioning_tests.rs | 9 +- .../tests/integ_tests/workflow_tests.rs | 25 +- .../integ_tests/workflow_tests/activities.rs | 124 ++-- .../workflow_tests/appdata_propagation.rs | 3 +- .../integ_tests/workflow_tests/determinism.rs | 7 +- .../workflow_tests/local_activities.rs | 624 ++++++++++-------- .../integ_tests/workflow_tests/resets.rs | 8 +- crates/sdk-core/tests/manual_tests.rs | 6 +- .../sdk-core/tests/shared_tests/priority.rs | 38 +- crates/sdk/src/lib.rs | 44 +- crates/sdk/src/workflow_context.rs | 2 + 21 files changed, 874 insertions(+), 593 deletions(-) diff --git a/crates/macros/src/definitions.rs b/crates/macros/src/definitions.rs index f1e9d523e..22f2d605a 100644 --- a/crates/macros/src/definitions.rs +++ b/crates/macros/src/definitions.rs @@ -179,26 +179,38 @@ fn extract_output_type(sig: &syn::Signature) -> Option { impl ActivitiesDefinition { pub(crate) fn codegen(&self) -> TokenStream { let impl_type = &self.impl_block.self_ty; + let impl_type_name = type_name_string(impl_type); let module_name = type_to_snake_case(impl_type); let module_ident = format_ident!("{}", module_name); - // Generate the original impl block with #[activity] attributes stripped. We need that since - // it's what's actually going to get called by the worker to run them. + // Generate the original impl block with: + // - #[activity] attributes stripped + // - Activity methods renamed with __ prefix let mut cleaned_impl = self.impl_block.clone(); for item in &mut cleaned_impl.items { if let ImplItem::Fn(method) = item { + let is_activity = method + .attrs + .iter() + .any(|attr| attr.path().is_ident("activity")); + method .attrs .retain(|attr| !attr.path().is_ident("activity")); + + // Rename activity methods with __ prefix + if is_activity { + let new_name = format_ident!("__{}", method.sig.ident); + method.sig.ident = new_name; + } } } + // Generate marker structs (inside module, no external references) let activity_structs: Vec<_> = self .activities .iter() .map(|act| { - // Default to pub(super) since the method structs need to be visible outside the - // generated module. let visibility = match &act.method.vis { syn::Visibility::Inherited => &syn::parse_quote!(pub(super)), o => o, @@ -213,13 +225,53 @@ impl ActivitiesDefinition { }) .collect(); + // Generate consts in impl block pointing to marker structs + let activity_consts: Vec<_> = self + .activities + .iter() + .map(|act| { + let visibility = &act.method.vis; + let method_ident = &act.method.sig.ident; + let struct_name = method_name_to_pascal_case(&act.method.sig.ident); + let struct_ident = format_ident!("{}", struct_name); + let span = act.method.span(); + // Copy #[allow(...)] attributes from the method to the const + let allow_attrs: Vec<_> = act + .method + .attrs + .iter() + .filter(|attr| attr.path().is_ident("allow")) + .collect(); + quote_spanned! { span=> + #[allow(non_upper_case_globals)] + #(#allow_attrs)* + #visibility const #method_ident: #module_ident::#struct_ident = #module_ident::#struct_ident; + } + }) + .collect(); + + // Generate run methods on marker structs (outside module to reference impl_type) + let run_impls: Vec<_> = self + .activities + .iter() + .map(|act| self.generate_run_impl(act, impl_type, &module_ident)) + .collect(); + + // Generate ActivityDefinition and ExecutableActivity impls (outside module) let activity_impls: Vec<_> = self .activities .iter() - .map(|act| self.generate_activity_definition_impl(act, impl_type, &module_name)) + .map(|act| { + self.generate_activity_definition_impl( + act, + impl_type, + &impl_type_name, + &module_ident, + ) + }) .collect(); - let implementer_impl = self.generate_activity_implementer_impl(impl_type); + let implementer_impl = self.generate_activity_implementer_impl(impl_type, &module_ident); let has_only_static = if self.activities.iter().all(|a| a.is_static) { quote! { @@ -229,34 +281,135 @@ impl ActivitiesDefinition { quote! {} }; + // Generate impl block with consts + let const_impl = quote! { + impl #impl_type { + #(#activity_consts)* + } + }; + let output = quote! { #cleaned_impl - pub mod #module_ident { - use super::*; + #const_impl + // Module contains only the marker structs (no use super::*) + mod #module_ident { #(#activity_structs)* + } - #(#activity_impls)* + // Run methods, trait impls are outside the module + #(#run_impls)* - #implementer_impl + #(#activity_impls)* - #has_only_static - } + #implementer_impl + + #has_only_static }; output.into() } + fn generate_run_impl( + &self, + activity: &ActivityMethod, + impl_type: &Type, + module_ident: &syn::Ident, + ) -> TokenStream2 { + let struct_name = method_name_to_pascal_case(&activity.method.sig.ident); + let struct_ident = format_ident!("{}", struct_name); + let prefixed_method = format_ident!("__{}", activity.method.sig.ident); + + let input_type = activity + .input_type + .as_ref() + .map(|t| quote! { #t }) + .unwrap_or(quote! { () }); + let output_type = activity + .output_type + .as_ref() + .map(|t| quote! { #t }) + .unwrap_or(quote! { () }); + + // Build the parameters and call based on static vs instance and input + let (params, method_call) = if activity.is_static { + let params = if activity.input_type.is_some() { + quote! { self, ctx: ::temporalio_sdk::activities::ActivityContext, input: #input_type } + } else { + quote! { self, ctx: ::temporalio_sdk::activities::ActivityContext } + }; + let call = if activity.input_type.is_some() { + quote! { #impl_type::#prefixed_method(ctx, input) } + } else { + quote! { #impl_type::#prefixed_method(ctx) } + }; + (params, call) + } else { + let params = if activity.input_type.is_some() { + quote! { self, instance: ::std::sync::Arc<#impl_type>, ctx: ::temporalio_sdk::activities::ActivityContext, input: #input_type } + } else { + quote! { self, instance: ::std::sync::Arc<#impl_type>, ctx: ::temporalio_sdk::activities::ActivityContext } + }; + let call = if activity.input_type.is_some() { + quote! { #impl_type::#prefixed_method(instance, ctx, input) } + } else { + quote! { #impl_type::#prefixed_method(instance, ctx) } + }; + (params, call) + }; + + let return_type = + quote! { Result<#output_type, ::temporalio_sdk::activities::ActivityError> }; + + // If the method returns void (no return type), wrap with Ok(()) + let result_wrapper = if activity.output_type.is_none() { + quote! { ; Ok(()) } + } else { + quote! {} + }; + + // Common methods for all marker structs + let common_methods = quote! { + /// Returns the activity name (delegates to ActivityDefinition::name()) + pub fn name(&self) -> &'static str { + ::name() + } + }; + + if activity.is_async { + quote! { + impl #module_ident::#struct_ident { + #common_methods + + pub async fn run(#params) -> #return_type { + #method_call.await #result_wrapper + } + } + } + } else { + quote! { + impl #module_ident::#struct_ident { + #common_methods + + pub fn run(#params) -> #return_type { + #method_call #result_wrapper + } + } + } + } + } + fn generate_activity_definition_impl( &self, activity: &ActivityMethod, impl_type: &Type, - module_name: &str, + impl_type_name: &str, + module_ident: &syn::Ident, ) -> TokenStream2 { let struct_name = method_name_to_pascal_case(&activity.method.sig.ident); let struct_ident = format_ident!("{}", struct_name); - let method_ident = &activity.method.sig.ident; + let prefixed_method = format_ident!("__{}", activity.method.sig.ident); let input_type = activity .input_type @@ -272,7 +425,7 @@ impl ActivitiesDefinition { let activity_name = if let Some(ref name_expr) = activity.attributes.name_override { quote! { #name_expr } } else { - let default_name = format!("{}::{}", module_name, struct_name); + let default_name = format!("{}::{}", impl_type_name, activity.method.sig.ident); quote! { #default_name } }; @@ -284,14 +437,14 @@ impl ActivitiesDefinition { let method_call = if activity.input_type.is_some() { if activity.is_static { - quote! { #impl_type::#method_ident(ctx, input) } + quote! { #impl_type::#prefixed_method(ctx, input) } } else { - quote! { #impl_type::#method_ident(receiver.unwrap(), ctx, input) } + quote! { #impl_type::#prefixed_method(receiver.unwrap(), ctx, input) } } } else if activity.is_static { - quote! { #impl_type::#method_ident(ctx) } + quote! { #impl_type::#prefixed_method(ctx) } } else { - quote! { #impl_type::#method_ident(receiver.unwrap(), ctx) } + quote! { #impl_type::#prefixed_method(receiver.unwrap(), ctx) } }; // Add input parameter to execute signature only if needed @@ -322,7 +475,7 @@ impl ActivitiesDefinition { }; quote! { - impl ::temporalio_common::ActivityDefinition for #struct_ident { + impl ::temporalio_common::ActivityDefinition for #module_ident::#struct_ident { type Input = #input_type; type Output = #output_type; @@ -334,7 +487,7 @@ impl ActivitiesDefinition { } } - impl ::temporalio_sdk::activities::ExecutableActivity for #struct_ident { + impl ::temporalio_sdk::activities::ExecutableActivity for #module_ident::#struct_ident { type Implementer = #impl_type; fn execute( @@ -352,7 +505,11 @@ impl ActivitiesDefinition { } } - fn generate_activity_implementer_impl(&self, impl_type: &Type) -> TokenStream2 { + fn generate_activity_implementer_impl( + &self, + impl_type: &Type, + module_ident: &syn::Ident, + ) -> TokenStream2 { let static_activities: Vec<_> = self .activities .iter() @@ -361,7 +518,7 @@ impl ActivitiesDefinition { let struct_name = method_name_to_pascal_case(&a.method.sig.ident); let struct_ident = format_ident!("{}", struct_name); quote! { - defs.register_activity::<#struct_ident>(); + defs.register_activity::<#module_ident::#struct_ident>(); } }) .collect(); @@ -374,7 +531,7 @@ impl ActivitiesDefinition { let struct_name = method_name_to_pascal_case(&a.method.sig.ident); let struct_ident = format_ident!("{}", struct_name); quote! { - defs.register_activity_with_instance::<#struct_ident>(self.clone()); + defs.register_activity_with_instance::<#module_ident::#struct_ident>(self.clone()); } }) .collect(); @@ -445,3 +602,12 @@ fn method_name_to_pascal_case(ident: &syn::Ident) -> String { } result } + +fn type_name_string(ty: &Type) -> String { + if let Type::Path(type_path) = ty + && let Some(segment) = type_path.path.segments.last() + { + return segment.ident.to_string(); + } + panic!("Cannot extract type name from impl block - expected a simple type path"); +} diff --git a/crates/sdk-core/tests/common/workflows.rs b/crates/sdk-core/tests/common/workflows.rs index 7f50bea37..8c32913ea 100644 --- a/crates/sdk-core/tests/common/workflows.rs +++ b/crates/sdk-core/tests/common/workflows.rs @@ -1,10 +1,11 @@ -use crate::common::activity_functions::std_activities; +use crate::common::activity_functions::StdActivities; use std::time::Duration; use temporalio_common::{prost_dur, protos::temporal::api::common::v1::RetryPolicy}; use temporalio_sdk::{ActivityOptions, LocalActivityOptions, WfContext, WorkflowResult}; pub(crate) async fn la_problem_workflow(ctx: WfContext) -> WorkflowResult<()> { - ctx.local_activity::( + ctx.local_activity( + StdActivities::delay, Duration::from_secs(15), LocalActivityOptions { retry_policy: RetryPolicy { @@ -19,7 +20,8 @@ pub(crate) async fn la_problem_workflow(ctx: WfContext) -> WorkflowResult<()> { }, )? .await; - ctx.activity::( + ctx.activity( + StdActivities::delay, Duration::from_secs(15), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(20)), diff --git a/crates/sdk-core/tests/heavy_tests.rs b/crates/sdk-core/tests/heavy_tests.rs index 5ac45d962..78190d6a6 100644 --- a/crates/sdk-core/tests/heavy_tests.rs +++ b/crates/sdk-core/tests/heavy_tests.rs @@ -7,9 +7,7 @@ mod fuzzy_workflow; use crate::common::get_integ_runtime_options; use common::{ - CoreWfStarter, - activity_functions::{StdActivities, std_activities}, - init_integ_telem, prom_metrics, rand_6_chars, + CoreWfStarter, activity_functions::StdActivities, init_integ_telem, prom_metrics, rand_6_chars, workflows::la_problem_workflow, }; use futures_util::{ @@ -67,7 +65,8 @@ async fn activity_load() { let input_str = "yo".to_string(); async move { let res = ctx - .activity::( + .activity( + StdActivities::echo, input_str.clone(), ActivityOptions { activity_id: Some(activity_id.to_string()), @@ -116,25 +115,6 @@ async fn activity_load() { dbg!(running.elapsed()); } -struct ChunkyActivities {} -#[activities] -impl ChunkyActivities { - #[activity] - async fn chunky_echo(_ctx: ActivityContext, echo: String) -> Result { - tokio::task::spawn_blocking(move || { - // Allocate a gig and then do some CPU stuff on it - let mut mem = vec![0_u8; 1000 * 1024 * 1024]; - for _ in 1..10 { - for i in 0..mem.len() { - mem[i] &= mem[mem.len() - 1 - i] - } - } - Ok(echo) - }) - .await? - } -} - #[tokio::test] async fn chunky_activities_resource_based() { const WORKFLOWS: usize = 100; @@ -151,6 +131,26 @@ async fn chunky_activities_resource_based() { )) .with_activity_slots_options(ResourceSlotOptions::new(5, 1000, Duration::from_millis(50))); starter.sdk_config.tuner = Arc::new(tuner); + + struct ChunkyActivities {} + #[activities] + impl ChunkyActivities { + #[activity] + async fn chunky_echo(_ctx: ActivityContext, echo: String) -> Result { + tokio::task::spawn_blocking(move || { + // Allocate a gig and then do some CPU stuff on it + let mut mem = vec![0_u8; 1000 * 1024 * 1024]; + for _ in 1..10 { + for i in 0..mem.len() { + mem[i] &= mem[mem.len() - 1 - i] + } + } + Ok(echo) + }) + .await? + } + } + starter .sdk_config .register_activities_static::(); @@ -163,7 +163,8 @@ async fn chunky_activities_resource_based() { let input_str = "yo".to_string(); async move { let res = ctx - .activity::( + .activity( + ChunkyActivities::chunky_echo, input_str.clone(), ActivityOptions { activity_id: Some(activity_id.to_string()), @@ -235,7 +236,8 @@ async fn workflow_load() { let real_stuff = async move { for _ in 0..5 { - ctx.activity::( + ctx.activity( + StdActivities::echo, "hi!".to_string(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(5)), @@ -400,18 +402,6 @@ async fn can_paginate_long_history() { worker.run_until_done().await.unwrap(); } -struct JitteryActivities {} -#[activities] -impl JitteryActivities { - #[activity] - async fn jittery_echo(_ctx: ActivityContext, echo: String) -> Result { - // Add some jitter to completions - let rand_millis = rand::rng().random_range(0..500); - tokio::time::sleep(Duration::from_millis(rand_millis)).await; - Ok(echo) - } -} - #[tokio::test] async fn poller_autoscaling_basic_loadtest() { const SIGNAME: &str = "signame"; @@ -430,6 +420,22 @@ async fn poller_autoscaling_basic_loadtest() { maximum: 200, initial: 5, }; + + struct JitteryActivities {} + #[activities] + impl JitteryActivities { + #[activity] + async fn jittery_echo( + _ctx: ActivityContext, + echo: String, + ) -> Result { + // Add some jitter to completions + let rand_millis = rand::rng().random_range(0..500); + tokio::time::sleep(Duration::from_millis(rand_millis)).await; + Ok(echo) + } + } + starter .sdk_config .register_activities_static::(); @@ -441,7 +447,8 @@ async fn poller_autoscaling_basic_loadtest() { let real_stuff = async move { for _ in 0..5 { - ctx.activity::( + ctx.activity( + JitteryActivities::jittery_echo, "hi!".to_string(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(5)), diff --git a/crates/sdk-core/tests/heavy_tests/fuzzy_workflow.rs b/crates/sdk-core/tests/heavy_tests/fuzzy_workflow.rs index 726a75cdf..ed0b5edd1 100644 --- a/crates/sdk-core/tests/heavy_tests/fuzzy_workflow.rs +++ b/crates/sdk-core/tests/heavy_tests/fuzzy_workflow.rs @@ -1,7 +1,4 @@ -use crate::common::{ - CoreWfStarter, - activity_functions::{StdActivities, std_activities}, -}; +use crate::common::{CoreWfStarter, activity_functions::StdActivities}; use futures_util::{FutureExt, StreamExt, sink, stream::FuturesUnordered}; use rand::{Rng, SeedableRng, prelude::Distribution, rngs::SmallRng}; use std::{future, sync::Arc, time::Duration}; @@ -43,7 +40,8 @@ async fn fuzzy_wf_def(ctx: WfContext) -> WorkflowResult<()> { .take_until(done.cancelled()) .for_each_concurrent(None, |action| match action { FuzzyWfAction::DoAct => ctx - .activity::( + .activity( + StdActivities::echo, "hi!".to_string(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(5)), @@ -54,7 +52,8 @@ async fn fuzzy_wf_def(ctx: WfContext) -> WorkflowResult<()> { .map(|_| ()) .boxed(), FuzzyWfAction::DoLocalAct => ctx - .local_activity::( + .local_activity( + StdActivities::echo, "hi!".to_string(), LocalActivityOptions { start_to_close_timeout: Some(Duration::from_secs(5)), diff --git a/crates/sdk-core/tests/integ_tests/heartbeat_tests.rs b/crates/sdk-core/tests/integ_tests/heartbeat_tests.rs index bfd5a7c1f..9f6c6b218 100644 --- a/crates/sdk-core/tests/integ_tests/heartbeat_tests.rs +++ b/crates/sdk-core/tests/integ_tests/heartbeat_tests.rs @@ -1,8 +1,4 @@ -use crate::common::{ - CoreWfStarter, - activity_functions::{StdActivities, std_activities}, - init_core_and_create_wf, -}; +use crate::common::{CoreWfStarter, activity_functions::StdActivities, init_core_and_create_wf}; use assert_matches::assert_matches; use std::time::Duration; use temporalio_client::{WfClientExt, WorkflowOptions}; @@ -195,7 +191,8 @@ async fn activity_doesnt_heartbeat_hits_timeout_then_completes() { worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let res = ctx - .activity::( + .activity( + StdActivities::delay, Duration::from_secs(4), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(10)), diff --git a/crates/sdk-core/tests/integ_tests/metrics_tests.rs b/crates/sdk-core/tests/integ_tests/metrics_tests.rs index f8d3fc6b8..f83495e39 100644 --- a/crates/sdk-core/tests/integ_tests/metrics_tests.rs +++ b/crates/sdk-core/tests/integ_tests/metrics_tests.rs @@ -765,22 +765,6 @@ async fn docker_metrics_with_prometheus( } } -struct PassFailActivities {} -#[activities] -impl PassFailActivities { - #[activity(name = "pass_fail_act")] - async fn pass_fail_act(ctx: ActivityContext, i: String) -> Result { - match i.as_str() { - "pass" => Ok("pass".to_string()), - "cancel" => { - ctx.cancelled().await; - Err(ActivityError::cancelled()) - } - _ => Err(anyhow!("fail").into()), - } - } -} - #[tokio::test] async fn activity_metrics() { let (telemopts, addr, _aborter) = prom_metrics(None); @@ -788,6 +772,23 @@ async fn activity_metrics() { let wf_name = "activity_metrics"; let mut starter = CoreWfStarter::new_with_runtime(wf_name, rt); starter.sdk_config.graceful_shutdown_period = Some(Duration::from_secs(1)); + + struct PassFailActivities {} + #[activities] + impl PassFailActivities { + #[activity(name = "pass_fail_act")] + async fn pass_fail_act(ctx: ActivityContext, i: String) -> Result { + match i.as_str() { + "pass" => Ok("pass".to_string()), + "cancel" => { + ctx.cancelled().await; + Err(ActivityError::cancelled()) + } + _ => Err(anyhow!("fail").into()), + } + } + } + starter .sdk_config .register_activities_static::(); @@ -796,7 +797,8 @@ async fn activity_metrics() { worker.register_wf(wf_name.to_string(), |ctx: WfContext| async move { let normal_act_pass = ctx - .activity::( + .activity( + PassFailActivities::pass_fail_act, "pass".to_string(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(1)), @@ -805,7 +807,8 @@ async fn activity_metrics() { ) .unwrap(); let normal_act_fail = ctx - .activity::( + .activity( + PassFailActivities::pass_fail_act, "fail".to_string(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(1)), @@ -818,11 +821,13 @@ async fn activity_metrics() { ) .unwrap(); join!(normal_act_pass, normal_act_fail); - let local_act_pass = ctx.local_activity::( + let local_act_pass = ctx.local_activity( + PassFailActivities::pass_fail_act, "pass".to_string(), LocalActivityOptions::default(), )?; - let local_act_fail = ctx.local_activity::( + let local_act_fail = ctx.local_activity( + PassFailActivities::pass_fail_act, "fail".to_string(), LocalActivityOptions { retry_policy: RetryPolicy { @@ -832,7 +837,8 @@ async fn activity_metrics() { ..Default::default() }, )?; - let local_act_cancel = ctx.local_activity::( + let local_act_cancel = ctx.local_activity( + PassFailActivities::pass_fail_act, "cancel".to_string(), LocalActivityOptions { retry_policy: RetryPolicy { diff --git a/crates/sdk-core/tests/integ_tests/polling_tests.rs b/crates/sdk-core/tests/integ_tests/polling_tests.rs index 6b05fb5b7..f0ef4ad2d 100644 --- a/crates/sdk-core/tests/integ_tests/polling_tests.rs +++ b/crates/sdk-core/tests/integ_tests/polling_tests.rs @@ -1,6 +1,5 @@ use crate::common::{ - CoreWfStarter, INTEG_CLIENT_NAME, INTEG_CLIENT_VERSION, - activity_functions::{StdActivities, std_activities}, + CoreWfStarter, INTEG_CLIENT_NAME, INTEG_CLIENT_VERSION, activity_functions::StdActivities, get_integ_client, init_core_and_create_wf, init_integ_telem, integ_dev_server_config, integ_worker_config, }; @@ -265,7 +264,8 @@ async fn small_workflow_slots_and_pollers(#[values(false, true)] use_autoscaling worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { for _ in 0..3 { - ctx.activity::( + ctx.activity( + StdActivities::echo, "hi!".to_string(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(5)), diff --git a/crates/sdk-core/tests/integ_tests/update_tests.rs b/crates/sdk-core/tests/integ_tests/update_tests.rs index e394725cb..cef6aff14 100644 --- a/crates/sdk-core/tests/integ_tests/update_tests.rs +++ b/crates/sdk-core/tests/integ_tests/update_tests.rs @@ -1,7 +1,6 @@ use crate::common::{ - CoreWfStarter, WorkflowHandleExt, - activity_functions::{StdActivities, std_activities}, - init_core_and_create_wf, init_core_replay_preloaded, + CoreWfStarter, WorkflowHandleExt, activity_functions::StdActivities, init_core_and_create_wf, + init_core_replay_preloaded, }; use anyhow::anyhow; use assert_matches::assert_matches; @@ -654,7 +653,8 @@ async fn update_with_local_acts() { |_: &_, _: ()| Ok(()), move |ctx: UpdateContext, _: ()| async move { ctx.wf_ctx - .local_activity::( + .local_activity( + StdActivities::delay, Duration::from_secs(3), LocalActivityOptions::default(), )? @@ -956,24 +956,25 @@ async fn task_failure_after_update() { static BARR: LazyLock = LazyLock::new(|| Barrier::new(2)); static ACT_RAN: AtomicBool = AtomicBool::new(false); -struct BlockingActivities {} -#[activities] -impl BlockingActivities { - #[activity] - async fn blocks(_ctx: ActivityContext, echo_me: String) -> Result { - BARR.wait().await; - if !ACT_RAN.fetch_or(true, Ordering::Relaxed) { - // On first run fail the task so we'll get retried on the new worker - return Err(anyhow!("Fail first time").into()); - } - Ok(echo_me) - } -} - #[tokio::test] async fn worker_restarted_in_middle_of_update() { let wf_name = "worker_restarted_in_middle_of_update"; let mut starter = CoreWfStarter::new(wf_name); + + struct BlockingActivities {} + #[activities] + impl BlockingActivities { + #[activity] + async fn blocks(_ctx: ActivityContext, echo_me: String) -> Result { + BARR.wait().await; + if !ACT_RAN.fetch_or(true, Ordering::Relaxed) { + // On first run fail the task so we'll get retried on the new worker + return Err(anyhow!("Fail first time").into()); + } + Ok(echo_me) + } + } + starter .sdk_config .register_activities_static::(); @@ -986,7 +987,8 @@ async fn worker_restarted_in_middle_of_update() { |_: &_, _: ()| Ok(()), move |ctx: UpdateContext, _: ()| async move { ctx.wf_ctx - .activity::( + .activity( + BlockingActivities::blocks, "hi!".to_string(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(2)), @@ -1081,7 +1083,8 @@ async fn update_after_empty_wft() { return Ok(()); } ctx.wf_ctx - .activity::( + .activity( + StdActivities::echo, "hi!".to_string(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(2)), @@ -1097,7 +1100,8 @@ async fn update_after_empty_wft() { let sig_handle = async { sig.next().await; ACT_STARTED.store(true, Ordering::Release); - ctx.activity::( + ctx.activity( + StdActivities::echo, "hi!".to_string(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(2)), @@ -1180,7 +1184,8 @@ async fn update_lost_on_activity_mismatch() { for _ in 1..=3 { let cr = can_run.clone(); ctx.wait_condition(|| cr.load(Ordering::Relaxed) > 0).await; - ctx.activity::( + ctx.activity( + StdActivities::echo, "hi!".to_string(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(2)), diff --git a/crates/sdk-core/tests/integ_tests/worker_heartbeat_tests.rs b/crates/sdk-core/tests/integ_tests/worker_heartbeat_tests.rs index 429f1a685..f7fefe47f 100644 --- a/crates/sdk-core/tests/integ_tests/worker_heartbeat_tests.rs +++ b/crates/sdk-core/tests/integ_tests/worker_heartbeat_tests.rs @@ -1,7 +1,5 @@ use crate::common::{ - ANY_PORT, CoreWfStarter, - activity_functions::{StdActivities, std_activities}, - eventually, get_integ_telem_options, + ANY_PORT, CoreWfStarter, activity_functions::StdActivities, eventually, get_integ_telem_options, }; use anyhow::anyhow; use crossbeam_utils::atomic::AtomicCell; @@ -95,24 +93,6 @@ async fn list_worker_heartbeats(client: &Client, query: impl Into) -> Ve .collect() } -struct NotifyActivities { - acts_started: Arc, - acts_done: Arc, -} -#[activities] -impl NotifyActivities { - #[activity] - async fn pass_fail_act( - self: Arc, - _ctx: ActivityContext, - i: String, - ) -> Result { - self.acts_started.notify_one(); - self.acts_done.notified().await; - Ok(i) - } -} - // Tests that rely on Prometheus running in a docker container need to start // with `docker_` and set the `DOCKER_PROMETHEUS_RUNNING` env variable to run #[rstest::rstest] @@ -172,6 +152,25 @@ async fn docker_worker_heartbeat_basic(#[values("otel", "prom", "no_metrics")] b }); let acts_started = Arc::new(Notify::new()); let acts_done = Arc::new(Notify::new()); + + struct NotifyActivities { + acts_started: Arc, + acts_done: Arc, + } + #[activities] + impl NotifyActivities { + #[activity] + async fn pass_fail_act( + self: Arc, + _ctx: ActivityContext, + i: String, + ) -> Result { + self.acts_started.notify_one(); + self.acts_done.notified().await; + Ok(i) + } + } + starter.sdk_config.register_activities(NotifyActivities { acts_started: acts_started.clone(), acts_done: acts_done.clone(), @@ -180,7 +179,8 @@ async fn docker_worker_heartbeat_basic(#[values("otel", "prom", "no_metrics")] b let worker_instance_key = worker.worker_instance_key(); worker.register_wf(wf_name.to_string(), |ctx: WfContext| async move { - ctx.activity::( + ctx.activity( + NotifyActivities::pass_fail_act, "pass".to_string(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(5)), @@ -322,7 +322,8 @@ async fn docker_worker_heartbeat_tuner() { // Run a workflow worker.register_wf(wf_name.to_string(), |ctx: WfContext| async move { - ctx.activity::( + ctx.activity( + StdActivities::echo, "pass".to_string(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(1)), @@ -557,35 +558,36 @@ static HISTORY_WF1_ACTIVITY_STARTED: Notify = Notify::const_new(); static HISTORY_WF1_ACTIVITY_FINISH: Notify = Notify::const_new(); static HISTORY_WF2_ACTIVITY_STARTED: Notify = Notify::const_new(); static HISTORY_WF2_ACTIVITY_FINISH: Notify = Notify::const_new(); -struct StickyCacheActivities; -#[activities] -impl StickyCacheActivities { - #[activity] - async fn sticky_cache_history_act( - _ctx: ActivityContext, - marker: String, - ) -> Result { - match marker.as_str() { - "wf1" => { - HISTORY_WF1_ACTIVITY_STARTED.notify_one(); - HISTORY_WF1_ACTIVITY_FINISH.notified().await; - } - "wf2" => { - HISTORY_WF2_ACTIVITY_STARTED.notify_one(); - HISTORY_WF2_ACTIVITY_FINISH.notified().await; - } - _ => {} - } - Ok(marker) - } -} - #[tokio::test] async fn worker_heartbeat_sticky_cache_miss() { let wf_name = "worker_heartbeat_cache_miss"; let mut starter = new_no_metrics_starter(wf_name); starter.sdk_config.max_cached_workflows = 1_usize; starter.sdk_config.tuner = Arc::new(TunerHolder::fixed_size(2, 10, 10, 10)); + + struct StickyCacheActivities; + #[activities] + impl StickyCacheActivities { + #[activity] + async fn sticky_cache_history_act( + _ctx: ActivityContext, + marker: String, + ) -> Result { + match marker.as_str() { + "wf1" => { + HISTORY_WF1_ACTIVITY_STARTED.notify_one(); + HISTORY_WF1_ACTIVITY_FINISH.notified().await; + } + "wf2" => { + HISTORY_WF2_ACTIVITY_STARTED.notify_one(); + HISTORY_WF2_ACTIVITY_FINISH.notified().await; + } + _ => {} + } + Ok(marker) + } + } + starter .sdk_config .register_activities_static::(); @@ -606,7 +608,8 @@ async fn worker_heartbeat_sticky_cache_miss() { .and_then(|p| String::from_json_payload(p).ok()) .unwrap_or_else(|| "wf1".to_string()); - ctx.activity::( + ctx.activity( + StickyCacheActivities::sticky_cache_history_act, wf_marker.clone(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(5)), @@ -780,20 +783,6 @@ static ACT_COUNT: AtomicU64 = AtomicU64::new(0); static WF_COUNT: AtomicU64 = AtomicU64::new(0); static ACT_FAIL: Notify = Notify::const_new(); static WF_FAIL: Notify = Notify::const_new(); -struct FailingActivities; -#[activities] -impl FailingActivities { - #[activity] - async fn failing_act(_ctx: ActivityContext, _: String) -> Result<(), ActivityError> { - if ACT_COUNT.load(Ordering::Relaxed) == 3 { - return Ok(()); - } - ACT_COUNT.fetch_add(1, Ordering::Relaxed); - ACT_FAIL.notify_one(); - Err(anyhow!("Expected error").into()) - } -} - #[tokio::test] async fn worker_heartbeat_failure_metrics() { const WORKFLOW_CONTINUE_SIGNAL: &str = "workflow-continue"; @@ -801,6 +790,21 @@ async fn worker_heartbeat_failure_metrics() { let wf_name = "worker_heartbeat_failure_metrics"; let mut starter = new_no_metrics_starter(wf_name); starter.sdk_config.tuner = Arc::new(TunerHolder::fixed_size(10, 5, 10, 10)); + + struct FailingActivities; + #[activities] + impl FailingActivities { + #[activity] + async fn failing_act(_ctx: ActivityContext, _: String) -> Result<(), ActivityError> { + if ACT_COUNT.load(Ordering::Relaxed) == 3 { + return Ok(()); + } + ACT_COUNT.fetch_add(1, Ordering::Relaxed); + ACT_FAIL.notify_one(); + Err(anyhow!("Expected error").into()) + } + } + starter .sdk_config .register_activities_static::(); @@ -810,7 +814,8 @@ async fn worker_heartbeat_failure_metrics() { worker.register_wf(wf_name.to_string(), |ctx: WfContext| async move { let _ = ctx - .activity::( + .activity( + FailingActivities::failing_act, "boom".to_string(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(5)), @@ -982,7 +987,8 @@ async fn worker_heartbeat_no_runtime_heartbeat() { let worker_instance_key = worker.worker_instance_key(); worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { - ctx.activity::( + ctx.activity( + StdActivities::echo, "pass".to_string(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(1)), @@ -1044,7 +1050,8 @@ async fn worker_heartbeat_skip_client_worker_set_check() { let worker_instance_key = worker.worker_instance_key(); worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { - ctx.activity::( + ctx.activity( + StdActivities::echo, "pass".to_string(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(1)), diff --git a/crates/sdk-core/tests/integ_tests/worker_tests.rs b/crates/sdk-core/tests/integ_tests/worker_tests.rs index 760e5ba01..00f1ebc43 100644 --- a/crates/sdk-core/tests/integ_tests/worker_tests.rs +++ b/crates/sdk-core/tests/integ_tests/worker_tests.rs @@ -1,8 +1,6 @@ use crate::{ common::{ - CoreWfStarter, - activity_functions::{StdActivities, std_activities}, - fake_grpc_server::fake_server, + CoreWfStarter, activity_functions::StdActivities, fake_grpc_server::fake_server, get_integ_runtime_options, get_integ_server_options, get_integ_telem_options, mock_sdk_cfg, }, shared_tests, @@ -725,7 +723,8 @@ async fn test_custom_slot_supplier_simple() { "SlotSupplierWorkflow".to_owned(), |ctx: WfContext| async move { let _result = ctx - .activity::( + .activity( + StdActivities::no_op, (), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(10)), @@ -734,7 +733,8 @@ async fn test_custom_slot_supplier_simple() { )? .await; let _result = ctx - .local_activity::( + .local_activity( + StdActivities::no_op, (), LocalActivityOptions { start_to_close_timeout: Some(Duration::from_secs(10)), @@ -822,7 +822,7 @@ async fn test_custom_slot_supplier_simple() { slot_type: "activity", activity_type: Some(act_type), .. - } if act_type.contains("NoOp"))) + } if act_type.contains("no_op"))) ); assert!( local_activity_events @@ -831,7 +831,7 @@ async fn test_custom_slot_supplier_simple() { slot_type: "local_activity", activity_type: Some(act_type), .. - } if act_type.contains("NoOp"))) + } if act_type.contains("no_op"))) ); assert!(wf_events.iter().any(|e| matches!( e, diff --git a/crates/sdk-core/tests/integ_tests/worker_versioning_tests.rs b/crates/sdk-core/tests/integ_tests/worker_versioning_tests.rs index aff6f331d..ab50c2228 100644 --- a/crates/sdk-core/tests/integ_tests/worker_versioning_tests.rs +++ b/crates/sdk-core/tests/integ_tests/worker_versioning_tests.rs @@ -1,8 +1,4 @@ -use crate::common::{ - CoreWfStarter, - activity_functions::{StdActivities, std_activities}, - eventually, -}; +use crate::common::{CoreWfStarter, activity_functions::StdActivities, eventually}; use std::time::Duration; use temporalio_client::{NamespacedClient, WorkflowOptions, WorkflowService}; use temporalio_common::{ @@ -163,7 +159,8 @@ async fn activity_has_deployment_stamp() { let client = starter.get_client().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { - ctx.activity::( + ctx.activity( + StdActivities::echo, "hi!".to_string(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(5)), diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests.rs b/crates/sdk-core/tests/integ_tests/workflow_tests.rs index bba31a498..26bbcce3e 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests.rs @@ -20,10 +20,9 @@ mod upsert_search_attrs; use crate::{ common::{ - CoreWfStarter, - activity_functions::{StdActivities, std_activities}, - get_integ_runtime_options, history_from_proto_binary, init_core_and_create_wf, - init_core_replay_preloaded, mock_sdk_cfg, prom_metrics, + CoreWfStarter, activity_functions::StdActivities, get_integ_runtime_options, + history_from_proto_binary, init_core_and_create_wf, init_core_replay_preloaded, + mock_sdk_cfg, prom_metrics, }, integ_tests::metrics_tests, }; @@ -480,7 +479,8 @@ async fn slow_completes_with_small_cache() { worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { for _ in 0..3 { - ctx.activity::( + ctx.activity( + StdActivities::echo, "hi!".to_string(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(5)), @@ -818,7 +818,8 @@ async fn nondeterminism_errors_fail_workflow_when_configured_to( .register_activities_static::(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), move |ctx: WfContext| async move { - ctx.activity::( + ctx.activity( + StdActivities::echo, "hi".to_owned(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(5)), @@ -864,7 +865,8 @@ async fn history_out_of_order_on_restart() { worker.register_activities_static::(); worker2.register_activities_static::(); worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { - ctx.local_activity::( + ctx.local_activity( + StdActivities::echo, "hi".to_string(), LocalActivityOptions { start_to_close_timeout: Some(Duration::from_secs(5)), @@ -872,7 +874,8 @@ async fn history_out_of_order_on_restart() { }, )? .await; - ctx.activity::( + ctx.activity( + StdActivities::echo, "hi".to_string(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(5)), @@ -887,7 +890,8 @@ async fn history_out_of_order_on_restart() { }); worker2.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { - ctx.local_activity::( + ctx.local_activity( + StdActivities::echo, "hi".to_string(), LocalActivityOptions { start_to_close_timeout: Some(Duration::from_secs(5)), @@ -897,7 +901,8 @@ async fn history_out_of_order_on_restart() { .await; // Timer is added after restarting workflow ctx.timer(Duration::from_secs(1)).await; - ctx.activity::( + ctx.activity( + StdActivities::echo, "hi".to_string(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(5)), diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/activities.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/activities.rs index d6de5fa5b..e2fb7f111 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/activities.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/activities.rs @@ -1,7 +1,7 @@ use crate::common::{ ActivationAssertionsInterceptor, CoreWfStarter, INTEG_CLIENT_IDENTITY, - activity_functions::{StdActivities, std_activities}, - build_fake_sdk, eventually, init_core_and_create_wf, mock_sdk, mock_sdk_cfg, + activity_functions::StdActivities, build_fake_sdk, eventually, init_core_and_create_wf, + mock_sdk, mock_sdk_cfg, }; use anyhow::anyhow; use assert_matches::assert_matches; @@ -74,7 +74,8 @@ impl SleepyActivities { async fn one_activity_wf(ctx: WfContext) -> WorkflowResult { // TODO [rust-sdk-branch]: activities need to return deserialzied results let r = ctx - .activity::( + .activity( + StdActivities::echo, "hi!".to_string(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(5)), @@ -919,7 +920,8 @@ async fn one_activity_abandon_cancelled_before_started() { let client = starter.get_client().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let act_fut = ctx - .activity::( + .activity( + SleepyActivities::sleepy_echo, "hi!".to_string(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(5)), @@ -962,7 +964,8 @@ async fn one_activity_abandon_cancelled_after_complete() { let client = starter.get_client().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let act_fut = ctx - .activity::( + .activity( + SleepyActivities::sleepy_echo, "hi!".to_string(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(5)), @@ -996,32 +999,33 @@ async fn one_activity_abandon_cancelled_after_complete() { assert_matches!(res, WorkflowExecutionResult::Succeeded(_)); } -struct AsyncActivities { - shared_token: Arc>>>, -} -#[activities] -impl AsyncActivities { - #[activity] - async fn complete_async_activity( - self: Arc, - ctx: ActivityContext, - _: String, - ) -> Result<(), ActivityError> { - // set the `activity_task_token` - let activity_info = ctx.get_info(); - let task_token = &activity_info.task_token; - let mut shared = self.shared_token.lock().await; - *shared = Some(task_token.clone()); - Err(ActivityError::WillCompleteAsync) - } -} - #[tokio::test] async fn it_can_complete_async() { let wf_name = "it_can_complete_async".to_owned(); let mut starter = CoreWfStarter::new(&wf_name); let async_response = "agence"; let shared_token = Arc::new(tokio::sync::Mutex::new(None)); + + struct AsyncActivities { + shared_token: Arc>>>, + } + #[activities] + impl AsyncActivities { + #[activity] + async fn complete_async_activity( + self: Arc, + ctx: ActivityContext, + _: String, + ) -> Result<(), ActivityError> { + // set the `activity_task_token` + let activity_info = ctx.get_info(); + let task_token = &activity_info.task_token; + let mut shared = self.shared_token.lock().await; + *shared = Some(task_token.clone()); + Err(ActivityError::WillCompleteAsync) + } + } + starter.sdk_config.register_activities(AsyncActivities { shared_token: shared_token.clone(), }); @@ -1031,7 +1035,8 @@ async fn it_can_complete_async() { worker.register_wf(wf_name.clone(), move |ctx: WfContext| async move { let activity_resolution = ctx - .activity::( + .activity( + AsyncActivities::complete_async_activity, "hi".to_string(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(30)), @@ -1086,24 +1091,25 @@ async fn it_can_complete_async() { static ACTS_STARTED: Semaphore = Semaphore::const_new(0); static ACTS_DONE: Semaphore = Semaphore::const_new(0); -struct SleeperActivities {} -#[activities] -impl SleeperActivities { - #[activity] - async fn sleeper(ctx: ActivityContext, _: String) -> Result<(), ActivityError> { - ACTS_STARTED.add_permits(1); - // just wait to be cancelled - ctx.cancelled().await; - ACTS_DONE.add_permits(1); - Err(ActivityError::cancelled()) - } -} - #[tokio::test] async fn graceful_shutdown() { let wf_name = "graceful_shutdown"; let mut starter = CoreWfStarter::new(wf_name); starter.sdk_config.graceful_shutdown_period = Some(Duration::from_millis(500)); + + struct SleeperActivities {} + #[activities] + impl SleeperActivities { + #[activity] + async fn sleeper(ctx: ActivityContext, _: String) -> Result<(), ActivityError> { + ACTS_STARTED.add_permits(1); + // just wait to be cancelled + ctx.cancelled().await; + ACTS_DONE.add_permits(1); + Err(ActivityError::cancelled()) + } + } + starter .sdk_config .register_activities_static::(); @@ -1111,7 +1117,8 @@ async fn graceful_shutdown() { let client = starter.get_client().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let act_futs = (1..=10).map(|_| { - ctx.activity::( + ctx.activity( + SleeperActivities::sleeper, "hi".to_string(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(5)), @@ -1159,34 +1166,36 @@ async fn graceful_shutdown() { } static WAS_CANCELLED: AtomicBool = AtomicBool::new(false); -struct CancellableEchoActivities {} -#[activities] -impl CancellableEchoActivities { - #[activity] - async fn cancellable_echo( - ctx: ActivityContext, - echo_me: String, - ) -> Result { - // Doesn't heartbeat - ctx.cancelled().await; - WAS_CANCELLED.store(true, Ordering::Relaxed); - Ok(echo_me) - } -} - #[tokio::test] async fn activity_can_be_cancelled_by_local_timeout() { let wf_name = "activity_can_be_cancelled_by_local_timeout"; let mut starter = CoreWfStarter::new(wf_name); starter .set_core_cfg_mutator(|m| m.local_timeout_buffer_for_activities = Duration::from_secs(0)); + + struct CancellableEchoActivities {} + #[activities] + impl CancellableEchoActivities { + #[activity] + async fn cancellable_echo( + ctx: ActivityContext, + echo_me: String, + ) -> Result { + // Doesn't heartbeat + ctx.cancelled().await; + WAS_CANCELLED.store(true, Ordering::Relaxed); + Ok(echo_me) + } + } + starter .sdk_config .register_activities_static::(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let res = ctx - .activity::( + .activity( + CancellableEchoActivities::cancellable_echo, "hi!".to_string(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(1)), @@ -1235,7 +1244,8 @@ async fn long_activity_timeout_repro() { let mut iter = 1; loop { let res = ctx - .activity::( + .activity( + StdActivities::echo, "hi!".to_string(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(1)), diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/appdata_propagation.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/appdata_propagation.rs index 53d96332f..aa76bf594 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/appdata_propagation.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/appdata_propagation.rs @@ -15,7 +15,8 @@ struct Data { } pub(crate) async fn appdata_activity_wf(ctx: WfContext) -> WorkflowResult<()> { - ctx.activity::( + ctx.activity( + AppdataActivities::echo, "hi!".to_string(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(5)), diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/determinism.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/determinism.rs index 354cbf306..518bea7a0 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/determinism.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/determinism.rs @@ -1,6 +1,4 @@ -use crate::common::{ - CoreWfStarter, WorkflowHandleExt, activity_functions::std_activities, mock_sdk, mock_sdk_cfg, -}; +use crate::common::{CoreWfStarter, WorkflowHandleExt, mock_sdk, mock_sdk_cfg}; use std::{ sync::atomic::{AtomicBool, AtomicUsize, Ordering}, time::Duration, @@ -83,7 +81,8 @@ async fn task_fail_causes_replay_unset_too_soon() { if DID_FAIL.load(Ordering::Relaxed) { assert!(ctx.is_replaying()); } - ctx.activity::( + ctx.activity( + StdActivities::echo, "hi!".to_string(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(2)), diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/local_activities.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/local_activities.rs index 0ac369031..55d36a640 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/local_activities.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/local_activities.rs @@ -1,8 +1,7 @@ use crate::common::{ ActivationAssertionsInterceptor, CoreWfStarter, WorkflowHandleExt, - activity_functions::{StdActivities, std_activities}, - build_fake_sdk, history_from_proto_binary, init_core_replay_preloaded, mock_sdk, mock_sdk_cfg, - replay_sdk_worker, + activity_functions::StdActivities, build_fake_sdk, history_from_proto_binary, + init_core_replay_preloaded, mock_sdk, mock_sdk_cfg, replay_sdk_worker, workflows::la_problem_workflow, }; use anyhow::anyhow; @@ -19,35 +18,30 @@ use std::{ time::{Duration, Instant, SystemTime}, }; use temporalio_client::{WfClientExt, WorkflowClientTrait, WorkflowOptions}; -use temporalio_common::{ - ActivityDefinition, - protos::{ - DEFAULT_ACTIVITY_TYPE, canned_histories, - coresdk::{ - ActivityTaskCompletion, AsJsonPayloadExt, FromJsonPayloadExt, IntoPayloadsExt, - activity_result::ActivityExecutionResult, - workflow_activation::{WorkflowActivationJob, workflow_activation_job}, - workflow_commands::{ - ActivityCancellationType, ScheduleLocalActivity, workflow_command::Variant, - }, - workflow_completion::{ - self, WorkflowActivationCompletion, workflow_activation_completion, - }, +use temporalio_common::protos::{ + DEFAULT_ACTIVITY_TYPE, canned_histories, + coresdk::{ + ActivityTaskCompletion, AsJsonPayloadExt, FromJsonPayloadExt, IntoPayloadsExt, + activity_result::ActivityExecutionResult, + workflow_activation::{WorkflowActivationJob, workflow_activation_job}, + workflow_commands::{ + ActivityCancellationType, ScheduleLocalActivity, workflow_command::Variant, }, - temporal::api::{ - command::v1::{RecordMarkerCommandAttributes, command}, - common::v1::RetryPolicy, - enums::v1::{ - CommandType, EventType, TimeoutType, UpdateWorkflowExecutionLifecycleStage, - WorkflowTaskFailedCause, - }, - failure::v1::{Failure, failure::FailureInfo}, - history::v1::history_event::Attributes::MarkerRecordedEventAttributes, - query::v1::WorkflowQuery, - update::v1::WaitPolicy, + workflow_completion::{self, WorkflowActivationCompletion, workflow_activation_completion}, + }, + temporal::api::{ + command::v1::{RecordMarkerCommandAttributes, command}, + common::v1::RetryPolicy, + enums::v1::{ + CommandType, EventType, TimeoutType, UpdateWorkflowExecutionLifecycleStage, + WorkflowTaskFailedCause, }, - test_utils::{query_ok, schedule_local_activity_cmd, start_timer_cmd}, + failure::v1::{Failure, failure::FailureInfo}, + history::v1::history_event::Attributes::MarkerRecordedEventAttributes, + query::v1::WorkflowQuery, + update::v1::WaitPolicy, }, + test_utils::{query_ok, schedule_local_activity_cmd, start_timer_cmd}, }; use temporalio_macros::activities; use temporalio_sdk::{ @@ -70,8 +64,12 @@ use tokio_util::sync::CancellationToken; pub(crate) async fn one_local_activity_wf(ctx: WfContext) -> WorkflowResult<()> { let initial_workflow_time = ctx.workflow_time().expect("Workflow time should be set"); - ctx.local_activity::("hi!".to_string(), LocalActivityOptions::default())? - .await; + ctx.local_activity( + StdActivities::echo, + "hi!".to_string(), + LocalActivityOptions::default(), + )? + .await; // Verify LA execution advances the clock assert!(initial_workflow_time < ctx.workflow_time().unwrap()); Ok(().into()) @@ -96,7 +94,8 @@ async fn one_local_activity() { } pub(crate) async fn local_act_concurrent_with_timer_wf(ctx: WfContext) -> WorkflowResult<()> { - let la = ctx.local_activity::( + let la = ctx.local_activity( + StdActivities::echo, "hi!".to_string(), LocalActivityOptions::default(), )?; @@ -128,7 +127,8 @@ async fn local_act_then_timer_then_wait_result() { .register_activities_static::(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { - let la = ctx.local_activity::( + let la = ctx.local_activity( + StdActivities::echo, "hi!".to_string(), LocalActivityOptions::default(), )?; @@ -143,7 +143,8 @@ async fn local_act_then_timer_then_wait_result() { } pub(crate) async fn local_act_then_timer_then_wait(ctx: WfContext) -> WorkflowResult<()> { - let la = ctx.local_activity::( + let la = ctx.local_activity( + StdActivities::delay, Duration::from_secs(4), LocalActivityOptions::default(), )?; @@ -171,7 +172,7 @@ async fn long_running_local_act_with_timer() { pub(crate) async fn local_act_fanout_wf(ctx: WfContext) -> WorkflowResult<()> { let las: Vec<_> = (1..=50) .map(|i| { - ctx.local_activity::(format!("Hi {i}"), Default::default()) + ctx.local_activity(StdActivities::echo, format!("Hi {i}"), Default::default()) .expect("serializes fine") }) .collect(); @@ -205,7 +206,8 @@ async fn local_act_retry_timer_backoff() { let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let res = ctx - .local_activity::( + .local_activity( + StdActivities::always_fail, (), LocalActivityOptions { retry_policy: RetryPolicy { @@ -243,24 +245,6 @@ async fn local_act_retry_timer_backoff() { .unwrap(); } -struct EchoWithManualCancel { - manual_cancel: CancellationToken, -} -#[activities] -impl EchoWithManualCancel { - #[activity] - async fn echo(self: Arc, ctx: ActivityContext, _: String) -> Result<(), ActivityError> { - tokio::select! { - _ = tokio::time::sleep(Duration::from_secs(10)) => {} - _ = ctx.cancelled() => { - return Err(ActivityError::cancelled()) - } - _ = self.manual_cancel.cancelled() => {} - } - Ok(()) - } -} - #[rstest::rstest] #[case::wait(ActivityCancellationType::WaitCancellationCompleted)] #[case::try_cancel(ActivityCancellationType::TryCancel)] @@ -271,6 +255,29 @@ async fn cancel_immediate(#[case] cancel_type: ActivityCancellationType) { // If we don't use this, we'd hang on shutdown for abandon cancel modes. let manual_cancel = CancellationToken::new(); let mut starter = CoreWfStarter::new(&wf_name); + + struct EchoWithManualCancel { + manual_cancel: CancellationToken, + } + #[activities] + impl EchoWithManualCancel { + #[activity] + async fn echo( + self: Arc, + ctx: ActivityContext, + _: String, + ) -> Result<(), ActivityError> { + tokio::select! { + _ = tokio::time::sleep(Duration::from_secs(10)) => {} + _ = ctx.cancelled() => { + return Err(ActivityError::cancelled()) + } + _ = self.manual_cancel.cancelled() => {} + } + Ok(()) + } + } + starter .sdk_config .register_activities(EchoWithManualCancel { @@ -278,7 +285,8 @@ async fn cancel_immediate(#[case] cancel_type: ActivityCancellationType) { }); let mut worker = starter.worker().await; worker.register_wf(&wf_name, move |ctx: WfContext| async move { - let la = ctx.local_activity::( + let la = ctx.local_activity( + EchoWithManualCancel::echo, "hi".to_string(), LocalActivityOptions { cancel_type, @@ -327,35 +335,6 @@ impl WorkerInterceptor for LACancellerInterceptor { } } -struct EchoWithManualCancelAndBackoff { - manual_cancel: CancellationToken, - cancel_on_backoff: Option, -} -#[activities] -impl EchoWithManualCancelAndBackoff { - #[activity] - async fn echo(self: Arc, ctx: ActivityContext, _: String) -> Result<(), ActivityError> { - if self.cancel_on_backoff.is_some() { - if ctx.is_cancelled() { - return Err(ActivityError::cancelled()); - } - // Just fail constantly so we get stuck on the backoff timer - return Err(anyhow!("Oh no I failed!").into()); - } else { - tokio::select! { - _ = tokio::time::sleep(Duration::from_secs(100)) => {} - _ = ctx.cancelled() => { - return Err(ActivityError::cancelled()) - } - _ = self.manual_cancel.cancelled() => { - return Ok(()) - } - } - } - Err(anyhow!("Oh no I failed!").into()) - } -} - #[rstest::rstest] #[case::while_running(None)] #[case::while_backing_off(Some(Duration::from_millis(1500)))] @@ -375,6 +354,40 @@ async fn cancel_after_act_starts( let manual_cancel = CancellationToken::new(); let mut starter = CoreWfStarter::new(&wf_name); starter.workflow_options.task_timeout = Some(Duration::from_secs(1)); + + struct EchoWithManualCancelAndBackoff { + manual_cancel: CancellationToken, + cancel_on_backoff: Option, + } + #[activities] + impl EchoWithManualCancelAndBackoff { + #[activity] + async fn echo( + self: Arc, + ctx: ActivityContext, + _: String, + ) -> Result<(), ActivityError> { + if self.cancel_on_backoff.is_some() { + if ctx.is_cancelled() { + return Err(ActivityError::cancelled()); + } + // Just fail constantly so we get stuck on the backoff timer + return Err(anyhow!("Oh no I failed!").into()); + } else { + tokio::select! { + _ = tokio::time::sleep(Duration::from_secs(100)) => {} + _ = ctx.cancelled() => { + return Err(ActivityError::cancelled()) + } + _ = self.manual_cancel.cancelled() => { + return Ok(()) + } + } + } + Err(anyhow!("Oh no I failed!").into()) + } + } + starter .sdk_config .register_activities(EchoWithManualCancelAndBackoff { @@ -388,7 +401,8 @@ async fn cancel_after_act_starts( let mut worker = starter.worker().await; let bo_dur = cancel_on_backoff.unwrap_or_else(|| Duration::from_secs(1)); worker.register_wf(&wf_name, move |ctx: WfContext| async move { - let la = ctx.local_activity::( + let la = ctx.local_activity( + EchoWithManualCancelAndBackoff::echo, "hi".to_string(), LocalActivityOptions { retry_policy: RetryPolicy { @@ -430,21 +444,6 @@ async fn cancel_after_act_starts( starter.shutdown().await; } -struct LongRunningWithCancellation; -#[activities] -impl LongRunningWithCancellation { - #[activity] - async fn go(ctx: ActivityContext) -> Result<(), ActivityError> { - tokio::select! { - _ = tokio::time::sleep(Duration::from_secs(100)) => {} - _ = ctx.cancelled() => { - return Err(ActivityError::cancelled()) - } - } - Ok(()) - } -} - #[rstest::rstest] #[case::schedule(true)] #[case::start(false)] @@ -455,6 +454,22 @@ async fn x_to_close_timeout(#[case] is_schedule: bool) { if is_schedule { "schedule" } else { "start" } ); let mut starter = CoreWfStarter::new(&wf_name); + + struct LongRunningWithCancellation; + #[activities] + impl LongRunningWithCancellation { + #[activity] + async fn go(ctx: ActivityContext) -> Result<(), ActivityError> { + tokio::select! { + _ = tokio::time::sleep(Duration::from_secs(100)) => {} + _ = ctx.cancelled() => { + return Err(ActivityError::cancelled()) + } + } + Ok(()) + } + } + starter .sdk_config .register_activities_static::(); @@ -472,7 +487,8 @@ async fn x_to_close_timeout(#[case] is_schedule: bool) { worker.register_wf(wf_name.to_owned(), move |ctx: WfContext| async move { let res = ctx - .local_activity::( + .local_activity( + LongRunningWithCancellation::go, (), LocalActivityOptions { retry_policy: RetryPolicy { @@ -497,18 +513,6 @@ async fn x_to_close_timeout(#[case] is_schedule: bool) { worker.run_until_done().await.unwrap(); } -struct FailWithAtomicCounter { - counter: Arc, -} -#[activities] -impl FailWithAtomicCounter { - #[activity] - async fn go(self: Arc, _: ActivityContext, _: String) -> Result<(), ActivityError> { - self.counter.fetch_add(1, Ordering::Relaxed); - Err(anyhow!("Oh no I failed!").into()) - } -} - #[rstest::rstest] #[case::cached(true)] #[case::not_cached(false)] @@ -525,7 +529,8 @@ async fn schedule_to_close_timeout_across_timer_backoff(#[case] cached: bool) { let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let res = ctx - .local_activity::( + .local_activity( + FailWithAtomicCounter::go, "hi".to_string(), LocalActivityOptions { retry_policy: RetryPolicy { @@ -545,6 +550,19 @@ async fn schedule_to_close_timeout_across_timer_backoff(#[case] cached: bool) { Ok(().into()) }); let num_attempts = Arc::new(AtomicU8::new(0)); + + struct FailWithAtomicCounter { + counter: Arc, + } + #[activities] + impl FailWithAtomicCounter { + #[activity] + async fn go(self: Arc, _: ActivityContext, _: String) -> Result<(), ActivityError> { + self.counter.fetch_add(1, Ordering::Relaxed); + Err(anyhow!("Oh no I failed!").into()) + } + } + worker.register_activities(FailWithAtomicCounter { counter: num_attempts.clone(), }); @@ -592,7 +610,8 @@ async fn timer_backoff_concurrent_with_non_timer_backoff() { .register_activities_static::(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { - let r1 = ctx.local_activity::( + let r1 = ctx.local_activity( + StdActivities::always_fail, (), LocalActivityOptions { retry_policy: RetryPolicy { @@ -606,7 +625,8 @@ async fn timer_backoff_concurrent_with_non_timer_backoff() { ..Default::default() }, )?; - let r2 = ctx.local_activity::( + let r2 = ctx.local_activity( + StdActivities::always_fail, (), LocalActivityOptions { retry_policy: RetryPolicy { @@ -641,7 +661,8 @@ async fn repro_nondeterminism_with_timer_bug() { worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let t1 = ctx.timer(Duration::from_secs(30)); - let r1 = ctx.local_activity::( + let r1 = ctx.local_activity( + StdActivities::delay, Duration::from_secs(2), LocalActivityOptions { retry_policy: RetryPolicy { @@ -749,19 +770,6 @@ async fn third_weird_la_nondeterminism_repro() { worker.run().await.unwrap(); } -struct DelayWithCancellation; -#[activities] -impl DelayWithCancellation { - #[activity] - async fn delay(ctx: ActivityContext, dur: Duration) -> Result<(), ActivityError> { - tokio::select! { - _ = tokio::time::sleep(dur) => {} - _ = ctx.cancelled() => {} - } - Ok(()) - } -} - /// This test demonstrates why it's important to send LA resolutions last within a job. /// If we were to (during replay) scan ahead, see the marker, and resolve the LA before the /// activity cancellation, that would be wrong because, during execution, the LA resolution is @@ -778,6 +786,20 @@ impl DelayWithCancellation { async fn la_resolve_same_time_as_other_cancel() { let wf_name = "la_resolve_same_time_as_other_cancel"; let mut starter = CoreWfStarter::new(wf_name); + + struct DelayWithCancellation; + #[activities] + impl DelayWithCancellation { + #[activity] + async fn delay(ctx: ActivityContext, dur: Duration) -> Result<(), ActivityError> { + tokio::select! { + _ = tokio::time::sleep(dur) => {} + _ = ctx.cancelled() => {} + } + Ok(()) + } + } + starter .sdk_config .register_activities_static::(); @@ -787,7 +809,8 @@ async fn la_resolve_same_time_as_other_cancel() { worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let normal_act = ctx - .activity::( + .activity( + DelayWithCancellation::delay, Duration::from_secs(9), ActivityOptions { cancellation_type: ActivityCancellationType::TryCancel, @@ -800,7 +823,8 @@ async fn la_resolve_same_time_as_other_cancel() { ctx.timer(Duration::from_millis(1)).await; // Start LA and cancel the activity at the same time - let local_act = ctx.local_activity::( + let local_act = ctx.local_activity( + DelayWithCancellation::delay, Duration::from_millis(100), LocalActivityOptions { ..Default::default() @@ -875,7 +899,8 @@ async fn long_local_activity_with_update( } }, ); - ctx.local_activity::( + ctx.local_activity( + StdActivities::delay, Duration::from_secs(6), LocalActivityOptions::default(), )? @@ -951,7 +976,8 @@ async fn local_activity_with_heartbeat_only_causes_one_wakeup() { let la_resolved = AtomicBool::new(false); tokio::join!( async { - ctx.local_activity::( + ctx.local_activity( + StdActivities::delay, Duration::from_secs(6), LocalActivityOptions::default(), ) @@ -985,7 +1011,8 @@ async fn local_activity_with_heartbeat_only_causes_one_wakeup() { } pub(crate) async fn local_activity_with_summary_wf(ctx: WfContext) -> WorkflowResult<()> { - ctx.local_activity::( + ctx.local_activity( + StdActivities::echo, "hi".to_string(), LocalActivityOptions { summary: Some("Echo summary".to_string()), @@ -1073,7 +1100,7 @@ async fn local_act_two_wfts_before_marker(#[case] replay: bool, #[case] cached: worker.register_wf( DEFAULT_WORKFLOW_TYPE.to_owned(), |ctx: WfContext| async move { - let la = ctx.local_activity::((), Default::default())?; + let la = ctx.local_activity(StdActivities::default, (), Default::default())?; ctx.timer(Duration::from_secs(1)).await; la.await; Ok(().into()) @@ -1125,28 +1152,6 @@ async fn local_act_many_concurrent() { worker.run_until_done().await.unwrap(); } -struct EchoWithConditionalBarrier { - shutdown_middle: bool, - shutdown_barr: &'static Barrier, - wft_timeout: Duration, -} -#[activities] -impl EchoWithConditionalBarrier { - #[activity] - async fn echo( - self: Arc, - _: ActivityContext, - str: String, - ) -> Result { - if self.shutdown_middle { - self.shutdown_barr.wait().await; - } - // Take slightly more than two workflow tasks - tokio::time::sleep(self.wft_timeout.mul_f32(2.2)).await; - Ok(str) - } -} - /// Verifies that local activities which take more than a workflow task timeout will cause /// us to issue additional (empty) WFT completions with the force flag on, thus preventing timeout /// of WFT while the local activity continues to execute. @@ -1181,7 +1186,8 @@ async fn local_act_heartbeat(#[case] shutdown_middle: bool) { worker.register_wf( DEFAULT_WORKFLOW_TYPE.to_owned(), |ctx: WfContext| async move { - ctx.local_activity::( + ctx.local_activity( + EchoWithConditionalBarrier::echo, "hi".to_string(), LocalActivityOptions::default(), )? @@ -1189,6 +1195,29 @@ async fn local_act_heartbeat(#[case] shutdown_middle: bool) { Ok(().into()) }, ); + + struct EchoWithConditionalBarrier { + shutdown_middle: bool, + shutdown_barr: &'static Barrier, + wft_timeout: Duration, + } + #[activities] + impl EchoWithConditionalBarrier { + #[activity] + async fn echo( + self: Arc, + _: ActivityContext, + str: String, + ) -> Result { + if self.shutdown_middle { + self.shutdown_barr.wait().await; + } + // Take slightly more than two workflow tasks + tokio::time::sleep(self.wft_timeout.mul_f32(2.2)).await; + Ok(str) + } + } + worker.register_activities(EchoWithConditionalBarrier { shutdown_middle, shutdown_barr, @@ -1215,23 +1244,6 @@ async fn local_act_heartbeat(#[case] shutdown_middle: bool) { runres.unwrap(); } -struct EventuallyPassingActivity { - attempts: Arc, - eventually_pass: bool, -} -#[activities] -impl EventuallyPassingActivity { - #[activity] - async fn echo(self: Arc, _: ActivityContext, _: String) -> Result<(), ActivityError> { - // Succeed on 3rd attempt (which is ==2 since fetch_add returns prev val) - if 2 == self.attempts.fetch_add(1, Ordering::Relaxed) && self.eventually_pass { - Ok(()) - } else { - Err(anyhow!("Oh no I failed!").into()) - } - } -} - #[rstest::rstest] #[case::retry_then_pass(true)] #[case::retry_until_fail(false)] @@ -1250,7 +1262,8 @@ async fn local_act_fail_and_retry(#[case] eventually_pass: bool) { DEFAULT_WORKFLOW_TYPE.to_owned(), move |ctx: WfContext| async move { let la_res = ctx - .local_activity::( + .local_activity( + EventuallyPassingActivity::echo, "hi".to_string(), LocalActivityOptions { retry_policy: RetryPolicy { @@ -1273,6 +1286,24 @@ async fn local_act_fail_and_retry(#[case] eventually_pass: bool) { }, ); let attempts = Arc::new(AtomicUsize::new(0)); + + struct EventuallyPassingActivity { + attempts: Arc, + eventually_pass: bool, + } + #[activities] + impl EventuallyPassingActivity { + #[activity] + async fn echo(self: Arc, _: ActivityContext, _: String) -> Result<(), ActivityError> { + // Succeed on 3rd attempt (which is ==2 since fetch_add returns prev val) + if 2 == self.attempts.fetch_add(1, Ordering::Relaxed) && self.eventually_pass { + Ok(()) + } else { + Err(anyhow!("Oh no I failed!").into()) + } + } + } + worker.register_activities(EventuallyPassingActivity { attempts: attempts.clone(), eventually_pass, @@ -1301,7 +1332,7 @@ async fn local_act_retry_long_backoff_uses_timer() { "1", None, Some(Failure::application_failure("la failed".to_string(), false)), - |m| m.activity_type = std_activities::AlwaysFail::name().to_owned(), + |m| m.activity_type = StdActivities::always_fail.name().to_owned(), ); let timer_started_event_id = t.add_by_type(EventType::TimerStarted); t.add_timer_fired(timer_started_event_id, "1".to_string()); @@ -1311,7 +1342,7 @@ async fn local_act_retry_long_backoff_uses_timer() { "2", None, Some(Failure::application_failure("la failed".to_string(), false)), - |m| m.activity_type = std_activities::AlwaysFail::name().to_owned(), + |m| m.activity_type = StdActivities::always_fail.name().to_owned(), ); let timer_started_event_id = t.add_by_type(EventType::TimerStarted); t.add_timer_fired(timer_started_event_id, "2".to_string()); @@ -1332,7 +1363,8 @@ async fn local_act_retry_long_backoff_uses_timer() { DEFAULT_WORKFLOW_TYPE.to_owned(), |ctx: WfContext| async move { let la_res = ctx - .local_activity::( + .local_activity( + StdActivities::always_fail, (), LocalActivityOptions { retry_policy: RetryPolicy { @@ -1372,7 +1404,7 @@ async fn local_act_null_result() { t.add_by_type(EventType::WorkflowExecutionStarted); t.add_full_wf_task(); t.add_local_activity_marker(1, "1", None, None, |m| { - m.activity_type = std_activities::NoOp::name().to_owned() + m.activity_type = StdActivities::no_op.name().to_owned() }); t.add_workflow_execution_completed(); @@ -1384,7 +1416,7 @@ async fn local_act_null_result() { worker.register_wf( DEFAULT_WORKFLOW_TYPE.to_owned(), |ctx: WfContext| async move { - ctx.local_activity::((), LocalActivityOptions::default())? + ctx.local_activity(StdActivities::no_op, (), LocalActivityOptions::default())? .await; Ok(().into()) }, @@ -1423,7 +1455,7 @@ async fn local_act_command_immediately_follows_la_marker() { worker.register_wf( DEFAULT_WORKFLOW_TYPE.to_owned(), |ctx: WfContext| async move { - ctx.local_activity::((), LocalActivityOptions::default())? + ctx.local_activity(StdActivities::no_op, (), LocalActivityOptions::default())? .await; ctx.timer(Duration::from_secs(1)).await; Ok(().into()) @@ -1713,7 +1745,8 @@ async fn test_schedule_to_start_timeout() { DEFAULT_WORKFLOW_TYPE.to_owned(), |ctx: WfContext| async move { let la_res = ctx - .local_activity::( + .local_activity( + StdActivities::echo, "hi".to_string(), LocalActivityOptions { // Impossibly small timeout so we timeout in the queue @@ -1799,7 +1832,8 @@ async fn test_schedule_to_start_timeout_not_based_on_original_time( DEFAULT_WORKFLOW_TYPE.to_owned(), move |ctx: WfContext| async move { let la_res = ctx - .local_activity::( + .local_activity( + StdActivities::echo, "hi".to_string(), LocalActivityOptions { retry_policy: RetryPolicy { @@ -1836,29 +1870,6 @@ async fn test_schedule_to_start_timeout_not_based_on_original_time( worker.run_until_done().await.unwrap(); } -struct ActivityWithRetriesAndCancellation { - attempts: Arc, - cancels: Arc, - la_completes: bool, -} -#[activities] -impl ActivityWithRetriesAndCancellation { - #[activity(name = DEFAULT_ACTIVITY_TYPE)] - async fn go(self: Arc, ctx: ActivityContext) -> Result<(), ActivityError> { - // Timeout the first 4 attempts, or all of them if we intend to fail - if self.attempts.fetch_add(1, Ordering::AcqRel) < 4 || !self.la_completes { - select! { - _ = tokio::time::sleep(Duration::from_millis(100)) => (), - _ = ctx.cancelled() => { - self.cancels.fetch_add(1, Ordering::AcqRel); - return Err(ActivityError::cancelled()); - } - } - } - Ok(()) - } -} - #[rstest::rstest] #[tokio::test] async fn start_to_close_timeout_allows_retries(#[values(true, false)] la_completes: bool) { @@ -1893,7 +1904,8 @@ async fn start_to_close_timeout_allows_retries(#[values(true, false)] la_complet DEFAULT_WORKFLOW_TYPE.to_owned(), move |ctx: WfContext| async move { let la_res = ctx - .local_activity::( + .local_activity( + ActivityWithRetriesAndCancellation::go, (), LocalActivityOptions { retry_policy: RetryPolicy { @@ -1918,6 +1930,30 @@ async fn start_to_close_timeout_allows_retries(#[values(true, false)] la_complet ); let attempts = Arc::new(AtomicUsize::new(0)); let cancels = Arc::new(AtomicUsize::new(0)); + + struct ActivityWithRetriesAndCancellation { + attempts: Arc, + cancels: Arc, + la_completes: bool, + } + #[activities] + impl ActivityWithRetriesAndCancellation { + #[activity(name = DEFAULT_ACTIVITY_TYPE)] + async fn go(self: Arc, ctx: ActivityContext) -> Result<(), ActivityError> { + // Timeout the first 4 attempts, or all of them if we intend to fail + if self.attempts.fetch_add(1, Ordering::AcqRel) < 4 || !self.la_completes { + select! { + _ = tokio::time::sleep(Duration::from_millis(100)) => (), + _ = ctx.cancelled() => { + self.cancels.fetch_add(1, Ordering::AcqRel); + return Err(ActivityError::cancelled()); + } + } + } + Ok(()) + } + } + worker.register_activities(ActivityWithRetriesAndCancellation { attempts: attempts.clone(), cancels: cancels.clone(), @@ -1939,19 +1975,6 @@ async fn start_to_close_timeout_allows_retries(#[values(true, false)] la_complet assert_eq!(cancels.load(Ordering::Acquire), num_cancels); } -struct ActivityThatExpectsCancellation; -#[activities] -impl ActivityThatExpectsCancellation { - #[activity] - async fn go(ctx: ActivityContext) -> Result<(), ActivityError> { - let res = tokio::time::timeout(Duration::from_millis(500), ctx.cancelled()).await; - if res.is_err() { - panic!("Activity must be cancelled!!!!"); - } - Err(ActivityError::cancelled()) - } -} - #[tokio::test] async fn wft_failure_cancels_running_las() { let mut t = TestHistoryBuilder::default(); @@ -1970,8 +1993,8 @@ async fn wft_failure_cancels_running_las() { worker.register_wf( DEFAULT_WORKFLOW_TYPE.to_owned(), |ctx: WfContext| async move { - let la_handle = ctx - .local_activity::((), Default::default())?; + let la_handle = + ctx.local_activity(ActivityThatExpectsCancellation::go, (), Default::default())?; tokio::join!( async { ctx.timer(Duration::from_secs(1)).await; @@ -1982,6 +2005,20 @@ async fn wft_failure_cancels_running_las() { Ok(().into()) }, ); + + struct ActivityThatExpectsCancellation; + #[activities] + impl ActivityThatExpectsCancellation { + #[activity] + async fn go(ctx: ActivityContext) -> Result<(), ActivityError> { + let res = tokio::time::timeout(Duration::from_millis(500), ctx.cancelled()).await; + if res.is_err() { + panic!("Activity must be cancelled!!!!"); + } + Err(ActivityError::cancelled()) + } + } + worker.register_activities_static::(); worker .submit_wf( @@ -2025,7 +2062,8 @@ async fn resolved_las_not_recorded_if_wft_fails_many_times() { worker.register_wf( DEFAULT_WORKFLOW_TYPE.to_owned(), WorkflowFunction::new::<_, _, ()>(|ctx: WfContext| async move { - ctx.local_activity::( + ctx.local_activity( + StdActivities::echo, "hi".to_string(), LocalActivityOptions { ..Default::default() @@ -2078,7 +2116,8 @@ async fn local_act_records_nonfirst_attempts_ok() { worker.register_wf( DEFAULT_WORKFLOW_TYPE.to_owned(), |ctx: WfContext| async move { - ctx.local_activity::( + ctx.local_activity( + StdActivities::always_fail, (), LocalActivityOptions { retry_policy: RetryPolicy { @@ -2381,28 +2420,6 @@ async fn local_activity_after_wf_complete_is_discarded() { core.drain_pollers_and_shutdown().await; } -struct ActivityWithExplicitBackoff { - attempts: Arc, -} -#[activities] -impl ActivityWithExplicitBackoff { - #[activity] - async fn go(self: Arc, _: ActivityContext) -> Result<(), ActivityError> { - // Succeed on 3rd attempt (which is ==2 since fetch_add returns prev val) - let last_attempt = self.attempts.fetch_add(1, Ordering::Relaxed); - if 0 == last_attempt { - Err(ActivityError::Retryable { - source: anyhow!("Explicit backoff error").into_boxed_dyn_error(), - explicit_delay: Some(Duration::from_millis(300)), - }) - } else if 2 == last_attempt { - Ok(()) - } else { - Err(anyhow!("Oh no I failed!").into()) - } - } -} - #[tokio::test] async fn local_act_retry_explicit_delay() { let mut t = TestHistoryBuilder::default(); @@ -2418,7 +2435,8 @@ async fn local_act_retry_explicit_delay() { DEFAULT_WORKFLOW_TYPE.to_owned(), move |ctx: WfContext| async move { let la_res = ctx - .local_activity::( + .local_activity( + ActivityWithExplicitBackoff::go, (), LocalActivityOptions { retry_policy: RetryPolicy { @@ -2436,6 +2454,29 @@ async fn local_act_retry_explicit_delay() { }, ); let attempts = Arc::new(AtomicUsize::new(0)); + + struct ActivityWithExplicitBackoff { + attempts: Arc, + } + #[activities] + impl ActivityWithExplicitBackoff { + #[activity] + async fn go(self: Arc, _: ActivityContext) -> Result<(), ActivityError> { + // Succeed on 3rd attempt (which is ==2 since fetch_add returns prev val) + let last_attempt = self.attempts.fetch_add(1, Ordering::Relaxed); + if 0 == last_attempt { + Err(ActivityError::Retryable { + source: anyhow!("Explicit backoff error").into_boxed_dyn_error(), + explicit_delay: Some(Duration::from_millis(300)), + }) + } else if 2 == last_attempt { + Ok(()) + } else { + Err(anyhow!("Oh no I failed!").into()) + } + } + } + worker.register_activities(ActivityWithExplicitBackoff { attempts: attempts.clone(), }); @@ -2472,29 +2513,6 @@ async fn la_wf(ctx: WfContext) -> WorkflowResult<()> { Ok(().into()) } -struct ActivityWithReplayCheck { - replay: bool, - completes_ok: bool, -} -#[activities] -impl ActivityWithReplayCheck { - #[activity(name = DEFAULT_ACTIVITY_TYPE)] - async fn echo( - self: Arc, - _: ActivityContext, - _: (), - ) -> Result<&'static str, ActivityError> { - if self.replay { - panic!("Should not be invoked on replay"); - } - if self.completes_ok { - Ok("hi") - } else { - Err(anyhow!("Oh no I failed!").into()) - } - } -} - #[rstest] #[case::incremental(false, true)] #[case::replay(true, true)] @@ -2562,6 +2580,31 @@ async fn one_la_success(#[case] replay: bool, #[case] completes_ok: bool) { let mut worker = build_fake_sdk(mock_cfg); worker.register_wf(DEFAULT_WORKFLOW_TYPE, la_wf); + + struct ActivityWithReplayCheck { + replay: bool, + completes_ok: bool, + } + #[activities] + impl ActivityWithReplayCheck { + #[activity(name = DEFAULT_ACTIVITY_TYPE)] + #[allow(unused)] + async fn echo( + self: Arc, + _: ActivityContext, + _: (), + ) -> Result<&'static str, ActivityError> { + if self.replay { + panic!("Should not be invoked on replay"); + } + if self.completes_ok { + Ok("hi") + } else { + Err(anyhow!("Oh no I failed!").into()) + } + } + } + worker.register_activities(ActivityWithReplayCheck { replay, completes_ok, @@ -2604,6 +2647,7 @@ async fn two_la_wf_parallel(ctx: WfContext) -> WorkflowResult<()> { struct ResolvedActivity; #[activities] impl ResolvedActivity { + #[allow(unused)] #[activity(name = DEFAULT_ACTIVITY_TYPE)] async fn echo(_: ActivityContext, _: ()) -> Result<&'static str, ActivityError> { Ok("Resolved") @@ -2873,22 +2917,6 @@ async fn immediate_cancel( worker.run().await.unwrap(); } -struct ActivityWithConditionalCancelWait { - cancel_type: ActivityCancellationType, - allow_cancel_barr: CancellationToken, -} -#[activities] -impl ActivityWithConditionalCancelWait { - #[activity(name = DEFAULT_ACTIVITY_TYPE)] - async fn echo(self: Arc, ctx: ActivityContext, _: ()) -> Result<(), ActivityError> { - if self.cancel_type == ActivityCancellationType::WaitCancellationCompleted { - ctx.cancelled().await; - } - self.allow_cancel_barr.cancelled().await; - Err(ActivityError::cancelled()) - } -} - #[rstest] #[case::incremental(false)] #[case::replay(true)] @@ -2974,7 +3002,8 @@ async fn cancel_after_act_starts_canned( let mut worker = build_fake_sdk(mock_cfg); worker.register_wf(DEFAULT_WORKFLOW_TYPE, move |ctx: WfContext| async move { - let la = ctx.local_activity::( + let la = ctx.local_activity( + ActivityWithConditionalCancelWait::echo, (), LocalActivityOptions { cancel_type, @@ -2999,6 +3028,23 @@ async fn cancel_after_act_starts_canned( ); Ok(().into()) }); + + struct ActivityWithConditionalCancelWait { + cancel_type: ActivityCancellationType, + allow_cancel_barr: CancellationToken, + } + #[activities] + impl ActivityWithConditionalCancelWait { + #[activity(name = DEFAULT_ACTIVITY_TYPE)] + async fn echo(self: Arc, ctx: ActivityContext, _: ()) -> Result<(), ActivityError> { + if self.cancel_type == ActivityCancellationType::WaitCancellationCompleted { + ctx.cancelled().await; + } + self.allow_cancel_barr.cancelled().await; + Err(ActivityError::cancelled()) + } + } + worker.register_activities(ActivityWithConditionalCancelWait { cancel_type, allow_cancel_barr: allow_cancel_barr_clone, diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/resets.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/resets.rs index a2697b708..c96d51781 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/resets.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/resets.rs @@ -1,7 +1,4 @@ -use crate::common::{ - CoreWfStarter, NAMESPACE, - activity_functions::{StdActivities, std_activities}, -}; +use crate::common::{CoreWfStarter, NAMESPACE, activity_functions::StdActivities}; use futures_util::StreamExt; use std::{ sync::{ @@ -150,7 +147,8 @@ async fn reset_randomseed() { if RAND_SEED.load(Ordering::Relaxed) == ctx.random_seed() { ctx.timer(Duration::from_millis(100)).await; } else { - ctx.local_activity::( + ctx.local_activity( + StdActivities::echo, "hi!".to_string(), LocalActivityOptions::default(), )? diff --git a/crates/sdk-core/tests/manual_tests.rs b/crates/sdk-core/tests/manual_tests.rs index 443822ee0..63622e680 100644 --- a/crates/sdk-core/tests/manual_tests.rs +++ b/crates/sdk-core/tests/manual_tests.rs @@ -83,7 +83,8 @@ async fn poller_load_spiky() { let real_stuff = async move { for _ in 0..5 { - ctx.activity::( + ctx.activity( + JitteryEchoActivities::echo, "hi!".to_string(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(5)), @@ -324,7 +325,8 @@ async fn poller_load_spike_then_sustained() { let real_stuff = async move { for _ in 0..5 { - ctx.activity::( + ctx.activity( + JitteryEchoActivities::echo, "hi!".to_string(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(5)), diff --git a/crates/sdk-core/tests/shared_tests/priority.rs b/crates/sdk-core/tests/shared_tests/priority.rs index 540974c7e..9388c9650 100644 --- a/crates/sdk-core/tests/shared_tests/priority.rs +++ b/crates/sdk-core/tests/shared_tests/priority.rs @@ -10,23 +10,6 @@ use temporalio_sdk::{ activities::{ActivityContext, ActivityError}, }; -struct PriorityActivities {} -#[activities] -impl PriorityActivities { - #[activity] - async fn echo(ctx: ActivityContext, echo_me: String) -> Result { - assert_eq!( - ctx.get_info().priority, - Priority { - priority_key: 5, - fairness_key: "fair-act".to_string(), - fairness_weight: 1.1 - } - ); - Ok(echo_me) - } -} - pub(crate) async fn priority_values_sent_to_server() { let mut starter = if let Some(wfs) = CoreWfStarter::new_cloud_or_local("priority_values_sent_to_server", ">=1.29.0-139.2").await @@ -42,6 +25,24 @@ pub(crate) async fn priority_values_sent_to_server() { }); let mut worker = starter.worker().await; let child_type = "child-wf"; + + struct PriorityActivities {} + #[activities] + impl PriorityActivities { + #[activity] + async fn echo(ctx: ActivityContext, echo_me: String) -> Result { + assert_eq!( + ctx.get_info().priority, + Priority { + priority_key: 5, + fairness_key: "fair-act".to_string(), + fairness_weight: 1.1 + } + ); + Ok(echo_me) + } + } + worker.register_activities_static::(); worker.register_wf(starter.get_task_queue(), move |ctx: WfContext| async move { let child = ctx.child_workflow(ChildWorkflowOptions { @@ -64,7 +65,8 @@ pub(crate) async fn priority_values_sent_to_server() { .into_started() .expect("Child should start OK"); let activity = ctx - .activity::( + .activity( + PriorityActivities::echo, "hello".to_string(), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(5)), diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index 02aad9e32..52aac8141 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -21,6 +21,8 @@ //! }; //! use temporalio_sdk_core::{CoreRuntime, RuntimeOptions, Url}; //! +//! struct MyActivities; +//! //! #[activities] //! impl MyActivities { //! #[activity] @@ -59,7 +61,7 @@ //! .register_activities_static::() //! .build(); //! -//! let worker = Worker::new(&runtime, client, worker_options)?; +//! let mut worker = Worker::new(&runtime, client, worker_options)?; //! worker.run().await?; //! //! Ok(()) @@ -251,7 +253,7 @@ pub struct WorkerOptions { // TODO [rust-sdk-branch]: Traitify this? impl WorkerOptionsBuilder { /// Registers all activities on an activity implementer that don't take a receiver. - pub fn register_activities_static(&mut self) -> &mut Self + pub fn register_activities_static(mut self) -> Self where AI: ActivityImplementer + HasOnlyStaticMethods, { @@ -259,20 +261,20 @@ impl WorkerOptionsBuilder { self } /// Registers all activities on an activity implementer that take a receiver. - pub fn register_activities(&mut self, instance: AI) -> &mut Self { + pub fn register_activities(mut self, instance: AI) -> Self { self.activities.register_activities::(instance); self } /// Registers a specific activitiy that does not take a receiver. - pub fn register_activity(&mut self) -> &mut Self { + pub fn register_activity(mut self) -> Self { self.activities.register_activity::(); self } /// Registers a specific activitiy that takes a receiver. pub fn register_activity_with_instance( - &mut self, + mut self, instance: Arc, - ) -> &mut Self { + ) -> Self { self.activities .register_activity_with_instance::(instance); self @@ -1379,11 +1381,39 @@ mod tests { async fn my_activity(_ctx: ActivityContext) -> Result<(), ActivityError> { Ok(()) } + + #[activity] + async fn takes_self( + self: Arc, + _ctx: ActivityContext, + _: String, + ) -> Result<(), ActivityError> { + Ok(()) + } } #[test] fn test_activity_registration() { let act_instance = MyActivities {}; - WorkerOptions::new("task_q").register_activities(act_instance); + let _ = WorkerOptions::new("task_q").register_activities(act_instance); + } + + // Compile-only test for workflow context invocation + #[allow(dead_code, unreachable_code, unused, clippy::diverging_sub_expression)] + fn test_activity_via_workflow_context() { + let wf_ctx: WfContext = unimplemented!(); + wf_ctx.activity(MyActivities::my_activity, (), ActivityOptions::default()); + wf_ctx.activity( + MyActivities::takes_self, + "Hi".to_owned(), + ActivityOptions::default(), + ); + } + + // Compile-only test for direct invocation via .run() + #[allow(dead_code, unreachable_code, unused, clippy::diverging_sub_expression)] + async fn test_activity_direct_invocation() { + let ctx: ActivityContext = unimplemented!(); + let _result = MyActivities::my_activity.run(ctx).await; } } diff --git a/crates/sdk/src/workflow_context.rs b/crates/sdk/src/workflow_context.rs index 3111558bc..5a2717aa2 100644 --- a/crates/sdk/src/workflow_context.rs +++ b/crates/sdk/src/workflow_context.rs @@ -219,6 +219,7 @@ impl WfContext { /// Request to run an activity pub fn activity( &self, + _activity: AD, input: AD::Input, opts: ActivityOptions, ) -> Result, PayloadConversionError> { @@ -253,6 +254,7 @@ impl WfContext { /// Request to run a local activity pub fn local_activity( &self, + _activity: AD, input: AD::Input, opts: LocalActivityOptions, ) -> Result + '_, PayloadConversionError> { From 3791149a73fe7ff1c6ee167bfbb8d000892fb9da Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Tue, 6 Jan 2026 16:09:15 -0800 Subject: [PATCH 12/13] Review comments --- crates/common/src/data_converters.rs | 36 +-- crates/macros/src/definitions.rs | 24 +- crates/macros/src/lib.rs | 5 +- .../ActivityMachine_Coverage.puml | 4 +- .../UpdateMachine_Coverage.puml | 2 +- .../WorkflowTaskMachine_Coverage.puml | 2 +- .../tests/common/activity_functions.rs | 2 +- crates/sdk-core/tests/common/mod.rs | 10 +- crates/sdk-core/tests/common/workflows.rs | 4 +- crates/sdk-core/tests/heavy_tests.rs | 32 +-- .../tests/heavy_tests/fuzzy_workflow.rs | 8 +- .../tests/integ_tests/heartbeat_tests.rs | 6 +- .../tests/integ_tests/metrics_tests.rs | 16 +- .../tests/integ_tests/polling_tests.rs | 6 +- .../tests/integ_tests/update_tests.rs | 28 +-- .../integ_tests/worker_heartbeat_tests.rs | 34 +-- .../tests/integ_tests/worker_tests.rs | 42 ++-- .../integ_tests/worker_versioning_tests.rs | 6 +- .../tests/integ_tests/workflow_tests.rs | 22 +- .../integ_tests/workflow_tests/activities.rs | 83 +++---- .../workflow_tests/appdata_propagation.rs | 8 +- .../integ_tests/workflow_tests/determinism.rs | 57 +++-- .../workflow_tests/local_activities.rs | 213 +++++++----------- .../integ_tests/workflow_tests/patches.rs | 68 +++--- .../integ_tests/workflow_tests/resets.rs | 4 +- crates/sdk-core/tests/manual_tests.rs | 10 +- .../sdk-core/tests/shared_tests/priority.rs | 6 +- crates/sdk/src/activities.rs | 90 +++++--- crates/sdk/src/lib.rs | 83 ++----- crates/sdk/src/workflow_context.rs | 34 +-- 30 files changed, 390 insertions(+), 555 deletions(-) diff --git a/crates/common/src/data_converters.rs b/crates/common/src/data_converters.rs index 70ae62405..4fddf5e1b 100644 --- a/crates/common/src/data_converters.rs +++ b/crates/common/src/data_converters.rs @@ -90,13 +90,13 @@ pub struct DefaultFailureConverter; pub trait PayloadCodec { fn encode( &self, - payloads: Vec, context: &SerializationContext, + payloads: Vec, ) -> BoxFuture<'static, Vec>; fn decode( &self, - payloads: Vec, context: &SerializationContext, + payloads: Vec, ) -> BoxFuture<'static, Vec>; } pub struct DefaultPayloadCodec; @@ -113,7 +113,7 @@ pub trait TemporalSerializable { None } } -/// + /// Indicates some type can be deserialized for use with Temporal. /// /// You don't need to implement this unless you are using a non-serde-compatible custom converter, @@ -121,8 +121,8 @@ pub trait TemporalSerializable { pub trait TemporalDeserializable: Sized { fn from_serde( _: &dyn ErasedSerdePayloadConverter, - _: Payload, _: &SerializationContext, + _: Payload, ) -> Option { None } @@ -141,26 +141,26 @@ pub struct RawValue { pub trait GenericPayloadConverter { fn to_payload( &self, - val: &T, context: &SerializationContext, + val: &T, ) -> Result; #[allow(clippy::wrong_self_convention)] fn from_payload( &self, - payload: Payload, context: &SerializationContext, + payload: Payload, ) -> Result; } impl GenericPayloadConverter for PayloadConverter { fn to_payload( &self, - val: &T, context: &SerializationContext, + val: &T, ) -> Result { match self { PayloadConverter::Serde(pc) => { - Ok(pc.to_payload(val.as_serde().ok_or_else(|| todo!())?, context)?) + Ok(pc.to_payload(context, val.as_serde().ok_or_else(|| todo!())?)?) } PayloadConverter::UseWrappers => { Ok(T::to_payload(val, context).ok_or_else(|| todo!())?) @@ -172,12 +172,12 @@ impl GenericPayloadConverter for PayloadConverter { fn from_payload( &self, - payload: Payload, context: &SerializationContext, + payload: Payload, ) -> Result { match self { PayloadConverter::Serde(pc) => { - Ok(T::from_serde(pc.as_ref(), payload, context).ok_or_else(|| todo!())?) + Ok(T::from_serde(pc.as_ref(), context, payload).ok_or_else(|| todo!())?) } PayloadConverter::UseWrappers => { Ok(T::from_payload(payload, context).ok_or_else(|| todo!())?) @@ -202,13 +202,13 @@ where { fn from_serde( pc: &dyn ErasedSerdePayloadConverter, - payload: Payload, context: &SerializationContext, + payload: Payload, ) -> Option where Self: Sized, { - erased_serde::deserialize(&mut pc.from_payload(payload, context).ok()?).ok() + erased_serde::deserialize(&mut pc.from_payload(context, payload).ok()?).ok() } } @@ -216,8 +216,8 @@ struct SerdeJsonPayloadConverter; impl ErasedSerdePayloadConverter for SerdeJsonPayloadConverter { fn to_payload( &self, + _: &SerializationContext, value: &dyn erased_serde::Serialize, - _context: &SerializationContext, ) -> Result { let as_json = serde_json::to_vec(value).map_err(|_| todo!())?; Ok(Payload { @@ -232,8 +232,8 @@ impl ErasedSerdePayloadConverter for SerdeJsonPayloadConverter { fn from_payload( &self, + _: &SerializationContext, payload: Payload, - _context: &SerializationContext, ) -> Result>, PayloadConversionError> { // TODO: Would check metadata let json_v: serde_json::Value = @@ -244,14 +244,14 @@ impl ErasedSerdePayloadConverter for SerdeJsonPayloadConverter { pub trait ErasedSerdePayloadConverter: Send + Sync { fn to_payload( &self, - value: &dyn erased_serde::Serialize, context: &SerializationContext, + value: &dyn erased_serde::Serialize, ) -> Result; #[allow(clippy::wrong_self_convention)] fn from_payload( &self, - payload: Payload, context: &SerializationContext, + payload: Payload, ) -> Result>, PayloadConversionError>; } @@ -318,15 +318,15 @@ impl FailureConverter for DefaultFailureConverter { impl PayloadCodec for DefaultPayloadCodec { fn encode( &self, - payloads: Vec, _: &SerializationContext, + payloads: Vec, ) -> BoxFuture<'static, Vec> { async move { payloads }.boxed() } fn decode( &self, - payloads: Vec, _: &SerializationContext, + payloads: Vec, ) -> BoxFuture<'static, Vec> { async move { payloads }.boxed() } diff --git a/crates/macros/src/definitions.rs b/crates/macros/src/definitions.rs index 22f2d605a..82fb0e25a 100644 --- a/crates/macros/src/definitions.rs +++ b/crates/macros/src/definitions.rs @@ -510,28 +510,14 @@ impl ActivitiesDefinition { impl_type: &Type, module_ident: &syn::Ident, ) -> TokenStream2 { - let static_activities: Vec<_> = self - .activities - .iter() - .filter(|a| a.is_static) - .map(|a| { - let struct_name = method_name_to_pascal_case(&a.method.sig.ident); - let struct_ident = format_ident!("{}", struct_name); - quote! { - defs.register_activity::<#module_ident::#struct_ident>(); - } - }) - .collect(); - let instance_activities: Vec<_> = self .activities .iter() - .filter(|a| !a.is_static) .map(|a| { let struct_name = method_name_to_pascal_case(&a.method.sig.ident); let struct_ident = format_ident!("{}", struct_name); quote! { - defs.register_activity_with_instance::<#module_ident::#struct_ident>(self.clone()); + defs.register_activity::<#module_ident::#struct_ident>(self.clone()); } }) .collect(); @@ -544,13 +530,7 @@ impl ActivitiesDefinition { quote! { impl ::temporalio_sdk::activities::ActivityImplementer for #impl_type { - fn register_all_static( - defs: &mut ::temporalio_sdk::activities::ActivityDefinitions, - ) { - #(#static_activities)* - } - - fn register_all_instance( + fn register_all( self: ::std::sync::Arc, defs: &mut ::temporalio_sdk::activities::ActivityDefinitions, ) { diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index 883576aa9..79fe035e4 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -4,7 +4,10 @@ use syn::parse_macro_input; mod definitions; mod fsm_impl; -// TODO [rust-sdk-branch]: Example docstring +/// Can be used to define Activities for invocation and execution. Using this macro requires that +/// you also depend on the `temporalio_sdk` crate. +/// +/// For a usage example, see that crate's documentation. #[proc_macro_attribute] pub fn activities(_attr: TokenStream, item: TokenStream) -> TokenStream { let def: definitions::ActivitiesDefinition = diff --git a/crates/sdk-core/machine_coverage/ActivityMachine_Coverage.puml b/crates/sdk-core/machine_coverage/ActivityMachine_Coverage.puml index 67f326270..a5b5ed085 100644 --- a/crates/sdk-core/machine_coverage/ActivityMachine_Coverage.puml +++ b/crates/sdk-core/machine_coverage/ActivityMachine_Coverage.puml @@ -26,7 +26,7 @@ StartedActivityCancelEventRecorded -[#blue]-> TimedOut: ActivityTaskTimedOut StartedActivityCancelEventRecorded -[#blue]-> Canceled: ActivityTaskCanceled Canceled -[#blue]-> Canceled: ActivityTaskStarted Canceled -[#blue]-> Canceled: ActivityTaskCompleted -TimedOut --> [*] -Failed --> [*] Completed --> [*] +Failed --> [*] +TimedOut --> [*] @enduml \ No newline at end of file diff --git a/crates/sdk-core/machine_coverage/UpdateMachine_Coverage.puml b/crates/sdk-core/machine_coverage/UpdateMachine_Coverage.puml index dfd4e1bd2..2d9f28548 100644 --- a/crates/sdk-core/machine_coverage/UpdateMachine_Coverage.puml +++ b/crates/sdk-core/machine_coverage/UpdateMachine_Coverage.puml @@ -14,6 +14,6 @@ CompletedCommandCreated -[#blue]-> CompletedCommandRecorded: WorkflowExecutionUp CompletedImmediately -[#blue]-> CompletedImmediatelyAcceptCreated: CommandProtocolMessage CompletedImmediatelyAcceptCreated -[#blue]-> CompletedImmediatelyCompleteCreated: CommandProtocolMessage CompletedImmediatelyCompleteCreated -[#blue]-> CompletedCommandCreated: WorkflowExecutionUpdateAccepted -Rejected --> [*] CompletedCommandRecorded --> [*] +Rejected --> [*] @enduml \ No newline at end of file diff --git a/crates/sdk-core/machine_coverage/WorkflowTaskMachine_Coverage.puml b/crates/sdk-core/machine_coverage/WorkflowTaskMachine_Coverage.puml index e65467351..d6fd502d6 100644 --- a/crates/sdk-core/machine_coverage/WorkflowTaskMachine_Coverage.puml +++ b/crates/sdk-core/machine_coverage/WorkflowTaskMachine_Coverage.puml @@ -5,7 +5,7 @@ Scheduled --> TimedOut: WorkflowTaskTimedOut Started -[#blue]-> Completed: WorkflowTaskCompleted Started -[#blue]-> Failed: WorkflowTaskFailed Started -[#blue]-> TimedOut: WorkflowTaskTimedOut -TimedOut --> [*] Completed --> [*] Failed --> [*] +TimedOut --> [*] @enduml \ No newline at end of file diff --git a/crates/sdk-core/tests/common/activity_functions.rs b/crates/sdk-core/tests/common/activity_functions.rs index aafd6ee90..92209cae7 100644 --- a/crates/sdk-core/tests/common/activity_functions.rs +++ b/crates/sdk-core/tests/common/activity_functions.rs @@ -4,7 +4,7 @@ use temporalio_macros::activities; use temporalio_sdk::activities::{ActivityContext, ActivityError}; use tokio::time::sleep; -pub(crate) struct StdActivities {} +pub(crate) struct StdActivities; #[activities] impl StdActivities { diff --git a/crates/sdk-core/tests/common/mod.rs b/crates/sdk-core/tests/common/mod.rs index 93e67387e..9b4cf6345 100644 --- a/crates/sdk-core/tests/common/mod.rs +++ b/crates/sdk-core/tests/common/mod.rs @@ -53,7 +53,7 @@ use temporalio_common::{ }; use temporalio_sdk::{ Worker, WorkerOptions, WorkflowFunction, - activities::{ActivityImplementer, HasOnlyStaticMethods}, + activities::ActivityImplementer, interceptors::{ FailOnNondeterminismInterceptor, InterceptorWithNext, ReturnWorkflowExitValueInterceptor, WorkerInterceptor, @@ -529,7 +529,6 @@ impl TestWorker { self.inner.worker_instance_key() } - // TODO: Maybe trait-ify? pub(crate) fn register_wf>( &mut self, workflow_type: impl Into, @@ -538,13 +537,6 @@ impl TestWorker { self.inner.register_wf(workflow_type, wf_function) } - pub(crate) fn register_activities_static(&mut self) -> &mut Self - where - AI: ActivityImplementer + HasOnlyStaticMethods, - { - self.inner.register_activities_static::(); - self - } pub(crate) fn register_activities( &mut self, instance: AI, diff --git a/crates/sdk-core/tests/common/workflows.rs b/crates/sdk-core/tests/common/workflows.rs index 8c32913ea..14eb74d21 100644 --- a/crates/sdk-core/tests/common/workflows.rs +++ b/crates/sdk-core/tests/common/workflows.rs @@ -4,7 +4,7 @@ use temporalio_common::{prost_dur, protos::temporal::api::common::v1::RetryPolic use temporalio_sdk::{ActivityOptions, LocalActivityOptions, WfContext, WorkflowResult}; pub(crate) async fn la_problem_workflow(ctx: WfContext) -> WorkflowResult<()> { - ctx.local_activity( + ctx.start_local_activity( StdActivities::delay, Duration::from_secs(15), LocalActivityOptions { @@ -20,7 +20,7 @@ pub(crate) async fn la_problem_workflow(ctx: WfContext) -> WorkflowResult<()> { }, )? .await; - ctx.activity( + ctx.start_activity( StdActivities::delay, Duration::from_secs(15), ActivityOptions { diff --git a/crates/sdk-core/tests/heavy_tests.rs b/crates/sdk-core/tests/heavy_tests.rs index 78190d6a6..1bbe49ec5 100644 --- a/crates/sdk-core/tests/heavy_tests.rs +++ b/crates/sdk-core/tests/heavy_tests.rs @@ -51,9 +51,7 @@ async fn activity_load() { starter.sdk_config.activity_task_poller_behavior = PollerBehavior::SimpleMaximum(10); starter.sdk_config.tuner = Arc::new(TunerHolder::fixed_size(CONCURRENCY, CONCURRENCY, 100, 100)); - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let mut worker = starter.worker().await; let activity_id = "act-1"; @@ -65,7 +63,7 @@ async fn activity_load() { let input_str = "yo".to_string(); async move { let res = ctx - .activity( + .start_activity( StdActivities::echo, input_str.clone(), ActivityOptions { @@ -132,7 +130,7 @@ async fn chunky_activities_resource_based() { .with_activity_slots_options(ResourceSlotOptions::new(5, 1000, Duration::from_millis(50))); starter.sdk_config.tuner = Arc::new(tuner); - struct ChunkyActivities {} + struct ChunkyActivities; #[activities] impl ChunkyActivities { #[activity] @@ -151,9 +149,7 @@ async fn chunky_activities_resource_based() { } } - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(ChunkyActivities); let mut worker = starter.worker().await; let activity_id = "act-1"; @@ -163,7 +159,7 @@ async fn chunky_activities_resource_based() { let input_str = "yo".to_string(); async move { let res = ctx - .activity( + .start_activity( ChunkyActivities::chunky_echo, input_str.clone(), ActivityOptions { @@ -226,9 +222,7 @@ async fn workflow_load() { starter.sdk_config.max_cached_workflows = 200; starter.sdk_config.activity_task_poller_behavior = PollerBehavior::SimpleMaximum(10); starter.sdk_config.tuner = Arc::new(TunerHolder::fixed_size(5, 100, 100, 100)); - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let sigchan = ctx.make_signal_channel(SIGNAME).map(Ok); @@ -236,7 +230,7 @@ async fn workflow_load() { let real_stuff = async move { for _ in 0..5 { - ctx.activity( + ctx.start_activity( StdActivities::echo, "hi!".to_string(), ActivityOptions { @@ -306,9 +300,7 @@ async fn evict_while_la_running_no_interference() { // introduces more instability that can be useful in the test. // starter.max_wft(20); starter.sdk_config.tuner = Arc::new(TunerHolder::fixed_size(100, 10, 20, 1)); - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), la_problem_workflow); @@ -421,7 +413,7 @@ async fn poller_autoscaling_basic_loadtest() { initial: 5, }; - struct JitteryActivities {} + struct JitteryActivities; #[activities] impl JitteryActivities { #[activity] @@ -436,9 +428,7 @@ async fn poller_autoscaling_basic_loadtest() { } } - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(JitteryActivities); let mut worker = starter.worker().await; let shutdown_handle = worker.inner_mut().shutdown_handle(); worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { @@ -447,7 +437,7 @@ async fn poller_autoscaling_basic_loadtest() { let real_stuff = async move { for _ in 0..5 { - ctx.activity( + ctx.start_activity( JitteryActivities::jittery_echo, "hi!".to_string(), ActivityOptions { diff --git a/crates/sdk-core/tests/heavy_tests/fuzzy_workflow.rs b/crates/sdk-core/tests/heavy_tests/fuzzy_workflow.rs index ed0b5edd1..48d6669e3 100644 --- a/crates/sdk-core/tests/heavy_tests/fuzzy_workflow.rs +++ b/crates/sdk-core/tests/heavy_tests/fuzzy_workflow.rs @@ -40,7 +40,7 @@ async fn fuzzy_wf_def(ctx: WfContext) -> WorkflowResult<()> { .take_until(done.cancelled()) .for_each_concurrent(None, |action| match action { FuzzyWfAction::DoAct => ctx - .activity( + .start_activity( StdActivities::echo, "hi!".to_string(), ActivityOptions { @@ -52,7 +52,7 @@ async fn fuzzy_wf_def(ctx: WfContext) -> WorkflowResult<()> { .map(|_| ()) .boxed(), FuzzyWfAction::DoLocalAct => ctx - .local_activity( + .start_local_activity( StdActivities::echo, "hi!".to_string(), LocalActivityOptions { @@ -83,9 +83,7 @@ async fn fuzzy_workflow() { let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), fuzzy_wf_def); - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let client = starter.get_client().await; diff --git a/crates/sdk-core/tests/integ_tests/heartbeat_tests.rs b/crates/sdk-core/tests/integ_tests/heartbeat_tests.rs index 9f6c6b218..4f2e0a077 100644 --- a/crates/sdk-core/tests/integ_tests/heartbeat_tests.rs +++ b/crates/sdk-core/tests/integ_tests/heartbeat_tests.rs @@ -183,15 +183,13 @@ async fn many_act_fails_with_heartbeats() { async fn activity_doesnt_heartbeat_hits_timeout_then_completes() { let wf_name = "activity_doesnt_heartbeat_hits_timeout_then_completes"; let mut starter = CoreWfStarter::new(wf_name); - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let mut worker = starter.worker().await; let client = starter.get_client().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let res = ctx - .activity( + .start_activity( StdActivities::delay, Duration::from_secs(4), ActivityOptions { diff --git a/crates/sdk-core/tests/integ_tests/metrics_tests.rs b/crates/sdk-core/tests/integ_tests/metrics_tests.rs index f83495e39..5eae3040f 100644 --- a/crates/sdk-core/tests/integ_tests/metrics_tests.rs +++ b/crates/sdk-core/tests/integ_tests/metrics_tests.rs @@ -773,7 +773,7 @@ async fn activity_metrics() { let mut starter = CoreWfStarter::new_with_runtime(wf_name, rt); starter.sdk_config.graceful_shutdown_period = Some(Duration::from_secs(1)); - struct PassFailActivities {} + struct PassFailActivities; #[activities] impl PassFailActivities { #[activity(name = "pass_fail_act")] @@ -789,15 +789,13 @@ async fn activity_metrics() { } } - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(PassFailActivities); let task_queue = starter.get_task_queue().to_owned(); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_string(), |ctx: WfContext| async move { let normal_act_pass = ctx - .activity( + .start_activity( PassFailActivities::pass_fail_act, "pass".to_string(), ActivityOptions { @@ -807,7 +805,7 @@ async fn activity_metrics() { ) .unwrap(); let normal_act_fail = ctx - .activity( + .start_activity( PassFailActivities::pass_fail_act, "fail".to_string(), ActivityOptions { @@ -821,12 +819,12 @@ async fn activity_metrics() { ) .unwrap(); join!(normal_act_pass, normal_act_fail); - let local_act_pass = ctx.local_activity( + let local_act_pass = ctx.start_local_activity( PassFailActivities::pass_fail_act, "pass".to_string(), LocalActivityOptions::default(), )?; - let local_act_fail = ctx.local_activity( + let local_act_fail = ctx.start_local_activity( PassFailActivities::pass_fail_act, "fail".to_string(), LocalActivityOptions { @@ -837,7 +835,7 @@ async fn activity_metrics() { ..Default::default() }, )?; - let local_act_cancel = ctx.local_activity( + let local_act_cancel = ctx.start_local_activity( PassFailActivities::pass_fail_act, "cancel".to_string(), LocalActivityOptions { diff --git a/crates/sdk-core/tests/integ_tests/polling_tests.rs b/crates/sdk-core/tests/integ_tests/polling_tests.rs index f0ef4ad2d..5fb63e04a 100644 --- a/crates/sdk-core/tests/integ_tests/polling_tests.rs +++ b/crates/sdk-core/tests/integ_tests/polling_tests.rs @@ -257,14 +257,12 @@ async fn small_workflow_slots_and_pollers(#[values(false, true)] use_autoscaling } starter.sdk_config.activity_task_poller_behavior = PollerBehavior::SimpleMaximum(1); starter.sdk_config.tuner = Arc::new(TunerHolder::fixed_size(2, 1, 1, 1)); - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { for _ in 0..3 { - ctx.activity( + ctx.start_activity( StdActivities::echo, "hi!".to_string(), ActivityOptions { diff --git a/crates/sdk-core/tests/integ_tests/update_tests.rs b/crates/sdk-core/tests/integ_tests/update_tests.rs index cef6aff14..b22e2f3ff 100644 --- a/crates/sdk-core/tests/integ_tests/update_tests.rs +++ b/crates/sdk-core/tests/integ_tests/update_tests.rs @@ -641,9 +641,7 @@ async fn update_with_local_acts() { let mut starter = CoreWfStarter::new(wf_name); // Short task timeout to get activities to heartbeat without taking ages starter.workflow_options.task_timeout = Some(Duration::from_secs(1)); - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let mut worker = starter.worker().await; let client = starter.get_client().await; @@ -653,7 +651,7 @@ async fn update_with_local_acts() { |_: &_, _: ()| Ok(()), move |ctx: UpdateContext, _: ()| async move { ctx.wf_ctx - .local_activity( + .start_local_activity( StdActivities::delay, Duration::from_secs(3), LocalActivityOptions::default(), @@ -961,7 +959,7 @@ async fn worker_restarted_in_middle_of_update() { let wf_name = "worker_restarted_in_middle_of_update"; let mut starter = CoreWfStarter::new(wf_name); - struct BlockingActivities {} + struct BlockingActivities; #[activities] impl BlockingActivities { #[activity] @@ -975,9 +973,7 @@ async fn worker_restarted_in_middle_of_update() { } } - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(BlockingActivities); let mut worker = starter.worker().await; let client = starter.get_client().await; @@ -987,7 +983,7 @@ async fn worker_restarted_in_middle_of_update() { |_: &_, _: ()| Ok(()), move |ctx: UpdateContext, _: ()| async move { ctx.wf_ctx - .activity( + .start_activity( BlockingActivities::blocks, "hi!".to_string(), ActivityOptions { @@ -1067,9 +1063,7 @@ async fn worker_restarted_in_middle_of_update() { async fn update_after_empty_wft() { let wf_name = "update_after_empty_wft"; let mut starter = CoreWfStarter::new(wf_name); - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let mut worker = starter.worker().await; let client = starter.get_client().await; @@ -1083,7 +1077,7 @@ async fn update_after_empty_wft() { return Ok(()); } ctx.wf_ctx - .activity( + .start_activity( StdActivities::echo, "hi!".to_string(), ActivityOptions { @@ -1100,7 +1094,7 @@ async fn update_after_empty_wft() { let sig_handle = async { sig.next().await; ACT_STARTED.store(true, Ordering::Release); - ctx.activity( + ctx.start_activity( StdActivities::echo, "hi!".to_string(), ActivityOptions { @@ -1161,9 +1155,7 @@ async fn update_after_empty_wft() { async fn update_lost_on_activity_mismatch() { let wf_name = "update_lost_on_activity_mismatch"; let mut starter = CoreWfStarter::new(wf_name); - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let mut worker = starter.worker().await; let client = starter.get_client().await; @@ -1184,7 +1176,7 @@ async fn update_lost_on_activity_mismatch() { for _ in 1..=3 { let cr = can_run.clone(); ctx.wait_condition(|| cr.load(Ordering::Relaxed) > 0).await; - ctx.activity( + ctx.start_activity( StdActivities::echo, "hi!".to_string(), ActivityOptions { diff --git a/crates/sdk-core/tests/integ_tests/worker_heartbeat_tests.rs b/crates/sdk-core/tests/integ_tests/worker_heartbeat_tests.rs index f7fefe47f..16019380d 100644 --- a/crates/sdk-core/tests/integ_tests/worker_heartbeat_tests.rs +++ b/crates/sdk-core/tests/integ_tests/worker_heartbeat_tests.rs @@ -179,7 +179,7 @@ async fn docker_worker_heartbeat_basic(#[values("otel", "prom", "no_metrics")] b let worker_instance_key = worker.worker_instance_key(); worker.register_wf(wf_name.to_string(), |ctx: WfContext| async move { - ctx.activity( + ctx.start_activity( NotifyActivities::pass_fail_act, "pass".to_string(), ActivityOptions { @@ -314,15 +314,13 @@ async fn docker_worker_heartbeat_tuner() { initial: 5, }; starter.sdk_config.tuner = Arc::new(tuner); - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let mut worker = starter.worker().await; let worker_instance_key = worker.worker_instance_key(); // Run a workflow worker.register_wf(wf_name.to_string(), |ctx: WfContext| async move { - ctx.activity( + ctx.start_activity( StdActivities::echo, "pass".to_string(), ActivityOptions { @@ -590,7 +588,7 @@ async fn worker_heartbeat_sticky_cache_miss() { starter .sdk_config - .register_activities_static::(); + .register_activities(StickyCacheActivities); let mut worker = starter.worker().await; worker.fetch_results = false; @@ -608,7 +606,7 @@ async fn worker_heartbeat_sticky_cache_miss() { .and_then(|p| String::from_json_payload(p).ok()) .unwrap_or_else(|| "wf1".to_string()); - ctx.activity( + ctx.start_activity( StickyCacheActivities::sticky_cache_history_act, wf_marker.clone(), ActivityOptions { @@ -690,9 +688,7 @@ async fn worker_heartbeat_multiple_workers() { let mut starter = new_no_metrics_starter(wf_name); starter.sdk_config.max_cached_workflows = 5_usize; starter.sdk_config.tuner = Arc::new(TunerHolder::fixed_size(5, 10, 10, 10)); - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let client = starter.get_client().await; let starting_hb_len = list_worker_heartbeats(&client, String::new()).await.len(); @@ -805,16 +801,14 @@ async fn worker_heartbeat_failure_metrics() { } } - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(FailingActivities); let mut worker = starter.worker().await; let worker_instance_key = worker.worker_instance_key(); worker.register_wf(wf_name.to_string(), |ctx: WfContext| async move { let _ = ctx - .activity( + .start_activity( FailingActivities::failing_act, "boom".to_string(), ActivityOptions { @@ -980,14 +974,12 @@ async fn worker_heartbeat_no_runtime_heartbeat() { .unwrap(); let rt = CoreRuntime::new_assume_tokio(runtimeopts).unwrap(); let mut starter = CoreWfStarter::new_with_runtime(wf_name, rt); - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let mut worker = starter.worker().await; let worker_instance_key = worker.worker_instance_key(); worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { - ctx.activity( + ctx.start_activity( StdActivities::echo, "pass".to_string(), ActivityOptions { @@ -1043,14 +1035,12 @@ async fn worker_heartbeat_skip_client_worker_set_check() { let rt = CoreRuntime::new_assume_tokio(runtimeopts).unwrap(); let mut starter = CoreWfStarter::new_with_runtime(wf_name, rt); starter.set_core_cfg_mutator(|m| m.skip_client_worker_set_check = true); - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let mut worker = starter.worker().await; let worker_instance_key = worker.worker_instance_key(); worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { - ctx.activity( + ctx.start_activity( StdActivities::echo, "pass".to_string(), ActivityOptions { diff --git a/crates/sdk-core/tests/integ_tests/worker_tests.rs b/crates/sdk-core/tests/integ_tests/worker_tests.rs index 00f1ebc43..a4fa11468 100644 --- a/crates/sdk-core/tests/integ_tests/worker_tests.rs +++ b/crates/sdk-core/tests/integ_tests/worker_tests.rs @@ -31,7 +31,7 @@ use temporalio_common::{ }, temporal::api::{ command::v1::command::Attributes, - common::v1::{Payload, WorkerVersionStamp}, + common::v1::WorkerVersionStamp, enums::v1::{ EventType, WorkflowTaskFailedCause::{self, GrpcMessageTooLarge}, @@ -52,8 +52,10 @@ use temporalio_common::{ }, worker::WorkerTaskTypes, }; +use temporalio_macros::activities; use temporalio_sdk::{ ActivityOptions, LocalActivityOptions, WfContext, WorkerOptions, + activities::{ActivityContext, ActivityError}, interceptors::WorkerInterceptor, }; use temporalio_sdk_core::{ @@ -359,21 +361,27 @@ async fn activity_tasks_from_completion_reserve_slots() { let workflow_complete_token = CancellationToken::new(); let workflow_complete_token_clone = workflow_complete_token.clone(); + struct FakeAct; + #[activities] + impl FakeAct { + #[activity(name = "act1")] + fn act1(_: ActivityContext) -> Result<(), ActivityError> { + unimplemented!() + } + + #[activity(name = "act2")] + fn act2(_: ActivityContext) -> Result<(), ActivityError> { + unimplemented!() + } + } + worker.register_wf(DEFAULT_WORKFLOW_TYPE, move |ctx: WfContext| { let complete_token = workflow_complete_token.clone(); async move { - ctx.activity_untyped( - "act1".to_string(), - Payload::default(), - ActivityOptions::default(), - ) - .await; - ctx.activity_untyped( - "act2".to_string(), - Payload::default(), - ActivityOptions::default(), - ) - .await; + ctx.start_activity(FakeAct::act1, (), ActivityOptions::default())? + .await; + ctx.start_activity(FakeAct::act2, (), ActivityOptions::default())? + .await; complete_token.cancel(); Ok(().into()) } @@ -707,9 +715,7 @@ async fn test_custom_slot_supplier_simple() { )); let mut starter = CoreWfStarter::new("test_custom_slot_supplier_simple"); - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let mut tb = TunerBuilder::default(); tb.workflow_slot_supplier(wf_supplier.clone()); @@ -723,7 +729,7 @@ async fn test_custom_slot_supplier_simple() { "SlotSupplierWorkflow".to_owned(), |ctx: WfContext| async move { let _result = ctx - .activity( + .start_activity( StdActivities::no_op, (), ActivityOptions { @@ -733,7 +739,7 @@ async fn test_custom_slot_supplier_simple() { )? .await; let _result = ctx - .local_activity( + .start_local_activity( StdActivities::no_op, (), LocalActivityOptions { diff --git a/crates/sdk-core/tests/integ_tests/worker_versioning_tests.rs b/crates/sdk-core/tests/integ_tests/worker_versioning_tests.rs index ab50c2228..827d079de 100644 --- a/crates/sdk-core/tests/integ_tests/worker_versioning_tests.rs +++ b/crates/sdk-core/tests/integ_tests/worker_versioning_tests.rs @@ -152,14 +152,12 @@ async fn activity_has_deployment_stamp() { use_worker_versioning: true, default_versioning_behavior: VersioningBehavior::AutoUpgrade.into(), }; - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let mut worker = starter.worker().await; let client = starter.get_client().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { - ctx.activity( + ctx.start_activity( StdActivities::echo, "hi!".to_string(), ActivityOptions { diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests.rs b/crates/sdk-core/tests/integ_tests/workflow_tests.rs index 26bbcce3e..9e2f52f36 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests.rs @@ -475,11 +475,11 @@ async fn slow_completes_with_small_cache() { starter.sdk_config.max_cached_workflows = 5_usize; let mut worker = starter.worker().await; - worker.register_activities_static::(); + worker.register_activities(StdActivities); worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { for _ in 0..3 { - ctx.activity( + ctx.start_activity( StdActivities::echo, "hi!".to_string(), ActivityOptions { @@ -813,12 +813,10 @@ async fn nondeterminism_errors_fail_workflow_when_configured_to( // Restart the worker with a new, incompatible wf definition which will cause nondeterminism let mut starter = starter.clone_no_worker(); - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), move |ctx: WfContext| async move { - ctx.activity( + ctx.start_activity( StdActivities::echo, "hi".to_owned(), ActivityOptions { @@ -862,10 +860,10 @@ async fn history_out_of_order_on_restart() { static HIT_SLEEP: Notify = Notify::const_new(); - worker.register_activities_static::(); - worker2.register_activities_static::(); + worker.register_activities(StdActivities); + worker2.register_activities(StdActivities); worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { - ctx.local_activity( + ctx.start_local_activity( StdActivities::echo, "hi".to_string(), LocalActivityOptions { @@ -874,7 +872,7 @@ async fn history_out_of_order_on_restart() { }, )? .await; - ctx.activity( + ctx.start_activity( StdActivities::echo, "hi".to_string(), ActivityOptions { @@ -890,7 +888,7 @@ async fn history_out_of_order_on_restart() { }); worker2.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { - ctx.local_activity( + ctx.start_local_activity( StdActivities::echo, "hi".to_string(), LocalActivityOptions { @@ -901,7 +899,7 @@ async fn history_out_of_order_on_restart() { .await; // Timer is added after restarting workflow ctx.timer(Duration::from_secs(1)).await; - ctx.activity( + ctx.start_activity( StdActivities::echo, "hi".to_string(), ActivityOptions { diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/activities.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/activities.rs index e2fb7f111..9e3948e19 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/activities.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/activities.rs @@ -59,22 +59,10 @@ use temporalio_sdk_core::{ }; use tokio::{join, sync::Semaphore, time::sleep}; -pub(crate) struct SleepyActivities {} - -#[activities] -impl SleepyActivities { - /// Activity that echoes input after sleeping for 2 seconds - #[activity] - async fn sleepy_echo(_ctx: ActivityContext, echo_me: String) -> Result { - sleep(Duration::from_secs(2)).await; - Ok(echo_me) - } -} - async fn one_activity_wf(ctx: WfContext) -> WorkflowResult { // TODO [rust-sdk-branch]: activities need to return deserialzied results let r = ctx - .activity( + .start_activity( StdActivities::echo, "hi!".to_string(), ActivityOptions { @@ -91,9 +79,7 @@ async fn one_activity_wf(ctx: WfContext) -> WorkflowResult { async fn one_activity_only() { let wf_name = "one_activity"; let mut starter = CoreWfStarter::new(wf_name); - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let mut worker = starter.worker().await; let client = starter.get_client().await; worker.register_wf(wf_name.to_owned(), one_activity_wf); @@ -913,16 +899,14 @@ async fn activity_heartbeat_not_flushed_on_success() { async fn one_activity_abandon_cancelled_before_started() { let wf_name = "one_activity_abandon_cancelled_before_started"; let mut starter = CoreWfStarter::new(wf_name); - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let mut worker = starter.worker().await; let client = starter.get_client().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let act_fut = ctx - .activity( - SleepyActivities::sleepy_echo, - "hi!".to_string(), + .start_activity( + StdActivities::delay, + Duration::from_secs(2), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(5)), cancellation_type: ActivityCancellationType::Abandon, @@ -957,16 +941,14 @@ async fn one_activity_abandon_cancelled_before_started() { async fn one_activity_abandon_cancelled_after_complete() { let wf_name = "one_activity_abandon_cancelled_after_complete"; let mut starter = CoreWfStarter::new(wf_name); - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let mut worker = starter.worker().await; let client = starter.get_client().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let act_fut = ctx - .activity( - SleepyActivities::sleepy_echo, - "hi!".to_string(), + .start_activity( + StdActivities::delay, + Duration::from_secs(2), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(5)), cancellation_type: ActivityCancellationType::Abandon, @@ -1035,7 +1017,7 @@ async fn it_can_complete_async() { worker.register_wf(wf_name.clone(), move |ctx: WfContext| async move { let activity_resolution = ctx - .activity( + .start_activity( AsyncActivities::complete_async_activity, "hi".to_string(), ActivityOptions { @@ -1097,7 +1079,7 @@ async fn graceful_shutdown() { let mut starter = CoreWfStarter::new(wf_name); starter.sdk_config.graceful_shutdown_period = Some(Duration::from_millis(500)); - struct SleeperActivities {} + struct SleeperActivities; #[activities] impl SleeperActivities { #[activity] @@ -1110,14 +1092,12 @@ async fn graceful_shutdown() { } } - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(SleeperActivities); let mut worker = starter.worker().await; let client = starter.get_client().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let act_futs = (1..=10).map(|_| { - ctx.activity( + ctx.start_activity( SleeperActivities::sleeper, "hi".to_string(), ActivityOptions { @@ -1173,7 +1153,7 @@ async fn activity_can_be_cancelled_by_local_timeout() { starter .set_core_cfg_mutator(|m| m.local_timeout_buffer_for_activities = Duration::from_secs(0)); - struct CancellableEchoActivities {} + struct CancellableEchoActivities; #[activities] impl CancellableEchoActivities { #[activity] @@ -1190,11 +1170,11 @@ async fn activity_can_be_cancelled_by_local_timeout() { starter .sdk_config - .register_activities_static::(); + .register_activities(CancellableEchoActivities); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let res = ctx - .activity( + .start_activity( CancellableEchoActivities::cancellable_echo, "hi!".to_string(), ActivityOptions { @@ -1236,15 +1216,13 @@ async fn long_activity_timeout_repro() { }; starter .set_core_cfg_mutator(|m| m.local_timeout_buffer_for_activities = Duration::from_secs(0)); - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let mut iter = 1; loop { let res = ctx - .activity( + .start_activity( StdActivities::echo, "hi!".to_string(), ActivityOptions { @@ -1302,14 +1280,14 @@ async fn pass_activity_summary_to_metadata() { let mut worker = mock_sdk_cfg(mock_cfg, |_| {}); worker.register_wf(wf_type, |ctx: WfContext| async move { - ctx.activity_untyped( - DEFAULT_ACTIVITY_TYPE.to_string(), - Payload::default(), + ctx.start_activity( + StdActivities::default, + (), ActivityOptions { summary: Some("activity summary".to_string()), ..Default::default() }, - ) + )? .await; Ok(().into()) }); @@ -1354,15 +1332,15 @@ async fn abandoned_activities_ignore_start_and_complete(hist_batches: &'static [ let mut worker = mock_sdk(MockPollCfg::from_resp_batches(wfid, t, hist_batches, mock)); worker.register_wf(wf_type.to_owned(), |ctx: WfContext| async move { - let act_fut = ctx.activity_untyped( - DEFAULT_ACTIVITY_TYPE.to_string(), - Payload::default(), + let act_fut = ctx.start_activity( + StdActivities::default, + (), ActivityOptions { start_to_close_timeout: Some(Duration::from_secs(5)), cancellation_type: ActivityCancellationType::Abandon, ..Default::default() }, - ); + )?; ctx.timer(Duration::from_secs(1)).await; act_fut.cancel(&ctx); ctx.timer(Duration::from_secs(3)).await; @@ -1379,11 +1357,8 @@ async fn abandoned_activities_ignore_start_and_complete(hist_batches: &'static [ #[tokio::test] async fn immediate_activity_cancelation() { let func = WorkflowFunction::new(|ctx: WfContext| async move { - let cancel_activity_future = ctx.activity_untyped( - DEFAULT_ACTIVITY_TYPE.to_string(), - Payload::default(), - ActivityOptions::default(), - ); + let cancel_activity_future = + ctx.start_activity(StdActivities::default, (), ActivityOptions::default())?; // Immediately cancel the activity cancel_activity_future.cancel(&ctx); cancel_activity_future.await; diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/appdata_propagation.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/appdata_propagation.rs index aa76bf594..3d983f4d1 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/appdata_propagation.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/appdata_propagation.rs @@ -15,7 +15,7 @@ struct Data { } pub(crate) async fn appdata_activity_wf(ctx: WfContext) -> WorkflowResult<()> { - ctx.activity( + ctx.start_activity( AppdataActivities::echo, "hi!".to_string(), ActivityOptions { @@ -28,7 +28,7 @@ pub(crate) async fn appdata_activity_wf(ctx: WfContext) -> WorkflowResult<()> { Ok(().into()) } -struct AppdataActivities {} +struct AppdataActivities; #[activities] impl AppdataActivities { #[activity] @@ -43,9 +43,7 @@ impl AppdataActivities { async fn appdata_access_in_activities_and_workflows() { let wf_name = "appdata_activity"; let mut starter = CoreWfStarter::new(wf_name); - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(AppdataActivities); let mut worker = starter.worker().await; worker.inner_mut().insert_app_data(Data { message: TEST_APPDATA_MESSAGE.to_owned(), diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/determinism.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/determinism.rs index 518bea7a0..517bb7218 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/determinism.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/determinism.rs @@ -1,4 +1,6 @@ -use crate::common::{CoreWfStarter, WorkflowHandleExt, mock_sdk, mock_sdk_cfg}; +use crate::common::{ + CoreWfStarter, WorkflowHandleExt, activity_functions::StdActivities, mock_sdk, mock_sdk_cfg, +}; use std::{ sync::atomic::{AtomicBool, AtomicUsize, Ordering}, time::Duration, @@ -6,9 +8,8 @@ use std::{ use temporalio_client::WorkflowOptions; use temporalio_common::{ protos::{ - DEFAULT_ACTIVITY_TYPE, TestHistoryBuilder, canned_histories, + TestHistoryBuilder, canned_histories, temporal::api::{ - common::v1::Payload, enums::v1::{EventType, WorkflowTaskFailedCause}, failure::v1::Failure, }, @@ -39,12 +40,8 @@ pub(crate) async fn timer_wf_nondeterministic(ctx: WfContext) -> WorkflowResult< } 2 => { // On the second attempt we should cause a nondeterminism error - ctx.activity_untyped( - "whatever".to_string(), - Payload::default(), - ActivityOptions::default(), - ) - .await; + ctx.start_activity(StdActivities::default, (), ActivityOptions::default())? + .await; } _ => panic!("Ran too many times"), } @@ -71,9 +68,7 @@ async fn task_fail_causes_replay_unset_too_soon() { let wf_name = "task_fail_causes_replay_unset_too_soon"; let mut starter = CoreWfStarter::new(wf_name); - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let mut worker = starter.worker().await; static DID_FAIL: AtomicBool = AtomicBool::new(false); @@ -81,7 +76,7 @@ async fn task_fail_causes_replay_unset_too_soon() { if DID_FAIL.load(Ordering::Relaxed) { assert!(ctx.is_replaying()); } - ctx.activity( + ctx.start_activity( StdActivities::echo, "hi!".to_string(), ActivityOptions { @@ -253,39 +248,41 @@ async fn activity_id_or_type_change_is_nondeterministic( worker.register_wf(wf_type.to_owned(), move |ctx: WfContext| async move { if local_act { if id_change { - ctx.local_activity_untyped( - DEFAULT_ACTIVITY_TYPE.to_string(), - Payload::default(), + ctx.start_local_activity( + StdActivities::default, + (), LocalActivityOptions { activity_id: Some("I'm bad and wrong!".to_string()), ..Default::default() }, - ) + )? .await; } else { - ctx.local_activity_untyped( - "not the default act type".to_string(), - Payload::default(), + ctx.start_local_activity( + // Different type causes nondeterminism + StdActivities::no_op, + (), Default::default(), - ) + )? .await; } } else if id_change { - ctx.activity_untyped( - DEFAULT_ACTIVITY_TYPE.to_string(), - Payload::default(), + ctx.start_activity( + StdActivities::default, + (), ActivityOptions { activity_id: Some("I'm bad and wrong!".to_string()), ..Default::default() }, - ) + )? .await; } else { - ctx.activity_untyped( - "not the default act type".to_string(), - Payload::default(), + ctx.start_activity( + // Different type causes nondeterminism + StdActivities::no_op, + (), ActivityOptions::default(), - ) + )? .await; } Ok(().into()) @@ -345,7 +342,7 @@ async fn child_wf_id_or_type_change_is_nondeterministic( ctx.child_workflow(if id_change { ChildWorkflowOptions { workflow_id: "I'm bad and wrong!".to_string(), - workflow_type: DEFAULT_ACTIVITY_TYPE.to_string(), + workflow_type: DEFAULT_WORKFLOW_TYPE.to_string(), ..Default::default() } } else { diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/local_activities.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/local_activities.rs index 55d36a640..8b451cbae 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/local_activities.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/local_activities.rs @@ -64,7 +64,7 @@ use tokio_util::sync::CancellationToken; pub(crate) async fn one_local_activity_wf(ctx: WfContext) -> WorkflowResult<()> { let initial_workflow_time = ctx.workflow_time().expect("Workflow time should be set"); - ctx.local_activity( + ctx.start_local_activity( StdActivities::echo, "hi!".to_string(), LocalActivityOptions::default(), @@ -79,9 +79,7 @@ pub(crate) async fn one_local_activity_wf(ctx: WfContext) -> WorkflowResult<()> async fn one_local_activity() { let wf_name = "one_local_activity"; let mut starter = CoreWfStarter::new(wf_name); - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), one_local_activity_wf); @@ -94,7 +92,7 @@ async fn one_local_activity() { } pub(crate) async fn local_act_concurrent_with_timer_wf(ctx: WfContext) -> WorkflowResult<()> { - let la = ctx.local_activity( + let la = ctx.start_local_activity( StdActivities::echo, "hi!".to_string(), LocalActivityOptions::default(), @@ -108,9 +106,7 @@ pub(crate) async fn local_act_concurrent_with_timer_wf(ctx: WfContext) -> Workfl async fn local_act_concurrent_with_timer() { let wf_name = "local_act_concurrent_with_timer"; let mut starter = CoreWfStarter::new(wf_name); - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), local_act_concurrent_with_timer_wf); @@ -122,12 +118,10 @@ async fn local_act_concurrent_with_timer() { async fn local_act_then_timer_then_wait_result() { let wf_name = "local_act_then_timer_then_wait_result"; let mut starter = CoreWfStarter::new(wf_name); - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { - let la = ctx.local_activity( + let la = ctx.start_local_activity( StdActivities::echo, "hi!".to_string(), LocalActivityOptions::default(), @@ -143,7 +137,7 @@ async fn local_act_then_timer_then_wait_result() { } pub(crate) async fn local_act_then_timer_then_wait(ctx: WfContext) -> WorkflowResult<()> { - let la = ctx.local_activity( + let la = ctx.start_local_activity( StdActivities::delay, Duration::from_secs(4), LocalActivityOptions::default(), @@ -159,9 +153,7 @@ async fn long_running_local_act_with_timer() { let wf_name = "long_running_local_act_with_timer"; let mut starter = CoreWfStarter::new(wf_name); starter.workflow_options.task_timeout = Some(Duration::from_secs(1)); - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), local_act_then_timer_then_wait); @@ -172,7 +164,7 @@ async fn long_running_local_act_with_timer() { pub(crate) async fn local_act_fanout_wf(ctx: WfContext) -> WorkflowResult<()> { let las: Vec<_> = (1..=50) .map(|i| { - ctx.local_activity(StdActivities::echo, format!("Hi {i}"), Default::default()) + ctx.start_local_activity(StdActivities::echo, format!("Hi {i}"), Default::default()) .expect("serializes fine") }) .collect(); @@ -186,9 +178,7 @@ async fn local_act_fanout() { let wf_name = "local_act_fanout"; let mut starter = CoreWfStarter::new(wf_name); starter.sdk_config.tuner = Arc::new(TunerHolder::fixed_size(5, 1, 1, 1)); - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), local_act_fanout_wf); @@ -200,13 +190,11 @@ async fn local_act_fanout() { async fn local_act_retry_timer_backoff() { let wf_name = "local_act_retry_timer_backoff"; let mut starter = CoreWfStarter::new(wf_name); - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let res = ctx - .local_activity( + .start_local_activity( StdActivities::always_fail, (), LocalActivityOptions { @@ -285,7 +273,7 @@ async fn cancel_immediate(#[case] cancel_type: ActivityCancellationType) { }); let mut worker = starter.worker().await; worker.register_wf(&wf_name, move |ctx: WfContext| async move { - let la = ctx.local_activity( + let la = ctx.start_local_activity( EchoWithManualCancel::echo, "hi".to_string(), LocalActivityOptions { @@ -401,7 +389,7 @@ async fn cancel_after_act_starts( let mut worker = starter.worker().await; let bo_dur = cancel_on_backoff.unwrap_or_else(|| Duration::from_secs(1)); worker.register_wf(&wf_name, move |ctx: WfContext| async move { - let la = ctx.local_activity( + let la = ctx.start_local_activity( EchoWithManualCancelAndBackoff::echo, "hi".to_string(), LocalActivityOptions { @@ -472,7 +460,7 @@ async fn x_to_close_timeout(#[case] is_schedule: bool) { starter .sdk_config - .register_activities_static::(); + .register_activities(LongRunningWithCancellation); let mut worker = starter.worker().await; let (sched, start) = if is_schedule { (Some(Duration::from_secs(2)), None) @@ -487,7 +475,7 @@ async fn x_to_close_timeout(#[case] is_schedule: bool) { worker.register_wf(wf_name.to_owned(), move |ctx: WfContext| async move { let res = ctx - .local_activity( + .start_local_activity( LongRunningWithCancellation::go, (), LocalActivityOptions { @@ -529,7 +517,7 @@ async fn schedule_to_close_timeout_across_timer_backoff(#[case] cached: bool) { let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let res = ctx - .local_activity( + .start_local_activity( FailWithAtomicCounter::go, "hi".to_string(), LocalActivityOptions { @@ -580,9 +568,7 @@ async fn eviction_wont_make_local_act_get_dropped(#[values(true, false)] short_w let wf_name = format!("eviction_wont_make_local_act_get_dropped_{short_wft_timeout}"); let mut starter = CoreWfStarter::new(&wf_name); starter.sdk_config.max_cached_workflows = 0_usize; - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), local_act_then_timer_then_wait); @@ -605,12 +591,10 @@ async fn eviction_wont_make_local_act_get_dropped(#[values(true, false)] short_w async fn timer_backoff_concurrent_with_non_timer_backoff() { let wf_name = "timer_backoff_concurrent_with_non_timer_backoff"; let mut starter = CoreWfStarter::new(wf_name); - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { - let r1 = ctx.local_activity( + let r1 = ctx.start_local_activity( StdActivities::always_fail, (), LocalActivityOptions { @@ -625,7 +609,7 @@ async fn timer_backoff_concurrent_with_non_timer_backoff() { ..Default::default() }, )?; - let r2 = ctx.local_activity( + let r2 = ctx.start_local_activity( StdActivities::always_fail, (), LocalActivityOptions { @@ -654,14 +638,12 @@ async fn timer_backoff_concurrent_with_non_timer_backoff() { async fn repro_nondeterminism_with_timer_bug() { let wf_name = "repro_nondeterminism_with_timer_bug"; let mut starter = CoreWfStarter::new(wf_name); - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let t1 = ctx.timer(Duration::from_secs(30)); - let r1 = ctx.local_activity( + let r1 = ctx.start_local_activity( StdActivities::delay, Duration::from_secs(2), LocalActivityOptions { @@ -726,7 +708,7 @@ async fn weird_la_nondeterminism_repro(#[values(true, false)] fix_hist: bool) { "evict_while_la_running_no_interference", la_problem_workflow, ); - worker.register_activities_static::(); + worker.register_activities(StdActivities); worker.run().await.unwrap(); } @@ -747,7 +729,7 @@ async fn second_weird_la_nondeterminism_repro() { "evict_while_la_running_no_interference", la_problem_workflow, ); - worker.register_activities_static::(); + worker.register_activities(StdActivities); worker.run().await.unwrap(); } @@ -766,7 +748,7 @@ async fn third_weird_la_nondeterminism_repro() { "evict_while_la_running_no_interference", la_problem_workflow, ); - worker.register_activities_static::(); + worker.register_activities(StdActivities); worker.run().await.unwrap(); } @@ -802,14 +784,14 @@ async fn la_resolve_same_time_as_other_cancel() { starter .sdk_config - .register_activities_static::(); + .register_activities(DelayWithCancellation); // The activity won't get a chance to receive the cancel so make sure we still exit fast starter.sdk_config.graceful_shutdown_period = Some(Duration::from_millis(100)); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let normal_act = ctx - .activity( + .start_activity( DelayWithCancellation::delay, Duration::from_secs(9), ActivityOptions { @@ -823,7 +805,7 @@ async fn la_resolve_same_time_as_other_cancel() { ctx.timer(Duration::from_millis(1)).await; // Start LA and cancel the activity at the same time - let local_act = ctx.local_activity( + let local_act = ctx.start_local_activity( DelayWithCancellation::delay, Duration::from_millis(100), LocalActivityOptions { @@ -874,9 +856,7 @@ async fn long_local_activity_with_update( let wf_name = format!("{}-{}", ctx.name, ctx.case.unwrap()); let mut starter = CoreWfStarter::new(&wf_name); starter.workflow_options.task_timeout = Some(Duration::from_secs(1)); - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let mut worker = starter.worker().await; let client = starter.get_client().await; @@ -899,7 +879,7 @@ async fn long_local_activity_with_update( } }, ); - ctx.local_activity( + ctx.start_local_activity( StdActivities::delay, Duration::from_secs(6), LocalActivityOptions::default(), @@ -966,9 +946,7 @@ async fn local_activity_with_heartbeat_only_causes_one_wakeup() { let wf_name = "local_activity_with_heartbeat_only_causes_one_wakeup"; let mut starter = CoreWfStarter::new(wf_name); starter.workflow_options.task_timeout = Some(Duration::from_secs(1)); - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), move |ctx: WfContext| async move { @@ -976,7 +954,7 @@ async fn local_activity_with_heartbeat_only_causes_one_wakeup() { let la_resolved = AtomicBool::new(false); tokio::join!( async { - ctx.local_activity( + ctx.start_local_activity( StdActivities::delay, Duration::from_secs(6), LocalActivityOptions::default(), @@ -1011,7 +989,7 @@ async fn local_activity_with_heartbeat_only_causes_one_wakeup() { } pub(crate) async fn local_activity_with_summary_wf(ctx: WfContext) -> WorkflowResult<()> { - ctx.local_activity( + ctx.start_local_activity( StdActivities::echo, "hi".to_string(), LocalActivityOptions { @@ -1027,9 +1005,7 @@ pub(crate) async fn local_activity_with_summary_wf(ctx: WfContext) -> WorkflowRe async fn local_activity_with_summary() { let wf_name = "local_activity_with_summary"; let mut starter = CoreWfStarter::new(wf_name); - starter - .sdk_config - .register_activities_static::(); + starter.sdk_config.register_activities(StdActivities); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), local_activity_with_summary_wf); @@ -1100,13 +1076,13 @@ async fn local_act_two_wfts_before_marker(#[case] replay: bool, #[case] cached: worker.register_wf( DEFAULT_WORKFLOW_TYPE.to_owned(), |ctx: WfContext| async move { - let la = ctx.local_activity(StdActivities::default, (), Default::default())?; + let la = ctx.start_local_activity(StdActivities::default, (), Default::default())?; ctx.timer(Duration::from_secs(1)).await; la.await; Ok(().into()) }, ); - worker.register_activities_static::(); + worker.register_activities(StdActivities); worker .submit_wf( wf_id.to_owned(), @@ -1139,7 +1115,7 @@ async fn local_act_many_concurrent() { let mut worker = mock_sdk(mh); worker.register_wf(DEFAULT_WORKFLOW_TYPE.to_owned(), local_act_fanout_wf); - worker.register_activities_static::(); + worker.register_activities(StdActivities); worker .submit_wf( wf_id.to_owned(), @@ -1186,7 +1162,7 @@ async fn local_act_heartbeat(#[case] shutdown_middle: bool) { worker.register_wf( DEFAULT_WORKFLOW_TYPE.to_owned(), |ctx: WfContext| async move { - ctx.local_activity( + ctx.start_local_activity( EchoWithConditionalBarrier::echo, "hi".to_string(), LocalActivityOptions::default(), @@ -1262,7 +1238,7 @@ async fn local_act_fail_and_retry(#[case] eventually_pass: bool) { DEFAULT_WORKFLOW_TYPE.to_owned(), move |ctx: WfContext| async move { let la_res = ctx - .local_activity( + .start_local_activity( EventuallyPassingActivity::echo, "hi".to_string(), LocalActivityOptions { @@ -1363,7 +1339,7 @@ async fn local_act_retry_long_backoff_uses_timer() { DEFAULT_WORKFLOW_TYPE.to_owned(), |ctx: WfContext| async move { let la_res = ctx - .local_activity( + .start_local_activity( StdActivities::always_fail, (), LocalActivityOptions { @@ -1385,7 +1361,7 @@ async fn local_act_retry_long_backoff_uses_timer() { Ok(().into()) }, ); - worker.register_activities_static::(); + worker.register_activities(StdActivities); worker .submit_wf( wf_id.to_owned(), @@ -1416,12 +1392,12 @@ async fn local_act_null_result() { worker.register_wf( DEFAULT_WORKFLOW_TYPE.to_owned(), |ctx: WfContext| async move { - ctx.local_activity(StdActivities::no_op, (), LocalActivityOptions::default())? + ctx.start_local_activity(StdActivities::no_op, (), LocalActivityOptions::default())? .await; Ok(().into()) }, ); - worker.register_activities_static::(); + worker.register_activities(StdActivities); worker .submit_wf( wf_id.to_owned(), @@ -1455,13 +1431,13 @@ async fn local_act_command_immediately_follows_la_marker() { worker.register_wf( DEFAULT_WORKFLOW_TYPE.to_owned(), |ctx: WfContext| async move { - ctx.local_activity(StdActivities::no_op, (), LocalActivityOptions::default())? + ctx.start_local_activity(StdActivities::no_op, (), LocalActivityOptions::default())? .await; ctx.timer(Duration::from_secs(1)).await; Ok(().into()) }, ); - worker.register_activities_static::(); + worker.register_activities(StdActivities); worker .submit_wf( wf_id.to_owned(), @@ -1745,7 +1721,7 @@ async fn test_schedule_to_start_timeout() { DEFAULT_WORKFLOW_TYPE.to_owned(), |ctx: WfContext| async move { let la_res = ctx - .local_activity( + .start_local_activity( StdActivities::echo, "hi".to_string(), LocalActivityOptions { @@ -1768,7 +1744,7 @@ async fn test_schedule_to_start_timeout() { Ok(().into()) }, ); - worker.register_activities_static::(); + worker.register_activities(StdActivities); worker .submit_wf( wf_id.to_owned(), @@ -1832,7 +1808,7 @@ async fn test_schedule_to_start_timeout_not_based_on_original_time( DEFAULT_WORKFLOW_TYPE.to_owned(), move |ctx: WfContext| async move { let la_res = ctx - .local_activity( + .start_local_activity( StdActivities::echo, "hi".to_string(), LocalActivityOptions { @@ -1857,7 +1833,7 @@ async fn test_schedule_to_start_timeout_not_based_on_original_time( Ok(().into()) }, ); - worker.register_activities_static::(); + worker.register_activities(StdActivities); worker .submit_wf( wf_id.to_owned(), @@ -1904,7 +1880,7 @@ async fn start_to_close_timeout_allows_retries(#[values(true, false)] la_complet DEFAULT_WORKFLOW_TYPE.to_owned(), move |ctx: WfContext| async move { let la_res = ctx - .local_activity( + .start_local_activity( ActivityWithRetriesAndCancellation::go, (), LocalActivityOptions { @@ -1993,8 +1969,11 @@ async fn wft_failure_cancels_running_las() { worker.register_wf( DEFAULT_WORKFLOW_TYPE.to_owned(), |ctx: WfContext| async move { - let la_handle = - ctx.local_activity(ActivityThatExpectsCancellation::go, (), Default::default())?; + let la_handle = ctx.start_local_activity( + ActivityThatExpectsCancellation::go, + (), + Default::default(), + )?; tokio::join!( async { ctx.timer(Duration::from_secs(1)).await; @@ -2019,7 +1998,7 @@ async fn wft_failure_cancels_running_las() { } } - worker.register_activities_static::(); + worker.register_activities(ActivityThatExpectsCancellation); worker .submit_wf( wf_id.to_owned(), @@ -2062,7 +2041,7 @@ async fn resolved_las_not_recorded_if_wft_fails_many_times() { worker.register_wf( DEFAULT_WORKFLOW_TYPE.to_owned(), WorkflowFunction::new::<_, _, ()>(|ctx: WfContext| async move { - ctx.local_activity( + ctx.start_local_activity( StdActivities::echo, "hi".to_string(), LocalActivityOptions { @@ -2073,7 +2052,7 @@ async fn resolved_las_not_recorded_if_wft_fails_many_times() { panic!() }), ); - worker.register_activities_static::(); + worker.register_activities(StdActivities); worker .submit_wf( wf_id.to_owned(), @@ -2116,7 +2095,7 @@ async fn local_act_records_nonfirst_attempts_ok() { worker.register_wf( DEFAULT_WORKFLOW_TYPE.to_owned(), |ctx: WfContext| async move { - ctx.local_activity( + ctx.start_local_activity( StdActivities::always_fail, (), LocalActivityOptions { @@ -2134,7 +2113,7 @@ async fn local_act_records_nonfirst_attempts_ok() { Ok(().into()) }, ); - worker.register_activities_static::(); + worker.register_activities(StdActivities); worker .submit_wf( wf_id.to_owned(), @@ -2435,7 +2414,7 @@ async fn local_act_retry_explicit_delay() { DEFAULT_WORKFLOW_TYPE.to_owned(), move |ctx: WfContext| async move { let la_res = ctx - .local_activity( + .start_local_activity( ActivityWithExplicitBackoff::go, (), LocalActivityOptions { @@ -2498,9 +2477,9 @@ async fn local_act_retry_explicit_delay() { } async fn la_wf(ctx: WfContext) -> WorkflowResult<()> { - ctx.local_activity_untyped( - DEFAULT_ACTIVITY_TYPE.to_string(), - ().as_json_payload().expect("serializes fine"), + ctx.start_local_activity( + StdActivities::default, + (), LocalActivityOptions { retry_policy: RetryPolicy { maximum_attempts: 1, @@ -2508,7 +2487,7 @@ async fn la_wf(ctx: WfContext) -> WorkflowResult<()> { }, ..Default::default() }, - ) + )? .await; Ok(().into()) } @@ -2613,33 +2592,17 @@ async fn one_la_success(#[case] replay: bool, #[case] completes_ok: bool) { } async fn two_la_wf(ctx: WfContext) -> WorkflowResult<()> { - ctx.local_activity_untyped( - DEFAULT_ACTIVITY_TYPE.to_string(), - ().as_json_payload().expect("serializes fine"), - LocalActivityOptions::default(), - ) - .await; - ctx.local_activity_untyped( - DEFAULT_ACTIVITY_TYPE.to_string(), - ().as_json_payload().expect("serializes fine"), - LocalActivityOptions::default(), - ) - .await; + ctx.start_local_activity(StdActivities::default, (), LocalActivityOptions::default())? + .await; + ctx.start_local_activity(StdActivities::default, (), LocalActivityOptions::default())? + .await; Ok(().into()) } async fn two_la_wf_parallel(ctx: WfContext) -> WorkflowResult<()> { tokio::join!( - ctx.local_activity_untyped( - DEFAULT_ACTIVITY_TYPE.to_string(), - ().as_json_payload().expect("serializes fine"), - LocalActivityOptions::default(), - ), - ctx.local_activity_untyped( - DEFAULT_ACTIVITY_TYPE.to_string(), - ().as_json_payload().expect("serializes fine"), - LocalActivityOptions::default(), - ) + ctx.start_local_activity(StdActivities::default, (), LocalActivityOptions::default())?, + ctx.start_local_activity(StdActivities::default, (), LocalActivityOptions::default())? ); Ok(().into()) } @@ -2743,24 +2706,16 @@ async fn two_sequential_las( } else { worker.register_wf(DEFAULT_WORKFLOW_TYPE, two_la_wf); } - worker.register_activities_static::(); + worker.register_activities(ResolvedActivity); worker.run().await.unwrap(); } async fn la_timer_la(ctx: WfContext) -> WorkflowResult<()> { - ctx.local_activity_untyped( - DEFAULT_ACTIVITY_TYPE.to_string(), - ().as_json_payload().expect("serializes fine"), - LocalActivityOptions::default(), - ) - .await; + ctx.start_local_activity(StdActivities::default, (), LocalActivityOptions::default())? + .await; ctx.timer(Duration::from_secs(5)).await; - ctx.local_activity_untyped( - DEFAULT_ACTIVITY_TYPE.to_string(), - ().as_json_payload().expect("serializes fine"), - LocalActivityOptions::default(), - ) - .await; + ctx.start_local_activity(StdActivities::default, (), LocalActivityOptions::default())? + .await; Ok(().into()) } @@ -2835,7 +2790,7 @@ async fn las_separated_by_timer(#[case] replay: bool) { let mut worker = build_fake_sdk(mock_cfg); worker.set_worker_interceptor(aai); worker.register_wf(DEFAULT_WORKFLOW_TYPE, la_timer_la); - worker.register_activities_static::(); + worker.register_activities(ResolvedActivity); worker.run().await.unwrap(); } @@ -2867,7 +2822,7 @@ async fn one_la_heartbeating_wft_failure_still_executes() { let mut worker = build_fake_sdk(mock_cfg); worker.register_wf(DEFAULT_WORKFLOW_TYPE, la_wf); - worker.register_activities_static::(); + worker.register_activities(ResolvedActivity); worker.run().await.unwrap(); } @@ -2901,14 +2856,14 @@ async fn immediate_cancel( let mut worker = build_fake_sdk(mock_cfg); worker.register_wf(DEFAULT_WORKFLOW_TYPE, move |ctx: WfContext| async move { - let la = ctx.local_activity_untyped( - DEFAULT_ACTIVITY_TYPE.to_string(), - ().as_json_payload().expect("serializes fine"), + let la = ctx.start_local_activity( + StdActivities::default, + (), LocalActivityOptions { cancel_type, ..Default::default() }, - ); + )?; la.cancel(&ctx); la.await; Ok(().into()) @@ -3002,7 +2957,7 @@ async fn cancel_after_act_starts_canned( let mut worker = build_fake_sdk(mock_cfg); worker.register_wf(DEFAULT_WORKFLOW_TYPE, move |ctx: WfContext| async move { - let la = ctx.local_activity( + let la = ctx.start_local_activity( ActivityWithConditionalCancelWait::echo, (), LocalActivityOptions { diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/patches.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/patches.rs index 1de1bdc63..c80e6fb55 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/patches.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/patches.rs @@ -21,7 +21,7 @@ use temporalio_common::protos::{ RecordMarkerCommandAttributes, ScheduleActivityTaskCommandAttributes, UpsertWorkflowSearchAttributesCommandAttributes, command::Attributes, }, - common::v1::{ActivityType, Payload}, + common::v1::ActivityType, enums::v1::{CommandType, EventType, IndexedValueType}, history::v1::{ ActivityTaskCompletedEventAttributes, ActivityTaskScheduledEventAttributes, @@ -31,7 +31,11 @@ use temporalio_common::protos::{ }; use temporalio_common::worker::WorkerTaskTypes; -use temporalio_sdk::{ActivityOptions, WfContext, WorkflowResult}; +use temporalio_macros::activities; +use temporalio_sdk::{ + ActivityOptions, WfContext, WorkflowResult, + activities::{ActivityContext, ActivityError}, +}; use temporalio_sdk_core::test_help::{CoreInternalFlags, MockPollCfg, ResponseType}; use tokio::{join, sync::Notify}; use tokio_stream::StreamExt; @@ -312,39 +316,51 @@ fn patch_marker_single_activity( t } +struct FakeAct; +#[activities] +impl FakeAct { + #[activity(name = "")] + fn nameless(_: ActivityContext) -> Result<(), ActivityError> { + unimplemented!() + } +} + async fn v1(ctx: &mut WfContext) { - ctx.activity_untyped( - "".to_string(), - Payload::default(), + ctx.start_activity( + FakeAct::nameless, + (), ActivityOptions { activity_id: Some("no_change".to_owned()), ..Default::default() }, ) + .unwrap() .await; } async fn v2(ctx: &mut WfContext) -> bool { if ctx.patched(MY_PATCH_ID) { - ctx.activity_untyped( - "".to_string(), - Payload::default(), + ctx.start_activity( + FakeAct::nameless, + (), ActivityOptions { activity_id: Some("had_change".to_owned()), ..Default::default() }, ) + .unwrap() .await; true } else { - ctx.activity_untyped( - "".to_string(), - Payload::default(), + ctx.start_activity( + FakeAct::nameless, + (), ActivityOptions { activity_id: Some("no_change".to_owned()), ..Default::default() }, ) + .unwrap() .await; false } @@ -352,26 +368,28 @@ async fn v2(ctx: &mut WfContext) -> bool { async fn v3(ctx: &mut WfContext) { ctx.deprecate_patch(MY_PATCH_ID); - ctx.activity_untyped( - "".to_string(), - Payload::default(), + ctx.start_activity( + FakeAct::nameless, + (), ActivityOptions { activity_id: Some("had_change".to_owned()), ..Default::default() }, ) + .unwrap() .await; } async fn v4(ctx: &mut WfContext) { - ctx.activity_untyped( - "".to_string(), - Payload::default(), + ctx.start_activity( + FakeAct::nameless, + (), ActivityOptions { activity_id: Some("had_change".to_owned()), ..Default::default() }, ) + .unwrap() .await; } @@ -662,23 +680,15 @@ async fn same_change_multiple_spots(#[case] have_marker_in_hist: bool, #[case] r let mut worker = build_fake_sdk(mock_cfg); worker.register_wf(DEFAULT_WORKFLOW_TYPE, move |ctx: WfContext| async move { if ctx.patched(MY_PATCH_ID) { - ctx.activity_untyped( - "".to_string(), - Payload::default(), - ActivityOptions::default(), - ) - .await; + ctx.start_activity(FakeAct::nameless, (), ActivityOptions::default())? + .await; } else { ctx.timer(ONE_SECOND).await; } ctx.timer(ONE_SECOND).await; if ctx.patched(MY_PATCH_ID) { - ctx.activity_untyped( - "".to_string(), - Payload::default(), - ActivityOptions::default(), - ) - .await; + ctx.start_activity(FakeAct::nameless, (), ActivityOptions::default())? + .await; } else { ctx.timer(ONE_SECOND).await; } diff --git a/crates/sdk-core/tests/integ_tests/workflow_tests/resets.rs b/crates/sdk-core/tests/integ_tests/workflow_tests/resets.rs index c96d51781..d75d2429d 100644 --- a/crates/sdk-core/tests/integ_tests/workflow_tests/resets.rs +++ b/crates/sdk-core/tests/integ_tests/workflow_tests/resets.rs @@ -147,7 +147,7 @@ async fn reset_randomseed() { if RAND_SEED.load(Ordering::Relaxed) == ctx.random_seed() { ctx.timer(Duration::from_millis(100)).await; } else { - ctx.local_activity( + ctx.start_local_activity( StdActivities::echo, "hi!".to_string(), LocalActivityOptions::default(), @@ -166,7 +166,7 @@ async fn reset_randomseed() { Ok(().into()) } }); - worker.register_activities_static::(); + worker.register_activities(StdActivities); let run_id = worker .submit_wf( diff --git a/crates/sdk-core/tests/manual_tests.rs b/crates/sdk-core/tests/manual_tests.rs index 63622e680..ee4dfb759 100644 --- a/crates/sdk-core/tests/manual_tests.rs +++ b/crates/sdk-core/tests/manual_tests.rs @@ -32,7 +32,7 @@ use temporalio_sdk::{ use temporalio_sdk_core::{CoreRuntime, PollerBehavior, TunerHolder}; use tracing::info; -struct JitteryEchoActivities {} +struct JitteryEchoActivities; #[activities] impl JitteryEchoActivities { #[activity] @@ -76,14 +76,14 @@ async fn poller_load_spiky() { let mut worker = starter.worker().await; let submitter = worker.get_submitter_handle(); - worker.register_activities_static::(); + worker.register_activities(JitteryEchoActivities); worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let sigchan = ctx.make_signal_channel(SIGNAME).map(Ok); let drained_fut = sigchan.forward(sink::drain()); let real_stuff = async move { for _ in 0..5 { - ctx.activity( + ctx.start_activity( JitteryEchoActivities::echo, "hi!".to_string(), ActivityOptions { @@ -318,14 +318,14 @@ async fn poller_load_spike_then_sustained() { let mut worker = starter.worker().await; let submitter = worker.get_submitter_handle(); - worker.register_activities_static::(); + worker.register_activities(JitteryEchoActivities); worker.register_wf(wf_name.to_owned(), |ctx: WfContext| async move { let sigchan = ctx.make_signal_channel(SIGNAME).map(Ok); let drained_fut = sigchan.forward(sink::drain()); let real_stuff = async move { for _ in 0..5 { - ctx.activity( + ctx.start_activity( JitteryEchoActivities::echo, "hi!".to_string(), ActivityOptions { diff --git a/crates/sdk-core/tests/shared_tests/priority.rs b/crates/sdk-core/tests/shared_tests/priority.rs index 9388c9650..bd2679d26 100644 --- a/crates/sdk-core/tests/shared_tests/priority.rs +++ b/crates/sdk-core/tests/shared_tests/priority.rs @@ -26,7 +26,7 @@ pub(crate) async fn priority_values_sent_to_server() { let mut worker = starter.worker().await; let child_type = "child-wf"; - struct PriorityActivities {} + struct PriorityActivities; #[activities] impl PriorityActivities { #[activity] @@ -43,7 +43,7 @@ pub(crate) async fn priority_values_sent_to_server() { } } - worker.register_activities_static::(); + worker.register_activities(PriorityActivities); worker.register_wf(starter.get_task_queue(), move |ctx: WfContext| async move { let child = ctx.child_workflow(ChildWorkflowOptions { workflow_id: format!("{}-child", ctx.task_queue()), @@ -65,7 +65,7 @@ pub(crate) async fn priority_values_sent_to_server() { .into_started() .expect("Child should start OK"); let activity = ctx - .activity( + .start_activity( PriorityActivities::echo, "hello".to_string(), ActivityOptions { diff --git a/crates/sdk/src/activities.rs b/crates/sdk/src/activities.rs index 42b68add0..985339f5f 100644 --- a/crates/sdk/src/activities.rs +++ b/crates/sdk/src/activities.rs @@ -1,4 +1,51 @@ //! Functionality related to defining and interacting with activities +//! +//! +//! An example of defining an activity: +//! ``` +//! use std::sync::{ +//! Arc, +//! atomic::{AtomicUsize, Ordering}, +//! }; +//! use temporalio_macros::activities; +//! use temporalio_sdk::activities::{ActivityContext, ActivityError}; +//! +//! struct MyActivities { +//! counter: AtomicUsize, +//! } +//! +//! #[activities] +//! impl MyActivities { +//! #[activity] +//! async fn echo(_ctx: ActivityContext, e: String) -> Result { +//! Ok(e) +//! } +//! +//! #[activity] +//! async fn uses_self(self: Arc, _ctx: ActivityContext) -> Result<(), ActivityError> { +//! self.counter.fetch_add(1, Ordering::Relaxed); +//! Ok(()) +//! } +//! } +//! +//! // If you need to refer to an activity that is defined externally, in a different codebase or +//! // possibly a differenet language, you can simply leave the function body unimplemented like so: +//! +//! struct ExternalActivities; +//! #[activities] +//! impl ExternalActivities { +//! #[activity(name = "foo")] +//! async fn foo(_ctx: ActivityContext, _: String) -> Result { +//! unimplemented!() +//! } +//! } +//! ``` +//! +//! This will allows you to call the activity from workflow code still, but the actual function +//! will never be invoked, since you won't have registered it with the worker. + +#[doc(inline)] +pub use temporalio_macros::activities; use crate::app_data::AppData; use futures_util::{FutureExt, future::BoxFuture}; @@ -312,8 +359,7 @@ pub(crate) type ActivityInvocation = Arc< #[doc(hidden)] pub trait ActivityImplementer { - fn register_all_static(defs: &mut ActivityDefinitions); - fn register_all_instance(self: Arc, defs: &mut ActivityDefinitions); + fn register_all(self: Arc, defs: &mut ActivityDefinitions); } #[doc(hidden)] @@ -336,54 +382,26 @@ pub struct ActivityDefinitions { } impl ActivityDefinitions { - /// Registers all activities on an activity implementer that don't take a receiver. - pub fn register_activities_static(&mut self) -> &mut Self - where - AI: ActivityImplementer + HasOnlyStaticMethods, - { - AI::register_all_static(self); - self - } - /// Registers all activities on an activity implementer that take a receiver. + /// Registers all activities on an activity implementer. pub fn register_activities(&mut self, instance: AI) -> &mut Self { - AI::register_all_static(self); let arcd = Arc::new(instance); - AI::register_all_instance(arcd, self); - self - } - /// Registers a specific activitiy that does not take a receiver. - pub fn register_activity(&mut self) -> &mut Self { - self.activities.insert( - AD::name(), - Arc::new(move |p, pc, c| { - let deserialized = pc.from_payload(p, &SerializationContext::Activity)?; - let pc2 = pc.clone(); - Ok(AD::execute(None, c, deserialized) - .map(move |v| match v { - Ok(okv) => pc2 - .to_payload(&okv, &SerializationContext::Activity) - .map_err(|e| e.into()), - Err(e) => Err(e), - }) - .boxed()) - }), - ); + AI::register_all(arcd, self); self } - /// Registers a specific activitiy that takes a receiver. - pub fn register_activity_with_instance( + /// Registers a specific activitiy. + pub fn register_activity( &mut self, instance: Arc, ) -> &mut Self { self.activities.insert( AD::name(), Arc::new(move |p, pc, c| { - let deserialized = pc.from_payload(p, &SerializationContext::Activity)?; + let deserialized = pc.from_payload(&SerializationContext::Activity, p)?; let pc2 = pc.clone(); Ok(AD::execute(Some(instance.clone()), c, deserialized) .map(move |v| match v { Ok(okv) => pc2 - .to_payload(&okv, &SerializationContext::Activity) + .to_payload(&SerializationContext::Activity, &okv) .map_err(|e| e.into()), Err(e) => Err(e), }) diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index 52aac8141..c8d168156 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -58,7 +58,7 @@ //! use_worker_versioning: false, //! default_versioning_behavior: None, //! }) -//! .register_activities_static::() +//! .register_activities(MyActivities) //! .build(); //! //! let mut worker = Worker::new(&runtime, client, worker_options)?; @@ -88,7 +88,7 @@ pub use workflow_context::{ use crate::{ activities::{ ActivityContext, ActivityDefinitions, ActivityError, ActivityImplementer, - ExecutableActivity, HasOnlyStaticMethods, + ExecutableActivity, }, interceptors::WorkerInterceptor, workflow_context::{ChildWfCommon, NexusUnblockData, StartedNexusOperation}, @@ -170,7 +170,7 @@ pub struct WorkerOptions { /// Set the deployment options for this worker. Defaults to a hash of the currently running /// executable. - #[builder(default = WorkerDeploymentOptions::from_build_id(build_id_from_current_exe().to_owned()))] + #[builder(default = def_build_id())] pub deployment_options: WorkerDeploymentOptions, /// A human-readable string that can identify this worker. Using something like sdk version /// and host name is a good default. If set, overrides the identity set (if any) on the client @@ -250,63 +250,39 @@ pub struct WorkerOptions { pub graceful_shutdown_period: Option, } -// TODO [rust-sdk-branch]: Traitify this? impl WorkerOptionsBuilder { - /// Registers all activities on an activity implementer that don't take a receiver. - pub fn register_activities_static(mut self) -> Self - where - AI: ActivityImplementer + HasOnlyStaticMethods, - { - self.activities.register_activities_static::(); - self - } - /// Registers all activities on an activity implementer that take a receiver. + /// Registers all activities on an activity implementer. pub fn register_activities(mut self, instance: AI) -> Self { self.activities.register_activities::(instance); self } - /// Registers a specific activitiy that does not take a receiver. - pub fn register_activity(mut self) -> Self { - self.activities.register_activity::(); - self - } - /// Registers a specific activitiy that takes a receiver. - pub fn register_activity_with_instance( + /// Registers a specific activitiy. + pub fn register_activity( mut self, instance: Arc, ) -> Self { - self.activities - .register_activity_with_instance::(instance); + self.activities.register_activity::(instance); self } } +// Needs to exist to avoid https://github.com/elastio/bon/issues/359 +fn def_build_id() -> WorkerDeploymentOptions { + WorkerDeploymentOptions::from_build_id(build_id_from_current_exe().to_owned()) +} + impl WorkerOptions { - /// Registers all activities on an activity implementer that don't take a receiver. - pub fn register_activities_static(&mut self) -> &mut Self - where - AI: ActivityImplementer + HasOnlyStaticMethods, - { - self.activities.register_activities_static::(); - self - } - /// Registers all activities on an activity implementer that take a receiver. + /// Registers all activities on an activity implementer. pub fn register_activities(&mut self, instance: AI) -> &mut Self { self.activities.register_activities::(instance); self } - /// Registers a specific activitiy that does not take a receiver. - pub fn register_activity(&mut self) -> &mut Self { - self.activities.register_activity::(); - self - } - /// Registers a specific activitiy that takes a receiver. - pub fn register_activity_with_instance( + /// Registers a specific activitiy. + pub fn register_activity( &mut self, instance: Arc, ) -> &mut Self { - self.activities - .register_activity_with_instance::(instance); + self.activities.register_activity::(instance); self } /// Returns all the registered activities by cloning the current set. @@ -451,36 +427,21 @@ impl Worker { .insert(workflow_type.into(), wf_function.into()); } - /// Registers all activities on an activity implementer that don't take a receiver. - pub fn register_activities_static(&mut self) -> &mut Self - where - AI: ActivityImplementer + HasOnlyStaticMethods, - { - self.activity_half - .activities - .register_activities_static::(); - self - } - /// Registers all activities on an activity implementer that take a receiver. + /// Registers all activities on an activity implementer. pub fn register_activities(&mut self, instance: AI) -> &mut Self { self.activity_half .activities .register_activities::(instance); self } - /// Registers a specific activitiy that does not take a receiver. - pub fn register_activity(&mut self) -> &mut Self { - self.activity_half.activities.register_activity::(); - self - } - /// Registers a specific activitiy that takes a receiver. - pub fn register_activity_with_instance( + /// Registers a specific activitiy. + pub fn register_activity( &mut self, instance: Arc, ) -> &mut Self { self.activity_half .activities - .register_activity_with_instance::(instance); + .register_activity::(instance); self } @@ -1402,8 +1363,8 @@ mod tests { #[allow(dead_code, unreachable_code, unused, clippy::diverging_sub_expression)] fn test_activity_via_workflow_context() { let wf_ctx: WfContext = unimplemented!(); - wf_ctx.activity(MyActivities::my_activity, (), ActivityOptions::default()); - wf_ctx.activity( + wf_ctx.start_activity(MyActivities::my_activity, (), ActivityOptions::default()); + wf_ctx.start_activity( MyActivities::takes_self, "Hi".to_owned(), ActivityOptions::default(), diff --git a/crates/sdk/src/workflow_context.rs b/crates/sdk/src/workflow_context.rs index 5a2717aa2..f5e3c3866 100644 --- a/crates/sdk/src/workflow_context.rs +++ b/crates/sdk/src/workflow_context.rs @@ -217,25 +217,15 @@ impl WfContext { } /// Request to run an activity - pub fn activity( + pub fn start_activity( &self, _activity: AD, input: AD::Input, - opts: ActivityOptions, + mut opts: ActivityOptions, ) -> Result, PayloadConversionError> { // TODO [rust-sdk-branch]: Get payload converter properly let pc = PayloadConverter::serde_json(); - let payload = pc.to_payload(&input, &SerializationContext::Workflow)?; - Ok(self.activity_untyped(AD::name().to_string(), payload, opts)) - } - - /// Request to run an activity with an explicit name and payload - pub fn activity_untyped( - &self, - activity_type: String, - input: Payload, - mut opts: ActivityOptions, - ) -> impl CancellableFuture { + let payload = pc.to_payload(&SerializationContext::Workflow, &input)?; let seq = self.seq_nums.write().next_activity_seq(); let (cmd, unblocker) = CancellableWFCommandFut::new(CancellableID::Activity(seq)); if opts.task_queue.is_none() { @@ -243,16 +233,16 @@ impl WfContext { } self.send( CommandCreateRequest { - cmd: opts.into_command(activity_type, input, seq), + cmd: opts.into_command(AD::name().to_string(), payload, seq), unblocker, } .into(), ); - cmd + Ok(cmd) } /// Request to run a local activity - pub fn local_activity( + pub fn start_local_activity( &self, _activity: AD, input: AD::Input, @@ -260,7 +250,7 @@ impl WfContext { ) -> Result + '_, PayloadConversionError> { // TODO [rust-sdk-branch]: Get payload converter properly let pc = PayloadConverter::serde_json(); - let payload = pc.to_payload(&input, &SerializationContext::Workflow)?; + let payload = pc.to_payload(&SerializationContext::Workflow, &input)?; Ok(LATimerBackoffFut::new( AD::name().to_string(), payload, @@ -269,16 +259,6 @@ impl WfContext { )) } - /// Request to run a local activity with an explicit name and payload - pub fn local_activity_untyped( - &self, - activity_type: String, - input: Payload, - opts: LocalActivityOptions, - ) -> impl CancellableFuture + '_ { - LATimerBackoffFut::new(activity_type, input, opts, self) - } - /// Request to run a local activity with no implementation of timer-backoff based retrying. fn local_activity_no_timer_retry( &self, From a341c95614be06518a804690d35b07f38fa890c8 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Thu, 8 Jan 2026 10:50:30 -0800 Subject: [PATCH 13/13] Fix tests --- crates/sdk-core/tests/common/mod.rs | 17 +++++++++-------- .../tests/heavy_tests/fuzzy_workflow.rs | 3 +-- .../tests/integ_tests/worker_heartbeat_tests.rs | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/sdk-core/tests/common/mod.rs b/crates/sdk-core/tests/common/mod.rs index 9b4cf6345..463db4675 100644 --- a/crates/sdk-core/tests/common/mod.rs +++ b/crates/sdk-core/tests/common/mod.rs @@ -484,14 +484,15 @@ impl CoreWfStarter { let client = Client::new(connection.clone(), client_opts); (connection, client) }; - let worker = init_worker( - rt, - self.sdk_config - .to_core_options(client.namespace()) - .expect("sdk config converts to core config"), - connection, - ) - .expect("Worker inits cleanly"); + let mut core_config = self + .sdk_config + .to_core_options(client.namespace()) + .expect("sdk config converts to core config"); + if let Some(ref ccm) = self.core_config_mutator { + ccm(&mut core_config); + } + let worker = + init_worker(rt, core_config, connection).expect("Worker inits cleanly"); InitializedWorker { worker: Arc::new(worker), client, diff --git a/crates/sdk-core/tests/heavy_tests/fuzzy_workflow.rs b/crates/sdk-core/tests/heavy_tests/fuzzy_workflow.rs index 48d6669e3..7faee7b1f 100644 --- a/crates/sdk-core/tests/heavy_tests/fuzzy_workflow.rs +++ b/crates/sdk-core/tests/heavy_tests/fuzzy_workflow.rs @@ -82,8 +82,7 @@ async fn fuzzy_workflow() { starter.sdk_config.tuner = Arc::new(TunerHolder::fixed_size(25, 25, 100, 100)); let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), fuzzy_wf_def); - - starter.sdk_config.register_activities(StdActivities); + worker.register_activities(StdActivities); let client = starter.get_client().await; diff --git a/crates/sdk-core/tests/integ_tests/worker_heartbeat_tests.rs b/crates/sdk-core/tests/integ_tests/worker_heartbeat_tests.rs index 16019380d..89312c132 100644 --- a/crates/sdk-core/tests/integ_tests/worker_heartbeat_tests.rs +++ b/crates/sdk-core/tests/integ_tests/worker_heartbeat_tests.rs @@ -135,7 +135,7 @@ async fn docker_worker_heartbeat_basic(#[values("otel", "prom", "no_metrics")] b let wf_name = format!("worker_heartbeat_basic_{backing}"); let mut starter = CoreWfStarter::new_with_runtime(&wf_name, rt); starter.sdk_config.max_cached_workflows = 5_usize; - starter.sdk_config.tuner = Arc::new(TunerHolder::fixed_size(5, 5, 1, 1)); + starter.sdk_config.tuner = Arc::new(TunerHolder::fixed_size(5, 5, 100, 0)); starter.set_core_cfg_mutator(|c| { c.plugins = vec![ PluginInfo {