@temporalio/core-bridge 1.13.0 → 1.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/Cargo.lock +239 -382
  2. package/Cargo.toml +11 -11
  3. package/lib/native.d.ts +10 -3
  4. package/package.json +3 -3
  5. package/releases/aarch64-apple-darwin/index.node +0 -0
  6. package/releases/aarch64-unknown-linux-gnu/index.node +0 -0
  7. package/releases/x86_64-apple-darwin/index.node +0 -0
  8. package/releases/x86_64-pc-windows-msvc/index.node +0 -0
  9. package/releases/x86_64-unknown-linux-gnu/index.node +0 -0
  10. package/sdk-core/.cargo/config.toml +71 -11
  11. package/sdk-core/.clippy.toml +1 -0
  12. package/sdk-core/.github/workflows/heavy.yml +2 -0
  13. package/sdk-core/.github/workflows/per-pr.yml +50 -18
  14. package/sdk-core/ARCHITECTURE.md +44 -48
  15. package/sdk-core/Cargo.toml +26 -7
  16. package/sdk-core/README.md +4 -0
  17. package/sdk-core/arch_docs/diagrams/TimerMachine_Coverage.puml +14 -0
  18. package/sdk-core/arch_docs/diagrams/initial_event_history.png +0 -0
  19. package/sdk-core/arch_docs/sdks_intro.md +299 -0
  20. package/sdk-core/client/Cargo.toml +8 -7
  21. package/sdk-core/client/src/callback_based.rs +1 -2
  22. package/sdk-core/client/src/lib.rs +485 -299
  23. package/sdk-core/client/src/metrics.rs +32 -8
  24. package/sdk-core/client/src/proxy.rs +124 -5
  25. package/sdk-core/client/src/raw.rs +598 -307
  26. package/sdk-core/client/src/replaceable.rs +253 -0
  27. package/sdk-core/client/src/retry.rs +9 -6
  28. package/sdk-core/client/src/worker_registry/mod.rs +19 -3
  29. package/sdk-core/client/src/workflow_handle/mod.rs +20 -17
  30. package/sdk-core/core/Cargo.toml +100 -31
  31. package/sdk-core/core/src/core_tests/activity_tasks.rs +55 -225
  32. package/sdk-core/core/src/core_tests/mod.rs +2 -8
  33. package/sdk-core/core/src/core_tests/queries.rs +3 -5
  34. package/sdk-core/core/src/core_tests/replay_flag.rs +3 -62
  35. package/sdk-core/core/src/core_tests/updates.rs +4 -5
  36. package/sdk-core/core/src/core_tests/workers.rs +4 -3
  37. package/sdk-core/core/src/core_tests/workflow_cancels.rs +10 -7
  38. package/sdk-core/core/src/core_tests/workflow_tasks.rs +28 -291
  39. package/sdk-core/core/src/ephemeral_server/mod.rs +15 -3
  40. package/sdk-core/core/src/internal_flags.rs +11 -1
  41. package/sdk-core/core/src/lib.rs +50 -36
  42. package/sdk-core/core/src/pollers/mod.rs +5 -5
  43. package/sdk-core/core/src/pollers/poll_buffer.rs +2 -2
  44. package/sdk-core/core/src/protosext/mod.rs +13 -5
  45. package/sdk-core/core/src/protosext/protocol_messages.rs +4 -11
  46. package/sdk-core/core/src/retry_logic.rs +256 -108
  47. package/sdk-core/core/src/telemetry/metrics.rs +1 -0
  48. package/sdk-core/core/src/telemetry/mod.rs +8 -2
  49. package/sdk-core/core/src/telemetry/prometheus_meter.rs +2 -2
  50. package/sdk-core/core/src/test_help/integ_helpers.rs +971 -0
  51. package/sdk-core/core/src/test_help/mod.rs +10 -1100
  52. package/sdk-core/core/src/test_help/unit_helpers.rs +218 -0
  53. package/sdk-core/core/src/worker/activities/activity_heartbeat_manager.rs +42 -6
  54. package/sdk-core/core/src/worker/activities/local_activities.rs +19 -19
  55. package/sdk-core/core/src/worker/activities.rs +10 -3
  56. package/sdk-core/core/src/worker/client/mocks.rs +3 -3
  57. package/sdk-core/core/src/worker/client.rs +130 -93
  58. package/sdk-core/core/src/worker/heartbeat.rs +12 -13
  59. package/sdk-core/core/src/worker/mod.rs +31 -21
  60. package/sdk-core/core/src/worker/nexus.rs +14 -3
  61. package/sdk-core/core/src/worker/slot_provider.rs +9 -0
  62. package/sdk-core/core/src/worker/tuner.rs +159 -0
  63. package/sdk-core/core/src/worker/workflow/history_update.rs +3 -265
  64. package/sdk-core/core/src/worker/workflow/machines/activity_state_machine.rs +1 -54
  65. package/sdk-core/core/src/worker/workflow/machines/cancel_external_state_machine.rs +0 -82
  66. package/sdk-core/core/src/worker/workflow/machines/cancel_workflow_state_machine.rs +0 -67
  67. package/sdk-core/core/src/worker/workflow/machines/child_workflow_state_machine.rs +1 -192
  68. package/sdk-core/core/src/worker/workflow/machines/continue_as_new_workflow_state_machine.rs +0 -43
  69. package/sdk-core/core/src/worker/workflow/machines/local_activity_state_machine.rs +6 -554
  70. package/sdk-core/core/src/worker/workflow/machines/modify_workflow_properties_state_machine.rs +0 -71
  71. package/sdk-core/core/src/worker/workflow/machines/nexus_operation_state_machine.rs +102 -3
  72. package/sdk-core/core/src/worker/workflow/machines/patch_state_machine.rs +10 -539
  73. package/sdk-core/core/src/worker/workflow/machines/signal_external_state_machine.rs +0 -139
  74. package/sdk-core/core/src/worker/workflow/machines/timer_state_machine.rs +1 -119
  75. package/sdk-core/core/src/worker/workflow/machines/upsert_search_attributes_state_machine.rs +6 -63
  76. package/sdk-core/core/src/worker/workflow/machines/workflow_machines.rs +9 -4
  77. package/sdk-core/core/src/worker/workflow/mod.rs +5 -1
  78. package/sdk-core/core/src/worker/workflow/workflow_stream.rs +8 -3
  79. package/sdk-core/core-api/Cargo.toml +4 -4
  80. package/sdk-core/core-api/src/envconfig.rs +153 -54
  81. package/sdk-core/core-api/src/lib.rs +68 -0
  82. package/sdk-core/core-api/src/telemetry/metrics.rs +2 -1
  83. package/sdk-core/core-api/src/telemetry.rs +13 -0
  84. package/sdk-core/core-c-bridge/Cargo.toml +13 -8
  85. package/sdk-core/core-c-bridge/include/temporal-sdk-core-c-bridge.h +184 -22
  86. package/sdk-core/core-c-bridge/src/client.rs +462 -184
  87. package/sdk-core/core-c-bridge/src/envconfig.rs +314 -0
  88. package/sdk-core/core-c-bridge/src/lib.rs +1 -0
  89. package/sdk-core/core-c-bridge/src/random.rs +4 -4
  90. package/sdk-core/core-c-bridge/src/runtime.rs +22 -23
  91. package/sdk-core/core-c-bridge/src/testing.rs +1 -4
  92. package/sdk-core/core-c-bridge/src/tests/context.rs +31 -31
  93. package/sdk-core/core-c-bridge/src/tests/mod.rs +32 -28
  94. package/sdk-core/core-c-bridge/src/tests/utils.rs +7 -7
  95. package/sdk-core/core-c-bridge/src/worker.rs +319 -66
  96. package/sdk-core/fsm/rustfsm_procmacro/src/lib.rs +6 -1
  97. package/sdk-core/fsm/rustfsm_procmacro/tests/trybuild/dupe_transitions_fail.stderr +5 -5
  98. package/sdk-core/sdk/Cargo.toml +8 -2
  99. package/sdk-core/sdk/src/activity_context.rs +1 -1
  100. package/sdk-core/sdk/src/app_data.rs +1 -1
  101. package/sdk-core/sdk/src/interceptors.rs +1 -4
  102. package/sdk-core/sdk/src/lib.rs +1 -5
  103. package/sdk-core/sdk/src/workflow_context/options.rs +10 -1
  104. package/sdk-core/sdk/src/workflow_future.rs +1 -1
  105. package/sdk-core/sdk-core-protos/Cargo.toml +6 -6
  106. package/sdk-core/sdk-core-protos/build.rs +10 -23
  107. package/sdk-core/sdk-core-protos/protos/api_upstream/.github/workflows/create-release.yml +9 -1
  108. package/sdk-core/sdk-core-protos/protos/api_upstream/openapi/openapiv2.json +254 -5
  109. package/sdk-core/sdk-core-protos/protos/api_upstream/openapi/openapiv3.yaml +234 -5
  110. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/common/v1/message.proto +1 -1
  111. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/deployment/v1/message.proto +6 -0
  112. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/namespace/v1/message.proto +6 -2
  113. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/workflowservice/v1/request_response.proto +60 -2
  114. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/workflowservice/v1/service.proto +30 -6
  115. package/sdk-core/sdk-core-protos/protos/local/temporal/sdk/core/workflow_activation/workflow_activation.proto +2 -0
  116. package/sdk-core/{test-utils → sdk-core-protos}/src/canned_histories.rs +5 -5
  117. package/sdk-core/sdk-core-protos/src/history_builder.rs +2 -2
  118. package/sdk-core/sdk-core-protos/src/lib.rs +25 -9
  119. package/sdk-core/sdk-core-protos/src/test_utils.rs +89 -0
  120. package/sdk-core/sdk-core-protos/src/utilities.rs +14 -5
  121. package/sdk-core/tests/c_bridge_smoke_test.c +10 -0
  122. package/sdk-core/tests/cloud_tests.rs +10 -8
  123. package/sdk-core/tests/common/http_proxy.rs +134 -0
  124. package/sdk-core/{test-utils/src/lib.rs → tests/common/mod.rs} +214 -281
  125. package/sdk-core/{test-utils/src → tests/common}/workflows.rs +4 -3
  126. package/sdk-core/tests/fuzzy_workflow.rs +1 -1
  127. package/sdk-core/tests/global_metric_tests.rs +8 -7
  128. package/sdk-core/tests/heavy_tests.rs +7 -3
  129. package/sdk-core/tests/integ_tests/client_tests.rs +111 -24
  130. package/sdk-core/tests/integ_tests/ephemeral_server_tests.rs +14 -9
  131. package/sdk-core/tests/integ_tests/heartbeat_tests.rs +4 -4
  132. package/sdk-core/tests/integ_tests/metrics_tests.rs +114 -14
  133. package/sdk-core/tests/integ_tests/pagination_tests.rs +273 -0
  134. package/sdk-core/tests/integ_tests/polling_tests.rs +311 -93
  135. package/sdk-core/tests/integ_tests/queries_tests.rs +4 -4
  136. package/sdk-core/tests/integ_tests/update_tests.rs +13 -7
  137. package/sdk-core/tests/integ_tests/visibility_tests.rs +26 -9
  138. package/sdk-core/tests/integ_tests/worker_tests.rs +668 -13
  139. package/sdk-core/tests/integ_tests/worker_versioning_tests.rs +40 -24
  140. package/sdk-core/tests/integ_tests/workflow_tests/activities.rs +244 -11
  141. package/sdk-core/tests/integ_tests/workflow_tests/appdata_propagation.rs +1 -1
  142. package/sdk-core/tests/integ_tests/workflow_tests/cancel_external.rs +78 -2
  143. package/sdk-core/tests/integ_tests/workflow_tests/cancel_wf.rs +61 -2
  144. package/sdk-core/tests/integ_tests/workflow_tests/child_workflows.rs +465 -7
  145. package/sdk-core/tests/integ_tests/workflow_tests/continue_as_new.rs +41 -2
  146. package/sdk-core/tests/integ_tests/workflow_tests/determinism.rs +315 -3
  147. package/sdk-core/tests/integ_tests/workflow_tests/eager.rs +1 -1
  148. package/sdk-core/tests/integ_tests/workflow_tests/local_activities.rs +1990 -14
  149. package/sdk-core/tests/integ_tests/workflow_tests/modify_wf_properties.rs +65 -2
  150. package/sdk-core/tests/integ_tests/workflow_tests/nexus.rs +123 -23
  151. package/sdk-core/tests/integ_tests/workflow_tests/patches.rs +525 -3
  152. package/sdk-core/tests/integ_tests/workflow_tests/replay.rs +65 -16
  153. package/sdk-core/tests/integ_tests/workflow_tests/resets.rs +32 -23
  154. package/sdk-core/tests/integ_tests/workflow_tests/signals.rs +126 -5
  155. package/sdk-core/tests/integ_tests/workflow_tests/stickyness.rs +1 -2
  156. package/sdk-core/tests/integ_tests/workflow_tests/timers.rs +124 -8
  157. package/sdk-core/tests/integ_tests/workflow_tests/upsert_search_attrs.rs +62 -2
  158. package/sdk-core/tests/integ_tests/workflow_tests.rs +67 -8
  159. package/sdk-core/tests/main.rs +26 -17
  160. package/sdk-core/tests/manual_tests.rs +5 -1
  161. package/sdk-core/tests/runner.rs +22 -40
  162. package/sdk-core/tests/shared_tests/mod.rs +1 -1
  163. package/sdk-core/tests/shared_tests/priority.rs +1 -1
  164. package/sdk-core/{core/benches/workflow_replay.rs → tests/workflow_replay_bench.rs} +10 -5
  165. package/src/client.rs +97 -20
  166. package/src/helpers/callbacks.rs +4 -4
  167. package/src/helpers/errors.rs +7 -1
  168. package/src/helpers/handles.rs +1 -0
  169. package/src/helpers/try_from_js.rs +4 -3
  170. package/src/lib.rs +3 -2
  171. package/src/metrics.rs +3 -0
  172. package/src/runtime.rs +5 -2
  173. package/src/worker.rs +9 -12
  174. package/ts/native.ts +13 -3
  175. package/sdk-core/arch_docs/diagrams/workflow_internals.svg +0 -1
  176. package/sdk-core/core/src/core_tests/child_workflows.rs +0 -281
  177. package/sdk-core/core/src/core_tests/determinism.rs +0 -318
  178. package/sdk-core/core/src/core_tests/local_activities.rs +0 -1442
  179. package/sdk-core/test-utils/Cargo.toml +0 -38
  180. package/sdk-core/test-utils/src/histfetch.rs +0 -28
  181. package/sdk-core/test-utils/src/interceptors.rs +0 -46
@@ -1,39 +1,64 @@
1
- use crate::integ_tests::activity_functions::echo;
1
+ use crate::common::{
2
+ ActivationAssertionsInterceptor, CoreWfStarter, WorkflowHandleExt, build_fake_sdk,
3
+ history_from_proto_binary, init_core_replay_preloaded, mock_sdk, mock_sdk_cfg,
4
+ replay_sdk_worker, workflows::la_problem_workflow,
5
+ };
2
6
  use anyhow::anyhow;
3
- use futures_util::future::join_all;
7
+ use crossbeam_queue::SegQueue;
8
+ use futures_util::{FutureExt, future::join_all};
4
9
  use rstest::Context;
5
10
  use std::{
11
+ collections::HashMap,
12
+ ops::Sub,
6
13
  sync::{
7
14
  Arc,
8
- atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering},
15
+ atomic::{AtomicBool, AtomicI64, AtomicU8, AtomicUsize, Ordering},
9
16
  },
10
- time::Duration,
17
+ time::{Duration, Instant, SystemTime},
11
18
  };
12
19
  use temporal_client::{WfClientExt, WorkflowClientTrait, WorkflowOptions};
13
20
  use temporal_sdk::{
14
21
  ActContext, ActivityError, ActivityOptions, CancellableFuture, LocalActivityOptions,
15
- UpdateContext, WfContext, WorkflowResult,
22
+ UpdateContext, WfContext, WorkflowFunction, WorkflowResult,
16
23
  interceptors::{FailOnNondeterminismInterceptor, WorkerInterceptor},
17
24
  };
18
- use temporal_sdk_core::replay::HistoryForReplay;
25
+ use temporal_sdk_core::{
26
+ prost_dur,
27
+ replay::{DEFAULT_WORKFLOW_TYPE, HistoryForReplay, TestHistoryBuilder, default_wes_attribs},
28
+ test_help::{
29
+ LEGACY_QUERY_ID, MockPollCfg, ResponseType, WorkerExt, WorkerTestHelpers,
30
+ build_mock_pollers, hist_to_poll_resp, mock_worker, mock_worker_client,
31
+ single_hist_mock_sg,
32
+ },
33
+ };
34
+ use temporal_sdk_core_api::{Worker, errors::PollError};
19
35
  use temporal_sdk_core_protos::{
20
- TestHistoryBuilder,
36
+ DEFAULT_ACTIVITY_TYPE, canned_histories,
21
37
  coresdk::{
22
- AsJsonPayloadExt, IntoPayloadsExt,
23
- workflow_commands::{ActivityCancellationType, workflow_command::Variant},
38
+ ActivityTaskCompletion, AsJsonPayloadExt, FromJsonPayloadExt, IntoPayloadsExt,
39
+ activity_result::ActivityExecutionResult,
40
+ workflow_activation::{WorkflowActivationJob, workflow_activation_job},
41
+ workflow_commands::{
42
+ ActivityCancellationType, ScheduleLocalActivity, workflow_command::Variant,
43
+ },
24
44
  workflow_completion,
25
45
  workflow_completion::{WorkflowActivationCompletion, workflow_activation_completion},
26
46
  },
27
47
  temporal::api::{
48
+ command::v1::{RecordMarkerCommandAttributes, command},
28
49
  common::v1::RetryPolicy,
29
- enums::v1::{TimeoutType, UpdateWorkflowExecutionLifecycleStage},
50
+ enums::v1::{
51
+ CommandType, EventType, TimeoutType, UpdateWorkflowExecutionLifecycleStage,
52
+ WorkflowTaskFailedCause,
53
+ },
54
+ failure::v1::{Failure, failure::FailureInfo},
55
+ history::v1::history_event::Attributes::MarkerRecordedEventAttributes,
56
+ query::v1::WorkflowQuery,
30
57
  update::v1::WaitPolicy,
31
58
  },
59
+ test_utils::{query_ok, schedule_local_activity_cmd, start_timer_cmd},
32
60
  };
33
- use temporal_sdk_core_test_utils::{
34
- CoreWfStarter, WorkflowHandleExt, history_from_proto_binary, init_core_replay_preloaded,
35
- replay_sdk_worker, workflows::la_problem_workflow,
36
- };
61
+ use tokio::{join, select, sync::Barrier};
37
62
  use tokio_util::sync::CancellationToken;
38
63
 
39
64
  pub(crate) async fn one_local_activity_wf(ctx: WfContext) -> WorkflowResult<()> {
@@ -906,3 +931,1954 @@ async fn local_activity_with_heartbeat_only_causes_one_wakeup() {
906
931
  .unwrap();
907
932
  assert_eq!(res[0], replay_res.unwrap());
908
933
  }
934
+
935
+ pub(crate) async fn local_activity_with_summary_wf(ctx: WfContext) -> WorkflowResult<()> {
936
+ ctx.local_activity(LocalActivityOptions {
937
+ activity_type: "echo_activity".to_string(),
938
+ input: "hi!".as_json_payload().expect("serializes fine"),
939
+ summary: Some("Echo summary".to_string()),
940
+ ..Default::default()
941
+ })
942
+ .await;
943
+ Ok(().into())
944
+ }
945
+
946
+ #[tokio::test]
947
+ async fn local_activity_with_summary() {
948
+ let wf_name = "local_activity_with_summary";
949
+ let mut starter = CoreWfStarter::new(wf_name);
950
+ let mut worker = starter.worker().await;
951
+ worker.register_wf(wf_name.to_owned(), local_activity_with_summary_wf);
952
+ worker.register_activity("echo_activity", echo);
953
+
954
+ let handle = starter.start_with_worker(wf_name, &mut worker).await;
955
+ worker.run_until_done().await.unwrap();
956
+ handle
957
+ .fetch_history_and_replay(worker.inner_mut())
958
+ .await
959
+ .unwrap();
960
+
961
+ let la_events = starter
962
+ .get_history()
963
+ .await
964
+ .events
965
+ .into_iter()
966
+ .filter(|e| match e.attributes {
967
+ Some(MarkerRecordedEventAttributes(ref a)) => a.marker_name == "core_local_activity",
968
+ _ => false,
969
+ })
970
+ .collect::<Vec<_>>();
971
+ assert_eq!(la_events.len(), 1);
972
+ let summary = la_events[0]
973
+ .user_metadata
974
+ .as_ref()
975
+ .expect("metadata missing from local activity marker")
976
+ .summary
977
+ .as_ref()
978
+ .expect("summary missing from local activity marker");
979
+ assert_eq!(
980
+ "Echo summary",
981
+ String::from_json_payload(summary).expect("failed to parse summary")
982
+ );
983
+ }
984
+
985
+ async fn echo(_ctx: ActContext, e: String) -> Result<String, ActivityError> {
986
+ Ok(e)
987
+ }
988
+
989
+ /// This test verifies that when replaying we are able to resolve local activities whose data we
990
+ /// don't see until after the workflow issues the command
991
+ #[rstest::rstest]
992
+ #[case::replay(true, true)]
993
+ #[case::not_replay(false, true)]
994
+ #[case::replay_cache_off(true, false)]
995
+ #[case::not_replay_cache_off(false, false)]
996
+ #[tokio::test]
997
+ async fn local_act_two_wfts_before_marker(#[case] replay: bool, #[case] cached: bool) {
998
+ let mut t = TestHistoryBuilder::default();
999
+ t.add_by_type(EventType::WorkflowExecutionStarted);
1000
+ t.add_full_wf_task();
1001
+ let timer_started_event_id = t.add_by_type(EventType::TimerStarted);
1002
+ t.add_full_wf_task();
1003
+ t.add_local_activity_result_marker(1, "1", b"echo".into());
1004
+ t.add_timer_fired(timer_started_event_id, "1".to_string());
1005
+ t.add_full_wf_task();
1006
+ t.add_workflow_execution_completed();
1007
+
1008
+ let wf_id = "fakeid";
1009
+ let mock = mock_worker_client();
1010
+ let resps = if replay {
1011
+ vec![ResponseType::AllHistory]
1012
+ } else {
1013
+ vec![1.into(), 2.into(), ResponseType::AllHistory]
1014
+ };
1015
+ let mh = MockPollCfg::from_resp_batches(wf_id, t, resps, mock);
1016
+ let mut worker = mock_sdk_cfg(mh, |cfg| {
1017
+ if cached {
1018
+ cfg.max_cached_workflows = 1;
1019
+ }
1020
+ });
1021
+
1022
+ worker.register_wf(
1023
+ DEFAULT_WORKFLOW_TYPE.to_owned(),
1024
+ |ctx: WfContext| async move {
1025
+ let la = ctx.local_activity(LocalActivityOptions {
1026
+ activity_type: DEFAULT_ACTIVITY_TYPE.to_string(),
1027
+ input: "hi".as_json_payload().expect("serializes fine"),
1028
+ ..Default::default()
1029
+ });
1030
+ ctx.timer(Duration::from_secs(1)).await;
1031
+ la.await;
1032
+ Ok(().into())
1033
+ },
1034
+ );
1035
+ worker.register_activity(DEFAULT_ACTIVITY_TYPE, echo);
1036
+ worker
1037
+ .submit_wf(
1038
+ wf_id.to_owned(),
1039
+ DEFAULT_WORKFLOW_TYPE.to_owned(),
1040
+ vec![],
1041
+ WorkflowOptions::default(),
1042
+ )
1043
+ .await
1044
+ .unwrap();
1045
+ worker.run_until_done().await.unwrap();
1046
+ }
1047
+
1048
+ #[tokio::test]
1049
+ async fn local_act_many_concurrent() {
1050
+ let mut t = TestHistoryBuilder::default();
1051
+ t.add_by_type(EventType::WorkflowExecutionStarted);
1052
+ t.add_full_wf_task();
1053
+ let timer_started_event_id = t.add_by_type(EventType::TimerStarted);
1054
+ t.add_full_wf_task();
1055
+ for i in 1..=50 {
1056
+ t.add_local_activity_result_marker(i, &i.to_string(), b"echo".into());
1057
+ }
1058
+ t.add_timer_fired(timer_started_event_id, "1".to_string());
1059
+ t.add_full_wf_task();
1060
+ t.add_workflow_execution_completed();
1061
+
1062
+ let wf_id = "fakeid";
1063
+ let mock = mock_worker_client();
1064
+ let mh = MockPollCfg::from_resp_batches(wf_id, t, [1, 2, 3], mock);
1065
+ let mut worker = mock_sdk(mh);
1066
+
1067
+ worker.register_wf(DEFAULT_WORKFLOW_TYPE.to_owned(), local_act_fanout_wf);
1068
+ worker.register_activity("echo_activity", echo);
1069
+ worker
1070
+ .submit_wf(
1071
+ wf_id.to_owned(),
1072
+ DEFAULT_WORKFLOW_TYPE.to_owned(),
1073
+ vec![],
1074
+ WorkflowOptions::default(),
1075
+ )
1076
+ .await
1077
+ .unwrap();
1078
+ worker.run_until_done().await.unwrap();
1079
+ }
1080
+
1081
+ /// Verifies that local activities which take more than a workflow task timeout will cause
1082
+ /// us to issue additional (empty) WFT completions with the force flag on, thus preventing timeout
1083
+ /// of WFT while the local activity continues to execute.
1084
+ ///
1085
+ /// The test with shutdown verifies if we call shutdown while the local activity is running that
1086
+ /// shutdown does not complete until it's finished.
1087
+ #[rstest::rstest]
1088
+ #[case::with_shutdown(true)]
1089
+ #[case::normal_complete(false)]
1090
+ #[tokio::test]
1091
+ async fn local_act_heartbeat(#[case] shutdown_middle: bool) {
1092
+ let mut t = TestHistoryBuilder::default();
1093
+ let wft_timeout = Duration::from_millis(200);
1094
+ t.add_wfe_started_with_wft_timeout(wft_timeout);
1095
+ t.add_full_wf_task();
1096
+ // Task created by WFT heartbeat
1097
+ t.add_full_wf_task();
1098
+ t.add_workflow_task_scheduled_and_started();
1099
+
1100
+ let wf_id = "fakeid";
1101
+ let mock = mock_worker_client();
1102
+ let mut mh = MockPollCfg::from_resp_batches(wf_id, t, [1, 2, 2, 2], mock);
1103
+ mh.enforce_correct_number_of_polls = false;
1104
+ let mut worker = mock_sdk_cfg(mh, |wc| {
1105
+ wc.max_cached_workflows = 1;
1106
+ wc.max_outstanding_workflow_tasks = Some(1);
1107
+ });
1108
+ let core = worker.core_worker.clone();
1109
+
1110
+ let shutdown_barr: &'static Barrier = Box::leak(Box::new(Barrier::new(2)));
1111
+
1112
+ worker.register_wf(
1113
+ DEFAULT_WORKFLOW_TYPE.to_owned(),
1114
+ |ctx: WfContext| async move {
1115
+ ctx.local_activity(LocalActivityOptions {
1116
+ activity_type: "echo".to_string(),
1117
+ input: "hi".as_json_payload().expect("serializes fine"),
1118
+ ..Default::default()
1119
+ })
1120
+ .await;
1121
+ Ok(().into())
1122
+ },
1123
+ );
1124
+ worker.register_activity("echo", move |_ctx: ActContext, str: String| async move {
1125
+ if shutdown_middle {
1126
+ shutdown_barr.wait().await;
1127
+ }
1128
+ // Take slightly more than two workflow tasks
1129
+ tokio::time::sleep(wft_timeout.mul_f32(2.2)).await;
1130
+ Ok(str)
1131
+ });
1132
+ worker
1133
+ .submit_wf(
1134
+ wf_id.to_owned(),
1135
+ DEFAULT_WORKFLOW_TYPE.to_owned(),
1136
+ vec![],
1137
+ WorkflowOptions::default(),
1138
+ )
1139
+ .await
1140
+ .unwrap();
1141
+ let (_, runres) = tokio::join!(
1142
+ async {
1143
+ if shutdown_middle {
1144
+ shutdown_barr.wait().await;
1145
+ core.shutdown().await;
1146
+ }
1147
+ },
1148
+ worker.run_until_done()
1149
+ );
1150
+ runres.unwrap();
1151
+ }
1152
+
1153
+ #[rstest::rstest]
1154
+ #[case::retry_then_pass(true)]
1155
+ #[case::retry_until_fail(false)]
1156
+ #[tokio::test]
1157
+ async fn local_act_fail_and_retry(#[case] eventually_pass: bool) {
1158
+ let mut t = TestHistoryBuilder::default();
1159
+ t.add_by_type(EventType::WorkflowExecutionStarted);
1160
+ t.add_workflow_task_scheduled_and_started();
1161
+
1162
+ let wf_id = "fakeid";
1163
+ let mock = mock_worker_client();
1164
+ let mh = MockPollCfg::from_resp_batches(wf_id, t, [1], mock);
1165
+ let mut worker = mock_sdk(mh);
1166
+
1167
+ worker.register_wf(
1168
+ DEFAULT_WORKFLOW_TYPE.to_owned(),
1169
+ move |ctx: WfContext| async move {
1170
+ let la_res = ctx
1171
+ .local_activity(LocalActivityOptions {
1172
+ activity_type: "echo".to_string(),
1173
+ input: "hi".as_json_payload().expect("serializes fine"),
1174
+ retry_policy: RetryPolicy {
1175
+ initial_interval: Some(prost_dur!(from_millis(50))),
1176
+ backoff_coefficient: 1.2,
1177
+ maximum_interval: None,
1178
+ maximum_attempts: 5,
1179
+ non_retryable_error_types: vec![],
1180
+ },
1181
+ ..Default::default()
1182
+ })
1183
+ .await;
1184
+ if eventually_pass {
1185
+ assert!(la_res.completed_ok())
1186
+ } else {
1187
+ assert!(la_res.failed())
1188
+ }
1189
+ Ok(().into())
1190
+ },
1191
+ );
1192
+ let attempts: &'static _ = Box::leak(Box::new(AtomicUsize::new(0)));
1193
+ worker.register_activity("echo", move |_ctx: ActContext, _: String| async move {
1194
+ // Succeed on 3rd attempt (which is ==2 since fetch_add returns prev val)
1195
+ if 2 == attempts.fetch_add(1, Ordering::Relaxed) && eventually_pass {
1196
+ Ok(())
1197
+ } else {
1198
+ Err(anyhow!("Oh no I failed!").into())
1199
+ }
1200
+ });
1201
+ worker
1202
+ .submit_wf(
1203
+ wf_id.to_owned(),
1204
+ DEFAULT_WORKFLOW_TYPE.to_owned(),
1205
+ vec![],
1206
+ WorkflowOptions::default(),
1207
+ )
1208
+ .await
1209
+ .unwrap();
1210
+ worker.run_until_done().await.unwrap();
1211
+ let expected_attempts = if eventually_pass { 3 } else { 5 };
1212
+ assert_eq!(expected_attempts, attempts.load(Ordering::Relaxed));
1213
+ }
1214
+
1215
+ #[tokio::test]
1216
+ async fn local_act_retry_long_backoff_uses_timer() {
1217
+ let mut t = TestHistoryBuilder::default();
1218
+ t.add_by_type(EventType::WorkflowExecutionStarted);
1219
+ t.add_full_wf_task();
1220
+ t.add_local_activity_fail_marker(
1221
+ 1,
1222
+ "1",
1223
+ Failure::application_failure("la failed".to_string(), false),
1224
+ );
1225
+ let timer_started_event_id = t.add_by_type(EventType::TimerStarted);
1226
+ t.add_timer_fired(timer_started_event_id, "1".to_string());
1227
+ t.add_full_wf_task();
1228
+ t.add_local_activity_fail_marker(
1229
+ 2,
1230
+ "2",
1231
+ Failure::application_failure("la failed".to_string(), false),
1232
+ );
1233
+ let timer_started_event_id = t.add_by_type(EventType::TimerStarted);
1234
+ t.add_timer_fired(timer_started_event_id, "2".to_string());
1235
+ t.add_full_wf_task();
1236
+ t.add_workflow_execution_completed();
1237
+
1238
+ let wf_id = "fakeid";
1239
+ let mock = mock_worker_client();
1240
+ let mh = MockPollCfg::from_resp_batches(
1241
+ wf_id,
1242
+ t,
1243
+ [1.into(), 2.into(), ResponseType::AllHistory],
1244
+ mock,
1245
+ );
1246
+ let mut worker = mock_sdk_cfg(mh, |w| w.max_cached_workflows = 1);
1247
+
1248
+ worker.register_wf(
1249
+ DEFAULT_WORKFLOW_TYPE.to_owned(),
1250
+ |ctx: WfContext| async move {
1251
+ let la_res = ctx
1252
+ .local_activity(LocalActivityOptions {
1253
+ activity_type: DEFAULT_ACTIVITY_TYPE.to_string(),
1254
+ input: "hi".as_json_payload().expect("serializes fine"),
1255
+ retry_policy: RetryPolicy {
1256
+ initial_interval: Some(prost_dur!(from_millis(65))),
1257
+ // This will make the second backoff 65 seconds, plenty to use timer
1258
+ backoff_coefficient: 1_000.,
1259
+ maximum_interval: Some(prost_dur!(from_secs(600))),
1260
+ maximum_attempts: 3,
1261
+ non_retryable_error_types: vec![],
1262
+ },
1263
+ ..Default::default()
1264
+ })
1265
+ .await;
1266
+ assert!(la_res.failed());
1267
+ // Extra timer just to have an extra workflow task which we can return full history for
1268
+ ctx.timer(Duration::from_secs(1)).await;
1269
+ Ok(().into())
1270
+ },
1271
+ );
1272
+ worker.register_activity(
1273
+ DEFAULT_ACTIVITY_TYPE,
1274
+ move |_ctx: ActContext, _: String| async move {
1275
+ Result::<(), _>::Err(anyhow!("Oh no I failed!").into())
1276
+ },
1277
+ );
1278
+ worker
1279
+ .submit_wf(
1280
+ wf_id.to_owned(),
1281
+ DEFAULT_WORKFLOW_TYPE.to_owned(),
1282
+ vec![],
1283
+ WorkflowOptions::default(),
1284
+ )
1285
+ .await
1286
+ .unwrap();
1287
+ worker.run_until_done().await.unwrap();
1288
+ }
1289
+
1290
+ #[tokio::test]
1291
+ async fn local_act_null_result() {
1292
+ let mut t = TestHistoryBuilder::default();
1293
+ t.add_by_type(EventType::WorkflowExecutionStarted);
1294
+ t.add_full_wf_task();
1295
+ t.add_local_activity_marker(1, "1", None, None, |_| {});
1296
+ t.add_workflow_execution_completed();
1297
+
1298
+ let wf_id = "fakeid";
1299
+ let mock = mock_worker_client();
1300
+ let mh = MockPollCfg::from_resp_batches(wf_id, t, [ResponseType::AllHistory], mock);
1301
+ let mut worker = mock_sdk_cfg(mh, |w| w.max_cached_workflows = 1);
1302
+
1303
+ worker.register_wf(
1304
+ DEFAULT_WORKFLOW_TYPE.to_owned(),
1305
+ |ctx: WfContext| async move {
1306
+ ctx.local_activity(LocalActivityOptions {
1307
+ activity_type: "nullres".to_string(),
1308
+ input: "hi".as_json_payload().expect("serializes fine"),
1309
+ ..Default::default()
1310
+ })
1311
+ .await;
1312
+ Ok(().into())
1313
+ },
1314
+ );
1315
+ worker.register_activity("nullres", |_ctx: ActContext, _: String| async { Ok(()) });
1316
+ worker
1317
+ .submit_wf(
1318
+ wf_id.to_owned(),
1319
+ DEFAULT_WORKFLOW_TYPE.to_owned(),
1320
+ vec![],
1321
+ WorkflowOptions::default(),
1322
+ )
1323
+ .await
1324
+ .unwrap();
1325
+ worker.run_until_done().await.unwrap();
1326
+ }
1327
+
1328
+ #[tokio::test]
1329
+ async fn local_act_command_immediately_follows_la_marker() {
1330
+ // This repro only works both when cache is off, and there is at least one heartbeat wft
1331
+ // before the marker & next command are recorded.
1332
+ let mut t = TestHistoryBuilder::default();
1333
+ t.add_by_type(EventType::WorkflowExecutionStarted);
1334
+ t.add_full_wf_task();
1335
+ t.add_full_wf_task();
1336
+ t.add_local_activity_result_marker(1, "1", "done".into());
1337
+ t.add_by_type(EventType::TimerStarted);
1338
+ t.add_full_wf_task();
1339
+
1340
+ let wf_id = "fakeid";
1341
+ let mock = mock_worker_client();
1342
+ // Bug only repros when seeing history up to third wft
1343
+ let mh = MockPollCfg::from_resp_batches(wf_id, t, [3], mock);
1344
+ let mut worker = mock_sdk_cfg(mh, |w| w.max_cached_workflows = 0);
1345
+
1346
+ worker.register_wf(
1347
+ DEFAULT_WORKFLOW_TYPE.to_owned(),
1348
+ |ctx: WfContext| async move {
1349
+ ctx.local_activity(LocalActivityOptions {
1350
+ activity_type: "nullres".to_string(),
1351
+ input: "hi".as_json_payload().expect("serializes fine"),
1352
+ ..Default::default()
1353
+ })
1354
+ .await;
1355
+ ctx.timer(Duration::from_secs(1)).await;
1356
+ Ok(().into())
1357
+ },
1358
+ );
1359
+ worker.register_activity("nullres", |_ctx: ActContext, _: String| async { Ok(()) });
1360
+ worker
1361
+ .submit_wf(
1362
+ wf_id.to_owned(),
1363
+ DEFAULT_WORKFLOW_TYPE.to_owned(),
1364
+ vec![],
1365
+ WorkflowOptions::default(),
1366
+ )
1367
+ .await
1368
+ .unwrap();
1369
+ worker.run_until_done().await.unwrap();
1370
+ }
1371
+
1372
+ #[tokio::test]
1373
+ async fn query_during_wft_heartbeat_doesnt_accidentally_fail_to_continue_heartbeat() {
1374
+ let wfid = "fake_wf_id";
1375
+ let mut t = TestHistoryBuilder::default();
1376
+ t.add_wfe_started_with_wft_timeout(Duration::from_millis(200));
1377
+ t.add_full_wf_task();
1378
+ // get query here
1379
+ t.add_full_wf_task();
1380
+ t.add_local_activity_result_marker(1, "1", "done".into());
1381
+ t.add_workflow_execution_completed();
1382
+
1383
+ let query_with_hist_task = {
1384
+ let mut pr = hist_to_poll_resp(&t, wfid, ResponseType::ToTaskNum(1));
1385
+ pr.queries = HashMap::new();
1386
+ pr.queries.insert(
1387
+ "the-query".to_string(),
1388
+ WorkflowQuery {
1389
+ query_type: "query-type".to_string(),
1390
+ query_args: Some(b"hi".into()),
1391
+ header: None,
1392
+ },
1393
+ );
1394
+ pr
1395
+ };
1396
+ let after_la_resolved = Arc::new(Barrier::new(2));
1397
+ let poll_barr = after_la_resolved.clone();
1398
+ let tasks = [
1399
+ query_with_hist_task,
1400
+ hist_to_poll_resp(
1401
+ &t,
1402
+ wfid,
1403
+ ResponseType::UntilResolved(
1404
+ async move {
1405
+ poll_barr.wait().await;
1406
+ }
1407
+ .boxed(),
1408
+ 3,
1409
+ ),
1410
+ ),
1411
+ ];
1412
+ let mock = mock_worker_client();
1413
+ let mut mock = single_hist_mock_sg(wfid, t, tasks, mock, true);
1414
+ mock.worker_cfg(|wc| wc.max_cached_workflows = 1);
1415
+ let core = mock_worker(mock);
1416
+
1417
+ let barrier = Barrier::new(2);
1418
+
1419
+ let wf_fut = async {
1420
+ let task = core.poll_workflow_activation().await.unwrap();
1421
+ core.complete_workflow_activation(WorkflowActivationCompletion::from_cmd(
1422
+ task.run_id,
1423
+ schedule_local_activity_cmd(
1424
+ 1,
1425
+ "1",
1426
+ ActivityCancellationType::TryCancel,
1427
+ Duration::from_secs(60),
1428
+ ),
1429
+ ))
1430
+ .await
1431
+ .unwrap();
1432
+ let task = core.poll_workflow_activation().await.unwrap();
1433
+ // Get query, and complete it
1434
+ let query = assert_matches!(
1435
+ task.jobs.as_slice(),
1436
+ [WorkflowActivationJob {
1437
+ variant: Some(workflow_activation_job::Variant::QueryWorkflow(q)),
1438
+ }] => q
1439
+ );
1440
+ // Now complete the LA
1441
+ barrier.wait().await;
1442
+ core.complete_workflow_activation(WorkflowActivationCompletion::from_cmd(
1443
+ task.run_id,
1444
+ query_ok(&query.query_id, "whatev"),
1445
+ ))
1446
+ .await
1447
+ .unwrap();
1448
+ // Activation with it resolving:
1449
+ let task = core.poll_workflow_activation().await.unwrap();
1450
+ assert_matches!(
1451
+ task.jobs.as_slice(),
1452
+ [WorkflowActivationJob {
1453
+ variant: Some(workflow_activation_job::Variant::ResolveActivity(_)),
1454
+ }]
1455
+ );
1456
+ core.complete_execution(&task.run_id).await;
1457
+ };
1458
+ let act_fut = async {
1459
+ let act_task = core.poll_activity_task().await.unwrap();
1460
+ barrier.wait().await;
1461
+ core.complete_activity_task(ActivityTaskCompletion {
1462
+ task_token: act_task.task_token,
1463
+ result: Some(ActivityExecutionResult::ok(vec![1].into())),
1464
+ })
1465
+ .await
1466
+ .unwrap();
1467
+ after_la_resolved.wait().await;
1468
+ };
1469
+
1470
+ tokio::join!(wf_fut, act_fut);
1471
+ }
1472
+
1473
+ #[rstest::rstest]
1474
+ #[case::impossible_query_in_task(true)]
1475
+ #[case::real_history(false)]
1476
+ #[tokio::test]
1477
+ async fn la_resolve_during_legacy_query_does_not_combine(#[case] impossible_query_in_task: bool) {
1478
+ // Ensures we do not send an activation with a legacy query and any other work, which should
1479
+ // never happen, but there was an issue where an LA resolving could trigger that.
1480
+ let wfid = "fake_wf_id";
1481
+ let mut t = TestHistoryBuilder::default();
1482
+ t.add(default_wes_attribs());
1483
+ // Since we don't send queries with start workflow, need one workflow task of something else
1484
+ // b/c we want to get an activation with a job and a nonlegacy query
1485
+ t.add_full_wf_task();
1486
+ let timer_started_event_id = t.add_by_type(EventType::TimerStarted);
1487
+ t.add_timer_fired(timer_started_event_id, "1".to_string());
1488
+
1489
+ // nonlegacy query got here & LA started here
1490
+ // then next task is incremental w/ legacy query (for impossible query case)
1491
+ t.add_full_wf_task();
1492
+
1493
+ let tasks = [
1494
+ hist_to_poll_resp(&t, wfid.to_owned(), ResponseType::ToTaskNum(1)),
1495
+ {
1496
+ let mut pr = hist_to_poll_resp(&t, wfid.to_owned(), ResponseType::OneTask(2));
1497
+ pr.queries = HashMap::new();
1498
+ pr.queries.insert(
1499
+ "q1".to_string(),
1500
+ WorkflowQuery {
1501
+ query_type: "query-type".to_string(),
1502
+ query_args: Some(b"hi".into()),
1503
+ header: None,
1504
+ },
1505
+ );
1506
+ pr
1507
+ },
1508
+ {
1509
+ let mut pr = hist_to_poll_resp(&t, wfid.to_owned(), ResponseType::ToTaskNum(2));
1510
+ // Strip beginning of history so the only events are WFT sched/started, we need to look
1511
+ // like we hit the cache
1512
+ {
1513
+ let h = pr.history.as_mut().unwrap();
1514
+ h.events = h.events.split_off(6);
1515
+ }
1516
+ // In the nonsense server response case, we attach a legacy query, otherwise this
1517
+ // response looks like a normal response to a forced WFT heartbeat.
1518
+ if impossible_query_in_task {
1519
+ pr.query = Some(WorkflowQuery {
1520
+ query_type: "query-type".to_string(),
1521
+ query_args: Some(b"hi".into()),
1522
+ header: None,
1523
+ });
1524
+ }
1525
+ pr
1526
+ },
1527
+ ];
1528
+ let mut mock = mock_worker_client();
1529
+ if impossible_query_in_task {
1530
+ mock.expect_respond_legacy_query()
1531
+ .times(1)
1532
+ .returning(move |_, _| Ok(Default::default()));
1533
+ }
1534
+ let mut mock = single_hist_mock_sg(wfid, t, tasks, mock, true);
1535
+ mock.worker_cfg(|wc| wc.max_cached_workflows = 1);
1536
+ let taskmap = mock.outstanding_task_map.clone().unwrap();
1537
+ let core = mock_worker(mock);
1538
+
1539
+ let wf_fut = async {
1540
+ let task = core.poll_workflow_activation().await.unwrap();
1541
+ assert_matches!(
1542
+ task.jobs.as_slice(),
1543
+ &[WorkflowActivationJob {
1544
+ variant: Some(workflow_activation_job::Variant::InitializeWorkflow(_)),
1545
+ },]
1546
+ );
1547
+ core.complete_workflow_activation(WorkflowActivationCompletion::from_cmd(
1548
+ task.run_id,
1549
+ start_timer_cmd(1, Duration::from_secs(1)),
1550
+ ))
1551
+ .await
1552
+ .unwrap();
1553
+
1554
+ let task = core.poll_workflow_activation().await.unwrap();
1555
+ assert_matches!(
1556
+ task.jobs.as_slice(),
1557
+ &[WorkflowActivationJob {
1558
+ variant: Some(workflow_activation_job::Variant::FireTimer(_)),
1559
+ },]
1560
+ );
1561
+ // We want to make sure the weird-looking query gets received while we're working on other
1562
+ // stuff, so that we don't see the workflow complete and choose to evict.
1563
+ taskmap.release_run(&task.run_id);
1564
+ core.complete_workflow_activation(WorkflowActivationCompletion::from_cmd(
1565
+ task.run_id,
1566
+ schedule_local_activity_cmd(
1567
+ 1,
1568
+ "act-id",
1569
+ ActivityCancellationType::TryCancel,
1570
+ Duration::from_secs(60),
1571
+ ),
1572
+ ))
1573
+ .await
1574
+ .unwrap();
1575
+
1576
+ let task = core.poll_workflow_activation().await.unwrap();
1577
+ // The next task needs to be resolve, since the LA is completed immediately
1578
+ assert_matches!(
1579
+ task.jobs.as_slice(),
1580
+ [WorkflowActivationJob {
1581
+ variant: Some(workflow_activation_job::Variant::ResolveActivity(_)),
1582
+ }]
1583
+ );
1584
+ // Complete workflow
1585
+ core.complete_execution(&task.run_id).await;
1586
+
1587
+ // Now we will get the query
1588
+ let task = core.poll_workflow_activation().await.unwrap();
1589
+ assert_matches!(
1590
+ task.jobs.as_slice(),
1591
+ &[WorkflowActivationJob {
1592
+ variant: Some(workflow_activation_job::Variant::QueryWorkflow(ref q)),
1593
+ }]
1594
+ if q.query_id == "q1"
1595
+ );
1596
+ core.complete_workflow_activation(WorkflowActivationCompletion::from_cmd(
1597
+ task.run_id,
1598
+ query_ok("q1", "whatev"),
1599
+ ))
1600
+ .await
1601
+ .unwrap();
1602
+
1603
+ if impossible_query_in_task {
1604
+ // finish last query
1605
+ let task = core.poll_workflow_activation().await.unwrap();
1606
+ core.complete_workflow_activation(WorkflowActivationCompletion::from_cmd(
1607
+ task.run_id,
1608
+ query_ok(LEGACY_QUERY_ID, "whatev"),
1609
+ ))
1610
+ .await
1611
+ .unwrap();
1612
+ }
1613
+ };
1614
+ let act_fut = async {
1615
+ let act_task = core.poll_activity_task().await.unwrap();
1616
+ core.complete_activity_task(ActivityTaskCompletion {
1617
+ task_token: act_task.task_token,
1618
+ result: Some(ActivityExecutionResult::ok(vec![1].into())),
1619
+ })
1620
+ .await
1621
+ .unwrap();
1622
+ };
1623
+
1624
+ join!(wf_fut, act_fut);
1625
+ core.drain_pollers_and_shutdown().await;
1626
+ }
1627
+
1628
+ #[tokio::test]
1629
+ async fn test_schedule_to_start_timeout() {
1630
+ let mut t = TestHistoryBuilder::default();
1631
+ t.add_by_type(EventType::WorkflowExecutionStarted);
1632
+ t.add_full_wf_task();
1633
+
1634
+ let wf_id = "fakeid";
1635
+ let mock = mock_worker_client();
1636
+ let mh = MockPollCfg::from_resp_batches(wf_id, t, [ResponseType::ToTaskNum(1)], mock);
1637
+ let mut worker = mock_sdk_cfg(mh, |w| w.max_cached_workflows = 1);
1638
+
1639
+ worker.register_wf(
1640
+ DEFAULT_WORKFLOW_TYPE.to_owned(),
1641
+ |ctx: WfContext| async move {
1642
+ let la_res = ctx
1643
+ .local_activity(LocalActivityOptions {
1644
+ activity_type: "echo".to_string(),
1645
+ input: "hi".as_json_payload().expect("serializes fine"),
1646
+ // Impossibly small timeout so we timeout in the queue
1647
+ schedule_to_start_timeout: prost_dur!(from_nanos(1)),
1648
+ ..Default::default()
1649
+ })
1650
+ .await;
1651
+ assert_eq!(la_res.timed_out(), Some(TimeoutType::ScheduleToStart));
1652
+ let rfail = la_res.unwrap_failure();
1653
+ assert_matches!(
1654
+ rfail.failure_info,
1655
+ Some(FailureInfo::ActivityFailureInfo(_))
1656
+ );
1657
+ assert_matches!(
1658
+ rfail.cause.unwrap().failure_info,
1659
+ Some(FailureInfo::TimeoutFailureInfo(_))
1660
+ );
1661
+ Ok(().into())
1662
+ },
1663
+ );
1664
+ worker.register_activity(
1665
+ "echo",
1666
+ move |_ctx: ActContext, _: String| async move { Ok(()) },
1667
+ );
1668
+ worker
1669
+ .submit_wf(
1670
+ wf_id.to_owned(),
1671
+ DEFAULT_WORKFLOW_TYPE.to_owned(),
1672
+ vec![],
1673
+ WorkflowOptions::default(),
1674
+ )
1675
+ .await
1676
+ .unwrap();
1677
+ worker.run_until_done().await.unwrap();
1678
+ }
1679
+
1680
+ #[rstest::rstest]
1681
+ #[case::sched_to_start(true)]
1682
+ #[case::sched_to_close(false)]
1683
+ #[tokio::test]
1684
+ async fn test_schedule_to_start_timeout_not_based_on_original_time(
1685
+ #[case] is_sched_to_start: bool,
1686
+ ) {
1687
+ // We used to carry over the schedule time of LAs from the "original" schedule time if these LAs
1688
+ // created newly after backing off across a timer. That was a mistake, since schedule-to-start
1689
+ // timeouts should apply to when the new attempt was scheduled. This test verifies:
1690
+ // * we don't time out on s-t-s timeouts because of that, when the param is true.
1691
+ // * we do properly time out on s-t-c timeouts when the param is false
1692
+
1693
+ let mut t = TestHistoryBuilder::default();
1694
+ t.add_by_type(EventType::WorkflowExecutionStarted);
1695
+ t.add_full_wf_task();
1696
+ let orig_sched = SystemTime::now().sub(Duration::from_secs(60 * 20));
1697
+ t.add_local_activity_marker(
1698
+ 1,
1699
+ "1",
1700
+ None,
1701
+ Some(Failure::application_failure("la failed".to_string(), false)),
1702
+ |deets| {
1703
+ // Really old schedule time, which should _not_ count against schedule_to_start
1704
+ deets.original_schedule_time = Some(orig_sched.into());
1705
+ // Backoff value must be present since we're simulating timer backoff
1706
+ deets.backoff = Some(prost_dur!(from_secs(100)));
1707
+ },
1708
+ );
1709
+ let timer_started_event_id = t.add_by_type(EventType::TimerStarted);
1710
+ t.add_timer_fired(timer_started_event_id, "1".to_string());
1711
+ t.add_workflow_task_scheduled_and_started();
1712
+
1713
+ let wf_id = "fakeid";
1714
+ let mock = mock_worker_client();
1715
+ let mh = MockPollCfg::from_resp_batches(wf_id, t, [ResponseType::AllHistory], mock);
1716
+ let mut worker = mock_sdk_cfg(mh, |w| w.max_cached_workflows = 1);
1717
+
1718
+ let schedule_to_close_timeout = Some(if is_sched_to_start {
1719
+ // This 60 minute timeout will not have elapsed according to the original
1720
+ // schedule time in the history.
1721
+ Duration::from_secs(60 * 60)
1722
+ } else {
1723
+ // This 10 minute timeout will have already elapsed
1724
+ Duration::from_secs(10 * 60)
1725
+ });
1726
+
1727
+ worker.register_wf(
1728
+ DEFAULT_WORKFLOW_TYPE.to_owned(),
1729
+ move |ctx: WfContext| async move {
1730
+ let la_res = ctx
1731
+ .local_activity(LocalActivityOptions {
1732
+ activity_type: "echo".to_string(),
1733
+ input: "hi".as_json_payload().expect("serializes fine"),
1734
+ retry_policy: RetryPolicy {
1735
+ initial_interval: Some(prost_dur!(from_millis(50))),
1736
+ backoff_coefficient: 1.2,
1737
+ maximum_interval: None,
1738
+ maximum_attempts: 5,
1739
+ non_retryable_error_types: vec![],
1740
+ },
1741
+ schedule_to_start_timeout: Some(Duration::from_secs(60)),
1742
+ schedule_to_close_timeout,
1743
+ ..Default::default()
1744
+ })
1745
+ .await;
1746
+ if is_sched_to_start {
1747
+ assert!(la_res.completed_ok());
1748
+ } else {
1749
+ assert_eq!(la_res.timed_out(), Some(TimeoutType::ScheduleToClose));
1750
+ }
1751
+ Ok(().into())
1752
+ },
1753
+ );
1754
+ worker.register_activity(
1755
+ "echo",
1756
+ move |_ctx: ActContext, _: String| async move { Ok(()) },
1757
+ );
1758
+ worker
1759
+ .submit_wf(
1760
+ wf_id.to_owned(),
1761
+ DEFAULT_WORKFLOW_TYPE.to_owned(),
1762
+ vec![],
1763
+ WorkflowOptions::default(),
1764
+ )
1765
+ .await
1766
+ .unwrap();
1767
+ worker.run_until_done().await.unwrap();
1768
+ }
1769
+
1770
+ #[rstest::rstest]
1771
+ #[tokio::test]
1772
+ async fn start_to_close_timeout_allows_retries(#[values(true, false)] la_completes: bool) {
1773
+ let mut t = TestHistoryBuilder::default();
1774
+ t.add_by_type(EventType::WorkflowExecutionStarted);
1775
+ t.add_full_wf_task();
1776
+ if la_completes {
1777
+ t.add_local_activity_marker(1, "1", Some("hi".into()), None, |_| {});
1778
+ } else {
1779
+ t.add_local_activity_marker(
1780
+ 1,
1781
+ "1",
1782
+ None,
1783
+ Some(Failure::timeout(TimeoutType::StartToClose)),
1784
+ |_| {},
1785
+ );
1786
+ }
1787
+ t.add_full_wf_task();
1788
+ t.add_workflow_execution_completed();
1789
+
1790
+ let wf_id = "fakeid";
1791
+ let mock = mock_worker_client();
1792
+ let mh = MockPollCfg::from_resp_batches(
1793
+ wf_id,
1794
+ t,
1795
+ [ResponseType::ToTaskNum(1), ResponseType::AllHistory],
1796
+ mock,
1797
+ );
1798
+ let mut worker = mock_sdk_cfg(mh, |w| w.max_cached_workflows = 1);
1799
+
1800
+ worker.register_wf(
1801
+ DEFAULT_WORKFLOW_TYPE.to_owned(),
1802
+ move |ctx: WfContext| async move {
1803
+ let la_res = ctx
1804
+ .local_activity(LocalActivityOptions {
1805
+ activity_type: DEFAULT_ACTIVITY_TYPE.to_string(),
1806
+ input: "hi".as_json_payload().expect("serializes fine"),
1807
+ retry_policy: RetryPolicy {
1808
+ initial_interval: Some(prost_dur!(from_millis(20))),
1809
+ backoff_coefficient: 1.0,
1810
+ maximum_interval: None,
1811
+ maximum_attempts: 5,
1812
+ non_retryable_error_types: vec![],
1813
+ },
1814
+ start_to_close_timeout: Some(prost_dur!(from_millis(25))),
1815
+ ..Default::default()
1816
+ })
1817
+ .await;
1818
+ if la_completes {
1819
+ assert!(la_res.completed_ok());
1820
+ } else {
1821
+ assert_eq!(la_res.timed_out(), Some(TimeoutType::StartToClose));
1822
+ }
1823
+ Ok(().into())
1824
+ },
1825
+ );
1826
+ let attempts: &'static _ = Box::leak(Box::new(AtomicUsize::new(0)));
1827
+ let cancels: &'static _ = Box::leak(Box::new(AtomicUsize::new(0)));
1828
+ worker.register_activity(
1829
+ DEFAULT_ACTIVITY_TYPE,
1830
+ move |ctx: ActContext, _: String| async move {
1831
+ // Timeout the first 4 attempts, or all of them if we intend to fail
1832
+ if attempts.fetch_add(1, Ordering::AcqRel) < 4 || !la_completes {
1833
+ select! {
1834
+ _ = tokio::time::sleep(Duration::from_millis(100)) => (),
1835
+ _ = ctx.cancelled() => {
1836
+ cancels.fetch_add(1, Ordering::AcqRel);
1837
+ return Err(ActivityError::cancelled());
1838
+ }
1839
+ }
1840
+ }
1841
+ Ok(())
1842
+ },
1843
+ );
1844
+ worker
1845
+ .submit_wf(
1846
+ wf_id.to_owned(),
1847
+ DEFAULT_WORKFLOW_TYPE.to_owned(),
1848
+ vec![],
1849
+ WorkflowOptions::default(),
1850
+ )
1851
+ .await
1852
+ .unwrap();
1853
+ worker.run_until_done().await.unwrap();
1854
+ // Activity should have been attempted all 5 times
1855
+ assert_eq!(attempts.load(Ordering::Acquire), 5);
1856
+ let num_cancels = if la_completes { 4 } else { 5 };
1857
+ assert_eq!(cancels.load(Ordering::Acquire), num_cancels);
1858
+ }
1859
+
1860
+ #[tokio::test]
1861
+ async fn wft_failure_cancels_running_las() {
1862
+ let mut t = TestHistoryBuilder::default();
1863
+ t.add_wfe_started_with_wft_timeout(Duration::from_millis(200));
1864
+ t.add_full_wf_task();
1865
+ let timer_started_event_id = t.add_by_type(EventType::TimerStarted);
1866
+ t.add_timer_fired(timer_started_event_id, "1".to_string());
1867
+ t.add_workflow_task_scheduled_and_started();
1868
+
1869
+ let wf_id = "fakeid";
1870
+ let mock = mock_worker_client();
1871
+ let mut mh = MockPollCfg::from_resp_batches(wf_id, t, [1, 2], mock);
1872
+ mh.num_expected_fails = 1;
1873
+ let mut worker = mock_sdk_cfg(mh, |w| w.max_cached_workflows = 1);
1874
+
1875
+ worker.register_wf(
1876
+ DEFAULT_WORKFLOW_TYPE.to_owned(),
1877
+ |ctx: WfContext| async move {
1878
+ let la_handle = ctx.local_activity(LocalActivityOptions {
1879
+ activity_type: DEFAULT_ACTIVITY_TYPE.to_string(),
1880
+ input: "hi".as_json_payload().expect("serializes fine"),
1881
+ ..Default::default()
1882
+ });
1883
+ tokio::join!(
1884
+ async {
1885
+ ctx.timer(Duration::from_secs(1)).await;
1886
+ panic!("ahhh I'm failing wft")
1887
+ },
1888
+ la_handle
1889
+ );
1890
+ Ok(().into())
1891
+ },
1892
+ );
1893
+ worker.register_activity(
1894
+ DEFAULT_ACTIVITY_TYPE,
1895
+ move |ctx: ActContext, _: String| async move {
1896
+ let res = tokio::time::timeout(Duration::from_millis(500), ctx.cancelled()).await;
1897
+ if res.is_err() {
1898
+ panic!("Activity must be cancelled!!!!");
1899
+ }
1900
+ Result::<(), _>::Err(ActivityError::cancelled())
1901
+ },
1902
+ );
1903
+ worker
1904
+ .submit_wf(
1905
+ wf_id.to_owned(),
1906
+ DEFAULT_WORKFLOW_TYPE.to_owned(),
1907
+ vec![],
1908
+ WorkflowOptions::default(),
1909
+ )
1910
+ .await
1911
+ .unwrap();
1912
+ worker.run_until_done().await.unwrap();
1913
+ }
1914
+
1915
+ #[tokio::test]
1916
+ async fn resolved_las_not_recorded_if_wft_fails_many_times() {
1917
+ // We shouldn't record any LA results if the workflow activation is repeatedly failing. There
1918
+ // was an issue that, because we stop reporting WFT failures after 2 tries, this meant the WFT
1919
+ // was not marked as "completed" and the WFT could accidentally be replied to with LA results.
1920
+ let mut t = TestHistoryBuilder::default();
1921
+ t.add_by_type(EventType::WorkflowExecutionStarted);
1922
+ t.add_workflow_task_scheduled_and_started();
1923
+ t.add_workflow_task_failed_with_failure(
1924
+ WorkflowTaskFailedCause::Unspecified,
1925
+ Default::default(),
1926
+ );
1927
+ t.add_workflow_task_scheduled_and_started();
1928
+
1929
+ let wf_id = "fakeid";
1930
+ let mock = mock_worker_client();
1931
+ let mut mh = MockPollCfg::from_resp_batches(
1932
+ wf_id,
1933
+ t,
1934
+ [1.into(), ResponseType::AllHistory, ResponseType::AllHistory],
1935
+ mock,
1936
+ );
1937
+ mh.num_expected_fails = 2;
1938
+ mh.num_expected_completions = Some(0.into());
1939
+ let mut worker = mock_sdk_cfg(mh, |w| w.max_cached_workflows = 1);
1940
+
1941
+ #[allow(unreachable_code)]
1942
+ worker.register_wf(
1943
+ DEFAULT_WORKFLOW_TYPE.to_owned(),
1944
+ WorkflowFunction::new::<_, _, ()>(|ctx: WfContext| async move {
1945
+ ctx.local_activity(LocalActivityOptions {
1946
+ activity_type: "echo".to_string(),
1947
+ input: "hi".as_json_payload().expect("serializes fine"),
1948
+ ..Default::default()
1949
+ })
1950
+ .await;
1951
+ panic!()
1952
+ }),
1953
+ );
1954
+ worker.register_activity(
1955
+ "echo",
1956
+ move |_: ActContext, _: String| async move { Ok(()) },
1957
+ );
1958
+ worker
1959
+ .submit_wf(
1960
+ wf_id.to_owned(),
1961
+ DEFAULT_WORKFLOW_TYPE.to_owned(),
1962
+ vec![],
1963
+ WorkflowOptions::default(),
1964
+ )
1965
+ .await
1966
+ .unwrap();
1967
+ worker.run_until_done().await.unwrap();
1968
+ }
1969
+
1970
+ #[tokio::test]
1971
+ async fn local_act_records_nonfirst_attempts_ok() {
1972
+ let mut t = TestHistoryBuilder::default();
1973
+ let wft_timeout = Duration::from_millis(200);
1974
+ t.add_wfe_started_with_wft_timeout(wft_timeout);
1975
+ t.add_full_wf_task();
1976
+ t.add_full_wf_task();
1977
+ t.add_full_wf_task();
1978
+ t.add_workflow_task_scheduled_and_started();
1979
+
1980
+ let wf_id = "fakeid";
1981
+ let mock = mock_worker_client();
1982
+ let mut mh = MockPollCfg::from_resp_batches(wf_id, t, [1, 2, 3], mock);
1983
+ let nonfirst_counts = Arc::new(SegQueue::new());
1984
+ let nfc_c = nonfirst_counts.clone();
1985
+ mh.completion_mock_fn = Some(Box::new(move |c| {
1986
+ nfc_c.push(
1987
+ c.metering_metadata
1988
+ .nonfirst_local_activity_execution_attempts,
1989
+ );
1990
+ Ok(Default::default())
1991
+ }));
1992
+ let mut worker = mock_sdk_cfg(mh, |wc| {
1993
+ wc.max_cached_workflows = 1;
1994
+ wc.max_outstanding_workflow_tasks = Some(1);
1995
+ });
1996
+
1997
+ worker.register_wf(
1998
+ DEFAULT_WORKFLOW_TYPE.to_owned(),
1999
+ |ctx: WfContext| async move {
2000
+ ctx.local_activity(LocalActivityOptions {
2001
+ activity_type: "echo".to_string(),
2002
+ input: "hi".as_json_payload().expect("serializes fine"),
2003
+ retry_policy: RetryPolicy {
2004
+ initial_interval: Some(prost_dur!(from_millis(10))),
2005
+ backoff_coefficient: 1.0,
2006
+ maximum_interval: None,
2007
+ maximum_attempts: 0,
2008
+ non_retryable_error_types: vec![],
2009
+ },
2010
+ ..Default::default()
2011
+ })
2012
+ .await;
2013
+ Ok(().into())
2014
+ },
2015
+ );
2016
+ worker.register_activity("echo", move |_ctx: ActContext, _: String| async move {
2017
+ Result::<(), _>::Err(anyhow!("I fail").into())
2018
+ });
2019
+ worker
2020
+ .submit_wf(
2021
+ wf_id.to_owned(),
2022
+ DEFAULT_WORKFLOW_TYPE.to_owned(),
2023
+ vec![],
2024
+ WorkflowOptions::default(),
2025
+ )
2026
+ .await
2027
+ .unwrap();
2028
+ worker.run_until_done().await.unwrap();
2029
+ // 3 workflow tasks
2030
+ assert_eq!(nonfirst_counts.len(), 3);
2031
+ // First task's non-first count should, of course, be 0
2032
+ assert_eq!(nonfirst_counts.pop().unwrap(), 0);
2033
+ // Next two, some nonzero amount which could vary based on test load
2034
+ assert!(nonfirst_counts.pop().unwrap() > 0);
2035
+ assert!(nonfirst_counts.pop().unwrap() > 0);
2036
+ }
2037
+
2038
+ #[tokio::test]
2039
+ async fn local_activities_can_be_delivered_during_shutdown() {
2040
+ let wfid = "fake_wf_id";
2041
+ let mut t = TestHistoryBuilder::default();
2042
+ t.add_wfe_started_with_wft_timeout(Duration::from_millis(200));
2043
+ t.add_full_wf_task();
2044
+ let timer_started_event_id = t.add_by_type(EventType::TimerStarted);
2045
+ t.add_timer_fired(timer_started_event_id, "1".to_string());
2046
+ t.add_workflow_task_scheduled_and_started();
2047
+
2048
+ let mock = mock_worker_client();
2049
+ let mut mock = single_hist_mock_sg(
2050
+ wfid,
2051
+ t,
2052
+ [ResponseType::ToTaskNum(1), ResponseType::AllHistory],
2053
+ mock,
2054
+ true,
2055
+ );
2056
+ mock.worker_cfg(|wc| wc.max_cached_workflows = 1);
2057
+ let core = mock_worker(mock);
2058
+
2059
+ let task = core.poll_workflow_activation().await.unwrap();
2060
+ core.complete_workflow_activation(WorkflowActivationCompletion::from_cmd(
2061
+ task.run_id,
2062
+ start_timer_cmd(1, Duration::from_secs(1)),
2063
+ ))
2064
+ .await
2065
+ .unwrap();
2066
+
2067
+ let task = core.poll_workflow_activation().await.unwrap();
2068
+ // Initiate shutdown once we have the WF activation, but before replying that we want to do an
2069
+ // LA
2070
+ core.initiate_shutdown();
2071
+ core.complete_workflow_activation(WorkflowActivationCompletion::from_cmd(
2072
+ task.run_id,
2073
+ ScheduleLocalActivity {
2074
+ seq: 1,
2075
+ activity_id: "1".to_string(),
2076
+ activity_type: "test_act".to_string(),
2077
+ start_to_close_timeout: Some(prost_dur!(from_secs(30))),
2078
+ ..Default::default()
2079
+ }
2080
+ .into(),
2081
+ ))
2082
+ .await
2083
+ .unwrap();
2084
+
2085
+ let wf_poller = async { core.poll_workflow_activation().await };
2086
+
2087
+ let at_poller = async {
2088
+ let act_task = core.poll_activity_task().await.unwrap();
2089
+ core.complete_activity_task(ActivityTaskCompletion {
2090
+ task_token: act_task.task_token,
2091
+ result: Some(ActivityExecutionResult::ok(vec![1].into())),
2092
+ })
2093
+ .await
2094
+ .unwrap();
2095
+ core.poll_activity_task().await
2096
+ };
2097
+
2098
+ let (wf_r, act_r) = join!(wf_poller, at_poller);
2099
+ assert_matches!(wf_r.unwrap_err(), PollError::ShutDown);
2100
+ assert_matches!(act_r.unwrap_err(), PollError::ShutDown);
2101
+ }
2102
+
2103
+ #[tokio::test]
2104
+ async fn queries_can_be_received_while_heartbeating() {
2105
+ let wfid = "fake_wf_id";
2106
+ let mut t = TestHistoryBuilder::default();
2107
+ t.add_wfe_started_with_wft_timeout(Duration::from_millis(200));
2108
+ t.add_full_wf_task();
2109
+ t.add_full_wf_task();
2110
+ t.add_full_wf_task();
2111
+
2112
+ let tasks = [
2113
+ hist_to_poll_resp(&t, wfid.to_owned(), ResponseType::ToTaskNum(1)),
2114
+ {
2115
+ let mut pr = hist_to_poll_resp(&t, wfid.to_owned(), ResponseType::OneTask(2));
2116
+ pr.queries = HashMap::new();
2117
+ pr.queries.insert(
2118
+ "q1".to_string(),
2119
+ WorkflowQuery {
2120
+ query_type: "query-type".to_string(),
2121
+ query_args: Some(b"hi".into()),
2122
+ header: None,
2123
+ },
2124
+ );
2125
+ pr
2126
+ },
2127
+ {
2128
+ let mut pr = hist_to_poll_resp(&t, wfid.to_owned(), ResponseType::OneTask(3));
2129
+ pr.query = Some(WorkflowQuery {
2130
+ query_type: "query-type".to_string(),
2131
+ query_args: Some(b"hi".into()),
2132
+ header: None,
2133
+ });
2134
+ pr
2135
+ },
2136
+ ];
2137
+ let mut mock = mock_worker_client();
2138
+ mock.expect_respond_legacy_query()
2139
+ .times(1)
2140
+ .returning(move |_, _| Ok(Default::default()));
2141
+ let mut mock = single_hist_mock_sg(wfid, t, tasks, mock, true);
2142
+ mock.worker_cfg(|wc| wc.max_cached_workflows = 1);
2143
+ let core = mock_worker(mock);
2144
+
2145
+ let task = core.poll_workflow_activation().await.unwrap();
2146
+ assert_matches!(
2147
+ task.jobs.as_slice(),
2148
+ &[WorkflowActivationJob {
2149
+ variant: Some(workflow_activation_job::Variant::InitializeWorkflow(_)),
2150
+ },]
2151
+ );
2152
+ core.complete_workflow_activation(WorkflowActivationCompletion::from_cmd(
2153
+ task.run_id,
2154
+ schedule_local_activity_cmd(
2155
+ 1,
2156
+ "act-id",
2157
+ ActivityCancellationType::TryCancel,
2158
+ Duration::from_secs(60),
2159
+ ),
2160
+ ))
2161
+ .await
2162
+ .unwrap();
2163
+
2164
+ let task = core.poll_workflow_activation().await.unwrap();
2165
+ assert_matches!(
2166
+ task.jobs.as_slice(),
2167
+ &[WorkflowActivationJob {
2168
+ variant: Some(workflow_activation_job::Variant::QueryWorkflow(ref q)),
2169
+ }]
2170
+ if q.query_id == "q1"
2171
+ );
2172
+ core.complete_workflow_activation(WorkflowActivationCompletion::from_cmd(
2173
+ task.run_id,
2174
+ query_ok("q1", "whatev"),
2175
+ ))
2176
+ .await
2177
+ .unwrap();
2178
+
2179
+ let task = core.poll_workflow_activation().await.unwrap();
2180
+ assert_matches!(
2181
+ task.jobs.as_slice(),
2182
+ &[WorkflowActivationJob {
2183
+ variant: Some(workflow_activation_job::Variant::QueryWorkflow(ref q)),
2184
+ }]
2185
+ if q.query_id == LEGACY_QUERY_ID
2186
+ );
2187
+ core.complete_workflow_activation(WorkflowActivationCompletion::from_cmd(
2188
+ task.run_id,
2189
+ query_ok(LEGACY_QUERY_ID, "whatev"),
2190
+ ))
2191
+ .await
2192
+ .unwrap();
2193
+
2194
+ // Handle the activity so we can shut down cleanly
2195
+ let act_task = core.poll_activity_task().await.unwrap();
2196
+ core.complete_activity_task(ActivityTaskCompletion {
2197
+ task_token: act_task.task_token,
2198
+ result: Some(ActivityExecutionResult::ok(vec![1].into())),
2199
+ })
2200
+ .await
2201
+ .unwrap();
2202
+
2203
+ core.drain_pollers_and_shutdown().await;
2204
+ }
2205
+
2206
+ #[tokio::test]
2207
+ async fn local_activity_after_wf_complete_is_discarded() {
2208
+ let wfid = "fake_wf_id";
2209
+ let mut t = TestHistoryBuilder::default();
2210
+ t.add_wfe_started_with_wft_timeout(Duration::from_millis(200));
2211
+ t.add_full_wf_task();
2212
+ t.add_workflow_task_scheduled_and_started();
2213
+
2214
+ let mock = mock_worker_client();
2215
+ let mut mock_cfg = MockPollCfg::from_resp_batches(
2216
+ wfid,
2217
+ t,
2218
+ [ResponseType::ToTaskNum(1), ResponseType::ToTaskNum(2)],
2219
+ mock,
2220
+ );
2221
+ mock_cfg.make_poll_stream_interminable = true;
2222
+ mock_cfg.completion_asserts_from_expectations(|mut asserts| {
2223
+ asserts
2224
+ .then(move |wft| {
2225
+ assert_eq!(wft.commands.len(), 0);
2226
+ })
2227
+ .then(move |wft| {
2228
+ assert_eq!(wft.commands.len(), 2);
2229
+ assert_eq!(wft.commands[0].command_type(), CommandType::RecordMarker);
2230
+ assert_eq!(
2231
+ wft.commands[1].command_type(),
2232
+ CommandType::CompleteWorkflowExecution
2233
+ );
2234
+ });
2235
+ });
2236
+ let mut mock = build_mock_pollers(mock_cfg);
2237
+ mock.worker_cfg(|wc| {
2238
+ wc.max_cached_workflows = 1;
2239
+ wc.ignore_evicts_on_shutdown = false;
2240
+ });
2241
+ let core = mock_worker(mock);
2242
+
2243
+ let barr = Barrier::new(2);
2244
+
2245
+ let task = core.poll_workflow_activation().await.unwrap();
2246
+ core.complete_workflow_activation(WorkflowActivationCompletion::from_cmds(
2247
+ task.run_id,
2248
+ vec![
2249
+ ScheduleLocalActivity {
2250
+ seq: 1,
2251
+ activity_id: "1".to_string(),
2252
+ activity_type: "test_act".to_string(),
2253
+ start_to_close_timeout: Some(prost_dur!(from_secs(30))),
2254
+ ..Default::default()
2255
+ }
2256
+ .into(),
2257
+ ScheduleLocalActivity {
2258
+ seq: 2,
2259
+ activity_id: "2".to_string(),
2260
+ activity_type: "test_act".to_string(),
2261
+ start_to_close_timeout: Some(prost_dur!(from_secs(30))),
2262
+ ..Default::default()
2263
+ }
2264
+ .into(),
2265
+ ],
2266
+ ))
2267
+ .await
2268
+ .unwrap();
2269
+
2270
+ let wf_poller = async {
2271
+ let task = core.poll_workflow_activation().await.unwrap();
2272
+ assert_matches!(
2273
+ task.jobs.as_slice(),
2274
+ [WorkflowActivationJob {
2275
+ variant: Some(workflow_activation_job::Variant::ResolveActivity(_)),
2276
+ }]
2277
+ );
2278
+ barr.wait().await;
2279
+ core.complete_execution(&task.run_id).await;
2280
+ };
2281
+
2282
+ let at_poller = async {
2283
+ let act_task = core.poll_activity_task().await.unwrap();
2284
+ core.complete_activity_task(ActivityTaskCompletion {
2285
+ task_token: act_task.task_token,
2286
+ result: Some(ActivityExecutionResult::ok(vec![1].into())),
2287
+ })
2288
+ .await
2289
+ .unwrap();
2290
+ let act_task = core.poll_activity_task().await.unwrap();
2291
+ barr.wait().await;
2292
+ core.complete_activity_task(ActivityTaskCompletion {
2293
+ task_token: act_task.task_token,
2294
+ result: Some(ActivityExecutionResult::ok(vec![2].into())),
2295
+ })
2296
+ .await
2297
+ .unwrap();
2298
+ };
2299
+
2300
+ join!(wf_poller, at_poller);
2301
+ core.drain_pollers_and_shutdown().await;
2302
+ }
2303
+
2304
+ #[tokio::test]
2305
+ async fn local_act_retry_explicit_delay() {
2306
+ let mut t = TestHistoryBuilder::default();
2307
+ t.add_by_type(EventType::WorkflowExecutionStarted);
2308
+ t.add_workflow_task_scheduled_and_started();
2309
+
2310
+ let wf_id = "fakeid";
2311
+ let mock = mock_worker_client();
2312
+ let mh = MockPollCfg::from_resp_batches(wf_id, t, [1], mock);
2313
+ let mut worker = mock_sdk(mh);
2314
+
2315
+ worker.register_wf(
2316
+ DEFAULT_WORKFLOW_TYPE.to_owned(),
2317
+ move |ctx: WfContext| async move {
2318
+ let la_res = ctx
2319
+ .local_activity(LocalActivityOptions {
2320
+ activity_type: "echo".to_string(),
2321
+ input: "hi".as_json_payload().expect("serializes fine"),
2322
+ retry_policy: RetryPolicy {
2323
+ initial_interval: Some(prost_dur!(from_millis(50))),
2324
+ backoff_coefficient: 1.0,
2325
+ maximum_attempts: 5,
2326
+ ..Default::default()
2327
+ },
2328
+ ..Default::default()
2329
+ })
2330
+ .await;
2331
+ assert!(la_res.completed_ok());
2332
+ Ok(().into())
2333
+ },
2334
+ );
2335
+ let attempts: &'static _ = Box::leak(Box::new(AtomicUsize::new(0)));
2336
+ worker.register_activity("echo", move |_ctx: ActContext, _: String| async move {
2337
+ // Succeed on 3rd attempt (which is ==2 since fetch_add returns prev val)
2338
+ let last_attempt = attempts.fetch_add(1, Ordering::Relaxed);
2339
+ if 0 == last_attempt {
2340
+ Err(ActivityError::Retryable {
2341
+ source: anyhow!("Explicit backoff error"),
2342
+ explicit_delay: Some(Duration::from_millis(300)),
2343
+ })
2344
+ } else if 2 == last_attempt {
2345
+ Ok(())
2346
+ } else {
2347
+ Err(anyhow!("Oh no I failed!").into())
2348
+ }
2349
+ });
2350
+ worker
2351
+ .submit_wf(
2352
+ wf_id.to_owned(),
2353
+ DEFAULT_WORKFLOW_TYPE.to_owned(),
2354
+ vec![],
2355
+ WorkflowOptions::default(),
2356
+ )
2357
+ .await
2358
+ .unwrap();
2359
+ let start = Instant::now();
2360
+ worker.run_until_done().await.unwrap();
2361
+ let expected_attempts = 3;
2362
+ assert_eq!(expected_attempts, attempts.load(Ordering::Relaxed));
2363
+ // There will be one 300ms backoff and one 50s backoff, so things should take at least that long
2364
+ assert!(start.elapsed() > Duration::from_millis(350));
2365
+ }
2366
+
2367
+ async fn la_wf(ctx: WfContext) -> WorkflowResult<()> {
2368
+ ctx.local_activity(LocalActivityOptions {
2369
+ activity_type: DEFAULT_ACTIVITY_TYPE.to_string(),
2370
+ input: ().as_json_payload().unwrap(),
2371
+ retry_policy: RetryPolicy {
2372
+ maximum_attempts: 1,
2373
+ ..Default::default()
2374
+ },
2375
+ ..Default::default()
2376
+ })
2377
+ .await;
2378
+ Ok(().into())
2379
+ }
2380
+
2381
+ #[rstest]
2382
+ #[case::incremental(false, true)]
2383
+ #[case::replay(true, true)]
2384
+ #[case::incremental_fail(false, false)]
2385
+ #[case::replay_fail(true, false)]
2386
+ #[tokio::test]
2387
+ async fn one_la_success(#[case] replay: bool, #[case] completes_ok: bool) {
2388
+ let activity_id = "1";
2389
+ let mut t = TestHistoryBuilder::default();
2390
+ t.add_by_type(EventType::WorkflowExecutionStarted);
2391
+ t.add_full_wf_task();
2392
+ if completes_ok {
2393
+ t.add_local_activity_result_marker(1, activity_id, b"hi".into());
2394
+ } else {
2395
+ t.add_local_activity_fail_marker(
2396
+ 1,
2397
+ activity_id,
2398
+ Failure::application_failure("I failed".to_string(), false),
2399
+ );
2400
+ }
2401
+ t.add_workflow_task_scheduled_and_started();
2402
+
2403
+ let mut mock_cfg = if replay {
2404
+ MockPollCfg::from_resps(t, [ResponseType::AllHistory])
2405
+ } else {
2406
+ MockPollCfg::from_hist_builder(t)
2407
+ };
2408
+ mock_cfg.completion_asserts_from_expectations(|mut asserts| {
2409
+ asserts.then(move |wft| {
2410
+ let commands = &wft.commands;
2411
+ if !replay {
2412
+ assert_eq!(commands.len(), 2);
2413
+ assert_eq!(commands[0].command_type(), CommandType::RecordMarker);
2414
+ if completes_ok {
2415
+ assert_matches!(
2416
+ commands[0].attributes.as_ref().unwrap(),
2417
+ command::Attributes::RecordMarkerCommandAttributes(
2418
+ RecordMarkerCommandAttributes { failure: None, .. }
2419
+ )
2420
+ );
2421
+ } else {
2422
+ assert_matches!(
2423
+ commands[0].attributes.as_ref().unwrap(),
2424
+ command::Attributes::RecordMarkerCommandAttributes(
2425
+ RecordMarkerCommandAttributes {
2426
+ failure: Some(_),
2427
+ ..
2428
+ }
2429
+ )
2430
+ );
2431
+ }
2432
+ assert_eq!(
2433
+ commands[1].command_type(),
2434
+ CommandType::CompleteWorkflowExecution
2435
+ );
2436
+ } else {
2437
+ assert_eq!(commands.len(), 1);
2438
+ assert_matches!(
2439
+ commands[0].command_type(),
2440
+ CommandType::CompleteWorkflowExecution
2441
+ );
2442
+ }
2443
+ });
2444
+ });
2445
+
2446
+ let mut worker = build_fake_sdk(mock_cfg);
2447
+ worker.register_wf(DEFAULT_WORKFLOW_TYPE, la_wf);
2448
+ worker.register_activity(
2449
+ DEFAULT_ACTIVITY_TYPE,
2450
+ move |_ctx: ActContext, _: ()| async move {
2451
+ if replay {
2452
+ panic!("Should not be invoked on replay");
2453
+ }
2454
+ if completes_ok {
2455
+ Ok("hi")
2456
+ } else {
2457
+ Err(anyhow!("Oh no I failed!").into())
2458
+ }
2459
+ },
2460
+ );
2461
+ worker.run().await.unwrap();
2462
+ }
2463
+
2464
+ async fn two_la_wf(ctx: WfContext) -> WorkflowResult<()> {
2465
+ ctx.local_activity(LocalActivityOptions {
2466
+ activity_type: DEFAULT_ACTIVITY_TYPE.to_string(),
2467
+ input: ().as_json_payload().unwrap(),
2468
+ ..Default::default()
2469
+ })
2470
+ .await;
2471
+ ctx.local_activity(LocalActivityOptions {
2472
+ activity_type: DEFAULT_ACTIVITY_TYPE.to_string(),
2473
+ input: ().as_json_payload().unwrap(),
2474
+ ..Default::default()
2475
+ })
2476
+ .await;
2477
+ Ok(().into())
2478
+ }
2479
+
2480
+ async fn two_la_wf_parallel(ctx: WfContext) -> WorkflowResult<()> {
2481
+ tokio::join!(
2482
+ ctx.local_activity(LocalActivityOptions {
2483
+ activity_type: DEFAULT_ACTIVITY_TYPE.to_string(),
2484
+ input: ().as_json_payload().unwrap(),
2485
+ ..Default::default()
2486
+ }),
2487
+ ctx.local_activity(LocalActivityOptions {
2488
+ activity_type: DEFAULT_ACTIVITY_TYPE.to_string(),
2489
+ input: ().as_json_payload().unwrap(),
2490
+ ..Default::default()
2491
+ })
2492
+ );
2493
+ Ok(().into())
2494
+ }
2495
+
2496
+ #[rstest]
2497
+ #[tokio::test]
2498
+ async fn two_sequential_las(
2499
+ #[values(true, false)] replay: bool,
2500
+ #[values(true, false)] parallel: bool,
2501
+ ) {
2502
+ let t = canned_histories::two_local_activities_one_wft(parallel);
2503
+ let mut mock_cfg = if replay {
2504
+ MockPollCfg::from_resps(t, [ResponseType::AllHistory])
2505
+ } else {
2506
+ MockPollCfg::from_hist_builder(t)
2507
+ };
2508
+
2509
+ let mut aai = ActivationAssertionsInterceptor::default();
2510
+ let first_act_ts_seconds: &'static _ = Box::leak(Box::new(AtomicI64::new(-1)));
2511
+ aai.then(|a| {
2512
+ first_act_ts_seconds.store(a.timestamp.as_ref().unwrap().seconds, Ordering::Relaxed)
2513
+ });
2514
+ // Verify LAs advance time (they take 1s as defined in the canned history)
2515
+ aai.then(move |a| {
2516
+ if !parallel {
2517
+ assert_matches!(
2518
+ a.jobs.as_slice(),
2519
+ [WorkflowActivationJob {
2520
+ variant: Some(workflow_activation_job::Variant::ResolveActivity(ra))
2521
+ }] => assert_eq!(ra.seq, 1)
2522
+ );
2523
+ } else {
2524
+ assert_matches!(
2525
+ a.jobs.as_slice(),
2526
+ [WorkflowActivationJob {
2527
+ variant: Some(workflow_activation_job::Variant::ResolveActivity(ra))
2528
+ }, WorkflowActivationJob {
2529
+ variant: Some(workflow_activation_job::Variant::ResolveActivity(ra2))
2530
+ }] => {assert_eq!(ra.seq, 1); assert_eq!(ra2.seq, 2)}
2531
+ );
2532
+ }
2533
+ if replay {
2534
+ assert!(
2535
+ a.timestamp.as_ref().unwrap().seconds
2536
+ > first_act_ts_seconds.load(Ordering::Relaxed)
2537
+ )
2538
+ }
2539
+ });
2540
+ if !parallel {
2541
+ aai.then(move |a| {
2542
+ assert_matches!(
2543
+ a.jobs.as_slice(),
2544
+ [WorkflowActivationJob {
2545
+ variant: Some(workflow_activation_job::Variant::ResolveActivity(ra))
2546
+ }] => assert_eq!(ra.seq, 2)
2547
+ );
2548
+ if replay {
2549
+ assert!(
2550
+ a.timestamp.as_ref().unwrap().seconds
2551
+ >= first_act_ts_seconds.load(Ordering::Relaxed) + 2
2552
+ )
2553
+ }
2554
+ });
2555
+ }
2556
+
2557
+ mock_cfg.completion_asserts_from_expectations(|mut asserts| {
2558
+ asserts.then(move |wft| {
2559
+ let commands = &wft.commands;
2560
+ if !replay {
2561
+ assert_eq!(commands.len(), 3);
2562
+ assert_eq!(commands[0].command_type(), CommandType::RecordMarker);
2563
+ assert_eq!(commands[1].command_type(), CommandType::RecordMarker);
2564
+ assert_matches!(
2565
+ commands[2].command_type(),
2566
+ CommandType::CompleteWorkflowExecution
2567
+ );
2568
+ } else {
2569
+ assert_eq!(commands.len(), 1);
2570
+ assert_matches!(
2571
+ commands[0].command_type(),
2572
+ CommandType::CompleteWorkflowExecution
2573
+ );
2574
+ }
2575
+ });
2576
+ });
2577
+
2578
+ let mut worker = build_fake_sdk(mock_cfg);
2579
+ worker.set_worker_interceptor(aai);
2580
+ if parallel {
2581
+ worker.register_wf(DEFAULT_WORKFLOW_TYPE, two_la_wf_parallel);
2582
+ } else {
2583
+ worker.register_wf(DEFAULT_WORKFLOW_TYPE, two_la_wf);
2584
+ }
2585
+ worker.register_activity(
2586
+ DEFAULT_ACTIVITY_TYPE,
2587
+ move |_ctx: ActContext, _: ()| async move { Ok("Resolved") },
2588
+ );
2589
+ worker.run().await.unwrap();
2590
+ }
2591
+
2592
+ async fn la_timer_la(ctx: WfContext) -> WorkflowResult<()> {
2593
+ ctx.local_activity(LocalActivityOptions {
2594
+ activity_type: DEFAULT_ACTIVITY_TYPE.to_string(),
2595
+ input: ().as_json_payload().unwrap(),
2596
+ ..Default::default()
2597
+ })
2598
+ .await;
2599
+ ctx.timer(Duration::from_secs(5)).await;
2600
+ ctx.local_activity(LocalActivityOptions {
2601
+ activity_type: DEFAULT_ACTIVITY_TYPE.to_string(),
2602
+ input: ().as_json_payload().unwrap(),
2603
+ ..Default::default()
2604
+ })
2605
+ .await;
2606
+ Ok(().into())
2607
+ }
2608
+
2609
+ #[rstest]
2610
+ #[case::incremental(false)]
2611
+ #[case::replay(true)]
2612
+ #[tokio::test]
2613
+ async fn las_separated_by_timer(#[case] replay: bool) {
2614
+ let mut t = TestHistoryBuilder::default();
2615
+ t.add_by_type(EventType::WorkflowExecutionStarted);
2616
+ t.add_full_wf_task();
2617
+ t.add_local_activity_result_marker(1, "1", b"hi".into());
2618
+ let timer_started_event_id = t.add_by_type(EventType::TimerStarted);
2619
+ t.add_timer_fired(timer_started_event_id, "1".to_string());
2620
+ t.add_full_wf_task();
2621
+ t.add_local_activity_result_marker(2, "2", b"hi2".into());
2622
+ t.add_workflow_task_scheduled_and_started();
2623
+ let mut mock_cfg = if replay {
2624
+ MockPollCfg::from_resps(t, [ResponseType::AllHistory])
2625
+ } else {
2626
+ MockPollCfg::from_hist_builder(t)
2627
+ };
2628
+
2629
+ let mut aai = ActivationAssertionsInterceptor::default();
2630
+ aai.skip_one()
2631
+ .then(|a| {
2632
+ assert_matches!(
2633
+ a.jobs.as_slice(),
2634
+ [WorkflowActivationJob {
2635
+ variant: Some(workflow_activation_job::Variant::ResolveActivity(ra))
2636
+ }] => assert_eq!(ra.seq, 1)
2637
+ );
2638
+ })
2639
+ .then(|a| {
2640
+ assert_matches!(
2641
+ a.jobs.as_slice(),
2642
+ [WorkflowActivationJob {
2643
+ variant: Some(workflow_activation_job::Variant::FireTimer(_))
2644
+ }]
2645
+ );
2646
+ });
2647
+
2648
+ mock_cfg.completion_asserts_from_expectations(|mut asserts| {
2649
+ if replay {
2650
+ asserts.then(|wft| {
2651
+ assert_eq!(wft.commands.len(), 1);
2652
+ assert_eq!(
2653
+ wft.commands[0].command_type,
2654
+ CommandType::CompleteWorkflowExecution as i32
2655
+ );
2656
+ });
2657
+ } else {
2658
+ asserts
2659
+ .then(|wft| {
2660
+ let commands = &wft.commands;
2661
+ assert_eq!(commands.len(), 2);
2662
+ assert_eq!(commands[0].command_type, CommandType::RecordMarker as i32);
2663
+ assert_eq!(commands[1].command_type, CommandType::StartTimer as i32);
2664
+ })
2665
+ .then(|wft| {
2666
+ let commands = &wft.commands;
2667
+ assert_eq!(commands.len(), 2);
2668
+ assert_eq!(commands[0].command_type, CommandType::RecordMarker as i32);
2669
+ assert_eq!(
2670
+ commands[1].command_type,
2671
+ CommandType::CompleteWorkflowExecution as i32
2672
+ );
2673
+ });
2674
+ }
2675
+ });
2676
+
2677
+ let mut worker = build_fake_sdk(mock_cfg);
2678
+ worker.set_worker_interceptor(aai);
2679
+ worker.register_wf(DEFAULT_WORKFLOW_TYPE, la_timer_la);
2680
+ worker.register_activity(
2681
+ DEFAULT_ACTIVITY_TYPE,
2682
+ move |_ctx: ActContext, _: ()| async move { Ok("Resolved") },
2683
+ );
2684
+ worker.run().await.unwrap();
2685
+ }
2686
+
2687
+ #[tokio::test]
2688
+ async fn one_la_heartbeating_wft_failure_still_executes() {
2689
+ let mut t = TestHistoryBuilder::default();
2690
+ t.add_by_type(EventType::WorkflowExecutionStarted);
2691
+ // Heartbeats
2692
+ t.add_full_wf_task();
2693
+ // fails a wft for some reason
2694
+ t.add_workflow_task_scheduled_and_started();
2695
+ t.add_workflow_task_failed_with_failure(
2696
+ WorkflowTaskFailedCause::NonDeterministicError,
2697
+ Default::default(),
2698
+ );
2699
+ t.add_workflow_task_scheduled_and_started();
2700
+
2701
+ let mut mock_cfg = MockPollCfg::from_hist_builder(t);
2702
+ mock_cfg.completion_asserts_from_expectations(|mut asserts| {
2703
+ asserts.then(move |wft| {
2704
+ assert_eq!(wft.commands.len(), 2);
2705
+ assert_eq!(wft.commands[0].command_type(), CommandType::RecordMarker);
2706
+ assert_matches!(
2707
+ wft.commands[1].command_type(),
2708
+ CommandType::CompleteWorkflowExecution
2709
+ );
2710
+ });
2711
+ });
2712
+
2713
+ let mut worker = build_fake_sdk(mock_cfg);
2714
+ worker.register_wf(DEFAULT_WORKFLOW_TYPE, la_wf);
2715
+ worker.register_activity(
2716
+ DEFAULT_ACTIVITY_TYPE,
2717
+ move |_ctx: ActContext, _: ()| async move { Ok("Resolved") },
2718
+ );
2719
+ worker.run().await.unwrap();
2720
+ }
2721
+
2722
+ #[rstest]
2723
+ #[tokio::test]
2724
+ async fn immediate_cancel(
2725
+ #[values(
2726
+ ActivityCancellationType::WaitCancellationCompleted,
2727
+ ActivityCancellationType::TryCancel,
2728
+ ActivityCancellationType::Abandon
2729
+ )]
2730
+ cancel_type: ActivityCancellationType,
2731
+ ) {
2732
+ let mut t = TestHistoryBuilder::default();
2733
+ t.add_by_type(EventType::WorkflowExecutionStarted);
2734
+ t.add_full_wf_task();
2735
+ t.add_workflow_execution_completed();
2736
+
2737
+ let mut mock_cfg = MockPollCfg::from_hist_builder(t);
2738
+ mock_cfg.completion_asserts_from_expectations(|mut asserts| {
2739
+ asserts.then(|wft| {
2740
+ assert_eq!(wft.commands.len(), 2);
2741
+ // We record the cancel marker
2742
+ assert_eq!(wft.commands[0].command_type(), CommandType::RecordMarker);
2743
+ assert_matches!(
2744
+ wft.commands[1].command_type(),
2745
+ CommandType::CompleteWorkflowExecution
2746
+ );
2747
+ });
2748
+ });
2749
+
2750
+ let mut worker = build_fake_sdk(mock_cfg);
2751
+ worker.register_wf(DEFAULT_WORKFLOW_TYPE, move |ctx: WfContext| async move {
2752
+ let la = ctx.local_activity(LocalActivityOptions {
2753
+ cancel_type,
2754
+ ..Default::default()
2755
+ });
2756
+ la.cancel(&ctx);
2757
+ la.await;
2758
+ Ok(().into())
2759
+ });
2760
+ // Explicitly don't register an activity, since we shouldn't need to run one.
2761
+ worker.run().await.unwrap();
2762
+ }
2763
+
2764
+ #[rstest]
2765
+ #[case::incremental(false)]
2766
+ #[case::replay(true)]
2767
+ #[tokio::test]
2768
+ async fn cancel_after_act_starts_canned(
2769
+ #[case] replay: bool,
2770
+ #[values(
2771
+ ActivityCancellationType::WaitCancellationCompleted,
2772
+ ActivityCancellationType::TryCancel,
2773
+ ActivityCancellationType::Abandon
2774
+ )]
2775
+ cancel_type: ActivityCancellationType,
2776
+ ) {
2777
+ let mut t = TestHistoryBuilder::default();
2778
+ t.add_wfe_started_with_wft_timeout(Duration::from_millis(100));
2779
+ t.add_full_wf_task();
2780
+ let timer_started_event_id = t.add_by_type(EventType::TimerStarted);
2781
+ t.add_timer_fired(timer_started_event_id, "1".to_string());
2782
+ t.add_full_wf_task();
2783
+ // This extra workflow task serves to prevent looking ahead and pre-resolving during
2784
+ // wait-cancel.
2785
+ // TODO: including this on non wait-cancel seems to cause double-send of
2786
+ // marker recorded cmd
2787
+ if cancel_type == ActivityCancellationType::WaitCancellationCompleted {
2788
+ t.add_full_wf_task();
2789
+ }
2790
+ if cancel_type != ActivityCancellationType::WaitCancellationCompleted {
2791
+ // With non-wait cancels, the cancel is immediate
2792
+ t.add_local_activity_cancel_marker(1, "1");
2793
+ }
2794
+ let timer_started_event_id = t.add_by_type(EventType::TimerStarted);
2795
+ if cancel_type == ActivityCancellationType::WaitCancellationCompleted {
2796
+ // With wait cancels, the cancel marker is not recorded until activity reports.
2797
+ t.add_local_activity_cancel_marker(1, "1");
2798
+ }
2799
+ t.add_timer_fired(timer_started_event_id, "2".to_string());
2800
+ t.add_full_wf_task();
2801
+ t.add_workflow_execution_completed();
2802
+
2803
+ let mut mock_cfg = if replay {
2804
+ MockPollCfg::from_resps(t, [ResponseType::AllHistory])
2805
+ } else {
2806
+ MockPollCfg::from_hist_builder(t)
2807
+ };
2808
+ let allow_cancel_barr = CancellationToken::new();
2809
+ let allow_cancel_barr_clone = allow_cancel_barr.clone();
2810
+
2811
+ if !replay {
2812
+ mock_cfg.completion_asserts_from_expectations(|mut asserts| {
2813
+ asserts
2814
+ .then(move |wft| {
2815
+ assert_eq!(wft.commands.len(), 1);
2816
+ assert_eq!(wft.commands[0].command_type, CommandType::StartTimer as i32);
2817
+ })
2818
+ .then(move |wft| {
2819
+ let commands = &wft.commands;
2820
+ if cancel_type == ActivityCancellationType::WaitCancellationCompleted {
2821
+ assert_eq!(commands.len(), 1);
2822
+ assert_eq!(commands[0].command_type, CommandType::StartTimer as i32);
2823
+ } else {
2824
+ // Try-cancel/abandon immediately recordsmarker (when not replaying)
2825
+ assert_eq!(commands.len(), 2);
2826
+ assert_eq!(commands[0].command_type, CommandType::RecordMarker as i32);
2827
+ assert_eq!(commands[1].command_type, CommandType::StartTimer as i32);
2828
+ }
2829
+ // Allow the wait-cancel to actually cancel
2830
+ allow_cancel_barr.cancel();
2831
+ })
2832
+ .then(move |wft| {
2833
+ let commands = &wft.commands;
2834
+ if cancel_type == ActivityCancellationType::WaitCancellationCompleted {
2835
+ assert_eq!(commands[0].command_type, CommandType::StartTimer as i32);
2836
+ assert_eq!(commands[1].command_type, CommandType::RecordMarker as i32);
2837
+ } else {
2838
+ assert_eq!(
2839
+ commands[0].command_type,
2840
+ CommandType::CompleteWorkflowExecution as i32
2841
+ );
2842
+ }
2843
+ });
2844
+ });
2845
+ }
2846
+
2847
+ let mut worker = build_fake_sdk(mock_cfg);
2848
+ worker.register_wf(DEFAULT_WORKFLOW_TYPE, move |ctx: WfContext| async move {
2849
+ let la = ctx.local_activity(LocalActivityOptions {
2850
+ cancel_type,
2851
+ input: ().as_json_payload().unwrap(),
2852
+ activity_type: DEFAULT_ACTIVITY_TYPE.to_string(),
2853
+ ..Default::default()
2854
+ });
2855
+ ctx.timer(Duration::from_secs(1)).await;
2856
+ la.cancel(&ctx);
2857
+ // This extra timer is here to ensure the presence of another WF task doesn't mess up
2858
+ // resolving the LA with cancel on replay
2859
+ ctx.timer(Duration::from_secs(1)).await;
2860
+ let resolution = la.await;
2861
+ assert!(resolution.cancelled());
2862
+ let rfail = resolution.unwrap_failure();
2863
+ assert_matches!(
2864
+ rfail.failure_info,
2865
+ Some(FailureInfo::ActivityFailureInfo(_))
2866
+ );
2867
+ assert_matches!(
2868
+ rfail.cause.unwrap().failure_info,
2869
+ Some(FailureInfo::CanceledFailureInfo(_))
2870
+ );
2871
+ Ok(().into())
2872
+ });
2873
+ worker.register_activity(DEFAULT_ACTIVITY_TYPE, move |ctx: ActContext, _: ()| {
2874
+ let allow_cancel_barr_clone = allow_cancel_barr_clone.clone();
2875
+ async move {
2876
+ if cancel_type == ActivityCancellationType::WaitCancellationCompleted {
2877
+ ctx.cancelled().await;
2878
+ }
2879
+ allow_cancel_barr_clone.cancelled().await;
2880
+ Result::<(), _>::Err(ActivityError::cancelled())
2881
+ }
2882
+ });
2883
+ worker.run().await.unwrap();
2884
+ }