diff --git a/README.md b/README.md index e12b3b18a..defbfb7c1 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,8 @@ You can buld and test the project using cargo: Run integ tests with `cargo integ-test`. You will need to already be running the server: `docker-compose -f .buildkite/docker/docker-compose.yaml up` +Run load tests with `cargo test --features=save_wf_inputs --test heavy_tests`. + ## Formatting To format all code run: `cargo fmt --all` diff --git a/core/Cargo.toml b/core/Cargo.toml index 34d3e9330..b70ab7f07 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -95,6 +95,8 @@ bimap = "0.6.1" clap = { version = "4.0", features = ["derive"] } criterion = "0.4" rstest = "0.17" +serde = "1.0" +serde_json = "1.0" temporal-sdk-core-test-utils = { path = "../test-utils" } temporal-sdk = { path = "../sdk" } diff --git a/core/benches/workflow_replay.rs b/core/benches/workflow_replay.rs index 5e657720a..27a407ca0 100644 --- a/core/benches/workflow_replay.rs +++ b/core/benches/workflow_replay.rs @@ -1,9 +1,9 @@ use criterion::{criterion_group, criterion_main, Criterion}; use futures::StreamExt; use std::time::Duration; -use temporal_sdk::{WfContext, WorkflowFunction}; +use temporal_sdk::{WfContext, WfExitValue, WorkflowFunction}; use temporal_sdk_core::replay::HistoryForReplay; -use temporal_sdk_core_protos::DEFAULT_WORKFLOW_TYPE; +use temporal_sdk_core_protos::{coresdk::AsJsonPayloadExt, DEFAULT_WORKFLOW_TYPE}; use temporal_sdk_core_test_utils::{canned_histories, replay_sdk_worker}; pub fn criterion_benchmark(c: &mut Criterion) { @@ -58,7 +58,9 @@ fn timers_wf(num_timers: u32) -> WorkflowFunction { for _ in 1..=num_timers { ctx.timer(Duration::from_secs(1)).await; } - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }) } @@ -71,6 +73,8 @@ fn big_signals_wf(num_tasks: usize) -> WorkflowFunction { } } - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }) } diff --git a/core/src/core_tests/activity_tasks.rs b/core/src/core_tests/activity_tasks.rs index 76461144e..a4fcf36d1 100644 --- a/core/src/core_tests/activity_tasks.rs +++ b/core/src/core_tests/activity_tasks.rs @@ -23,7 +23,7 @@ use std::{ time::Duration, }; use temporal_client::WorkflowOptions; -use temporal_sdk::{ActivityOptions, WfContext}; +use temporal_sdk::{ActivityOptions, WfContext, WfExitValue}; use temporal_sdk_core_api::{ errors::{CompleteActivityError, PollActivityError}, Worker as WorkerTrait, @@ -41,7 +41,7 @@ use temporal_sdk_core_protos::{ ScheduleActivity, }, workflow_completion::WorkflowActivationCompletion, - ActivityTaskCompletion, + ActivityTaskCompletion, AsJsonPayloadExt, }, temporal::api::{ command::v1::{command::Attributes, ScheduleActivityTaskCommandAttributes}, @@ -945,7 +945,9 @@ async fn activity_tasks_from_completion_reserve_slots() { }) .await; complete_token.cancel(); - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } }); diff --git a/core/src/core_tests/child_workflows.rs b/core/src/core_tests/child_workflows.rs index a72093602..1a73b7702 100644 --- a/core/src/core_tests/child_workflows.rs +++ b/core/src/core_tests/child_workflows.rs @@ -6,15 +6,21 @@ use crate::{ worker::{client::mocks::mock_workflow_client, ManagedWFFunc}, }; use temporal_client::WorkflowOptions; -use temporal_sdk::{ChildWorkflowOptions, Signal, WfContext, WorkflowFunction, WorkflowResult}; +use temporal_sdk::{ + ChildWorkflowOptions, Signal, WfContext, WfExitValue, WorkflowFunction, WorkflowResult, +}; use temporal_sdk_core_api::Worker; -use temporal_sdk_core_protos::coresdk::{ - child_workflow::{child_workflow_result, ChildWorkflowCancellationType}, - workflow_activation::{workflow_activation_job, WorkflowActivationJob}, - workflow_commands::{ - CancelChildWorkflowExecution, CompleteWorkflowExecution, StartChildWorkflowExecution, +use temporal_sdk_core_protos::{ + coresdk::{ + child_workflow::{child_workflow_result, ChildWorkflowCancellationType}, + workflow_activation::{workflow_activation_job, WorkflowActivationJob}, + workflow_commands::{ + CancelChildWorkflowExecution, CompleteWorkflowExecution, StartChildWorkflowExecution, + }, + workflow_completion::WorkflowActivationCompletion, + AsJsonPayloadExt, }, - workflow_completion::WorkflowActivationCompletion, + temporal::api::common::v1::Payload, }; use tokio::join; @@ -59,7 +65,9 @@ async fn signal_child_workflow(#[case] serial: bool) { }; sigres.expect("signal result is ok"); res.status.expect("child wf result is ok"); - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }; worker.register_wf(wf_type.to_owned(), wf); @@ -75,7 +83,7 @@ async fn signal_child_workflow(#[case] serial: bool) { worker.run_until_done().await.unwrap(); } -async fn parent_cancels_child_wf(ctx: WfContext) -> WorkflowResult<()> { +async fn parent_cancels_child_wf(ctx: WfContext) -> WorkflowResult { let child = ctx.child_workflow(ChildWorkflowOptions { workflow_id: "child-id-1".to_string(), workflow_type: "child".to_string(), @@ -95,7 +103,9 @@ async fn parent_cancels_child_wf(ctx: WfContext) -> WorkflowResult<()> { .status .expect("child wf result is ok"); assert_matches!(stat, child_workflow_result::Status::Cancelled(_)); - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } #[tokio::test] diff --git a/core/src/core_tests/determinism.rs b/core/src/core_tests/determinism.rs index 492d92947..832a227b4 100644 --- a/core/src/core_tests/determinism.rs +++ b/core/src/core_tests/determinism.rs @@ -10,10 +10,13 @@ use std::{ }; use temporal_client::WorkflowOptions; use temporal_sdk::{ - ActivityOptions, ChildWorkflowOptions, LocalActivityOptions, WfContext, WorkflowResult, + ActivityOptions, ChildWorkflowOptions, LocalActivityOptions, WfContext, WfExitValue, + WorkflowResult, }; use temporal_sdk_core_protos::{ + coresdk::AsJsonPayloadExt, temporal::api::{ + common::v1::Payload, enums::v1::{EventType, WorkflowTaskFailedCause}, failure::v1::Failure, }, @@ -21,7 +24,7 @@ use temporal_sdk_core_protos::{ }; static DID_FAIL: AtomicBool = AtomicBool::new(false); -pub async fn timer_wf_fails_once(ctx: WfContext) -> WorkflowResult<()> { +pub async fn timer_wf_fails_once(ctx: WfContext) -> WorkflowResult { ctx.timer(Duration::from_secs(1)).await; if DID_FAIL .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed) @@ -29,7 +32,9 @@ pub async fn timer_wf_fails_once(ctx: WfContext) -> WorkflowResult<()> { { panic!("Ahh"); } - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } /// Verifies that workflow panics (which in this case the Rust SDK turns into workflow activation @@ -97,7 +102,9 @@ async fn test_wf_task_rejected_properly_due_to_nondeterminism(#[case] use_cache: ctx.timer(Duration::from_secs(1)).await; } ctx.timer(Duration::from_secs(1)).await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }); worker @@ -188,7 +195,9 @@ async fn activity_id_or_type_change_is_nondeterministic( }) .await; } - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }); worker @@ -257,7 +266,9 @@ async fn child_wf_id_or_type_change_is_nondeterministic( }) .start(&ctx) .await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }); worker @@ -300,7 +311,9 @@ async fn repro_channel_missing_because_nondeterminism() { worker.register_wf(wf_type.to_owned(), move |ctx: WfContext| async move { ctx.patched("wrongid"); ctx.timer(Duration::from_secs(1)).await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }); worker diff --git a/core/src/core_tests/local_activities.rs b/core/src/core_tests/local_activities.rs index cd0e39a41..fcd18a23f 100644 --- a/core/src/core_tests/local_activities.rs +++ b/core/src/core_tests/local_activities.rs @@ -21,7 +21,8 @@ use std::{ }; use temporal_client::WorkflowOptions; use temporal_sdk::{ - ActContext, ActivityCancelledError, LocalActivityOptions, WfContext, WorkflowResult, + ActContext, ActExitValue, ActivityCancelledError, LocalActivityOptions, WfContext, WfExitValue, + WorkflowResult, }; use temporal_sdk_core_api::{ errors::{PollActivityError, PollWfError}, @@ -38,7 +39,7 @@ use temporal_sdk_core_protos::{ ActivityTaskCompletion, AsJsonPayloadExt, }, temporal::api::{ - common::v1::RetryPolicy, + common::v1::{Payload, RetryPolicy}, enums::v1::{EventType, TimeoutType, WorkflowTaskFailedCause}, failure::v1::{failure::FailureInfo, Failure}, query::v1::WorkflowQuery, @@ -50,8 +51,8 @@ use temporal_sdk_core_test_utils::{ }; use tokio::{join, sync::Barrier}; -async fn echo(_ctx: ActContext, e: String) -> anyhow::Result { - Ok(e) +async fn echo(ctx: ActContext) -> anyhow::Result> { + Ok(ActExitValue::Normal(ctx.get_args()[0].clone())) } /// This test verifies that when replaying we are able to resolve local activities whose data we @@ -92,12 +93,14 @@ async fn local_act_two_wfts_before_marker(#[case] replay: bool, #[case] cached: |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"), + input: vec!["hi".as_json_payload().expect("serializes fine")], ..Default::default() }); ctx.timer(Duration::from_secs(1)).await; la.await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }, ); worker.register_activity(DEFAULT_ACTIVITY_TYPE, echo); @@ -113,21 +116,23 @@ async fn local_act_two_wfts_before_marker(#[case] replay: bool, #[case] cached: worker.run_until_done().await.unwrap(); } -pub async fn local_act_fanout_wf(ctx: WfContext) -> WorkflowResult<()> { +pub async fn local_act_fanout_wf(ctx: WfContext) -> WorkflowResult { let las: Vec<_> = (1..=50) .map(|i| { ctx.local_activity(LocalActivityOptions { activity_type: "echo".to_string(), - input: format!("Hi {i}") + input: vec![format!("Hi {i}") .as_json_payload() - .expect("serializes fine"), + .expect("serializes fine")], ..Default::default() }) }) .collect(); ctx.timer(Duration::from_secs(1)).await; join_all(las).await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } #[tokio::test] @@ -199,20 +204,22 @@ async fn local_act_heartbeat(#[case] shutdown_middle: bool) { |ctx: WfContext| async move { ctx.local_activity(LocalActivityOptions { activity_type: "echo".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), + input: vec!["hi".as_json_payload().expect("serializes fine")], ..Default::default() }) .await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }, ); - worker.register_activity("echo", move |_ctx: ActContext, str: String| async move { + worker.register_activity("echo", move |ctx: ActContext| 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) + Ok(ActExitValue::Normal(ctx.get_args()[0].clone())) }); worker .submit_wf( @@ -255,7 +262,7 @@ async fn local_act_fail_and_retry(#[case] eventually_pass: bool) { let la_res = ctx .local_activity(LocalActivityOptions { activity_type: "echo".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), + input: vec!["hi".as_json_payload().expect("serializes fine")], retry_policy: RetryPolicy { initial_interval: Some(prost_dur!(from_millis(50))), backoff_coefficient: 1.2, @@ -271,14 +278,16 @@ async fn local_act_fail_and_retry(#[case] eventually_pass: bool) { } else { assert!(la_res.failed()) } - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }, ); let attempts: &'static _ = Box::leak(Box::new(AtomicUsize::new(0))); - worker.register_activity("echo", move |_ctx: ActContext, _: String| async move { + worker.register_activity("echo", move |ctx: ActContext| 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(()) + Ok(ActExitValue::Normal(ctx.get_args()[0].clone())) } else { Err(anyhow!("Oh no I failed!")) } @@ -336,7 +345,7 @@ async fn local_act_retry_long_backoff_uses_timer() { let la_res = ctx .local_activity(LocalActivityOptions { activity_type: DEFAULT_ACTIVITY_TYPE.to_string(), - input: "hi".as_json_payload().expect("serializes fine"), + input: vec!["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 @@ -351,15 +360,14 @@ async fn local_act_retry_long_backoff_uses_timer() { assert!(la_res.failed()); // Extra timer just to have an extra workflow task which we can return full history for ctx.timer(Duration::from_secs(1)).await; - Ok(().into()) - }, - ); - worker.register_activity( - DEFAULT_ACTIVITY_TYPE, - move |_ctx: ActContext, _: String| async move { - Result::<(), _>::Err(anyhow!("Oh no I failed!")) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }, ); + worker.register_activity(DEFAULT_ACTIVITY_TYPE, move |_ctx: ActContext| async move { + Err(anyhow!("Oh no I failed!")) + }); worker .submit_wf( wf_id.to_owned(), @@ -390,14 +398,18 @@ async fn local_act_null_result() { |ctx: WfContext| async move { ctx.local_activity(LocalActivityOptions { activity_type: "nullres".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), + input: vec!["hi".as_json_payload().expect("serializes fine")], ..Default::default() }) .await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }, ); - worker.register_activity("nullres", |_ctx: ActContext, _: String| async { Ok(()) }); + worker.register_activity("nullres", |ctx: ActContext| async move { + Ok(ActExitValue::Normal(ctx.get_args()[0].clone())) + }); worker .submit_wf( wf_id.to_owned(), @@ -433,15 +445,19 @@ async fn local_act_command_immediately_follows_la_marker() { |ctx: WfContext| async move { ctx.local_activity(LocalActivityOptions { activity_type: "nullres".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), + input: vec!["hi".as_json_payload().expect("serializes fine")], ..Default::default() }) .await; ctx.timer(Duration::from_secs(1)).await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }, ); - worker.register_activity("nullres", |_ctx: ActContext, _: String| async { Ok(()) }); + worker.register_activity("nullres", |ctx: ActContext| async move { + Ok(ActExitValue::Normal(ctx.get_args()[0].clone())) + }); worker .submit_wf( wf_id.to_owned(), @@ -758,7 +774,7 @@ async fn test_schedule_to_start_timeout() { let la_res = ctx .local_activity(LocalActivityOptions { activity_type: "echo".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), + input: vec!["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() @@ -774,13 +790,14 @@ async fn test_schedule_to_start_timeout() { rfail.cause.unwrap().failure_info, Some(FailureInfo::TimeoutFailureInfo(_)) ); - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }, ); - worker.register_activity( - "echo", - move |_ctx: ActContext, _: String| async move { Ok(()) }, - ); + worker.register_activity("echo", move |ctx: ActContext| async move { + Ok(ActExitValue::Normal(ctx.get_args()[0].clone())) + }); worker .submit_wf( wf_id.to_owned(), @@ -846,7 +863,7 @@ async fn test_schedule_to_start_timeout_not_based_on_original_time( let la_res = ctx .local_activity(LocalActivityOptions { activity_type: "echo".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), + input: vec!["hi".as_json_payload().expect("serializes fine")], retry_policy: RetryPolicy { initial_interval: Some(prost_dur!(from_millis(50))), backoff_coefficient: 1.2, @@ -864,13 +881,14 @@ async fn test_schedule_to_start_timeout_not_based_on_original_time( } else { assert_eq!(la_res.timed_out(), Some(TimeoutType::ScheduleToClose)); } - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }, ); - worker.register_activity( - "echo", - move |_ctx: ActContext, _: String| async move { Ok(()) }, - ); + worker.register_activity("echo", move |ctx: ActContext| async move { + Ok(ActExitValue::Normal(ctx.get_args()[0].clone())) + }); worker .submit_wf( wf_id.to_owned(), @@ -903,7 +921,7 @@ async fn wft_failure_cancels_running_las() { |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"), + input: vec!["hi".as_json_payload().expect("serializes fine")], ..Default::default() }); tokio::join!( @@ -913,19 +931,18 @@ async fn wft_failure_cancels_running_las() { }, la_handle ); - Ok(().into()) - }, - ); - worker.register_activity( - DEFAULT_ACTIVITY_TYPE, - move |ctx: ActContext, _: 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(ActivityCancelledError::default().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }, ); + worker.register_activity(DEFAULT_ACTIVITY_TYPE, move |ctx: ActContext| async move { + let res = tokio::time::timeout(Duration::from_millis(500), ctx.cancelled()).await; + if res.is_err() { + panic!("Activity must be cancelled!!!!"); + } + Err(ActivityCancelledError::default().into()) + }); worker .submit_wf( wf_id.to_owned(), @@ -969,17 +986,16 @@ async fn resolved_las_not_recorded_if_wft_fails_many_times() { |ctx: WfContext| async move { ctx.local_activity(LocalActivityOptions { activity_type: "echo".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), + input: vec!["hi".as_json_payload().expect("serializes fine")], ..Default::default() }) .await; panic!("Oh nooooo") }, ); - worker.register_activity( - "echo", - move |_: ActContext, _: String| async move { Ok(()) }, - ); + worker.register_activity("echo", move |ctx: ActContext| async move { + Ok(ActExitValue::Normal(ctx.get_args()[0].clone())) + }); worker .submit_wf( wf_id.to_owned(), @@ -1023,7 +1039,7 @@ async fn local_act_records_nonfirst_attempts_ok() { |ctx: WfContext| async move { ctx.local_activity(LocalActivityOptions { activity_type: "echo".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), + input: vec!["hi".as_json_payload().expect("serializes fine")], retry_policy: RetryPolicy { initial_interval: Some(prost_dur!(from_millis(10))), backoff_coefficient: 1.0, @@ -1034,11 +1050,13 @@ async fn local_act_records_nonfirst_attempts_ok() { ..Default::default() }) .await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }, ); - worker.register_activity("echo", move |_ctx: ActContext, _: String| async move { - Result::<(), _>::Err(anyhow!("I fail")) + worker.register_activity("echo", move |_ctx: ActContext| async move { + Err(anyhow!("I fail")) }); worker .submit_wf( diff --git a/core/src/core_tests/replay_flag.rs b/core/src/core_tests/replay_flag.rs index 16e3912cd..aef5282dd 100644 --- a/core/src/core_tests/replay_flag.rs +++ b/core/src/core_tests/replay_flag.rs @@ -1,15 +1,16 @@ use crate::{test_help::canned_histories, worker::ManagedWFFunc}; use rstest::{fixture, rstest}; use std::time::Duration; -use temporal_sdk::{WfContext, WorkflowFunction}; -use temporal_sdk_core_protos::temporal::api::enums::v1::CommandType; - +use temporal_sdk::{WfContext, WfExitValue, WorkflowFunction}; +use temporal_sdk_core_protos::{coresdk::AsJsonPayloadExt, temporal::api::enums::v1::CommandType}; fn timers_wf(num_timers: u32) -> WorkflowFunction { WorkflowFunction::new(move |command_sink: WfContext| async move { for _ in 1..=num_timers { command_sink.timer(Duration::from_secs(1)).await; } - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }) } diff --git a/core/src/core_tests/workflow_tasks.rs b/core/src/core_tests/workflow_tasks.rs index 37ed3a0af..9d4850e80 100644 --- a/core/src/core_tests/workflow_tasks.rs +++ b/core/src/core_tests/workflow_tasks.rs @@ -24,7 +24,7 @@ use std::{ time::Duration, }; use temporal_client::WorkflowOptions; -use temporal_sdk::{ActivityOptions, CancellableFuture, WfContext}; +use temporal_sdk::{ActivityOptions, CancellableFuture, WfContext, WfExitValue}; use temporal_sdk_core_api::{errors::PollWfError, Worker as WorkerTrait}; use temporal_sdk_core_protos::{ coresdk::{ @@ -39,6 +39,7 @@ use temporal_sdk_core_protos::{ ScheduleActivity, SetPatchMarker, }, workflow_completion::WorkflowActivationCompletion, + AsJsonPayloadExt, }, default_act_sched, default_wes_attribs, temporal::api::{ @@ -491,7 +492,9 @@ async fn abandoned_activities_ignore_start_and_complete(hist_batches: &'static [ act_fut.cancel(&ctx); ctx.timer(Duration::from_secs(3)).await; act_fut.await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }); worker .submit_wf(wfid, wf_type, vec![], Default::default()) @@ -918,7 +921,9 @@ async fn max_wft_respected() { .expect("No multiple concurrent workflow tasks!"), ); ctx.timer(Duration::from_secs(1)).await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }); for wf_id in wf_ids { @@ -2575,7 +2580,9 @@ async fn history_length_with_fail_and_timeout( assert_eq!(ctx.history_length(), 14); ctx.timer(Duration::from_secs(1)).await; assert_eq!(ctx.history_length(), 19); - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }); worker .submit_wf( diff --git a/core/src/worker/workflow/history_update.rs b/core/src/worker/workflow/history_update.rs index da7eaa992..68cf49a5c 100644 --- a/core/src/worker/workflow/history_update.rs +++ b/core/src/worker/workflow/history_update.rs @@ -784,8 +784,9 @@ pub mod tests { use futures_util::TryStreamExt; use std::sync::atomic::{AtomicUsize, Ordering}; use temporal_client::WorkflowOptions; - use temporal_sdk::WfContext; + use temporal_sdk::{WfContext, WfExitValue}; use temporal_sdk_core_protos::{ + coresdk::AsJsonPayloadExt, temporal::api::{ common::v1::WorkflowExecution, enums::v1::WorkflowTaskFailedCause, workflowservice::v1::GetWorkflowExecutionHistoryResponse, @@ -1422,7 +1423,9 @@ pub mod tests { break; } } - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } }); @@ -1670,7 +1673,9 @@ pub mod tests { break; } } - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } }); diff --git a/core/src/worker/workflow/machines/activity_state_machine.rs b/core/src/worker/workflow/machines/activity_state_machine.rs index cc1e56850..12f08096c 100644 --- a/core/src/worker/workflow/machines/activity_state_machine.rs +++ b/core/src/worker/workflow/machines/activity_state_machine.rs @@ -803,10 +803,15 @@ mod test { use rstest::{fixture, rstest}; use std::{cell::RefCell, mem::discriminant, rc::Rc}; use temporal_sdk::{ - ActivityOptions, CancellableFuture, WfContext, WorkflowFunction, WorkflowResult, + ActivityOptions, CancellableFuture, WfContext, WfExitValue, WorkflowFunction, + WorkflowResult, }; use temporal_sdk_core_protos::{ - coresdk::workflow_activation::{workflow_activation_job, WorkflowActivationJob}, + coresdk::{ + workflow_activation::{workflow_activation_job, WorkflowActivationJob}, + AsJsonPayloadExt, + }, + temporal::api::common::v1::Payload, DEFAULT_ACTIVITY_TYPE, }; @@ -826,7 +831,7 @@ mod test { ManagedWFFunc::new(t, func, vec![]) } - async fn activity_wf(command_sink: WfContext) -> WorkflowResult<()> { + async fn activity_wf(command_sink: WfContext) -> WorkflowResult { command_sink .activity(ActivityOptions { activity_id: Some("activity-id-1".to_string()), @@ -834,7 +839,9 @@ mod test { ..Default::default() }) .await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } #[rstest( @@ -886,7 +893,9 @@ mod test { // Immediately cancel the activity cancel_activity_future.cancel(&ctx); cancel_activity_future.await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }); let mut t = TestHistoryBuilder::default(); diff --git a/core/src/worker/workflow/machines/cancel_external_state_machine.rs b/core/src/worker/workflow/machines/cancel_external_state_machine.rs index 3f34ac4fb..075747ed4 100644 --- a/core/src/worker/workflow/machines/cancel_external_state_machine.rs +++ b/core/src/worker/workflow/machines/cancel_external_state_machine.rs @@ -227,9 +227,10 @@ impl Cancellable for CancelExternalMachine {} mod tests { use super::*; use crate::{replay::TestHistoryBuilder, worker::workflow::ManagedWFFunc}; - use temporal_sdk::{WfContext, WorkflowFunction, WorkflowResult}; + use temporal_sdk::{WfContext, WfExitValue, WorkflowFunction, WorkflowResult}; + use temporal_sdk_core_protos::{coresdk::AsJsonPayloadExt, temporal::api::common::v1::Payload}; - async fn cancel_sender(ctx: WfContext) -> WorkflowResult<()> { + async fn cancel_sender(ctx: WfContext) -> WorkflowResult { let res = ctx .cancel_external(NamespacedWorkflowExecution { namespace: "some_namespace".to_string(), @@ -240,7 +241,9 @@ mod tests { if res.is_err() { Err(anyhow::anyhow!("Cancel fail!")) } else { - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } } diff --git a/core/src/worker/workflow/machines/cancel_workflow_state_machine.rs b/core/src/worker/workflow/machines/cancel_workflow_state_machine.rs index 908288e0a..febd77216 100644 --- a/core/src/worker/workflow/machines/cancel_workflow_state_machine.rs +++ b/core/src/worker/workflow/machines/cancel_workflow_state_machine.rs @@ -119,11 +119,12 @@ mod tests { use crate::{test_help::canned_histories, worker::workflow::ManagedWFFunc}; use std::time::Duration; use temporal_sdk::{WfContext, WfExitValue, WorkflowFunction, WorkflowResult}; - use temporal_sdk_core_protos::coresdk::workflow_activation::{ - workflow_activation_job, WorkflowActivationJob, + use temporal_sdk_core_protos::{ + coresdk::workflow_activation::{workflow_activation_job, WorkflowActivationJob}, + temporal::api::common::v1::Payload, }; - async fn wf_with_timer(ctx: WfContext) -> WorkflowResult<()> { + async fn wf_with_timer(ctx: WfContext) -> WorkflowResult { ctx.timer(Duration::from_millis(500)).await; Ok(WfExitValue::Cancelled) } diff --git a/core/src/worker/workflow/machines/child_workflow_state_machine.rs b/core/src/worker/workflow/machines/child_workflow_state_machine.rs index 325a4bcac..b15910f37 100644 --- a/core/src/worker/workflow/machines/child_workflow_state_machine.rs +++ b/core/src/worker/workflow/machines/child_workflow_state_machine.rs @@ -726,11 +726,13 @@ mod test { use rstest::{fixture, rstest}; use std::{cell::RefCell, mem::discriminant, rc::Rc}; use temporal_sdk::{ - CancellableFuture, ChildWorkflowOptions, WfContext, WorkflowFunction, WorkflowResult, + CancellableFuture, ChildWorkflowOptions, WfContext, WfExitValue, WorkflowFunction, + WorkflowResult, }; use temporal_sdk_core_protos::coresdk::{ child_workflow::child_workflow_result, workflow_activation::resolve_child_workflow_execution_start::Status as StartStatus, + AsJsonPayloadExt, }; #[derive(Clone, Copy)] @@ -775,7 +777,7 @@ mod test { ManagedWFFunc::new(t, func, vec![[Expectation::StartFailure as u8].into()]) } - async fn parent_wf(ctx: WfContext) -> WorkflowResult<()> { + async fn parent_wf(ctx: WfContext) -> WorkflowResult { let expectation = Expectation::try_from_u8(ctx.get_args()[0].data[0]).unwrap(); let child = ctx.child_workflow(ChildWorkflowOptions { workflow_id: "child-id-1".to_string(), @@ -786,17 +788,23 @@ mod test { let start_res = child.start(&ctx).await; match (expectation, &start_res.status) { (Expectation::Success | Expectation::Failure, StartStatus::Succeeded(_)) => {} - (Expectation::StartFailure, StartStatus::Failed(_)) => return Ok(().into()), + (Expectation::StartFailure, StartStatus::Failed(_)) => { + return Ok(WfExitValue::Normal( + "failure".as_json_payload().expect("serializes fine"), + )) + } _ => return Err(anyhow!("Unexpected start status")), }; match ( expectation, start_res.into_started().unwrap().result().await.status, ) { - (Expectation::Success, Some(child_workflow_result::Status::Completed(_))) => { - Ok(().into()) - } - (Expectation::Failure, _) => Ok(().into()), + (Expectation::Success, Some(child_workflow_result::Status::Completed(_))) => Ok( + WfExitValue::Normal("success".as_json_payload().expect("serializes fine")), + ), + (Expectation::Failure, _) => Ok(WfExitValue::Normal( + "failure".as_json_payload().expect("serializes fine"), + )), _ => Err(anyhow!("Unexpected child WF status")), } } @@ -853,7 +861,7 @@ mod test { wfm.shutdown().await.unwrap(); } - async fn cancel_before_send_wf(ctx: WfContext) -> WorkflowResult<()> { + async fn cancel_before_send_wf(ctx: WfContext) -> WorkflowResult { let workflow_id = "child-id-1"; let child = ctx.child_workflow(ChildWorkflowOptions { workflow_id: workflow_id.to_string(), @@ -863,7 +871,9 @@ mod test { let start = child.start(&ctx); start.cancel(&ctx); match start.await.status { - StartStatus::Cancelled(_) => Ok(().into()), + StartStatus::Cancelled(_) => Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )), _ => Err(anyhow!("Unexpected start status")), } } diff --git a/core/src/worker/workflow/machines/continue_as_new_workflow_state_machine.rs b/core/src/worker/workflow/machines/continue_as_new_workflow_state_machine.rs index ffeb170f3..d4aee387d 100644 --- a/core/src/worker/workflow/machines/continue_as_new_workflow_state_machine.rs +++ b/core/src/worker/workflow/machines/continue_as_new_workflow_state_machine.rs @@ -118,8 +118,9 @@ mod tests { use crate::{test_help::canned_histories, worker::workflow::ManagedWFFunc}; use std::time::Duration; use temporal_sdk::{WfContext, WfExitValue, WorkflowFunction, WorkflowResult}; + use temporal_sdk_core_protos::temporal::api::common::v1::Payload; - async fn wf_with_timer(ctx: WfContext) -> WorkflowResult<()> { + async fn wf_with_timer(ctx: WfContext) -> WorkflowResult { ctx.timer(Duration::from_millis(500)).await; Ok(WfExitValue::continue_as_new( ContinueAsNewWorkflowExecution { diff --git a/core/src/worker/workflow/machines/local_activity_state_machine.rs b/core/src/worker/workflow/machines/local_activity_state_machine.rs index b3dd873ea..6c0504bc2 100644 --- a/core/src/worker/workflow/machines/local_activity_state_machine.rs +++ b/core/src/worker/workflow/machines/local_activity_state_machine.rs @@ -884,26 +884,31 @@ mod tests { use rstest::rstest; use std::time::Duration; use temporal_sdk::{ - CancellableFuture, LocalActivityOptions, WfContext, WorkflowFunction, WorkflowResult, + CancellableFuture, LocalActivityOptions, WfContext, WfExitValue, WorkflowFunction, + WorkflowResult, }; use temporal_sdk_core_protos::{ coresdk::{ activity_result::ActivityExecutionResult, workflow_activation::{workflow_activation_job, WorkflowActivationJob}, + AsJsonPayloadExt, }, temporal::api::{ - command::v1::command, enums::v1::WorkflowTaskFailedCause, failure::v1::Failure, + command::v1::command, common::v1::Payload, enums::v1::WorkflowTaskFailedCause, + failure::v1::Failure, }, DEFAULT_ACTIVITY_TYPE, }; - async fn la_wf(ctx: WfContext) -> WorkflowResult<()> { + async fn la_wf(ctx: WfContext) -> WorkflowResult { ctx.local_activity(LocalActivityOptions { activity_type: DEFAULT_ACTIVITY_TYPE.to_string(), ..Default::default() }) .await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } #[rstest] @@ -1014,7 +1019,7 @@ mod tests { wfm.shutdown().await.unwrap(); } - async fn two_la_wf(ctx: WfContext) -> WorkflowResult<()> { + async fn two_la_wf(ctx: WfContext) -> WorkflowResult { ctx.local_activity(LocalActivityOptions { activity_type: DEFAULT_ACTIVITY_TYPE.to_string(), ..Default::default() @@ -1025,7 +1030,9 @@ mod tests { ..Default::default() }) .await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } #[rstest] @@ -1115,7 +1122,7 @@ mod tests { wfm.shutdown().await.unwrap(); } - async fn two_la_wf_parallel(ctx: WfContext) -> WorkflowResult<()> { + async fn two_la_wf_parallel(ctx: WfContext) -> WorkflowResult { tokio::join!( ctx.local_activity(LocalActivityOptions { activity_type: DEFAULT_ACTIVITY_TYPE.to_string(), @@ -1126,7 +1133,9 @@ mod tests { ..Default::default() }) ); - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } #[rstest] @@ -1206,7 +1215,7 @@ mod tests { wfm.shutdown().await.unwrap(); } - async fn la_timer_la(ctx: WfContext) -> WorkflowResult<()> { + async fn la_timer_la(ctx: WfContext) -> WorkflowResult { ctx.local_activity(LocalActivityOptions { activity_type: DEFAULT_ACTIVITY_TYPE.to_string(), ..Default::default() @@ -1218,7 +1227,9 @@ mod tests { ..Default::default() }) .await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } #[rstest] @@ -1410,7 +1421,9 @@ mod tests { }); la.cancel(&ctx); la.await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }); let mut t = TestHistoryBuilder::default(); t.add_by_type(EventType::WorkflowExecutionStarted); @@ -1477,7 +1490,9 @@ mod tests { rfail.cause.unwrap().failure_info, Some(FailureInfo::CanceledFailureInfo(_)) ); - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }); let mut t = TestHistoryBuilder::default(); diff --git a/core/src/worker/workflow/machines/modify_workflow_properties_state_machine.rs b/core/src/worker/workflow/machines/modify_workflow_properties_state_machine.rs index b33b43c3e..cadcc9020 100644 --- a/core/src/worker/workflow/machines/modify_workflow_properties_state_machine.rs +++ b/core/src/worker/workflow/machines/modify_workflow_properties_state_machine.rs @@ -118,9 +118,10 @@ impl From for CommandIssued { mod tests { use super::*; use crate::{replay::TestHistoryBuilder, worker::workflow::ManagedWFFunc}; - use temporal_sdk::{WfContext, WorkflowFunction}; - use temporal_sdk_core_protos::temporal::api::{ - command::v1::command::Attributes, common::v1::Payload, + use temporal_sdk::{WfContext, WfExitValue, WorkflowFunction}; + use temporal_sdk_core_protos::{ + coresdk::AsJsonPayloadExt, + temporal::api::{command::v1::command::Attributes, common::v1::Payload}, }; #[tokio::test] @@ -149,7 +150,9 @@ mod tests { }, ), ]); - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }); let mut wfm = ManagedWFFunc::new(t, wff, vec![]); diff --git a/core/src/worker/workflow/machines/patch_state_machine.rs b/core/src/worker/workflow/machines/patch_state_machine.rs index aa72ec187..3312b19ca 100644 --- a/core/src/worker/workflow/machines/patch_state_machine.rs +++ b/core/src/worker/workflow/machines/patch_state_machine.rs @@ -285,7 +285,7 @@ mod tests { collections::{hash_map::RandomState, HashSet, VecDeque}, time::Duration, }; - use temporal_sdk::{ActivityOptions, WfContext, WorkflowFunction}; + use temporal_sdk::{ActivityOptions, WfContext, WfExitValue, WorkflowFunction}; use temporal_sdk_core_protos::{ constants::PATCH_MARKER_NAME, coresdk::{ @@ -458,7 +458,9 @@ mod tests { } _ => panic!("Invalid workflow version for test setup"), } - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }); let t = patch_marker_single_activity(marker_type, workflow_version, replaying); @@ -648,7 +650,9 @@ mod tests { } else { ctx.timer(ONE_SECOND).await; } - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }); let mut t = TestHistoryBuilder::default(); @@ -786,7 +790,9 @@ mod tests { let _dontcare = ctx.patched(&format!("patch-{i}")); ctx.timer(ONE_SECOND).await; } - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }), vec![], ); diff --git a/core/src/worker/workflow/machines/signal_external_state_machine.rs b/core/src/worker/workflow/machines/signal_external_state_machine.rs index 5ce531aac..8055b76a4 100644 --- a/core/src/worker/workflow/machines/signal_external_state_machine.rs +++ b/core/src/worker/workflow/machines/signal_external_state_machine.rs @@ -308,22 +308,29 @@ mod tests { use crate::{replay::TestHistoryBuilder, worker::workflow::ManagedWFFunc}; use std::mem::discriminant; use temporal_sdk::{ - CancellableFuture, SignalWorkflowOptions, WfContext, WorkflowFunction, WorkflowResult, + CancellableFuture, SignalWorkflowOptions, WfContext, WfExitValue, WorkflowFunction, + WorkflowResult, }; - use temporal_sdk_core_protos::coresdk::workflow_activation::{ - workflow_activation_job, WorkflowActivationJob, + use temporal_sdk_core_protos::{ + coresdk::{ + workflow_activation::{workflow_activation_job, WorkflowActivationJob}, + AsJsonPayloadExt, + }, + temporal::api::common::v1::Payload, }; const SIGNAME: &str = "signame"; - async fn signal_sender(ctx: WfContext) -> WorkflowResult<()> { + async fn signal_sender(ctx: WfContext) -> WorkflowResult { let mut dat = SignalWorkflowOptions::new("fake_wid", "fake_rid", SIGNAME, [b"hi!"]); dat.with_header("tupac", b"shakur"); let res = ctx.signal_workflow(dat).await; if res.is_err() { Err(anyhow::anyhow!("Signal fail!")) } else { - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } } @@ -393,7 +400,9 @@ mod tests { )); sig.cancel(&ctx); let _res = sig.await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }); let mut wfm = ManagedWFFunc::new(t, wff, vec![]); diff --git a/core/src/worker/workflow/machines/timer_state_machine.rs b/core/src/worker/workflow/machines/timer_state_machine.rs index ecea10e02..e608c1922 100644 --- a/core/src/worker/workflow/machines/timer_state_machine.rs +++ b/core/src/worker/workflow/machines/timer_state_machine.rs @@ -268,8 +268,8 @@ mod test { }; use rstest::{fixture, rstest}; use std::{mem::discriminant, time::Duration}; - use temporal_sdk::{CancellableFuture, WfContext, WorkflowFunction}; - + use temporal_sdk::{CancellableFuture, WfContext, WfExitValue, WorkflowFunction}; + use temporal_sdk_core_protos::coresdk::AsJsonPayloadExt; #[fixture] fn happy_wfm() -> ManagedWFFunc { /* @@ -285,7 +285,9 @@ mod test { */ let func = WorkflowFunction::new(|command_sink: WfContext| async move { command_sink.timer(Duration::from_secs(5)).await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }); let t = canned_histories::single_timer("1"); @@ -329,7 +331,9 @@ mod test { async fn mismatched_timer_ids_errors() { let func = WorkflowFunction::new(|command_sink: WfContext| async move { command_sink.timer(Duration::from_secs(5)).await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }); let t = canned_histories::single_timer("badid"); @@ -350,7 +354,9 @@ mod test { // Cancel the first timer after having waited on the second cancel_timer_fut.cancel(&ctx); cancel_timer_fut.await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }); let t = canned_histories::cancel_timer("2", "1"); @@ -399,7 +405,9 @@ mod test { // Immediately cancel the timer cancel_timer_fut.cancel(&ctx); cancel_timer_fut.await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }); let mut t = TestHistoryBuilder::default(); diff --git a/core/src/worker/workflow/machines/upsert_search_attributes_state_machine.rs b/core/src/worker/workflow/machines/upsert_search_attributes_state_machine.rs index ae25472b3..d80008018 100644 --- a/core/src/worker/workflow/machines/upsert_search_attributes_state_machine.rs +++ b/core/src/worker/workflow/machines/upsert_search_attributes_state_machine.rs @@ -204,7 +204,7 @@ mod tests { }; use rustfsm::StateMachine; use std::collections::HashMap; - use temporal_sdk::{WfContext, WorkflowFunction}; + use temporal_sdk::{WfContext, WfExitValue, WorkflowFunction}; use temporal_sdk_core_api::Worker; use temporal_sdk_core_protos::{ coresdk::{ @@ -217,7 +217,6 @@ mod tests { }, }; use temporal_sdk_core_test_utils::WorkerTestHelpers; - #[tokio::test] async fn upsert_search_attrs_from_workflow() { let mut t = TestHistoryBuilder::default(); @@ -244,7 +243,9 @@ mod tests { }, ), ]); - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }); let mut wfm = ManagedWFFunc::new(t, wff, vec![]); diff --git a/core/src/worker/workflow/managed_run/managed_wf_test.rs b/core/src/worker/workflow/managed_run/managed_wf_test.rs index 8f6bba12c..cc747842a 100644 --- a/core/src/worker/workflow/managed_run/managed_wf_test.rs +++ b/core/src/worker/workflow/managed_run/managed_wf_test.rs @@ -61,7 +61,7 @@ pub struct ManagedWFFunc { activation_tx: UnboundedSender, completions_rx: UnboundedReceiver, completions_sync_tx: crossbeam::channel::Sender, - future_handle: Option>>, + future_handle: Option>>, was_shutdown: bool, } @@ -175,7 +175,7 @@ impl ManagedWFFunc { Ok(last_act) } - pub async fn shutdown(&mut self) -> WorkflowResult<()> { + pub async fn shutdown(&mut self) -> WorkflowResult { self.was_shutdown = true; // Send an eviction to ensure wf exits if it has not finished (ex: feeding partial hist) let _ = self.activation_tx.send(create_evict_activation( diff --git a/sdk-core-protos/src/lib.rs b/sdk-core-protos/src/lib.rs index f71e9493a..021703b48 100644 --- a/sdk-core-protos/src/lib.rs +++ b/sdk-core-protos/src/lib.rs @@ -1259,6 +1259,13 @@ pub mod coresdk { } } + /// Trait to map the Failure to Rust Error for idiomatic error handling in Workflow code
+ /// NOTE: This is *NOT* the generic + /// [failure converter](https://docs.temporal.io/dataconversion#failure-converter) + pub trait FromFailureExt: std::error::Error { + fn from_failure(failure: Failure) -> Self; + } + pub trait FromPayloadsExt { fn from_payloads(p: Option) -> Self; } diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index f5610cbef..2e9177365 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -20,6 +20,7 @@ base64 = "0.21" crossbeam = "0.8" derive_more = "0.99" futures = "0.3" +futures-core = "0.3" once_cell = "1.10" parking_lot = { version = "0.12", features = ["send_guard"] } prost-types = { version = "0.4", package = "prost-wkt-types" } diff --git a/sdk/src/activity_context.rs b/sdk/src/activity_context.rs index a2cf11cdb..b3297ccbd 100644 --- a/sdk/src/activity_context.rs +++ b/sdk/src/activity_context.rs @@ -60,7 +60,7 @@ impl ActContext { task_queue: String, task_token: Vec, task: activity_task::Start, - ) -> (Self, Payload) { + ) -> Self { let activity_task::Start { workflow_namespace, workflow_type, @@ -68,7 +68,7 @@ impl ActContext { activity_id, activity_type, header_fields, - mut input, + input, heartbeat_details, scheduled_time, current_attempt_scheduled_time, @@ -86,37 +86,32 @@ impl ActContext { start_to_close_timeout.as_ref(), schedule_to_close_timeout.as_ref(), ); - let first_arg = input.pop().unwrap_or_default(); - ( - ActContext { - worker, - app_data, - cancellation_token, - input, - heartbeat_details, - header_fields, - info: ActivityInfo { - task_token, - task_queue, - workflow_type, - workflow_namespace, - workflow_execution, - activity_id, - activity_type, - heartbeat_timeout: heartbeat_timeout.try_into_or_none(), - scheduled_time: scheduled_time.try_into_or_none(), - started_time: started_time.try_into_or_none(), - deadline, - attempt, - current_attempt_scheduled_time: current_attempt_scheduled_time - .try_into_or_none(), - retry_policy, - is_local, - }, + ActContext { + worker, + app_data, + cancellation_token, + input, + heartbeat_details, + header_fields, + info: ActivityInfo { + task_token, + task_queue, + workflow_type, + workflow_namespace, + workflow_execution, + activity_id, + activity_type, + heartbeat_timeout: heartbeat_timeout.try_into_or_none(), + scheduled_time: scheduled_time.try_into_or_none(), + started_time: started_time.try_into_or_none(), + deadline, + attempt, + current_attempt_scheduled_time: current_attempt_scheduled_time.try_into_or_none(), + retry_policy, + is_local, }, - first_arg, - ) + } } /// Returns a future the completes if and when the activity this was called inside has been @@ -133,8 +128,8 @@ impl ActContext { /// Retrieve extra parameters to the Activity. The first input is always popped and passed to /// the Activity function for the currently executing activity. However, if more parameters are /// passed, perhaps from another language's SDK, explicit access is available from extra_inputs - pub fn extra_inputs(&mut self) -> &mut [Payload] { - &mut self.input + pub fn get_args(&self) -> &[Payload] { + &self.input } /// Extract heartbeat details from last failed attempt. This is used in combination with retry policy. diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 2e5330339..24b8213a2 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -9,7 +9,7 @@ //! An example of running an activity worker: //! ```no_run //! use std::{str::FromStr, sync::Arc}; -//! use temporal_sdk::{sdk_client_options, ActContext, Worker}; +//! use temporal_sdk::{sdk_client_options, ActContext, ActExitValue, Worker}; //! use temporal_sdk_core::{init_worker, Url, CoreRuntime}; //! use temporal_sdk_core_api::{worker::WorkerConfigBuilder, telemetry::TelemetryOptionsBuilder}; //! @@ -32,7 +32,7 @@ //! let mut worker = Worker::new_from_core(Arc::new(core_worker), "task_queue"); //! worker.register_activity( //! "echo_activity", -//! |_ctx: ActContext, echo_me: String| async move { Ok(echo_me) }, +//! |ctx: ActContext| async move { Ok(ActExitValue::Normal(ctx.get_args()[0].clone())) }, //! ); //! //! worker.run().await?; @@ -48,6 +48,8 @@ mod activity_context; mod app_data; pub mod interceptors; mod payload_converter; +pub mod prelude; +pub mod workflow; mod workflow_context; mod workflow_future; @@ -147,7 +149,7 @@ struct WorkflowData { activation_chan: UnboundedSender, } -struct WorkflowFutureHandle, JoinError>>> { +struct WorkflowFutureHandle, JoinError>>> { join_handle: F, run_id: String, } @@ -193,10 +195,10 @@ impl Worker { /// Register a Workflow function to invoke when the Worker is asked to run a workflow of /// `workflow_type` - pub fn register_wf>( + pub fn register_wf( &mut self, workflow_type: impl Into, - wf_function: F, + wf_function: impl Into, ) { self.workflow_half .workflow_fns @@ -206,17 +208,14 @@ impl Worker { /// Register an Activity function to invoke when the Worker is asked to run an activity of /// `activity_type` - pub fn register_activity( + pub fn register_activity( &mut self, activity_type: impl Into, - act_function: impl IntoActivityFunc, + act_function: impl Into, ) { - self.activity_half.activity_fns.insert( - activity_type.into(), - ActivityFunction { - act_func: act_function.into_activity_fn(), - }, - ); + self.activity_half + .activity_fns + .insert(activity_type.into(), act_function.into()); } /// Insert Custom App Context for Workflows and Activities @@ -383,7 +382,9 @@ impl WorkflowHalf { activation: WorkflowActivation, completions_tx: &UnboundedSender, ) -> Result< - Option, JoinError>>>>, + Option< + WorkflowFutureHandle, JoinError>>>, + >, anyhow::Error, > { let mut res = None; @@ -474,7 +475,7 @@ impl ActivityHalf { self.task_tokens_to_cancels .insert(task_token.clone().into(), ct.clone()); - let (ctx, arg) = ActContext::new( + let ctx = ActContext::new( worker.clone(), app_data, ct, @@ -483,7 +484,7 @@ impl ActivityHalf { start, ); tokio::spawn(async move { - let output = AssertUnwindSafe((act_fn.act_func)(ctx, arg)) + let output = AssertUnwindSafe((act_fn.act_func)(ctx)) .catch_unwind() .await; let result = match output { @@ -694,7 +695,10 @@ struct CommandSubscribeChildWorkflowCompletion { unblocker: oneshot::Sender, } -type WfFunc = dyn Fn(WfContext) -> BoxFuture<'static, WorkflowResult<()>> + Send + Sync + 'static; +type WfFunc = dyn Fn(WfContext) -> BoxFuture<'static, Result, anyhow::Error>> + + Send + + Sync + + 'static; /// The user's async function / workflow code pub struct WorkflowFunction { @@ -704,7 +708,7 @@ pub struct WorkflowFunction { impl From for WorkflowFunction where F: Fn(WfContext) -> Fut + Send + Sync + 'static, - Fut: Future> + Send + 'static, + Fut: Future, anyhow::Error>> + Send + 'static, { fn from(wf_func: F) -> Self { Self::new(wf_func) @@ -716,7 +720,7 @@ impl WorkflowFunction { pub fn new(wf_func: F) -> Self where F: Fn(WfContext) -> Fut + Send + Sync + 'static, - Fut: Future> + Send + 'static, + Fut: Future, anyhow::Error>> + Send + 'static, { Self { wf_func: Box::new(move |ctx: WfContext| wf_func(ctx).boxed()), @@ -728,7 +732,7 @@ impl WorkflowFunction { pub type WorkflowResult = Result, anyhow::Error>; /// Workflow functions may return these values when exiting -#[derive(Debug, derive_more::From)] +#[derive(derive_more::From)] pub enum WfExitValue { /// Continue the workflow as a new execution #[from(ignore)] @@ -750,6 +754,9 @@ impl WfExitValue { } } +/// The result of running an activity +pub type ActivityResult = Result, anyhow::Error>; + /// Activity functions may return these values when exiting #[derive(derive_more::From)] pub enum ActExitValue { @@ -761,7 +768,7 @@ pub enum ActExitValue { } type BoxActFn = Arc< - dyn Fn(ActContext, Payload) -> BoxFuture<'static, Result, anyhow::Error>> + dyn Fn(ActContext) -> BoxFuture<'static, Result, anyhow::Error>> + Send + Sync, >; @@ -772,6 +779,29 @@ pub struct ActivityFunction { act_func: BoxActFn, } +impl From for ActivityFunction +where + F: Fn(ActContext) -> Fut + Send + Sync + 'static, + Fut: Future, anyhow::Error>> + Send + 'static, +{ + fn from(act_func: F) -> Self { + Self::new(act_func) + } +} + +impl ActivityFunction { + /// Create new activity function + pub fn new(act_func: F) -> Self + where + F: Fn(ActContext) -> Fut + Send + Sync + 'static, + Fut: Future, anyhow::Error>> + Send + 'static, + { + Self { + act_func: Arc::new(move |ctx: ActContext| act_func(ctx).boxed()), + } + } +} + /// Return this error to indicate your activity is cancelling #[derive(Debug, Default)] pub struct ActivityCancelledError { @@ -817,10 +847,10 @@ where O: AsJsonPayloadExt + Debug, { fn into_activity_fn(self) -> BoxActFn { - let wrapper = move |ctx: ActContext, input: Payload| { + let wrapper = move |ctx: ActContext| { // Some minor gymnastics are required to avoid needing to clone the function - match A::from_json_payload(&input) { - Ok(deser) => (self)(ctx, deser) + match A::from_json_payload(&ctx.get_args()[0]) { + Ok(a) => (self)(ctx, a) .map(|r| { r.and_then(|r| { let exit_val: ActExitValue = r.into(); diff --git a/sdk/src/prelude.rs b/sdk/src/prelude.rs new file mode 100644 index 000000000..609be5462 --- /dev/null +++ b/sdk/src/prelude.rs @@ -0,0 +1,55 @@ +//! Prelude for easy importing of required types when defining workflow and activity definitions + +/// Registry prelude +#[allow(unused_imports)] +pub mod registry { + pub use crate::workflow::{ + into_activity_0_args, into_activity_0_args_without_ctx, into_activity_1_args, + into_activity_1_args_exit_value, into_activity_1_args_with_errors, into_activity_2_args, + into_workflow_0_args, into_workflow_1_args, into_workflow_1_args_exit_value, + into_workflow_1_args_with_errors, into_workflow_2_args, + }; +} + +/// Activity prelude +#[allow(unused_imports)] +pub mod activity { + pub use crate::{ActContext, ActExitValue, ActivityCancelledError, NonRetryableActivityError}; + pub use serde::{Deserialize, Serialize}; + pub use temporal_sdk_core_protos::{ + coresdk::FromFailureExt, temporal::api::failure::v1::Failure, + }; +} + +/// Workflow prelude +#[allow(unused_imports)] +pub mod workflow { + pub use crate::workflow::{ + execute_activity_0_args, execute_activity_0_args_without_ctx, execute_activity_1_args, + execute_activity_1_args_exit_value, execute_activity_1_args_with_errors, + execute_activity_2_args, execute_child_workflow_0_args_exit_value, + execute_child_workflow_1_args, execute_child_workflow_2_args, + execute_child_workflow_2_args_exit_value, + }; + pub use crate::workflow::{AsyncFn0, AsyncFn1, AsyncFn2, AsyncFn3}; + pub use crate::{ + ActivityOptions, ChildWorkflowOptions, LocalActivityOptions, Signal, SignalData, + SignalWorkflowOptions, WfContext, WfExitValue, WorkflowResult, + }; + use futures::FutureExt; + pub use serde::{Deserialize, Serialize}; + pub use std::{ + fmt::{Debug, Display}, + future::Future, + time::Duration, + }; + pub use temporal_sdk_core_protos::{ + coresdk::{ + activity_result::{self, activity_resolution}, + child_workflow::{child_workflow_result, Failure, Success}, + workflow_commands::ActivityCancellationType, + AsJsonPayloadExt, FromFailureExt, FromJsonPayloadExt, + }, + temporal::api::common::v1::Payload, + }; +} diff --git a/sdk/src/workflow.rs b/sdk/src/workflow.rs new file mode 100644 index 000000000..c861a2cd4 --- /dev/null +++ b/sdk/src/workflow.rs @@ -0,0 +1,1186 @@ +//! Abstractions based on SDK [crate] for defining workflow and activity definitions in idiomatic Async Rust +//! +//! This module has a large API surface mainly due to Rust's lack of [reflection](https://en.wikipedia.org/wiki/Reflective_programming) +//! capability and variadic arguments/generics on which other language SDKs depend on. Once the API is good enough, the variations can be hidden behind macros. +//! +//! Even though we can just use 'tuples' as arguments for all the functions for easy implementation, +//! the goal is to support general enough Workflow and Activity functions to the most extent possible but only with some limitations on arguments and results +//! as required by [temporal_sdk_core] and [Workflow definitions](https://docs.temporal.io/workflows#workflow-definition) and in general by Temporal platform. +//! +//! There are two main types of high-level functions. Other convenience facilities and possibly macros will be included in the future. +//! 1) Register functions (Example: [into_activity_1_args_exit_value], [into_workflow_0_args]) to register Activity/Workflow functions. +//! 2) Command functions (Example: [execute_activity_1_args], [execute_child_workflow_2_args_exit_value]) for workflow [commands](https://docs.temporal.io/workflows#command). +//! +//! These functions vary in which type of user-defined Activity/Workflow functions they accept. +//! Generic traits representing the user-defined functions are defined in the module. For example [AsyncFn2] is a function which takes two arguments. +//! +//! Currently, Activity/Workflow Functions with below arguments and result variations are supported. +//! +//! Argument variations +//! 1) Activity Functions which take [ActContext] and zero or more arguments +//! 2) Workflow Functions which take [WfContext] and zero or more arguments +//! 3) Activity Functions which take zero or more arguments. These are for simple Activity implementations which do not rely on Activity Context (Cancellation, Heartbeats etc.) +//! +//! Result variations +//! 1) Returning [ActExitValue]/[WfExitValue] and [anyhow::Error]. +//! 2) Returning [ActExitValue]/[WfExitValue] and user-defined error types.
+//! Define Activity/Workflow Functions as these if you want to return typed errors(Ex: errors defined using [thiserror](https://docs.rs/thiserror/latest/thiserror/)). +//! These require an implementation of [FromFailureExt] to map [Failure](temporal_sdk_core_protos::temporal::api::failure::v1::Failure) to the user-defined error types in addition to Activity/Workflow definitions. +//! 3) Returning just the user-defined result and [anyhow::Error] +//! 4) Returning just the user-defined result and error types.(Same requirements as type 2 above) +//! +//! Note: Any type `T` which implements [Debug] has generic implementation of [`Into>`] and [`Into>`] +//! +//! Defining workflows should feel like just a normal Rust(Async) program. Use [Prelude](crate::prelude) for easy import of types needed. +//! +//! +//! An example workflow definition is below. For more, refer to tests and examples. +//! ```no_run +//! use temporal_sdk::prelude::registry::*; +//! +//! #[tokio::main] +//! async fn main() -> Result<(), Box> { +//! let mut worker = worker::worker().await.unwrap(); +//! +//! worker.register_activity( +//! "sdk_example_activity", +//! into_activity_1_args_with_errors(activity::sdk_example_activity), +//! ); +//! +//! worker.register_wf( +//! "sdk_example_workflow", +//! into_workflow_1_args(workflow::sdk_example_workflow), +//! ); +//! +//! worker.run().await?; +//! +//! Ok(()) +//! } +//! +//! mod worker { +//! use std::{str::FromStr, sync::Arc}; +//! use temporal_sdk::{sdk_client_options, Worker}; +//! use temporal_sdk_core::{init_worker, CoreRuntime, Url}; +//! use temporal_sdk_core_api::{telemetry::TelemetryOptionsBuilder, worker::WorkerConfigBuilder}; +//! pub(crate) async fn worker() -> Result> { +//! let server_options = sdk_client_options(Url::from_str("http://!localhost:7233")?).build()?; +//! +//! let client = server_options.connect("default", None, None).await?; +//! +//! let telemetry_options = TelemetryOptionsBuilder::default().build()?; +//! let runtime = CoreRuntime::new_assume_tokio(telemetry_options)?; +//! +//! let worker_config = WorkerConfigBuilder::default() +//! .namespace("default") +//! .task_queue("task_queue") +//! .build()?; +//! +//! let core_worker = init_worker(&runtime, worker_config, client)?; +//! +//! Ok(Worker::new_from_core(Arc::new(core_worker), "task_queue")) +//! } +//! } +//! +//! mod activity { +//! use temporal_sdk::prelude::activity::*; +//! +//! #[derive(Debug, thiserror::Error)] +//! #[non_exhaustive] +//! pub enum Error { +//! #[error(transparent)] +//! Io(#[from] std::io::Error), +//! #[error(transparent)] +//! Any(anyhow::Error), +//! } +//! +//! impl FromFailureExt for Error { +//! fn from_failure(failure: Failure) -> Error { +//! Error::Any(anyhow::anyhow!("{:?}", failure.message)) +//! } +//! } +//! +//! #[derive(Default, Deserialize, Serialize, Debug, Clone)] +//! pub struct ActivityInput { +//! pub activity: String, +//! pub workflow: String, +//! } +//! +//! #[derive(Default, Deserialize, Serialize, Debug, Clone)] +//! pub struct ActivityOutput { +//! pub language: String, +//! pub platform: String, +//! } +//! +//! pub async fn sdk_example_activity( +//! _ctx: ActContext, +//! input: ActivityInput, +//! ) -> Result<(String, ActivityOutput), Error> { +//! Ok(( +//! format!("{}-{}", input.activity, input.workflow), +//! ActivityOutput { +//! language: "rust".to_string(), +//! platform: "temporal".to_string(), +//! }, +//! )) +//! } +//! } +//! +//! mod workflow { +//! use super::activity::*; +//! use temporal_sdk::prelude::workflow::*; +//! +//! #[derive(Default, Deserialize, Serialize, Debug, Clone)] +//! pub struct WorkflowInput { +//! pub activity: String, +//! pub workflow: String, +//! } +//! +//! pub async fn sdk_example_workflow( +//! ctx: WfContext, +//! input: WorkflowInput, +//! ) -> Result, anyhow::Error> { +//! let activity_timeout = Duration::from_secs(5); +//! let output = execute_activity_1_args_with_errors( +//! &ctx, +//! ActivityOptions { +//! activity_id: Some("sdk_example_activity".to_string()), +//! activity_type: "sdk_example_activity".to_string(), +//! schedule_to_close_timeout: Some(activity_timeout), +//! ..Default::default() +//! }, +//! sdk_example_activity, +//! ActivityInput { +//! activity: input.activity, +//! workflow: input.workflow, +//! }, +//! ) +//! .await; +//! match output { +//! Ok(output) => Ok(WfExitValue::Normal(output.1)), +//! Err(e) => Err(anyhow::Error::from(e)), +//! } +//! } +//! } +//! ``` + +// TODO: Remove this after complete implementation +#![allow(unused_imports)] + +// TODO: Create ActivityOptions, ChildOptions type and builders for workflow abstractions + +use crate::{ + ActContext, ActExitValue, ActivityOptions, ChildWorkflowOptions, WfContext, WfExitValue, +}; +use anyhow::anyhow; +use futures::FutureExt; +use futures_core::future::BoxFuture; +use serde::{Deserialize, Serialize}; +use std::{fmt::Debug, future::Future}; +use temporal_sdk_core_protos::{ + coresdk::{ + activity_result::{self, activity_resolution}, + child_workflow::{self, child_workflow_result}, + workflow_commands::ActivityCancellationType, + AsJsonPayloadExt, FromFailureExt, FromJsonPayloadExt, + }, + temporal::api::common::v1::Payload, +}; + +/// Trait to represent an async function with 0 arguments +pub trait AsyncFn0: Fn() -> Self::OutputFuture { + /// Output type of the async function which implements serde traits + type Output; + /// Future of the output + type OutputFuture: Future::Output> + Send + 'static; +} + +impl AsyncFn0 for F +where + F: Fn() -> Fut, + Fut: Future + Send + 'static, +{ + type Output = Fut::Output; + type OutputFuture = Fut; +} + +/// Trait to represent an async function with 1 argument +pub trait AsyncFn1: Fn(Arg0) -> Self::OutputFuture { + /// Output type of the async function which implements serde traits + type Output; + /// Future of the output + type OutputFuture: Future>::Output> + Send + 'static; +} + +impl AsyncFn1 for F +where + F: Fn(Arg0) -> Fut, + Fut: Future + Send + 'static, +{ + type Output = Fut::Output; + type OutputFuture = Fut; +} + +/// Trait to represent an async function with 2 arguments +pub trait AsyncFn2: Fn(Arg0, Arg1) -> Self::OutputFuture { + /// Output type of the async function which implements serde traits + type Output; + /// Future of the output + type OutputFuture: Future>::Output> + Send + 'static; +} + +impl AsyncFn2 for F +where + F: Fn(Arg0, Arg1) -> Fut, + Fut: Future + Send + 'static, +{ + type Output = Fut::Output; + type OutputFuture = Fut; +} + +/// Trait to represent an async function with 3 arguments +pub trait AsyncFn3: Fn(Arg0, Arg1, Arg2) -> Self::OutputFuture { + /// Output type of the async function which implements serde traits + type Output; + /// Future of the output + type OutputFuture: Future>::Output> + + Send + + 'static; +} + +impl AsyncFn3 for F +where + F: Fn(Arg0, Arg1, Arg2) -> Fut, + Fut: Future + Send + 'static, +{ + type Output = Fut::Output; + type OutputFuture = Fut; +} + +/// Register activity which takes [ActContext] with 0 arguments and returns a [Result] with 'R' +/// and [anyhow::Error] where 'R' implements [serde] traits for +/// serialization into Temporal [Payload](temporal_sdk_core_protos::temporal::api::common::v1::Payload). +/// Use [execute_activity_0_args] to execute the activity in the workflow definition. +pub fn into_activity_0_args( + f: F, +) -> impl Fn(ActContext) -> BoxFuture<'static, Result, anyhow::Error>> + Send + Sync +where + F: AsyncFn1> + Send + Sync + 'static, + R: Into>, + O: AsJsonPayloadExt + Debug, +{ + move |ctx: ActContext| { + (f)(ctx) + .map(|r| { + r.and_then(|r| { + let r = r.into(); + Ok(match r { + ActExitValue::WillCompleteAsync => ActExitValue::WillCompleteAsync, + ActExitValue::Normal(o) => ActExitValue::Normal(o.as_json_payload()?), + }) + }) + }) + .boxed() + } +} + +/// Execute activity which takes [ActContext] with 0 arguments and returns a [Result] with 'R' +/// and [anyhow::Error] where 'R' implements [serde] traits for +/// serialization into Temporal [Payload].
+/// Use [into_activity_0_args] to register the activity with the worker. +pub async fn execute_activity_0_args<'a, F, R>( + ctx: &WfContext, + options: ActivityOptions, + _f: F, +) -> Result +where + F: AsyncFn1> + Send + Sync + 'static, + R: Serialize + Deserialize<'a> + FromJsonPayloadExt + Debug, +{ + let activity_type = if options.activity_type.is_empty() { + std::any::type_name::().to_string() + } else { + options.activity_type + }; + let options = ActivityOptions { + activity_type, + input: vec![], + ..options + }; + let activity_resolution = ctx.activity(options).await; + + match activity_resolution.status { + Some(activity_resolution::Status::Completed(activity_result::Success { result })) => { + Ok(R::from_json_payload(&result.unwrap()).unwrap()) + } + Some(activity_resolution::Status::Failed(activity_result::Failure { failure })) => { + Err(anyhow::anyhow!("{:?}", failure)) + } + _ => panic!("activity task failed {activity_resolution:?}"), + } +} + +/// Execute activity which takes [ActContext] with 0 arguments and returns a [ActExitValue] wrapping 'R' +/// and [anyhow::Error] where 'R' implements [serde] traits for +/// serialization into Temporal [Payload](temporal_sdk_core_protos::temporal::api::common::v1::Payload). +/// Use [into_activity_0_args] to register the activity with the worker. +pub async fn execute_activity_0_args_exit_value<'a, F, R>( + ctx: &WfContext, + options: ActivityOptions, + _f: F, +) -> Result +where + F: AsyncFn1, anyhow::Error>> + + Send + + Sync + + 'static, + R: Serialize + Deserialize<'a> + FromJsonPayloadExt + Debug, +{ + let activity_type = if options.activity_type.is_empty() { + std::any::type_name::().to_string() + } else { + options.activity_type + }; + let options = ActivityOptions { + activity_type, + input: vec![], + ..options + }; + let activity_resolution = ctx.activity(options).await; + + match activity_resolution.status { + Some(activity_resolution::Status::Completed(activity_result::Success { result })) => { + Ok(R::from_json_payload(&result.unwrap()).unwrap()) + } + Some(activity_resolution::Status::Failed(activity_result::Failure { failure })) => { + Err(anyhow::anyhow!("{:?}", failure)) + } + _ => panic!("activity task failed {activity_resolution:?}"), + } +} + +/// Register activity which takes 0 arguments and returns a [Result] with 'R' +/// and [anyhow::Error] where 'R' implements [serde] traits for +/// serialization into Temporal [Payload](temporal_sdk_core_protos::temporal::api::common::v1::Payload). +/// Use [execute_activity_0_args_without_ctx] to execute the activity in the workflow definition. +pub fn into_activity_0_args_without_ctx( + f: F, +) -> impl Fn(ActContext) -> BoxFuture<'static, Result, anyhow::Error>> + Send + Sync +where + F: AsyncFn0> + Send + Sync + 'static, + R: Into>, + O: AsJsonPayloadExt + Debug, +{ + move |_ctx: ActContext| { + (f)() + .map(|r| { + r.and_then(|r| { + let r: ActExitValue = r.into(); + Ok(match r { + ActExitValue::WillCompleteAsync => ActExitValue::WillCompleteAsync, + ActExitValue::Normal(o) => ActExitValue::Normal(o.as_json_payload()?), + }) + }) + }) + .boxed() + } +} + +/// Execute activity which takes 0 arguments. +/// Use this for an activity which does not need to handle +/// [Cancellation](https://docs.temporal.io/activities#cancellation), +/// [Heartbeat](https://docs.temporal.io/activities#activity-heartbeat) +/// or any context dependent actions. +/// Use [into_activity_0_args_without_ctx] to register the activity with the worker. +pub async fn execute_activity_0_args_without_ctx<'a, F, R>( + ctx: &WfContext, + options: ActivityOptions, + _f: F, +) -> Result +where + F: AsyncFn0> + Send + Sync + 'static, + R: Serialize + Deserialize<'a> + FromJsonPayloadExt + Debug, +{ + let activity_type = if options.activity_type.is_empty() { + std::any::type_name::().to_string() + } else { + options.activity_type + }; + let options = ActivityOptions { + activity_type, + input: vec![], + ..options + }; + let activity_resolution = ctx.activity(options).await; + + match activity_resolution.status { + Some(activity_resolution::Status::Completed(activity_result::Success { result })) => { + Ok(R::from_json_payload(&result.unwrap()).unwrap()) + } + Some(activity_resolution::Status::Failed(activity_result::Failure { failure })) => { + Err(anyhow::anyhow!("{:?}", failure)) + } + _ => panic!("activity task failed {activity_resolution:?}"), + } +} + +/// Execute activity which takes 0 arguments. +/// Use this for an activity which does not need to handle +/// [Cancellation](https://docs.temporal.io/activities#cancellation), +/// [Heartbeat](https://docs.temporal.io/activities#activity-heartbeat) +/// or any context dependent actions. +/// Use [into_activity_0_args_without_ctx] to register the activity with the worker. +pub async fn execute_activity_0_args_without_ctx_exit_value<'a, F, R>( + ctx: &WfContext, + options: ActivityOptions, + _f: F, +) -> Result +where + F: AsyncFn0, anyhow::Error>> + Send + Sync + 'static, + R: Serialize + Deserialize<'a> + FromJsonPayloadExt + Debug, +{ + let activity_type = if options.activity_type.is_empty() { + std::any::type_name::().to_string() + } else { + options.activity_type + }; + let options = ActivityOptions { + activity_type, + input: vec![], + ..options + }; + let activity_resolution = ctx.activity(options).await; + + match activity_resolution.status { + Some(activity_resolution::Status::Completed(activity_result::Success { result })) => { + Ok(R::from_json_payload(&result.unwrap()).unwrap()) + } + Some(activity_resolution::Status::Failed(activity_result::Failure { failure })) => { + Err(anyhow::anyhow!("{:?}", failure)) + } + _ => panic!("activity task failed {activity_resolution:?}"), + } +} + +/// Register activity which takes [ActContext] with 1 arguments and returns a [Result] with 'R' +/// and [anyhow::Error] where 'R' implements [serde] traits for +/// serialization into Temporal [Payload](temporal_sdk_core_protos::temporal::api::common::v1::Payload). +/// Use [execute_activity_1_args] to execute the activity in the workflow definition. +pub fn into_activity_1_args( + f: F, +) -> impl Fn(ActContext) -> BoxFuture<'static, Result, anyhow::Error>> + Send + Sync +where + A: FromJsonPayloadExt + Send, + F: AsyncFn2> + Send + Sync + 'static, + R: Into>, + O: AsJsonPayloadExt + Debug, +{ + move |ctx: ActContext| match A::from_json_payload(&ctx.get_args()[0]) { + Ok(a) => (f)(ctx, a) + .map(|r| { + r.and_then(|r| { + let r = r.into(); + Ok(match r { + ActExitValue::WillCompleteAsync => ActExitValue::WillCompleteAsync, + ActExitValue::Normal(o) => ActExitValue::Normal(o.as_json_payload()?), + }) + }) + }) + .boxed(), + Err(e) => async move { Err(anyhow::Error::new(e)) }.boxed(), + } +} + +/// Execute activity which takes [ActContext] with 1 arguments and returns a [Result] with 'R' +/// and [anyhow::Error] where 'R' implements [serde] traits for +/// serialization into Temporal [Payload](temporal_sdk_core_protos::temporal::api::common::v1::Payload). +/// Use [into_activity_1_args] to register the activity with the worker. +pub async fn execute_activity_1_args<'a, A, F, R>( + ctx: &WfContext, + options: ActivityOptions, + _f: F, + a: A, +) -> Result +where + F: AsyncFn2> + Send + Sync + 'static, + A: Serialize + Deserialize<'a> + AsJsonPayloadExt + Debug, + R: Serialize + Deserialize<'a> + FromJsonPayloadExt + Debug, +{ + let a = A::as_json_payload(&a).expect("serializes fine"); + let activity_type = if options.activity_type.is_empty() { + std::any::type_name::().to_string() + } else { + options.activity_type + }; + let options = ActivityOptions { + activity_type, + input: vec![a], + ..options + }; + let activity_resolution = ctx.activity(options).await; + + match activity_resolution.status { + Some(activity_resolution::Status::Completed(activity_result::Success { result })) => { + Ok(R::from_json_payload(&result.unwrap()).unwrap()) + } + Some(activity_resolution::Status::Failed(activity_result::Failure { failure })) => { + Err(anyhow::anyhow!("{:?}", failure)) + } + _ => panic!("activity task failed {activity_resolution:?}"), + } +} + +/// Register activity which takes [ActContext] with 1 arguments and returns a [Result] with 'R' +/// and [anyhow::Error] where 'R' implements [serde] traits for +/// serialization into Temporal [Payload](temporal_sdk_core_protos::temporal::api::common::v1::Payload). +/// Use [execute_activity_1_args] to execute the activity in the workflow definition. +pub fn into_activity_1_args_exit_value( + f: F, +) -> impl Fn(ActContext) -> BoxFuture<'static, Result, anyhow::Error>> + Send + Sync +where + A: FromJsonPayloadExt + Send, + F: AsyncFn2> + Send + Sync + 'static, + R: Into>, + O: AsJsonPayloadExt + Debug, +{ + move |ctx: ActContext| match A::from_json_payload(&ctx.get_args()[0]) { + Ok(a) => (f)(ctx, a) + .map(|r| { + r.and_then(|r| { + let r = r.into(); + Ok(match r { + ActExitValue::WillCompleteAsync => ActExitValue::WillCompleteAsync, + ActExitValue::Normal(o) => ActExitValue::Normal(o.as_json_payload()?), + }) + }) + }) + .boxed(), + Err(e) => async move { Err(anyhow::Error::new(e)) }.boxed(), + } +} + +/// Execute activity which takes [ActContext] with 1 arguments and returns a [ActExitValue] wrapping 'R' +/// and [anyhow::Error] where 'R' implements [serde] traits for +/// serialization into Temporal [Payload](temporal_sdk_core_protos::temporal::api::common::v1::Payload). +/// Use [into_activity_1_args] to register the activity with the worker. +pub async fn execute_activity_1_args_exit_value<'a, A, F, R>( + ctx: &WfContext, + options: ActivityOptions, + _f: F, + a: A, +) -> Result +where + F: AsyncFn2, anyhow::Error>> + + Send + + Sync + + 'static, + A: Serialize + Deserialize<'a> + AsJsonPayloadExt + Debug, + R: Serialize + Deserialize<'a> + FromJsonPayloadExt + Debug, +{ + let a = A::as_json_payload(&a).expect("serializes fine"); + let activity_type = if options.activity_type.is_empty() { + std::any::type_name::().to_string() + } else { + options.activity_type + }; + let options = ActivityOptions { + activity_type, + input: vec![a], + ..options + }; + let activity_resolution = ctx.activity(options).await; + + match activity_resolution.status { + Some(activity_resolution::Status::Completed(activity_result::Success { result })) => { + Ok(R::from_json_payload(&result.unwrap()).unwrap()) + } + Some(activity_resolution::Status::Failed(activity_result::Failure { failure })) => { + Err(anyhow::anyhow!("{:?}", failure)) + } + _ => panic!("activity task failed {activity_resolution:?}"), + } +} + +/// Register activity which takes [ActContext] with 1 arguments and returns a result [Result] where 'R' +/// implements [serde] traits for serialization into Temporal [Payload](temporal_sdk_core_protos::temporal::api::common::v1::Payload). +/// and 'E' is custom error type +/// Use [execute_activity_1_args_with_errors] to execute the activity in the workflow definition. +pub fn into_activity_1_args_with_errors( + f: F, +) -> impl Fn(ActContext) -> BoxFuture<'static, Result, anyhow::Error>> + Send + Sync +where + A: FromJsonPayloadExt + Send, + F: AsyncFn2> + Send + Sync + 'static, + R: Into>, + O: AsJsonPayloadExt + Debug, + E: std::error::Error + Send + Sync + 'static, +{ + move |ctx: ActContext| match A::from_json_payload(&ctx.get_args()[0]) { + Ok(a) => (f)(ctx, a) + .map(|r| match r { + Ok(r) => { + let exit_val: ActExitValue = r.into(); + Ok(match exit_val { + ActExitValue::WillCompleteAsync => ActExitValue::WillCompleteAsync, + ActExitValue::Normal(x) => ActExitValue::Normal(x.as_json_payload()?), + }) + } + Err(e) => Err(anyhow::Error::from(e)), + }) + .boxed(), + Err(e) => async move { Err(anyhow::Error::from(e)) }.boxed(), + } +} + +/// Execute activity which takes [ActContext] with 1 arguments and returns a result [Result] where 'R' +/// implements [serde] traits for serialization into Temporal [Payload](temporal_sdk_core_protos::temporal::api::common::v1::Payload). +/// and 'E' is custom error type +/// Use [into_activity_1_args_with_errors] to register the activity with the worker. +pub async fn execute_activity_1_args_with_errors<'a, A, F, R, E>( + ctx: &WfContext, + options: ActivityOptions, + _f: F, + a: A, +) -> Result +where + F: AsyncFn2> + Send + Sync + 'static, + A: Serialize + Deserialize<'a> + AsJsonPayloadExt + Debug, + R: Serialize + Deserialize<'a> + FromJsonPayloadExt + Debug, + E: FromFailureExt, +{ + let a = A::as_json_payload(&a).expect("serializes fine"); + let activity_type = if options.activity_type.is_empty() { + std::any::type_name::().to_string() + } else { + options.activity_type + }; + let options = ActivityOptions { + activity_type, + input: vec![a], + ..options + }; + let activity_resolution = ctx.activity(options).await; + match activity_resolution.status { + Some(activity_resolution::Status::Completed(activity_result::Success { result })) => { + Ok(R::from_json_payload(&result.unwrap()).unwrap()) + } + Some(activity_resolution::Status::Failed(activity_result::Failure { failure })) => { + Err(E::from_failure(failure.unwrap())) + } + _ => panic!("Unexpected activity resolution status"), + } +} + +/// Register activity which takes [ActContext] with 2 arguments and returns a [Result] with 'R' +/// and [anyhow::Error] where 'R' implements [serde] traits for +/// serialization into Temporal [Payload](temporal_sdk_core_protos::temporal::api::common::v1::Payload). +/// Use [execute_activity_2_args] to execute the activity in the workflow definition. +pub fn into_activity_2_args( + f: F, +) -> impl Fn(ActContext) -> BoxFuture<'static, Result, anyhow::Error>> + Send + Sync +where + A: FromJsonPayloadExt + Send, + B: FromJsonPayloadExt + Send, + F: AsyncFn3> + Send + Sync + 'static, + R: Into>, + O: AsJsonPayloadExt + Debug, +{ + move |ctx: ActContext| match A::from_json_payload(&ctx.get_args()[0]) { + Ok(a) => match B::from_json_payload(&ctx.get_args()[1]) { + Ok(b) => (f)(ctx, a, b) + .map(|r| { + r.and_then(|r| { + let r = r.into(); + Ok(match r { + ActExitValue::WillCompleteAsync => ActExitValue::WillCompleteAsync, + ActExitValue::Normal(o) => ActExitValue::Normal(o.as_json_payload()?), + }) + }) + }) + .boxed(), + Err(e) => async move { Err(e.into()) }.boxed(), + }, + Err(e) => async move { Err(e.into()) }.boxed(), + } +} + +/// Execute activity which takes [ActContext] with 2 arguments and returns a [Result] with 'R' +/// and [anyhow::Error] where 'R' implements [serde] traits for +/// serialization into Temporal [Payload](temporal_sdk_core_protos::temporal::api::common::v1::Payload). +/// Use [into_activity_2_args] to register the activity with the worker. +pub async fn execute_activity_2_args<'a, 'b, A, B, F, R>( + ctx: &WfContext, + options: ActivityOptions, + _f: F, + a: A, + b: B, +) -> Result +where + F: AsyncFn3> + Send + Sync + 'static, + A: Serialize + Deserialize<'a> + AsJsonPayloadExt + Debug, + B: Serialize + Deserialize<'b> + AsJsonPayloadExt + Debug, + R: Serialize + Deserialize<'a> + FromJsonPayloadExt, +{ + let a = A::as_json_payload(&a).expect("serializes fine"); + let b = B::as_json_payload(&b).expect("serializes fine"); + let activity_type = if options.activity_type.is_empty() { + std::any::type_name::().to_string() + } else { + options.activity_type + }; + let options = ActivityOptions { + activity_type, + input: vec![a, b], + //input, + ..options + }; + let output = ctx.activity(options).await.unwrap_ok_payload(); + Ok(R::from_json_payload(&output).unwrap()) +} + +/// Register child workflow which takes [WfContext] with 0 arguments and returns a [Result] with 'R' +/// and [anyhow::Error] where 'R' implements [serde] traits for +/// serialization into Temporal [Payload](temporal_sdk_core_protos::temporal::api::common::v1::Payload). +/// Use [execute_child_workflow_0_args] to execute the workflow in the workflow definition. +pub fn into_workflow_0_args( + f: F, +) -> impl Fn(WfContext) -> BoxFuture<'static, Result, anyhow::Error>> + Send + Sync +where + F: AsyncFn1> + Send + Sync + 'static, + R: Into>, + O: AsJsonPayloadExt + Debug, +{ + move |ctx: WfContext| { + (f)(ctx) + .map(|r| { + r.and_then(|r| { + let r = r.into(); + Ok(match r { + WfExitValue::ContinueAsNew(b) => WfExitValue::ContinueAsNew(b), + WfExitValue::Cancelled => WfExitValue::Cancelled, + WfExitValue::Evicted => WfExitValue::Evicted, + WfExitValue::Normal(a) => WfExitValue::Normal(a.as_json_payload()?), + }) + }) + }) + .boxed() + } +} + +/// Execute child workflow which takes [WfContext] with 0 arguments and returns a [Result] with 'R' +/// and [anyhow::Error] where 'R' implements [serde] traits for +/// serialization into Temporal [Payload](temporal_sdk_core_protos::temporal::api::common::v1::Payload). +/// Use [into_workflow_0_args] to register the workflow with the worker. +pub async fn execute_child_workflow_0_args<'a, F, R>( + ctx: &WfContext, + options: ChildWorkflowOptions, + _f: F, +) -> Result +where + F: AsyncFn1> + Send + Sync + 'static, + R: Serialize + Deserialize<'a> + FromJsonPayloadExt + Debug, +{ + let workflow_type = std::any::type_name::(); + + let child = ctx.child_workflow(ChildWorkflowOptions { + workflow_type: workflow_type.to_string(), + input: vec![], + ..options + }); + + let started = child + .start(ctx) + .await + .into_started() + .expect("Child should start OK"); + + match started.result().await.status { + Some(child_workflow_result::Status::Completed(child_workflow::Success { result })) => { + Ok(R::from_json_payload(&result.unwrap()).unwrap()) + } + _ => Err(anyhow!("Unexpected child WF status")), + } +} + +/// Execute child workflow which takes [WfContext] with 0 arguments and returns a [Result] with 'R' +/// and [anyhow::Error] where 'R' implements [serde] traits for +/// serialization into Temporal [Payload](temporal_sdk_core_protos::temporal::api::common::v1::Payload). +/// Use [into_workflow_0_args] to register the workflow with the worker. +pub async fn execute_child_workflow_0_args_exit_value<'a, F, R>( + ctx: &WfContext, + options: ChildWorkflowOptions, + _f: F, +) -> Result +where + F: AsyncFn1, anyhow::Error>> + Send + Sync + 'static, + R: Serialize + Deserialize<'a> + FromJsonPayloadExt + Debug, +{ + let workflow_type = std::any::type_name::(); + + let child = ctx.child_workflow(ChildWorkflowOptions { + workflow_type: workflow_type.to_string(), + input: vec![], + ..options + }); + + let started = child + .start(ctx) + .await + .into_started() + .expect("Child should start OK"); + + match started.result().await.status { + Some(child_workflow_result::Status::Completed(child_workflow::Success { result })) => { + Ok(R::from_json_payload(&result.unwrap()).unwrap()) + } + _ => Err(anyhow!("Unexpected child WF status")), + } +} + +/// Register child workflow which takes [WfContext] with 1 arguments and returns a [Result] with 'R' +/// and [anyhow::Error] where 'R' implements [serde] traits for +/// serialization into Temporal [Payload](temporal_sdk_core_protos::temporal::api::common::v1::Payload). +/// Use [execute_child_workflow_1_args] to execute the workflow in the workflow definition. +pub fn into_workflow_1_args( + f: F, +) -> impl Fn(WfContext) -> BoxFuture<'static, Result, anyhow::Error>> + Send + Sync +where + A: FromJsonPayloadExt + Send, + F: AsyncFn2> + Send + Sync + 'static, + R: Into>, + O: AsJsonPayloadExt + Debug, +{ + move |ctx: WfContext| match A::from_json_payload(&ctx.get_args()[0]) { + Ok(a) => (f)(ctx, a) + .map(|r| { + r.and_then(|r| { + let r: WfExitValue = r.into(); + Ok(match r { + WfExitValue::ContinueAsNew(b) => WfExitValue::ContinueAsNew(b), + WfExitValue::Cancelled => WfExitValue::Cancelled, + WfExitValue::Evicted => WfExitValue::Evicted, + WfExitValue::Normal(a) => WfExitValue::Normal(a.as_json_payload()?), + }) + }) + }) + .boxed(), + Err(e) => async move { Err(e.into()) }.boxed(), + } +} + +/// Execute child workflow which takes [WfContext] with 2 arguments and returns a [Result] with 'R' +/// and [anyhow::Error] where 'R' implements [serde] traits for +/// serialization into Temporal [Payload](temporal_sdk_core_protos::temporal::api::common::v1::Payload). +/// Use [into_workflow_1_args] to register the workflow with the worker +pub async fn execute_child_workflow_1_args<'a, A, F, R>( + ctx: &WfContext, + options: ChildWorkflowOptions, + _f: F, + a: A, +) -> Result +where + F: AsyncFn2> + Send + Sync + 'static, + A: Serialize + Deserialize<'a> + AsJsonPayloadExt + Debug, + R: Serialize + Deserialize<'a> + FromJsonPayloadExt + Debug, +{ + let a = A::as_json_payload(&a).expect("serializes fine"); + let workflow_type = std::any::type_name::(); + + let child = ctx.child_workflow(ChildWorkflowOptions { + workflow_type: workflow_type.to_string(), + input: vec![a], + ..options + }); + + let started = child + .start(ctx) + .await + .into_started() + .expect("Child should start OK"); + + match started.result().await.status { + Some(child_workflow_result::Status::Completed(child_workflow::Success { result })) => { + Ok(R::from_json_payload(&result.unwrap()).unwrap()) + } + _ => Err(anyhow!("Unexpected child WF status")), + } +} + +/// Register child workflow which takes [WfContext] with 1 arguments and returns a [Result] with 'R' +/// and [anyhow::Error] where 'R' implements [serde] traits for +/// serialization into Temporal [Payload](temporal_sdk_core_protos::temporal::api::common::v1::Payload). +/// Use [execute_child_workflow_1_args_exit_value] to execute the workflow in the workflow definition. +pub fn into_workflow_1_args_exit_value( + f: F, +) -> impl Fn(WfContext) -> BoxFuture<'static, Result, anyhow::Error>> + Send + Sync +where + A: FromJsonPayloadExt + Send, + F: AsyncFn2> + Send + Sync + 'static, + R: Into>, + O: AsJsonPayloadExt + Debug, +{ + move |ctx: WfContext| match A::from_json_payload(&ctx.get_args()[0]) { + Ok(a) => (f)(ctx, a) + .map(|r| { + r.and_then(|r| { + let r = r.into(); + Ok(match r { + WfExitValue::ContinueAsNew(b) => WfExitValue::ContinueAsNew(b), + WfExitValue::Cancelled => WfExitValue::Cancelled, + WfExitValue::Evicted => WfExitValue::Evicted, + WfExitValue::Normal(a) => WfExitValue::Normal(a.as_json_payload()?), + }) + }) + }) + .boxed(), + Err(e) => async move { Err(anyhow::Error::new(e)) }.boxed(), + } +} + +/// Execute child workflow which takes [WfContext] with 1 arguments and returns a [Result] with 'R' +/// and [anyhow::Error] where 'R' implements [serde] traits for +/// serialization into Temporal [Payload](temporal_sdk_core_protos::temporal::api::common::v1::Payload). +/// Use [into_workflow_1_args_exit_value] to register the workflow with the worker. +pub async fn execute_child_workflow_1_args_exit_value<'a, A, F, R>( + ctx: &WfContext, + options: ChildWorkflowOptions, + _f: F, + a: A, +) -> Result +where + F: AsyncFn2, anyhow::Error>> + + Send + + Sync + + 'static, + A: Serialize + Deserialize<'a> + AsJsonPayloadExt + Debug, + R: Serialize + Deserialize<'a> + FromJsonPayloadExt + Debug, +{ + let a = A::as_json_payload(&a).expect("serializes fine"); + let workflow_type = std::any::type_name::(); + + let child = ctx.child_workflow(ChildWorkflowOptions { + workflow_type: workflow_type.to_string(), + input: vec![a], + ..options + }); + + let started = child + .start(ctx) + .await + .into_started() + .expect("Child should start OK"); + + match started.result().await.status { + Some(child_workflow_result::Status::Completed(child_workflow::Success { result })) => { + Ok(R::from_json_payload(&result.unwrap()).unwrap()) + } + Some(child_workflow_result::Status::Failed(child_workflow::Failure { failure })) => { + Err(anyhow::anyhow!("{:?}", failure)) + } + _ => Err(anyhow!("Unexpected child WF status")), + } +} + +/// Execute activity which takes [ActContext] with 1 arguments and returns a result [Result] where 'R' +/// implements [serde] traits for serialization into Temporal [Payload](temporal_sdk_core_protos::temporal::api::common::v1::Payload). +/// and 'E' is custom error type +/// Use [execute_child_workflow_1_args_with_errors] to register the activity with the worker. +pub fn into_workflow_1_args_with_errors( + f: F, +) -> impl Fn(WfContext) -> BoxFuture<'static, Result, anyhow::Error>> + Send + Sync +where + A: FromJsonPayloadExt + Send, + F: AsyncFn2> + Send + Sync + 'static, + R: Into>, + O: AsJsonPayloadExt + Debug, + E: std::error::Error + Send + Sync + 'static, +{ + move |ctx: WfContext| match A::from_json_payload(&ctx.get_args()[0]) { + Ok(a) => (f)(ctx, a) + .map(|r| match r { + Ok(r) => { + let r: WfExitValue = r.into(); + Ok(match r { + WfExitValue::ContinueAsNew(b) => WfExitValue::ContinueAsNew(b), + WfExitValue::Cancelled => WfExitValue::Cancelled, + WfExitValue::Evicted => WfExitValue::Evicted, + WfExitValue::Normal(a) => WfExitValue::Normal(a.as_json_payload()?), + }) + } + Err(e) => Err(anyhow::Error::from(e)), + }) + .boxed(), + Err(e) => async move { Err(anyhow::Error::from(e)) }.boxed(), + } +} + +/// Execute child workflow which takes [WfContext] with 1 arguments and returns a [Result] with 'R' +/// and [anyhow::Error] where 'R' implements [serde] traits for +/// serialization into Temporal [Payload](temporal_sdk_core_protos::temporal::api::common::v1::Payload). +/// Use [into_workflow_0_args] to register the workflow with the worker. +pub async fn execute_child_workflow_1_args_with_errors<'a, A, F, R, E>( + ctx: &WfContext, + options: ChildWorkflowOptions, + _f: F, + a: A, +) -> Result +where + F: AsyncFn2> + Send + Sync + 'static, + A: Serialize + Deserialize<'a> + AsJsonPayloadExt + Debug, + R: Serialize + Deserialize<'a> + FromJsonPayloadExt + Debug, + E: FromFailureExt, +{ + let a = A::as_json_payload(&a).expect("serializes fine"); + let workflow_type = std::any::type_name::(); + + let child = ctx.child_workflow(ChildWorkflowOptions { + workflow_type: workflow_type.to_string(), + input: vec![a], + ..options + }); + + let started = child + .start(ctx) + .await + .into_started() + .expect("Child should start OK"); + + match started.result().await.status { + Some(child_workflow_result::Status::Completed(child_workflow::Success { result })) => { + Ok(R::from_json_payload(&result.unwrap()).unwrap()) + } + Some(child_workflow_result::Status::Failed(child_workflow::Failure { failure })) => { + Err(E::from_failure(failure.unwrap())) + } + _ => panic!("Unexpected child WF status"), + } +} + +/// Register child workflow which takes [WfContext] with 1 arguments and returns a [Result] with 'R' +/// and [anyhow::Error] where 'R' implements [serde] traits for +/// serialization into Temporal [Payload](temporal_sdk_core_protos::temporal::api::common::v1::Payload). +/// Use [execute_child_workflow_2_args] to execute the workflow in the workflow definition. +pub fn into_workflow_2_args( + f: F, +) -> impl Fn(WfContext) -> BoxFuture<'static, Result, anyhow::Error>> + Send + Sync +where + A: FromJsonPayloadExt + Send, + B: FromJsonPayloadExt + Send, + F: AsyncFn3> + Send + Sync + 'static, + R: Into>, + O: AsJsonPayloadExt + Debug, +{ + move |ctx: WfContext| match A::from_json_payload(&ctx.get_args()[0]) { + Ok(a) => match B::from_json_payload(&ctx.get_args()[1]) { + Ok(b) => (f)(ctx, a, b) + .map(|r| { + r.and_then(|r| { + let exit_val: WfExitValue = r.into(); + Ok(match exit_val { + WfExitValue::ContinueAsNew(b) => WfExitValue::ContinueAsNew(b), + WfExitValue::Cancelled => WfExitValue::Cancelled, + WfExitValue::Evicted => WfExitValue::Evicted, + WfExitValue::Normal(a) => WfExitValue::Normal(a.as_json_payload()?), + }) + }) + }) + .boxed(), + Err(e) => async move { Err(e.into()) }.boxed(), + }, + Err(e) => async move { Err(e.into()) }.boxed(), + } +} + +/// Execute child workflow which takes [WfContext] with 2 arguments and returns a [Result] with 'R' +/// and [anyhow::Error] where 'R' implements [serde] traits for +/// serialization into Temporal [Payload](temporal_sdk_core_protos::temporal::api::common::v1::Payload). +/// Use [into_workflow_2_args] to register the workflow with the worker +pub async fn execute_child_workflow_2_args<'a, A, B, F, R>( + ctx: &WfContext, + options: ChildWorkflowOptions, + _f: F, + a: A, + b: B, +) -> Result +where + F: AsyncFn3> + Send + Sync + 'static, + A: Serialize + Deserialize<'a> + AsJsonPayloadExt + Debug, + B: Serialize + Deserialize<'a> + AsJsonPayloadExt + Debug, + R: Serialize + Deserialize<'a> + FromJsonPayloadExt + Debug, +{ + let a = A::as_json_payload(&a).expect("serializes fine"); + let b = B::as_json_payload(&b).expect("serializes fine"); + let workflow_type = std::any::type_name::(); + + let child = ctx.child_workflow(ChildWorkflowOptions { + workflow_type: workflow_type.to_string(), + input: vec![a, b], + ..options + }); + + let started = child + .start(ctx) + .await + .into_started() + .expect("Child should start OK"); + + match started.result().await.status { + Some(child_workflow_result::Status::Completed(child_workflow::Success { result })) => { + Ok(R::from_json_payload(&result.unwrap()).unwrap()) + } + _ => Err(anyhow!("Unexpected child WF status")), + } +} + +/// Execute child workflow which takes [WfContext] with 2 arguments and returns a [Result] with 'R' +/// and [anyhow::Error] where 'R' implements [serde] traits for +/// serialization into Temporal [Payload](temporal_sdk_core_protos::temporal::api::common::v1::Payload). +/// Use [into_workflow_2_args] to register the workflow with the worker +pub async fn execute_child_workflow_2_args_exit_value<'a, A, B, F, R>( + ctx: &WfContext, + options: ChildWorkflowOptions, + _f: F, + a: A, + b: B, +) -> Result, anyhow::Error> +where + F: AsyncFn3, anyhow::Error>> + + Send + + Sync + + 'static, + A: Serialize + Deserialize<'a> + AsJsonPayloadExt + Debug, + B: Serialize + Deserialize<'a> + AsJsonPayloadExt + Debug, + R: Serialize + Deserialize<'a> + FromJsonPayloadExt + Debug, +{ + let a = A::as_json_payload(&a).expect("serializes fine"); + let b = B::as_json_payload(&b).expect("serializes fine"); + let workflow_type = std::any::type_name::(); + + let child = ctx.child_workflow(ChildWorkflowOptions { + workflow_type: workflow_type.to_string(), + input: vec![a, b], + ..options + }); + + let started = child + .start(ctx) + .await + .into_started() + .expect("Child should start OK"); + + match started.result().await.status { + Some(child_workflow_result::Status::Completed(child_workflow::Success { result })) => Ok( + WfExitValue::Normal(R::from_json_payload(&result.unwrap()).unwrap()), + ), + _ => Err(anyhow!("Unexpected child WF status")), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + pub fn test_trait_bounds() { + assert_eq!(6, 6); + } +} diff --git a/sdk/src/workflow_context/options.rs b/sdk/src/workflow_context/options.rs index bf3823872..83451502c 100644 --- a/sdk/src/workflow_context/options.rs +++ b/sdk/src/workflow_context/options.rs @@ -36,7 +36,7 @@ pub struct ActivityOptions { /// Type of activity to schedule pub activity_type: String, /// Input to the activity - pub input: Payload, + pub input: Vec, /// Task queue to schedule the activity in pub task_queue: String, /// Time that the Activity Task can stay in the Task Queue before it is picked up by a Worker. @@ -88,7 +88,7 @@ impl IntoWorkflowCommand for ActivityOptions { start_to_close_timeout: self.start_to_close_timeout.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], + arguments: self.input, retry_policy: self.retry_policy, ..Default::default() } @@ -107,7 +107,7 @@ pub struct LocalActivityOptions { /// Type of activity to schedule pub activity_type: String, /// Input to the activity - pub input: Payload, + pub input: Vec, /// Retry policy pub retry_policy: RetryPolicy, /// Override attempt number rather than using 1. @@ -154,7 +154,7 @@ impl IntoWorkflowCommand for LocalActivityOptions { Some(aid) => aid, }, activity_type: self.activity_type, - arguments: vec![self.input], + arguments: self.input, retry_policy: Some(self.retry_policy), local_retry_threshold: self.timer_backoff_threshold.and_then(|d| d.try_into().ok()), cancellation_type: self.cancel_type.into(), diff --git a/sdk/src/workflow_future.rs b/sdk/src/workflow_future.rs index 06375016f..de8225dd3 100644 --- a/sdk/src/workflow_future.rs +++ b/sdk/src/workflow_future.rs @@ -50,7 +50,7 @@ impl WorkflowFunction { args: Vec, outgoing_completions: UnboundedSender, ) -> ( - impl Future>, + impl Future>, UnboundedSender, ) { let (cancel_tx, cancel_rx) = watch::channel(false); @@ -93,7 +93,7 @@ enum SigChanOrBuffer { pub struct WorkflowFuture { /// Future produced by calling the workflow function - inner: BoxFuture<'static, WorkflowResult<()>>, + inner: BoxFuture<'static, WorkflowResult>, /// Commands produced inside user's wf code incoming_commands: Receiver, /// Once blocked or the workflow has finished or errored out, the result is sent here @@ -234,7 +234,7 @@ impl WorkflowFuture { } impl Future for WorkflowFuture { - type Output = WorkflowResult<()>; + type Output = WorkflowResult; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 'activations: loop { @@ -443,10 +443,12 @@ impl Future for WorkflowFuture { match res { Ok(exit_val) => match exit_val { // TODO: Generic values - WfExitValue::Normal(_) => { + WfExitValue::Normal(result) => { activation_cmds.push( workflow_command::Variant::CompleteWorkflowExecution( - CompleteWorkflowExecution { result: None }, + CompleteWorkflowExecution { + result: Some(result), + }, ), ); } diff --git a/test-utils/src/lib.rs b/test-utils/src/lib.rs index bfd6bc3ae..3669feddd 100644 --- a/test-utils/src/lib.rs +++ b/test-utils/src/lib.rs @@ -27,7 +27,7 @@ use temporal_client::{ }; use temporal_sdk::{ interceptors::{FailOnNondeterminismInterceptor, WorkerInterceptor}, - IntoActivityFunc, Worker, WorkflowFunction, + ActivityFunction, Worker, WorkflowFunction, }; use temporal_sdk_core::{ ephemeral_server::{EphemeralExe, EphemeralExeVersion}, @@ -386,10 +386,10 @@ impl TestWorker { self.inner.register_wf(workflow_type, wf_function) } - pub fn register_activity( + pub fn register_activity( &mut self, activity_type: impl Into, - act_function: impl IntoActivityFunc, + act_function: impl Into, ) { self.inner.register_activity(activity_type, act_function) } diff --git a/test-utils/src/workflows.rs b/test-utils/src/workflows.rs index e85e7f8d1..6acdce430 100644 --- a/test-utils/src/workflows.rs +++ b/test-utils/src/workflows.rs @@ -1,12 +1,15 @@ use crate::prost_dur; use std::time::Duration; -use temporal_sdk::{ActivityOptions, LocalActivityOptions, WfContext, WorkflowResult}; -use temporal_sdk_core_protos::{coresdk::AsJsonPayloadExt, temporal::api::common::v1::RetryPolicy}; +use temporal_sdk::{ActivityOptions, LocalActivityOptions, WfContext, WfExitValue, WorkflowResult}; +use temporal_sdk_core_protos::{ + coresdk::AsJsonPayloadExt, + temporal::api::common::v1::{Payload, RetryPolicy}, +}; -pub async fn la_problem_workflow(ctx: WfContext) -> WorkflowResult<()> { +pub 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"), + input: vec!["hi".as_json_payload().expect("serializes fine")], retry_policy: RetryPolicy { initial_interval: Some(prost_dur!(from_micros(15))), backoff_coefficient: 1_000., @@ -21,9 +24,11 @@ pub async fn la_problem_workflow(ctx: WfContext) -> WorkflowResult<()> { 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"), + input: vec!["hi!".as_json_payload().expect("serializes fine")], ..Default::default() }) .await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } diff --git a/tests/fuzzy_workflow.rs b/tests/fuzzy_workflow.rs index 8c7c10660..4ac712242 100644 --- a/tests/fuzzy_workflow.rs +++ b/tests/fuzzy_workflow.rs @@ -2,8 +2,12 @@ use futures_util::{sink, stream::FuturesUnordered, FutureExt, StreamExt}; use rand::{prelude::Distribution, rngs::SmallRng, Rng, SeedableRng}; use std::{future, time::Duration}; use temporal_client::{WfClientExt, WorkflowClientTrait, WorkflowOptions}; -use temporal_sdk::{ActContext, ActivityOptions, LocalActivityOptions, WfContext, WorkflowResult}; +use temporal_sdk::{ + ActContext, ActExitValue, ActivityOptions, LocalActivityOptions, WfContext, WfExitValue, + WorkflowResult, +}; use temporal_sdk_core_protos::coresdk::{AsJsonPayloadExt, FromJsonPayloadExt, IntoPayloadsExt}; +use temporal_sdk_core_protos::temporal::api::common::v1::Payload; use temporal_sdk_core_test_utils::CoreWfStarter; use tokio_util::sync::CancellationToken; @@ -28,11 +32,11 @@ impl Distribution for FuzzyWfActionSampler { } } -async fn echo(_ctx: ActContext, echo_me: String) -> Result { - Ok(echo_me) +async fn echo(ctx: ActContext) -> Result, anyhow::Error> { + Ok(ActExitValue::Normal(ctx.get_args()[0].clone())) } -async fn fuzzy_wf_def(ctx: WfContext) -> WorkflowResult<()> { +async fn fuzzy_wf_def(ctx: WfContext) -> WorkflowResult { let sigchan = ctx .make_signal_channel(FUZZY_SIG) .map(|sd| FuzzyWfAction::from_json_payload(&sd.input[0]).expect("Can deserialize signal")); @@ -47,7 +51,7 @@ async fn fuzzy_wf_def(ctx: WfContext) -> WorkflowResult<()> { .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"), + input: vec!["hi!".as_json_payload().expect("serializes fine")], ..Default::default() }) .map(|_| ()) @@ -56,7 +60,7 @@ async fn fuzzy_wf_def(ctx: WfContext) -> WorkflowResult<()> { .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"), + input: vec!["hi!".as_json_payload().expect("serializes fine")], ..Default::default() }) .map(|_| ()) @@ -70,7 +74,9 @@ async fn fuzzy_wf_def(ctx: WfContext) -> WorkflowResult<()> { }) .await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } #[tokio::test(flavor = "multi_thread", worker_threads = 4)] diff --git a/tests/heavy_tests.rs b/tests/heavy_tests.rs index 68b1f7774..c3fd78081 100644 --- a/tests/heavy_tests.rs +++ b/tests/heavy_tests.rs @@ -1,9 +1,12 @@ use futures::{future::join_all, sink, stream::FuturesUnordered, StreamExt}; use std::time::{Duration, Instant}; use temporal_client::{WfClientExt, WorkflowClientTrait, WorkflowOptions}; -use temporal_sdk::{ActContext, ActivityOptions, WfContext, WorkflowResult}; -use temporal_sdk_core_protos::coresdk::{ - workflow_commands::ActivityCancellationType, AsJsonPayloadExt, +use temporal_sdk::{ + ActContext, ActExitValue, ActivityOptions, WfContext, WfExitValue, WorkflowResult, +}; +use temporal_sdk_core_protos::{ + coresdk::{workflow_commands::ActivityCancellationType, AsJsonPayloadExt}, + temporal::api::common::v1::Payload, }; use temporal_sdk_core_test_utils::{workflows::la_problem_workflow, CoreWfStarter}; @@ -32,7 +35,7 @@ async fn activity_load() { let activity = ActivityOptions { activity_id: Some(activity_id.to_string()), activity_type: "test_activity".to_string(), - input: payload.clone(), + input: vec![payload.clone()], task_queue, schedule_to_start_timeout: Some(activity_timeout), start_to_close_timeout: Some(activity_timeout), @@ -43,17 +46,18 @@ async fn activity_load() { }; let res = ctx.activity(activity).await.unwrap_ok_payload(); assert_eq!(res.data, payload.data); - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } }; let starting = Instant::now(); let wf_type = "activity_load"; worker.register_wf(wf_type.to_owned(), wf_fn); - worker.register_activity( - "test_activity", - |_ctx: ActContext, echo: String| async move { Ok(echo) }, - ); + worker.register_activity("test_activity", |ctx: ActContext| async move { + Ok(ActExitValue::Normal(ctx.get_args()[0].clone())) + }); join_all((0..CONCURRENCY).map(|i| { let worker = &worker; let wf_id = format!("activity_load_{i}"); @@ -99,7 +103,7 @@ async fn workflow_load() { 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"), + input: vec!["hi!".as_json_payload().expect("serializes fine")], ..Default::default() }) .await; @@ -111,12 +115,13 @@ async fn workflow_load() { _ = real_stuff => {} } - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) + }); + worker.register_activity("echo_activity", |ctx: ActContext| async move { + Ok(ActExitValue::Normal(ctx.get_args()[0].clone())) }); - worker.register_activity( - "echo_activity", - |_ctx: ActContext, echo_me: String| async move { Ok(echo_me) }, - ); let client = starter.get_client().await; let mut workflow_handles = vec![]; @@ -170,9 +175,9 @@ async fn evict_while_la_running_no_interference() { let mut worker = starter.worker().await; worker.register_wf(wf_name.to_owned(), la_problem_workflow); - worker.register_activity("delay", |_: ActContext, _: String| async { + worker.register_activity("delay", |ctx: ActContext| async move { tokio::time::sleep(Duration::from_secs(15)).await; - Ok(()) + Ok(ActExitValue::Normal(ctx.get_args()[0].clone())) }); let client = starter.get_client().await; @@ -213,7 +218,7 @@ async fn evict_while_la_running_no_interference() { tokio::join!(subfs.collect::>(), runf); } -pub async fn many_parallel_timers_longhist(ctx: WfContext) -> WorkflowResult<()> { +pub async fn many_parallel_timers_longhist(ctx: WfContext) -> WorkflowResult { for _ in 0..120 { let mut futs = vec![]; for _ in 0..100 { @@ -221,7 +226,9 @@ pub async fn many_parallel_timers_longhist(ctx: WfContext) -> WorkflowResult<()> } join_all(futs).await; } - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } #[tokio::test] diff --git a/tests/integ_tests/activity_functions.rs b/tests/integ_tests/activity_functions.rs index c1b9e524e..b913d88ef 100644 --- a/tests/integ_tests/activity_functions.rs +++ b/tests/integ_tests/activity_functions.rs @@ -1,5 +1,6 @@ -use temporal_sdk::ActContext; +use temporal_sdk::{ActContext, ActExitValue}; +use temporal_sdk_core_protos::temporal::api::common::v1::Payload; -pub async fn echo(_ctx: ActContext, e: String) -> anyhow::Result { - Ok(e) +pub async fn echo(ctx: ActContext) -> anyhow::Result> { + Ok(ActExitValue::Normal(ctx.get_args()[0].clone())) } diff --git a/tests/integ_tests/heartbeat_tests.rs b/tests/integ_tests/heartbeat_tests.rs index 35d00609b..4fc367b27 100644 --- a/tests/integ_tests/heartbeat_tests.rs +++ b/tests/integ_tests/heartbeat_tests.rs @@ -1,7 +1,7 @@ use assert_matches::assert_matches; use std::time::Duration; use temporal_client::{WfClientExt, WorkflowOptions}; -use temporal_sdk::{ActContext, ActivityOptions, WfContext}; +use temporal_sdk::{ActContext, ActExitValue, ActivityOptions, WfContext, WfExitValue}; use temporal_sdk_core_protos::{ coresdk::{ activity_result::{ @@ -180,18 +180,15 @@ async fn activity_doesnt_heartbeat_hits_timeout_then_completes() { let mut starter = CoreWfStarter::new(wf_name); let mut worker = starter.worker().await; let client = starter.get_client().await; - worker.register_activity( - "echo_activity", - |_ctx: ActContext, echo_me: String| async move { - sleep(Duration::from_secs(4)).await; - Ok(echo_me) - }, - ); + worker.register_activity("echo_activity", |ctx: ActContext| async move { + sleep(Duration::from_secs(4)).await; + Ok(ActExitValue::Normal(ctx.get_args()[0].clone())) + }); 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"), + input: vec!["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 { @@ -202,7 +199,9 @@ async fn activity_doesnt_heartbeat_hits_timeout_then_completes() { }) .await; assert_eq!(res.timed_out(), Some(TimeoutType::Heartbeat)); - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }); let run_id = worker diff --git a/tests/integ_tests/workflow_tests.rs b/tests/integ_tests/workflow_tests.rs index d542a392c..30f57975f 100644 --- a/tests/integ_tests/workflow_tests.rs +++ b/tests/integ_tests/workflow_tests.rs @@ -5,6 +5,7 @@ mod cancel_wf; mod child_workflows; mod continue_as_new; mod determinism; +mod examples; mod local_activities; mod modify_wf_properties; mod patches; @@ -27,7 +28,9 @@ use std::{ time::Duration, }; use temporal_client::{WorkflowClientTrait, WorkflowOptions}; -use temporal_sdk::{interceptors::WorkerInterceptor, ActivityOptions, WfContext, WorkflowResult}; +use temporal_sdk::{ + interceptors::WorkerInterceptor, ActivityOptions, WfContext, WfExitValue, WorkflowResult, +}; use temporal_sdk_core::replay::HistoryForReplay; use temporal_sdk_core_api::{errors::PollWfError, Worker}; use temporal_sdk_core_protos::{ @@ -38,7 +41,7 @@ use temporal_sdk_core_protos::{ workflow_completion::WorkflowActivationCompletion, ActivityTaskCompletion, AsJsonPayloadExt, IntoCompletion, }, - temporal::api::{failure::v1::Failure, history::v1::history_event}, + temporal::api::{common::v1::Payload, failure::v1::Failure, history::v1::history_event}, }; use temporal_sdk_core_test_utils::{ drain_pollers_and_shutdown, history_from_proto_binary, init_core_and_create_wf, @@ -106,10 +109,12 @@ async fn parallel_workflows_same_queue() { } static RUN_CT: AtomicUsize = AtomicUsize::new(0); -pub async fn cache_evictions_wf(command_sink: WfContext) -> WorkflowResult<()> { +pub async fn cache_evictions_wf(command_sink: WfContext) -> WorkflowResult { RUN_CT.fetch_add(1, Ordering::SeqCst); command_sink.timer(Duration::from_secs(1)).await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } #[tokio::test] @@ -555,13 +560,15 @@ async fn slow_completes_with_small_cache() { 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"), + input: vec!["hi!".as_json_payload().expect("serializes fine")], ..Default::default() }) .await; ctx.timer(Duration::from_secs(1)).await; } - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }); worker.register_activity("echo_activity", echo); for i in 0..20 { diff --git a/tests/integ_tests/workflow_tests/activities.rs b/tests/integ_tests/workflow_tests/activities.rs index 32331ee6c..2ee1242b3 100644 --- a/tests/integ_tests/workflow_tests/activities.rs +++ b/tests/integ_tests/workflow_tests/activities.rs @@ -6,7 +6,7 @@ use std::time::Duration; use temporal_client::{WfClientExt, WorkflowClientTrait, WorkflowExecutionResult, WorkflowOptions}; use temporal_sdk::{ ActContext, ActExitValue, ActivityCancelledError, ActivityOptions, CancellableFuture, - WfContext, WorkflowResult, + WfContext, WfExitValue, WorkflowResult, }; use temporal_sdk_core_protos::{ coresdk::{ @@ -35,15 +35,17 @@ use temporal_sdk_core_test_utils::{ }; use tokio::{join, sync::Semaphore, time::sleep}; -pub async fn one_activity_wf(ctx: WfContext) -> WorkflowResult<()> { +pub async fn one_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"), + input: vec!["hi!".as_json_payload().expect("serializes fine")], ..Default::default() }) .await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } #[tokio::test] @@ -785,7 +787,7 @@ async fn one_activity_abandon_cancelled_after_complete() { 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"), + input: vec!["hi!".as_json_payload().expect("serializes fine")], cancellation_type: ActivityCancellationType::Abandon, ..Default::default() }); @@ -793,15 +795,14 @@ async fn one_activity_abandon_cancelled_after_complete() { act_fut.cancel(&ctx); ctx.timer(Duration::from_secs(3)).await; act_fut.await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) + }); + worker.register_activity("echo_activity", |ctx: ActContext| async move { + sleep(Duration::from_secs(2)).await; + Ok(ActExitValue::Normal(ctx.get_args()[0].clone())) }); - worker.register_activity( - "echo_activity", - |_ctx: ActContext, echo_me: String| async move { - sleep(Duration::from_secs(2)).await; - Ok(echo_me) - }, - ); let run_id = worker .submit_wf( @@ -836,7 +837,7 @@ async fn it_can_complete_async() { let activity_resolution = ctx .activity(ActivityOptions { activity_type: "complete_async_activity".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), + input: vec!["hi".as_json_payload().expect("serializes fine")], start_to_close_timeout: Some(Duration::from_secs(30)), ..Default::default() }) @@ -850,24 +851,23 @@ async fn it_can_complete_async() { }; assert_eq!(&res, async_response); - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }); let shared_token_ref = shared_token.clone(); - worker.register_activity( - "complete_async_activity", - move |ctx: ActContext, _: 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()); - Ok::, _>(ActExitValue::WillCompleteAsync) - } - }, - ); + worker.register_activity("complete_async_activity", move |ctx: ActContext| { + 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()); + Ok(ActExitValue::WillCompleteAsync) + } + }); let shared_token_ref2 = shared_token.clone(); tokio::spawn(async move { @@ -918,21 +918,23 @@ async fn graceful_shutdown() { ..Default::default() }), cancellation_type: ActivityCancellationType::WaitCancellationCompleted, - input: "hi".as_json_payload().unwrap(), + input: vec!["hi".as_json_payload().unwrap()], ..Default::default() }) }); join_all(act_futs).await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }); static ACTS_STARTED: Semaphore = Semaphore::const_new(0); static ACTS_DONE: Semaphore = Semaphore::const_new(0); - worker.register_activity("sleeper", |ctx: ActContext, _: String| async move { + worker.register_activity("sleeper", |ctx: ActContext| async move { ACTS_STARTED.add_permits(1); // just wait to be cancelled ctx.cancelled().await; ACTS_DONE.add_permits(1); - Result::<(), _>::Err(ActivityCancelledError::default().into()) + Err(ActivityCancelledError::default().into()) }); worker diff --git a/tests/integ_tests/workflow_tests/appdata_propagation.rs b/tests/integ_tests/workflow_tests/appdata_propagation.rs index df7972bec..44545304c 100644 --- a/tests/integ_tests/workflow_tests/appdata_propagation.rs +++ b/tests/integ_tests/workflow_tests/appdata_propagation.rs @@ -1,8 +1,10 @@ use assert_matches::assert_matches; use std::time::Duration; use temporal_client::{WfClientExt, WorkflowExecutionResult, WorkflowOptions}; -use temporal_sdk::{ActContext, ActivityOptions, WfContext, WorkflowResult}; -use temporal_sdk_core_protos::coresdk::AsJsonPayloadExt; +use temporal_sdk::{ + ActContext, ActExitValue, ActivityOptions, WfContext, WfExitValue, WorkflowResult, +}; +use temporal_sdk_core_protos::{coresdk::AsJsonPayloadExt, temporal::api::common::v1::Payload}; use temporal_sdk_core_test_utils::CoreWfStarter; const TEST_APPDATA_MESSAGE: &str = "custom app data, yay"; @@ -11,15 +13,17 @@ struct Data { message: String, } -pub async fn appdata_activity_wf(ctx: WfContext) -> WorkflowResult<()> { +pub 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"), + input: vec!["hi!".as_json_payload().expect("serializes fine")], ..Default::default() }) .await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } #[tokio::test] @@ -33,14 +37,11 @@ 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: ActContext, 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) - }, - ); + worker.register_activity("echo_activity", |ctx: ActContext| async move { + let data = ctx.app_data::().expect("appdata exists. qed"); + assert_eq!(data.message, TEST_APPDATA_MESSAGE.to_owned()); + Ok(ActExitValue::Normal(ctx.get_args()[0].clone())) + }); let run_id = worker .submit_wf( diff --git a/tests/integ_tests/workflow_tests/cancel_external.rs b/tests/integ_tests/workflow_tests/cancel_external.rs index a28f403de..17b8aafd4 100644 --- a/tests/integ_tests/workflow_tests/cancel_external.rs +++ b/tests/integ_tests/workflow_tests/cancel_external.rs @@ -1,11 +1,14 @@ use temporal_client::WorkflowOptions; -use temporal_sdk::{WfContext, WorkflowResult}; -use temporal_sdk_core_protos::coresdk::common::NamespacedWorkflowExecution; +use temporal_sdk::{WfContext, WfExitValue, WorkflowResult}; +use temporal_sdk_core_protos::{ + coresdk::{common::NamespacedWorkflowExecution, AsJsonPayloadExt}, + temporal::api::common::v1::Payload, +}; use temporal_sdk_core_test_utils::CoreWfStarter; const RECEIVER_WFID: &str = "sends-cancel-receiver"; -async fn cancel_sender(ctx: WfContext) -> WorkflowResult<()> { +async fn cancel_sender(ctx: WfContext) -> WorkflowResult { let run_id = std::str::from_utf8(&ctx.get_args()[0].data) .unwrap() .to_owned(); @@ -22,12 +25,16 @@ async fn cancel_sender(ctx: WfContext) -> WorkflowResult<()> { } else { sigres.unwrap(); } - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } -async fn cancel_receiver(mut ctx: WfContext) -> WorkflowResult<()> { +async fn cancel_receiver(mut ctx: WfContext) -> WorkflowResult { ctx.cancelled().await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } #[tokio::test] diff --git a/tests/integ_tests/workflow_tests/cancel_wf.rs b/tests/integ_tests/workflow_tests/cancel_wf.rs index a29eb2bf8..a0a4b5427 100644 --- a/tests/integ_tests/workflow_tests/cancel_wf.rs +++ b/tests/integ_tests/workflow_tests/cancel_wf.rs @@ -1,10 +1,11 @@ use std::time::Duration; use temporal_client::WorkflowClientTrait; use temporal_sdk::{WfContext, WfExitValue, WorkflowResult}; +use temporal_sdk_core_protos::temporal::api::common::v1::Payload; use temporal_sdk_core_protos::temporal::api::enums::v1::WorkflowExecutionStatus; use temporal_sdk_core_test_utils::CoreWfStarter; -async fn cancelled_wf(mut ctx: WfContext) -> WorkflowResult<()> { +async fn cancelled_wf(mut ctx: WfContext) -> WorkflowResult { let cancelled = tokio::select! { _ = ctx.timer(Duration::from_secs(500)) => false, _ = ctx.cancelled() => true diff --git a/tests/integ_tests/workflow_tests/child_workflows.rs b/tests/integ_tests/workflow_tests/child_workflows.rs index 6a46187d7..832397b11 100644 --- a/tests/integ_tests/workflow_tests/child_workflows.rs +++ b/tests/integ_tests/workflow_tests/child_workflows.rs @@ -3,8 +3,11 @@ use std::time::Duration; use temporal_client::{WorkflowClientTrait, WorkflowOptions}; use temporal_sdk::{ChildWorkflowOptions, WfContext, WfExitValue, WorkflowResult}; use temporal_sdk_core_protos::{ - coresdk::child_workflow::{child_workflow_result, ChildWorkflowCancellationType, Success}, - temporal::api::enums::v1::ParentClosePolicy, + coresdk::{ + child_workflow::{child_workflow_result, ChildWorkflowCancellationType, Success}, + AsJsonPayloadExt, + }, + temporal::api::{common::v1::Payload, enums::v1::ParentClosePolicy}, }; use temporal_sdk_core_test_utils::CoreWfStarter; use tokio::sync::Barrier; @@ -12,11 +15,13 @@ use tokio::sync::Barrier; static PARENT_WF_TYPE: &str = "parent_wf"; static CHILD_WF_TYPE: &str = "child_wf"; -async fn child_wf(_ctx: WfContext) -> WorkflowResult<()> { - Ok(().into()) +async fn child_wf(_ctx: WfContext) -> WorkflowResult { + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } -async fn parent_wf(ctx: WfContext) -> WorkflowResult<()> { +async fn parent_wf(ctx: WfContext) -> WorkflowResult { let child = ctx.child_workflow(ChildWorkflowOptions { workflow_id: "child-1".to_owned(), workflow_type: CHILD_WF_TYPE.to_owned(), @@ -29,7 +34,9 @@ async fn parent_wf(ctx: WfContext) -> WorkflowResult<()> { .into_started() .expect("Child chould start OK"); match started.result().await.status { - Some(child_workflow_result::Status::Completed(Success { .. })) => Ok(().into()), + Some(child_workflow_result::Status::Completed(Success { .. })) => Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )), _ => Err(anyhow!("Unexpected child WF status")), } } @@ -86,7 +93,9 @@ async fn abandoned_child_bug_repro() { // Need to do something else, so we'll see the ChildWorkflowExecutionCanceled event ctx.timer(Duration::from_secs(1)).await; started.result().await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }, ); worker.register_wf(CHILD_WF_TYPE.to_string(), |mut ctx: WfContext| async move { diff --git a/tests/integ_tests/workflow_tests/continue_as_new.rs b/tests/integ_tests/workflow_tests/continue_as_new.rs index c2780ae77..e763075bd 100644 --- a/tests/integ_tests/workflow_tests/continue_as_new.rs +++ b/tests/integ_tests/workflow_tests/continue_as_new.rs @@ -2,9 +2,11 @@ use std::time::Duration; use temporal_client::WorkflowOptions; use temporal_sdk::{WfContext, WfExitValue, WorkflowResult}; use temporal_sdk_core_protos::coresdk::workflow_commands::ContinueAsNewWorkflowExecution; +use temporal_sdk_core_protos::coresdk::AsJsonPayloadExt; +use temporal_sdk_core_protos::temporal::api::common::v1::Payload; use temporal_sdk_core_test_utils::CoreWfStarter; -async fn continue_as_new_wf(ctx: WfContext) -> WorkflowResult<()> { +async fn continue_as_new_wf(ctx: WfContext) -> WorkflowResult { let run_ct = ctx.get_args()[0].data[0]; ctx.timer(Duration::from_millis(500)).await; Ok(if run_ct < 5 { @@ -13,7 +15,7 @@ async fn continue_as_new_wf(ctx: WfContext) -> WorkflowResult<()> { ..Default::default() }) } else { - ().into() + WfExitValue::Normal("success".as_json_payload().expect("serializes fine")) }) } diff --git a/tests/integ_tests/workflow_tests/determinism.rs b/tests/integ_tests/workflow_tests/determinism.rs index af56c3ccb..81a10430b 100644 --- a/tests/integ_tests/workflow_tests/determinism.rs +++ b/tests/integ_tests/workflow_tests/determinism.rs @@ -2,11 +2,12 @@ use std::{ sync::atomic::{AtomicUsize, Ordering}, time::Duration, }; -use temporal_sdk::{ActivityOptions, WfContext, WorkflowResult}; +use temporal_sdk::{ActivityOptions, WfContext, WfExitValue, WorkflowResult}; +use temporal_sdk_core_protos::{coresdk::AsJsonPayloadExt, temporal::api::common::v1::Payload}; use temporal_sdk_core_test_utils::CoreWfStarter; static RUN_CT: AtomicUsize = AtomicUsize::new(1); -pub async fn timer_wf_nondeterministic(ctx: WfContext) -> WorkflowResult<()> { +pub async fn timer_wf_nondeterministic(ctx: WfContext) -> WorkflowResult { let run_ct = RUN_CT.fetch_add(1, Ordering::Relaxed); match run_ct { @@ -28,7 +29,9 @@ pub async fn timer_wf_nondeterministic(ctx: WfContext) -> WorkflowResult<()> { } _ => panic!("Ran too many times"), } - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } #[tokio::test] diff --git a/tests/integ_tests/workflow_tests/examples.rs b/tests/integ_tests/workflow_tests/examples.rs new file mode 100644 index 000000000..dd1677c6f --- /dev/null +++ b/tests/integ_tests/workflow_tests/examples.rs @@ -0,0 +1,469 @@ +use serde_json::json; +use temporal_client::WorkflowOptions; +use temporal_sdk::prelude::registry::*; +use temporal_sdk_core_protos::coresdk::AsJsonPayloadExt; +use temporal_sdk_core_test_utils::CoreWfStarter; + +mod activity { + use temporal_sdk::prelude::activity::*; + + #[derive(Debug, thiserror::Error)] + #[non_exhaustive] + pub enum Error { + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + Any(anyhow::Error), + } + + impl FromFailureExt for Error { + fn from_failure(failure: Failure) -> Error { + Error::Any(anyhow::anyhow!("{:?}", failure.message)) + } + } + + #[derive(Default, Deserialize, Serialize, Debug, Clone)] + pub struct ActivityOutput0 { + pub echo: String, + pub name: Option, + } + pub async fn activity_0_args( + _ctx: ActContext, + ) -> Result<(String, ActivityOutput0), anyhow::Error> { + Ok(( + "Temporal".to_string(), + ActivityOutput0 { + echo: "Hello".to_string(), + name: Some("Rust".to_string()), + }, + )) + } + pub async fn activity_0_args_exit_value( + _ctx: ActContext, + ) -> Result, anyhow::Error> { + Ok(ActExitValue::Normal(ActivityOutput0 { + echo: "Hello".to_string(), + name: Some("Rust".to_string()), + })) + } + pub async fn activity_0_args_without_ctx() -> Result<(String, ActivityOutput0), anyhow::Error> { + Ok(( + "test_tuple".to_string(), + ActivityOutput0 { + echo: "Hello".to_string(), + name: Some("Rust".to_string()), + }, + )) + } + pub async fn activity_0_args_without_ctx_exit_value( + ) -> Result, anyhow::Error> { + Ok(ActExitValue::Normal(ActivityOutput0 { + echo: "Hello".to_string(), + name: Some("Rust".to_string()), + })) + } + + #[derive(Default, Deserialize, Serialize, Debug, Clone)] + pub struct ActivityInput1(pub String, pub i32); + #[derive(Default, Deserialize, Serialize, Debug, Clone)] + pub struct ActivityOutput1(pub String, pub i32); + pub async fn activity_1_args( + _ctx: ActContext, + input: ActivityInput1, + ) -> Result { + Ok(ActivityOutput1(input.0, input.1)) + } + pub async fn activity_1_args_exit_value( + _ctx: ActContext, + input: ActivityInput1, + ) -> Result, anyhow::Error> { + Ok(ActExitValue::Normal(ActivityOutput1(input.0, input.1))) + } + pub async fn activity_1_args_with_errors( + _ctx: ActContext, + input: ActivityInput1, + ) -> Result { + Ok(ActivityOutput1(input.0, input.1)) + } + + #[derive(Default, Deserialize, Serialize, Debug, Clone)] + pub struct ActivityOutput2(pub Vec, pub String); + pub async fn activity_2_args( + _ctx: ActContext, + name: String, + size: i32, + ) -> Result { + Ok(ActivityOutput2(vec![42; size as usize], name)) + } +} + +mod workflow { + use crate::integ_tests::workflow_tests::examples::activity::{self, *}; + use std::collections::HashMap; + use temporal_sdk::prelude::workflow::*; + use temporal_sdk::workflow; + + #[derive(Default, Deserialize, Serialize, Debug, Clone)] + pub struct ChildInput1 { + pub echo: (String,), + } + #[derive(Default, Deserialize, Serialize, Debug, Clone)] + pub struct ChildOutput1 { + pub result: (String,), + } + pub async fn child_workflow_0_args(_ctx: WfContext) -> Result { + Ok(ChildOutput1 { + result: ("success".to_string(),), + }) + } + pub async fn child_workflow_0_args_exit_value( + _ctx: WfContext, + ) -> Result, anyhow::Error> { + Ok(WfExitValue::Normal("success".to_string())) + } + + #[derive(Default, Deserialize, Serialize, Debug, Clone)] + pub struct ChildInput2 { + pub echo_again: (String,), + } + #[derive(Default, Deserialize, Serialize, Debug, Clone)] + pub struct ChildOutput2 { + pub result: HashMap, + } + pub async fn child_workflow_1_args( + _ctx: WfContext, + _input: ChildInput1, + ) -> Result { + Ok(ChildOutput1 { + result: ("success".to_string(),), + }) + } + pub async fn child_workflow_1_args_exit_value( + _ctx: WfContext, + _input: ChildInput1, + ) -> Result, anyhow::Error> { + Ok(WfExitValue::Normal(ChildOutput1 { + result: ("success".to_string(),), + })) + } + pub async fn child_workflow_1_args_with_errors( + ctx: WfContext, + _input: (String,), + ) -> Result { + let activity_timeout = Duration::from_secs(5); + let output = execute_activity_1_args_with_errors( + &ctx, + ActivityOptions { + activity_id: Some("activity_1_args_with_errors".to_string()), + schedule_to_close_timeout: Some(activity_timeout), + ..Default::default() + }, + activity::activity_1_args_with_errors, + activity::ActivityInput1("hello".to_string(), 7), + ) + .await; + match output { + Ok(output) => Ok(output), + Err(e) => Err(e), + } + } + pub async fn child_workflow_2_args( + _ctx: WfContext, + _input: ChildInput1, + _input2: ChildInput2, + ) -> Result { + let mut result = HashMap::new(); + result.insert("one".to_string(), 1); + result.insert("two".to_string(), 2); + Ok(ChildOutput2 { result }) + } + + #[derive(Default, Deserialize, Serialize, Debug, Clone)] + pub struct Input { + pub one: String, + pub two: i64, + } + #[derive(Default, Deserialize, Serialize, Debug, Clone)] + pub struct Output { + pub result: HashMap, + } + pub async fn examples_workflow(ctx: WfContext) -> Result { + let activity_timeout = Duration::from_secs(5); + + let _output = workflow::execute_activity_0_args( + &ctx, + ActivityOptions { + activity_id: Some("activity_0_args".to_string()), + schedule_to_close_timeout: Some(activity_timeout), + ..Default::default() + }, + activity::activity_0_args, + ) + .await; + + let _output = workflow::execute_activity_0_args_exit_value( + &ctx, + ActivityOptions { + activity_id: Some("activity_0_args_exit_value".to_string()), + schedule_to_close_timeout: Some(activity_timeout), + ..Default::default() + }, + activity::activity_0_args_exit_value, + ) + .await; + + let _output = workflow::execute_activity_0_args_without_ctx( + &ctx, + ActivityOptions { + activity_id: Some("activity_0_args_without_ctx".to_string()), + schedule_to_close_timeout: Some(activity_timeout), + ..Default::default() + }, + activity::activity_0_args_without_ctx, + ) + .await; + + let _output = workflow::execute_activity_0_args_without_ctx_exit_value( + &ctx, + ActivityOptions { + activity_id: Some("activity_0_args_without_ctx_exit_value".to_string()), + schedule_to_close_timeout: Some(activity_timeout), + ..Default::default() + }, + activity::activity_0_args_without_ctx_exit_value, + ) + .await; + + let _stop = workflow::execute_activity_1_args( + &ctx, + ActivityOptions { + activity_id: Some("activity_1_args".to_string()), + schedule_to_close_timeout: Some(activity_timeout), + ..Default::default() + }, + activity::activity_1_args, + ActivityInput1("hello".to_string(), 7), + ) + .await; + + let _stop = workflow::execute_activity_1_args_exit_value( + &ctx, + ActivityOptions { + activity_id: Some("stop_activity".to_string()), + schedule_to_close_timeout: Some(activity_timeout), + ..Default::default() + }, + activity::activity_1_args_exit_value, + ActivityInput1("hello".to_string(), 7), + ) + .await; + + let _output = workflow::execute_activity_2_args( + &ctx, + ActivityOptions { + activity_id: Some("activity_2_args".to_string()), + schedule_to_close_timeout: Some(activity_timeout), + ..Default::default() + }, + activity::activity_2_args, + "hello".to_string(), + 7, + ) + .await; + + let _child_output0 = workflow::execute_child_workflow_0_args( + &ctx, + ChildWorkflowOptions { + workflow_id: "child_workflow_0_args".to_owned(), + ..Default::default() + }, + child_workflow_0_args, + //"test".to_string(), + ) + .await; + + let _child_output0 = workflow::execute_child_workflow_0_args_exit_value( + &ctx, + ChildWorkflowOptions { + workflow_id: "child_workflow_0_args_exit_value".to_owned(), + ..Default::default() + }, + child_workflow_0_args_exit_value, + //"test".to_string(), + ) + .await; + + let _child_output3 = workflow::execute_child_workflow_1_args( + &ctx, + ChildWorkflowOptions { + workflow_id: "child_workflow_1_args".to_owned(), + ..Default::default() + }, + child_workflow_1_args, + ChildInput1 { + echo: ("child_workflow_1_args".to_string(),), + }, + ) + .await + .unwrap(); + + let _child_output3 = workflow::execute_child_workflow_1_args_exit_value( + &ctx, + ChildWorkflowOptions { + workflow_id: "child_workflow_1_args_exit_value".to_owned(), + ..Default::default() + }, + child_workflow_1_args_exit_value, + ChildInput1 { + echo: ("child_workflow_1_args_exit_value".to_string(),), + }, + ) + .await + .unwrap(); + + let _child_output4 = workflow::execute_child_workflow_1_args_with_errors( + &ctx, + ChildWorkflowOptions { + workflow_id: "child_workflow_1_args_with_errors".to_owned(), + ..Default::default() + }, + child_workflow_1_args_with_errors, + ("hello".to_string(),), + ) + .await + .unwrap(); + + let child_output5 = workflow::execute_child_workflow_2_args( + &ctx, + ChildWorkflowOptions { + workflow_id: "child_workflow_2_args".to_owned(), + ..Default::default() + }, + child_workflow_2_args, + ChildInput1 { + echo: ("hello".to_string(),), + }, + ChildInput2 { + echo_again: ("hello".to_string(),), + }, + ) + .await; + + match child_output5 { + Ok(r) => Ok(Output { result: r.result }), + Err(e) => Err(e), + } + } +} + +#[tokio::test] +async fn example_workflows_test() { + use crate::integ_tests::workflow_tests::examples::activity; + use crate::integ_tests::workflow_tests::examples::workflow; + + let mut starter = CoreWfStarter::new("sdk-example-workflows"); + let mut worker = starter.worker().await; + + // Register zero argument activities + + worker.register_activity( + "integ_tests::integ_tests::workflow_tests::examples::activity::activity_0_args", + into_activity_0_args(activity::activity_0_args), + ); + worker.register_activity( + "integ_tests::integ_tests::workflow_tests::examples::activity::activity_0_args_exit_value", + into_activity_0_args(activity::activity_0_args_exit_value), + ); + worker.register_activity( + "integ_tests::integ_tests::workflow_tests::examples::activity::activity_0_args_without_ctx", + into_activity_0_args_without_ctx(activity::activity_0_args_without_ctx), + ); + worker.register_activity( + "integ_tests::integ_tests::workflow_tests::examples::activity::activity_0_args_without_ctx_exit_value", + into_activity_0_args_without_ctx(activity::activity_0_args_without_ctx_exit_value), + ); + + // Register one argument activities + + worker.register_activity( + "integ_tests::integ_tests::workflow_tests::examples::activity::activity_1_args", + into_activity_1_args(activity::activity_1_args), + ); + + worker.register_activity( + "integ_tests::integ_tests::workflow_tests::examples::activity::activity_1_args_exit_value", + into_activity_1_args_exit_value(activity::activity_1_args_exit_value), + ); + + worker.register_activity( + "integ_tests::integ_tests::workflow_tests::examples::activity::activity_1_args_with_errors", + into_activity_1_args_with_errors(activity::activity_1_args_with_errors), + ); + + // Register two argument activities + + worker.register_activity( + "integ_tests::integ_tests::workflow_tests::examples::activity::activity_2_args", + into_activity_2_args(activity::activity_2_args), + ); + + // Register zero argument workflows + + worker.register_wf( + "integ_tests::integ_tests::workflow_tests::examples::workflow::child_workflow_0_args", + into_workflow_0_args(workflow::child_workflow_0_args), + ); + + worker.register_wf( + "integ_tests::integ_tests::workflow_tests::examples::workflow::child_workflow_0_args_exit_value", + into_workflow_0_args(workflow::child_workflow_0_args_exit_value), + ); + + // Register one argument workflows + + worker.register_wf( + "integ_tests::integ_tests::workflow_tests::examples::workflow::child_workflow_1_args", + into_workflow_1_args(workflow::child_workflow_1_args), + ); + + worker.register_wf( + "integ_tests::integ_tests::workflow_tests::examples::workflow::child_workflow_1_args_exit_value", + into_workflow_1_args_exit_value(workflow::child_workflow_1_args_exit_value), + ); + + worker.register_wf( + "integ_tests::integ_tests::workflow_tests::examples::workflow::child_workflow_1_args_with_errors", + into_workflow_1_args_with_errors(workflow::child_workflow_1_args_with_errors), + ); + + // Register two argument workflows + + worker.register_wf( + "integ_tests::integ_tests::workflow_tests::examples::workflow::child_workflow_2_args", + into_workflow_2_args(workflow::child_workflow_2_args), + ); + + worker.register_wf( + "integ_tests::integ_tests::workflow_tests::examples::workflow::examples_workflow", + into_workflow_0_args(workflow::examples_workflow), + ); + + // Run parent workflow + + worker + .submit_wf( + "examples_workflow".to_string(), + "integ_tests::integ_tests::workflow_tests::examples::workflow::examples_workflow" + .to_string(), + vec![json!({ + "one":"one", + "two":42 + }) + .as_json_payload() + .expect("serialize fine")], + WorkflowOptions::default(), + ) + .await + .unwrap(); + worker.run_until_done().await.unwrap(); +} diff --git a/tests/integ_tests/workflow_tests/local_activities.rs b/tests/integ_tests/workflow_tests/local_activities.rs index 76b522afc..34dfb049d 100644 --- a/tests/integ_tests/workflow_tests/local_activities.rs +++ b/tests/integ_tests/workflow_tests/local_activities.rs @@ -7,8 +7,8 @@ use std::{ }; use temporal_client::WorkflowOptions; use temporal_sdk::{ - interceptors::WorkerInterceptor, ActContext, ActivityCancelledError, CancellableFuture, - LocalActivityOptions, WfContext, WorkflowResult, + interceptors::WorkerInterceptor, ActContext, ActExitValue, ActivityCancelledError, + CancellableFuture, LocalActivityOptions, WfContext, WfExitValue, WorkflowResult, }; use temporal_sdk_core::replay::HistoryForReplay; use temporal_sdk_core_protos::{ @@ -18,7 +18,10 @@ use temporal_sdk_core_protos::{ workflow_completion::{workflow_activation_completion, WorkflowActivationCompletion}, AsJsonPayloadExt, }, - temporal::api::{common::v1::RetryPolicy, enums::v1::TimeoutType}, + temporal::api::{ + common::v1::{Payload, RetryPolicy}, + enums::v1::TimeoutType, + }, TestHistoryBuilder, }; use temporal_sdk_core_test_utils::{ @@ -27,17 +30,19 @@ use temporal_sdk_core_test_utils::{ }; use tokio_util::sync::CancellationToken; -pub async fn one_local_activity_wf(ctx: WfContext) -> WorkflowResult<()> { +pub 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"), + input: vec!["hi!".as_json_payload().expect("serializes fine")], ..Default::default() }) .await; // Verify LA execution advances the clock assert!(initial_workflow_time < ctx.workflow_time().unwrap()); - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } #[tokio::test] @@ -52,15 +57,17 @@ async fn one_local_activity() { worker.run_until_done().await.unwrap(); } -pub async fn local_act_concurrent_with_timer_wf(ctx: WfContext) -> WorkflowResult<()> { +pub 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"), + input: vec!["hi!".as_json_payload().expect("serializes fine")], ..Default::default() }); let timer = ctx.timer(Duration::from_secs(1)); tokio::join!(la, timer); - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } #[tokio::test] @@ -75,16 +82,18 @@ async fn local_act_concurrent_with_timer() { worker.run_until_done().await.unwrap(); } -pub async fn local_act_then_timer_then_wait(ctx: WfContext) -> WorkflowResult<()> { +pub 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"), + input: vec!["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()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } #[tokio::test] @@ -106,30 +115,32 @@ async fn long_running_local_act_with_timer() { starter.workflow_options.task_timeout = Some(Duration::from_secs(1)); 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: ActContext, str: String| async { + worker.register_activity("echo_activity", |ctx: ActContext| async move { tokio::time::sleep(Duration::from_secs(4)).await; - Ok(str) + Ok(ActExitValue::Normal(ctx.get_args()[0].clone())) }); starter.start_with_worker(wf_name, &mut worker).await; worker.run_until_done().await.unwrap(); } -pub async fn local_act_fanout_wf(ctx: WfContext) -> WorkflowResult<()> { +pub 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}") + input: vec![format!("Hi {i}") .as_json_payload() - .expect("serializes fine"), + .expect("serializes fine")], ..Default::default() }) }) .collect(); ctx.timer(Duration::from_secs(1)).await; join_all(las).await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } #[tokio::test] @@ -154,7 +165,7 @@ async fn local_act_retry_timer_backoff() { let res = ctx .local_activity(LocalActivityOptions { activity_type: "echo".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), + input: vec!["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 @@ -168,10 +179,12 @@ async fn local_act_retry_timer_backoff() { }) .await; assert!(res.failed()); - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }); - worker.register_activity("echo", |_: ActContext, _: String| async { - Result::<(), _>::Err(anyhow!("Oh no I failed!")) + worker.register_activity("echo", |_: ActContext| async { + Err(anyhow!("Oh no I failed!")) }); let run_id = worker @@ -202,21 +215,23 @@ async fn cancel_immediate(#[case] cancel_type: ActivityCancellationType) { 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"), + input: vec!["hi".as_json_payload().expect("serializes fine")], cancel_type, ..Default::default() }); la.cancel(&ctx); let resolution = la.await; assert!(resolution.cancelled()); - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }); // 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: ActContext, _: String| { + worker.register_activity("echo", move |ctx: ActContext| { let manual_cancel_act = manual_cancel_act.clone(); async move { tokio::select! { @@ -226,7 +241,7 @@ async fn cancel_immediate(#[case] cancel_type: ActivityCancellationType) { } _ = manual_cancel_act.cancelled() => {} } - Ok(()) + Ok(ActExitValue::Normal(ctx.get_args()[0].clone())) } }); @@ -290,7 +305,7 @@ async fn cancel_after_act_starts( 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"), + input: vec!["hi".as_json_payload().expect("serializes fine")], retry_policy: RetryPolicy { initial_interval: Some(bo_dur.try_into().unwrap()), backoff_coefficient: 1., @@ -313,14 +328,16 @@ async fn cancel_after_act_starts( ctx.timer(Duration::from_secs(1)).await; let resolution = la.await; assert!(resolution.cancelled()); - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }); // 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: ActContext, _: String| { + worker.register_activity("echo", move |ctx: ActContext| { let manual_cancel_act = manual_cancel_act.clone(); async move { if cancel_on_backoff.is_some() { @@ -336,7 +353,7 @@ async fn cancel_after_act_starts( return Err(anyhow!(ActivityCancelledError::default())) } _ = manual_cancel_act.cancelled() => { - return Ok(()) + return Ok(ActExitValue::Normal(ctx.get_args()[0].clone())) } } } @@ -383,7 +400,7 @@ async fn x_to_close_timeout(#[case] is_schedule: bool) { let res = ctx .local_activity(LocalActivityOptions { activity_type: "echo".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), + input: vec!["hi".as_json_payload().expect("serializes fine")], retry_policy: RetryPolicy { initial_interval: Some(prost_dur!(from_micros(15))), backoff_coefficient: 1_000., @@ -398,16 +415,18 @@ async fn x_to_close_timeout(#[case] is_schedule: bool) { }) .await; assert_eq!(res.timed_out(), Some(timeout_type)); - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }); - worker.register_activity("echo", |ctx: ActContext, _: String| async move { + worker.register_activity("echo", |ctx: ActContext| async move { tokio::select! { _ = tokio::time::sleep(Duration::from_secs(100)) => {}, _ = ctx.cancelled() => { return Err(anyhow!(ActivityCancelledError::default())) } - }; - Ok(()) + } + Ok(ActExitValue::Normal(ctx.get_args()[0].clone())) }); starter.start_with_worker(wf_name, &mut worker).await; @@ -432,7 +451,7 @@ async fn schedule_to_close_timeout_across_timer_backoff(#[case] cached: bool) { let res = ctx .local_activity(LocalActivityOptions { activity_type: "echo".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), + input: vec!["hi".as_json_payload().expect("serializes fine")], retry_policy: RetryPolicy { initial_interval: Some(prost_dur!(from_millis(15))), backoff_coefficient: 1_000., @@ -446,12 +465,14 @@ async fn schedule_to_close_timeout_across_timer_backoff(#[case] cached: bool) { }) .await; assert_eq!(res.timed_out(), Some(TimeoutType::ScheduleToClose)); - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }); let num_attempts: &'static _ = Box::leak(Box::new(AtomicU8::new(0))); - worker.register_activity("echo", move |_: ActContext, _: String| async { + worker.register_activity("echo", move |_: ActContext| async { num_attempts.fetch_add(1, Ordering::Relaxed); - Result::<(), _>::Err(anyhow!("Oh no I failed!")) + Err(anyhow!("Oh no I failed!")) }); starter.start_with_worker(wf_name, &mut worker).await; @@ -469,9 +490,9 @@ async fn eviction_wont_make_local_act_get_dropped(#[values(true, false)] short_w starter.max_cached_workflows(0); 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: ActContext, str: String| async { + worker.register_activity("echo_activity", |ctx: ActContext| async move { tokio::time::sleep(Duration::from_secs(4)).await; - Ok(str) + Ok(ActExitValue::Normal(ctx.get_args()[0].clone())) }); let opts = if short_wft_timeout { @@ -497,7 +518,7 @@ async fn timer_backoff_concurrent_with_non_timer_backoff() { 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"), + input: vec!["hi".as_json_payload().expect("serializes fine")], retry_policy: RetryPolicy { initial_interval: Some(prost_dur!(from_micros(15))), backoff_coefficient: 1_000., @@ -510,7 +531,7 @@ async fn timer_backoff_concurrent_with_non_timer_backoff() { }); let r2 = ctx.local_activity(LocalActivityOptions { activity_type: "echo".to_string(), - input: "hi".as_json_payload().expect("serializes fine"), + input: vec!["hi".as_json_payload().expect("serializes fine")], retry_policy: RetryPolicy { initial_interval: Some(prost_dur!(from_millis(15))), backoff_coefficient: 10., @@ -524,10 +545,12 @@ async fn timer_backoff_concurrent_with_non_timer_backoff() { let (r1, r2) = tokio::join!(r1, r2); assert!(r1.failed()); assert!(r2.failed()); - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }); - worker.register_activity("echo", |_: ActContext, _: String| async { - Result::<(), _>::Err(anyhow!("Oh no I failed!")) + worker.register_activity("echo", |_: ActContext| async { + Err(anyhow!("Oh no I failed!")) }); starter.start_with_worker(wf_name, &mut worker).await; @@ -544,7 +567,7 @@ async fn repro_nondeterminism_with_timer_bug() { 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"), + input: vec!["hi".as_json_payload().expect("serializes fine")], retry_policy: RetryPolicy { initial_interval: Some(prost_dur!(from_micros(15))), backoff_coefficient: 1_000., @@ -563,11 +586,13 @@ async fn repro_nondeterminism_with_timer_bug() { }, } ctx.timer(Duration::from_secs(1)).await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }); - worker.register_activity("delay", |_: ActContext, _: String| async { + worker.register_activity("delay", |ctx: ActContext| async move { tokio::time::sleep(Duration::from_secs(2)).await; - Ok(()) + Ok(ActExitValue::Normal(ctx.get_args()[0].clone())) }); let run_id = worker @@ -609,9 +634,9 @@ 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", |_: ActContext, _: String| async { + worker.register_activity("delay", |ctx: ActContext| async move { tokio::time::sleep(Duration::from_secs(15)).await; - Ok(()) + Ok(ActExitValue::Normal(ctx.get_args()[0].clone())) }); worker.run().await.unwrap(); } @@ -635,9 +660,9 @@ async fn second_weird_la_nondeterminism_repro() { "evict_while_la_running_no_interference", la_problem_workflow, ); - worker.register_activity("delay", |_: ActContext, _: String| async { + worker.register_activity("delay", |ctx: ActContext| async move { tokio::time::sleep(Duration::from_secs(15)).await; - Ok(()) + Ok(ActExitValue::Normal(ctx.get_args()[0].clone())) }); worker.run().await.unwrap(); } @@ -659,9 +684,9 @@ async fn third_weird_la_nondeterminism_repro() { "evict_while_la_running_no_interference", la_problem_workflow, ); - worker.register_activity("delay", |_: ActContext, _: String| async { + worker.register_activity("delay", |ctx: ActContext| async move { tokio::time::sleep(Duration::from_secs(15)).await; - Ok(()) + Ok(ActExitValue::Normal(ctx.get_args()[0].clone())) }); worker.run().await.unwrap(); } diff --git a/tests/integ_tests/workflow_tests/modify_wf_properties.rs b/tests/integ_tests/workflow_tests/modify_wf_properties.rs index 17d9cadeb..6a40ffe7e 100644 --- a/tests/integ_tests/workflow_tests/modify_wf_properties.rs +++ b/tests/integ_tests/workflow_tests/modify_wf_properties.rs @@ -1,18 +1,23 @@ use temporal_client::WorkflowClientTrait; -use temporal_sdk::{WfContext, WorkflowResult}; -use temporal_sdk_core_protos::coresdk::{AsJsonPayloadExt, FromJsonPayloadExt}; +use temporal_sdk::{WfContext, WfExitValue, WorkflowResult}; +use temporal_sdk_core_protos::{ + coresdk::{AsJsonPayloadExt, FromJsonPayloadExt}, + temporal::api::common::v1::Payload, +}; use temporal_sdk_core_test_utils::CoreWfStarter; use uuid::Uuid; static FIELD_A: &str = "cat_name"; static FIELD_B: &str = "cute_level"; -async fn memo_upserter(ctx: WfContext) -> WorkflowResult<()> { +async fn memo_upserter(ctx: WfContext) -> WorkflowResult { ctx.upsert_memo([ (FIELD_A.to_string(), "enchi".as_json_payload().unwrap()), (FIELD_B.to_string(), 9001.as_json_payload().unwrap()), ]); - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } #[tokio::test] diff --git a/tests/integ_tests/workflow_tests/patches.rs b/tests/integ_tests/workflow_tests/patches.rs index bf90eac15..a703cbd9a 100644 --- a/tests/integ_tests/workflow_tests/patches.rs +++ b/tests/integ_tests/workflow_tests/patches.rs @@ -3,12 +3,13 @@ use std::{ time::Duration, }; -use temporal_sdk::{WfContext, WorkflowResult}; +use temporal_sdk::{WfContext, WfExitValue, WorkflowResult}; +use temporal_sdk_core_protos::{coresdk::AsJsonPayloadExt, temporal::api::common::v1::Payload}; use temporal_sdk_core_test_utils::CoreWfStarter; const MY_PATCH_ID: &str = "integ_test_change_name"; -pub async fn changes_wf(ctx: WfContext) -> WorkflowResult<()> { +pub async fn changes_wf(ctx: WfContext) -> WorkflowResult { if ctx.patched(MY_PATCH_ID) { ctx.timer(Duration::from_millis(100)).await; } else { @@ -20,7 +21,9 @@ pub async fn changes_wf(ctx: WfContext) -> WorkflowResult<()> { } else { ctx.timer(Duration::from_millis(200)).await; } - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } #[tokio::test] @@ -38,7 +41,7 @@ async fn writes_change_markers() { /// This one simulates a run as if the worker had the "old" code, then it fails at the end as /// a cheapo way of being re-run, at which point it runs with change checks and the "new" code. static DID_DIE: AtomicBool = AtomicBool::new(false); -pub async fn no_change_then_change_wf(ctx: WfContext) -> WorkflowResult<()> { +pub async fn no_change_then_change_wf(ctx: WfContext) -> WorkflowResult { if DID_DIE.load(Ordering::Acquire) { assert!(!ctx.patched(MY_PATCH_ID)); } @@ -53,7 +56,9 @@ pub async fn no_change_then_change_wf(ctx: WfContext) -> WorkflowResult<()> { DID_DIE.store(true, Ordering::Release); ctx.force_task_fail(anyhow::anyhow!("i'm ded")); } - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } #[tokio::test] @@ -69,14 +74,16 @@ async fn can_add_change_markers() { } static DID_DIE_2: AtomicBool = AtomicBool::new(false); -pub async fn replay_with_change_marker_wf(ctx: WfContext) -> WorkflowResult<()> { +pub async fn replay_with_change_marker_wf(ctx: WfContext) -> WorkflowResult { assert!(ctx.patched(MY_PATCH_ID)); ctx.timer(Duration::from_millis(200)).await; if !DID_DIE_2.load(Ordering::Acquire) { DID_DIE_2.store(true, Ordering::Release); ctx.force_task_fail(anyhow::anyhow!("i'm ded")); } - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } #[tokio::test] @@ -111,7 +118,9 @@ async fn patched_on_second_workflow_task_is_deterministic() { } assert!(ctx.patched(MY_PATCH_ID)); ctx.timer(Duration::from_millis(1)).await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }); starter.start_with_worker(wf_name, &mut worker).await; diff --git a/tests/integ_tests/workflow_tests/replay.rs b/tests/integ_tests/workflow_tests/replay.rs index ea8e74b94..8032eb713 100644 --- a/tests/integ_tests/workflow_tests/replay.rs +++ b/tests/integ_tests/workflow_tests/replay.rs @@ -2,7 +2,9 @@ use crate::integ_tests::workflow_tests::patches::changes_wf; use assert_matches::assert_matches; use parking_lot::Mutex; use std::{collections::HashSet, sync::Arc, time::Duration}; -use temporal_sdk::{interceptors::WorkerInterceptor, WfContext, Worker, WorkflowFunction}; +use temporal_sdk::{ + interceptors::WorkerInterceptor, WfContext, WfExitValue, Worker, WorkflowFunction, +}; use temporal_sdk_core::replay::{HistoryFeeder, HistoryForReplay}; use temporal_sdk_core_api::errors::{PollActivityError, PollWfError}; use temporal_sdk_core_protos::{ @@ -10,6 +12,7 @@ use temporal_sdk_core_protos::{ workflow_activation::remove_from_cache::EvictionReason, workflow_commands::{ScheduleActivity, StartTimer}, workflow_completion::WorkflowActivationCompletion, + AsJsonPayloadExt, }, temporal::api::enums::v1::EventType, TestHistoryBuilder, DEFAULT_WORKFLOW_TYPE, @@ -274,7 +277,9 @@ fn timers_wf(num_timers: u32) -> WorkflowFunction { for _ in 1..=num_timers { ctx.timer(Duration::from_secs(1)).await; } - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }) } diff --git a/tests/integ_tests/workflow_tests/resets.rs b/tests/integ_tests/workflow_tests/resets.rs index 5fdbd90fe..b2590c059 100644 --- a/tests/integ_tests/workflow_tests/resets.rs +++ b/tests/integ_tests/workflow_tests/resets.rs @@ -1,9 +1,12 @@ use futures::StreamExt; use std::{sync::Arc, time::Duration}; use temporal_client::{WfClientExt, WorkflowClientTrait, WorkflowOptions, WorkflowService}; -use temporal_sdk::WfContext; -use temporal_sdk_core_protos::temporal::api::{ - common::v1::WorkflowExecution, workflowservice::v1::ResetWorkflowExecutionRequest, +use temporal_sdk::{WfContext, WfExitValue}; +use temporal_sdk_core_protos::{ + coresdk::AsJsonPayloadExt, + temporal::api::{ + common::v1::WorkflowExecution, workflowservice::v1::ResetWorkflowExecutionRequest, + }, }; use temporal_sdk_core_test_utils::{CoreWfStarter, NAMESPACE}; use tokio::sync::Notify; @@ -33,7 +36,9 @@ async fn reset_workflow() { .next() .await .unwrap(); - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } }); diff --git a/tests/integ_tests/workflow_tests/signals.rs b/tests/integ_tests/workflow_tests/signals.rs index e0c76f1cd..b2c85920c 100644 --- a/tests/integ_tests/workflow_tests/signals.rs +++ b/tests/integ_tests/workflow_tests/signals.rs @@ -5,16 +5,19 @@ use temporal_client::{ SignalWithStartOptions, WorkflowClientTrait, WorkflowExecutionInfo, WorkflowOptions, }; use temporal_sdk::{ - ChildWorkflowOptions, Signal, SignalWorkflowOptions, WfContext, WorkflowResult, + ChildWorkflowOptions, Signal, SignalWorkflowOptions, WfContext, WfExitValue, WorkflowResult, +}; +use temporal_sdk_core_protos::{ + coresdk::{AsJsonPayloadExt, IntoPayloadsExt}, + temporal::api::common::v1::Payload, }; -use temporal_sdk_core_protos::{coresdk::IntoPayloadsExt, temporal::api::common::v1::Payload}; use temporal_sdk_core_test_utils::CoreWfStarter; use uuid::Uuid; const SIGNAME: &str = "signame"; const RECEIVER_WFID: &str = "sends-signal-signal-receiver"; -async fn signal_sender(ctx: WfContext) -> WorkflowResult<()> { +async fn signal_sender(ctx: WfContext) -> WorkflowResult { let run_id = std::str::from_utf8(&ctx.get_args()[0].data) .unwrap() .to_owned(); @@ -27,7 +30,9 @@ async fn signal_sender(ctx: WfContext) -> WorkflowResult<()> { } else { sigres.unwrap(); } - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } #[tokio::test] @@ -50,24 +55,28 @@ async fn sends_signal_to_missing_wf() { worker.run_until_done().await.unwrap(); } -async fn signal_receiver(ctx: WfContext) -> WorkflowResult<()> { +async fn signal_receiver(ctx: WfContext) -> WorkflowResult { let res = ctx.make_signal_channel(SIGNAME).next().await.unwrap(); assert_eq!(&res.input, &[b"hi!".into()]); assert_eq!( *res.headers.get("tupac").expect("tupac header exists"), b"shakur".into() ); - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } -async fn signal_with_create_wf_receiver(ctx: WfContext) -> WorkflowResult<()> { +async fn signal_with_create_wf_receiver(ctx: WfContext) -> WorkflowResult { let res = ctx.make_signal_channel(SIGNAME).next().await.unwrap(); assert_eq!(&res.input, &[b"tada".into()]); assert_eq!( *res.headers.get("tupac").expect("tupac header exists"), b"shakur".into() ); - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } #[tokio::test] @@ -132,7 +141,7 @@ async fn sends_signal_with_create_wf() { worker.run_until_done().await.unwrap(); } -async fn signals_child(ctx: WfContext) -> WorkflowResult<()> { +async fn signals_child(ctx: WfContext) -> WorkflowResult { let started_child = ctx .child_workflow(ChildWorkflowOptions { workflow_id: "my_precious_child".to_string(), @@ -147,7 +156,9 @@ async fn signals_child(ctx: WfContext) -> WorkflowResult<()> { sig.data.with_header("tupac", b"shakur"); started_child.signal(&ctx, sig).await.unwrap(); started_child.result().await.status.unwrap(); - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } #[tokio::test] diff --git a/tests/integ_tests/workflow_tests/stickyness.rs b/tests/integ_tests/workflow_tests/stickyness.rs index d1c1948b5..048c79ca9 100644 --- a/tests/integ_tests/workflow_tests/stickyness.rs +++ b/tests/integ_tests/workflow_tests/stickyness.rs @@ -4,7 +4,8 @@ use std::{ time::Duration, }; use temporal_client::WorkflowOptions; -use temporal_sdk::{WfContext, WorkflowResult}; +use temporal_sdk::{WfContext, WfExitValue, WorkflowResult}; +use temporal_sdk_core_protos::{coresdk::AsJsonPayloadExt, temporal::api::common::v1::Payload}; use temporal_sdk_core_test_utils::CoreWfStarter; use tokio::sync::Barrier; @@ -23,7 +24,7 @@ async fn timer_workflow_not_sticky() { static TIMED_OUT_ONCE: AtomicBool = AtomicBool::new(false); static RUN_CT: AtomicUsize = AtomicUsize::new(0); -async fn timer_timeout_wf(ctx: WfContext) -> WorkflowResult<()> { +async fn timer_timeout_wf(ctx: WfContext) -> WorkflowResult { RUN_CT.fetch_add(1, Ordering::SeqCst); let t = ctx.timer(Duration::from_secs(1)); if !TIMED_OUT_ONCE.load(Ordering::SeqCst) { @@ -31,7 +32,9 @@ async fn timer_timeout_wf(ctx: WfContext) -> WorkflowResult<()> { TIMED_OUT_ONCE.store(true, Ordering::SeqCst); } t.await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } #[tokio::test] @@ -62,7 +65,9 @@ async fn cache_miss_ok() { worker.register_wf(wf_name.to_owned(), move |ctx: WfContext| async move { barr.wait().await; ctx.timer(Duration::from_secs(1)).await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) }); let run_id = worker diff --git a/tests/integ_tests/workflow_tests/timers.rs b/tests/integ_tests/workflow_tests/timers.rs index 653666257..0c592a2c3 100644 --- a/tests/integ_tests/workflow_tests/timers.rs +++ b/tests/integ_tests/workflow_tests/timers.rs @@ -1,18 +1,24 @@ use std::time::Duration; -use temporal_sdk::{WfContext, WorkflowResult}; -use temporal_sdk_core_protos::coresdk::{ - workflow_commands::{CancelTimer, CompleteWorkflowExecution, StartTimer}, - workflow_completion::WorkflowActivationCompletion, +use temporal_sdk::{WfContext, WfExitValue, WorkflowResult}; +use temporal_sdk_core_protos::{ + coresdk::{ + workflow_commands::{CancelTimer, CompleteWorkflowExecution, StartTimer}, + workflow_completion::WorkflowActivationCompletion, + AsJsonPayloadExt, + }, + temporal::api::common::v1::Payload, }; use temporal_sdk_core_test_utils::{ drain_pollers_and_shutdown, init_core_and_create_wf, start_timer_cmd, CoreWfStarter, WorkerTestHelpers, }; -pub async fn timer_wf(command_sink: WfContext) -> WorkflowResult<()> { +pub async fn timer_wf(command_sink: WfContext) -> WorkflowResult { command_sink.timer(Duration::from_secs(1)).await; - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } #[tokio::test] @@ -100,11 +106,13 @@ async fn timer_immediate_cancel_workflow() { .unwrap(); } -async fn parallel_timer_wf(command_sink: WfContext) -> WorkflowResult<()> { +async fn parallel_timer_wf(command_sink: WfContext) -> WorkflowResult { let t1 = command_sink.timer(Duration::from_secs(1)); let t2 = command_sink.timer(Duration::from_secs(1)); let _ = tokio::join!(t1, t2); - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } #[tokio::test] diff --git a/tests/integ_tests/workflow_tests/upsert_search_attrs.rs b/tests/integ_tests/workflow_tests/upsert_search_attrs.rs index bfbe515a9..b59db7d14 100644 --- a/tests/integ_tests/workflow_tests/upsert_search_attrs.rs +++ b/tests/integ_tests/workflow_tests/upsert_search_attrs.rs @@ -1,7 +1,10 @@ use std::{collections::HashMap, env}; use temporal_client::{WorkflowClientTrait, WorkflowOptions}; -use temporal_sdk::{WfContext, WorkflowResult}; -use temporal_sdk_core_protos::coresdk::{AsJsonPayloadExt, FromJsonPayloadExt}; +use temporal_sdk::{WfContext, WfExitValue, WorkflowResult}; +use temporal_sdk_core_protos::{ + coresdk::{AsJsonPayloadExt, FromJsonPayloadExt}, + temporal::api::common::v1::Payload, +}; use temporal_sdk_core_test_utils::{CoreWfStarter, INTEG_TEMPORAL_DEV_SERVER_USED_ENV_VAR}; use tracing::warn; use uuid::Uuid; @@ -11,12 +14,14 @@ use uuid::Uuid; static TXT_ATTR: &str = "CustomTextField"; static INT_ATTR: &str = "CustomIntField"; -async fn search_attr_updater(ctx: WfContext) -> WorkflowResult<()> { +async fn search_attr_updater(ctx: WfContext) -> WorkflowResult { ctx.upsert_search_attributes([ (TXT_ATTR.to_string(), "goodbye".as_json_payload().unwrap()), (INT_ATTR.to_string(), 98.as_json_payload().unwrap()), ]); - Ok(().into()) + Ok(WfExitValue::Normal( + "success".as_json_payload().expect("serializes fine"), + )) } #[tokio::test]