@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
@@ -0,0 +1,218 @@
1
+ //! Unit test helpers - only available in unit tests (cfg(test))
2
+
3
+ use futures_util::{StreamExt, stream::FuturesUnordered};
4
+ use std::{collections::HashSet, future::Future};
5
+ use temporal_sdk_core_api::Worker as CoreWorker;
6
+ use temporal_sdk_core_protos::coresdk::{
7
+ workflow_activation::workflow_activation_job,
8
+ workflow_completion::{WorkflowActivationCompletion, workflow_activation_completion},
9
+ };
10
+
11
+ /// Given a desired number of concurrent executions and a provided function that produces a future,
12
+ /// run that many instances of the future concurrently.
13
+ ///
14
+ /// Annoyingly, because of a sorta-bug in the way async blocks work, the async block produced by
15
+ /// the closure must be `async move` if it uses the provided iteration number. On the plus side,
16
+ /// since you're usually just accessing core in the closure, if core is a reference everything just
17
+ /// works. See <https://github.com/rust-lang/rust/issues/81653>
18
+ pub async fn fanout_tasks<FutureMaker, Fut>(num: usize, fm: FutureMaker)
19
+ where
20
+ FutureMaker: Fn(usize) -> Fut,
21
+ Fut: Future,
22
+ {
23
+ let mut tasks = FuturesUnordered::new();
24
+ for i in 0..num {
25
+ tasks.push(fm(i));
26
+ }
27
+
28
+ while tasks.next().await.is_some() {}
29
+ }
30
+
31
+ /// Generate asserts for [poll_and_reply] by passing patterns to match against the job list
32
+ #[macro_export]
33
+ macro_rules! job_assert {
34
+ ($($pat:pat),+) => {
35
+ |res| {
36
+ assert_matches!(
37
+ res.jobs.as_slice(),
38
+ [$(WorkflowActivationJob {
39
+ variant: Some($pat),
40
+ }),+]
41
+ );
42
+ }
43
+ };
44
+ }
45
+
46
+ type AsserterWithReply<'a> = (
47
+ &'a dyn Fn(&temporal_sdk_core_protos::coresdk::workflow_activation::WorkflowActivation),
48
+ workflow_activation_completion::Status,
49
+ );
50
+
51
+ /// Determines when workflows are kept in the cache or evicted for [poll_and_reply] type tests
52
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
53
+ pub(crate) enum WorkflowCachingPolicy {
54
+ /// Workflows are evicted after each workflow task completion. Note that this is *not* after
55
+ /// each workflow activation - there are often multiple activations per workflow task.
56
+ NonSticky,
57
+
58
+ /// Not a real mode, but good for imitating crashes. Evict workflows after *every* reply,
59
+ /// even if there are pending activations
60
+ AfterEveryReply,
61
+ }
62
+
63
+ /// This function accepts a list of asserts and replies to workflow activations to run against the
64
+ /// provided instance of fake core.
65
+ ///
66
+ /// It handles the business of re-sending the same activation replies over again in the event
67
+ /// of eviction or workflow activation failure. Activation failures specifically are only run once,
68
+ /// since they clearly can't be returned every time we replay the workflow, or it could never
69
+ /// proceed
70
+ pub(crate) async fn poll_and_reply<'a>(
71
+ worker: &'a crate::Worker,
72
+ eviction_mode: WorkflowCachingPolicy,
73
+ expect_and_reply: &'a [AsserterWithReply<'a>],
74
+ ) {
75
+ poll_and_reply_clears_outstanding_evicts(worker, None, eviction_mode, expect_and_reply).await;
76
+ }
77
+
78
+ use crate::{pollers::MockPoller, test_help::OutstandingWFTMap};
79
+
80
+ pub(crate) async fn poll_and_reply_clears_outstanding_evicts<'a>(
81
+ worker: &'a crate::Worker,
82
+ outstanding_map: Option<OutstandingWFTMap>,
83
+ eviction_mode: WorkflowCachingPolicy,
84
+ expect_and_reply: &'a [AsserterWithReply<'a>],
85
+ ) {
86
+ let mut evictions = 0;
87
+ let expected_evictions = expect_and_reply.len() - 1;
88
+ let mut executed_failures = HashSet::new();
89
+ let expected_fail_count = expect_and_reply
90
+ .iter()
91
+ .filter(|(_, reply)| !reply.is_success())
92
+ .count();
93
+
94
+ 'outer: loop {
95
+ let expect_iter = expect_and_reply.iter();
96
+
97
+ for (i, interaction) in expect_iter.enumerate() {
98
+ let (asserter, reply) = interaction;
99
+ let complete_is_failure = !reply.is_success();
100
+ // Only send activation failures once
101
+ if executed_failures.contains(&i) {
102
+ continue;
103
+ }
104
+
105
+ let mut res = worker.poll_workflow_activation().await.unwrap();
106
+ if res.jobs.iter().any(|j| {
107
+ matches!(
108
+ j.variant,
109
+ Some(workflow_activation_job::Variant::RemoveFromCache(_))
110
+ )
111
+ }) && res.jobs.len() > 1
112
+ {
113
+ panic!("Saw an activation with an eviction & other work! {res:?}");
114
+ }
115
+ let is_eviction = res.is_only_eviction();
116
+
117
+ let mut do_release = false;
118
+
119
+ if is_eviction {
120
+ // If the job is an eviction, clear it, since in the tests we don't explicitly
121
+ // specify evict assertions
122
+ res.jobs.clear();
123
+ do_release = true;
124
+ }
125
+
126
+ // TODO: Can remove this if?
127
+ if !res.jobs.is_empty() {
128
+ asserter(&res);
129
+ }
130
+
131
+ let reply = if res.jobs.is_empty() {
132
+ // Just an eviction
133
+ WorkflowActivationCompletion::empty(res.run_id.clone())
134
+ } else {
135
+ // Eviction plus some work, we still want to issue the reply
136
+ WorkflowActivationCompletion {
137
+ run_id: res.run_id.clone(),
138
+ status: Some(reply.clone()),
139
+ }
140
+ };
141
+
142
+ let ends_execution = reply.has_execution_ending();
143
+
144
+ worker.complete_workflow_activation(reply).await.unwrap();
145
+
146
+ if do_release && let Some(omap) = outstanding_map.as_ref() {
147
+ omap.release_run(&res.run_id);
148
+ }
149
+ // Restart assertions from the beginning if it was an eviction (and workflow execution
150
+ // isn't over)
151
+ if is_eviction && !ends_execution {
152
+ continue 'outer;
153
+ }
154
+
155
+ if complete_is_failure {
156
+ executed_failures.insert(i);
157
+ }
158
+
159
+ match eviction_mode {
160
+ WorkflowCachingPolicy::NonSticky => (),
161
+ WorkflowCachingPolicy::AfterEveryReply => {
162
+ if evictions < expected_evictions {
163
+ worker.request_workflow_eviction(&res.run_id);
164
+ evictions += 1;
165
+ }
166
+ }
167
+ }
168
+ }
169
+
170
+ break;
171
+ }
172
+
173
+ assert_eq!(expected_fail_count, executed_failures.len());
174
+ assert_eq!(worker.outstanding_workflow_tasks().await, 0);
175
+ }
176
+
177
+ pub(crate) fn gen_assert_and_reply(
178
+ asserter: &dyn Fn(&temporal_sdk_core_protos::coresdk::workflow_activation::WorkflowActivation),
179
+ reply_commands: Vec<
180
+ temporal_sdk_core_protos::coresdk::workflow_commands::workflow_command::Variant,
181
+ >,
182
+ ) -> AsserterWithReply<'_> {
183
+ (
184
+ asserter,
185
+ temporal_sdk_core_protos::coresdk::workflow_completion::Success::from_variants(
186
+ reply_commands,
187
+ )
188
+ .into(),
189
+ )
190
+ }
191
+
192
+ pub(crate) fn gen_assert_and_fail(
193
+ asserter: &dyn Fn(&temporal_sdk_core_protos::coresdk::workflow_activation::WorkflowActivation),
194
+ ) -> AsserterWithReply<'_> {
195
+ (
196
+ asserter,
197
+ temporal_sdk_core_protos::coresdk::workflow_completion::Failure {
198
+ failure: Some(
199
+ temporal_sdk_core_protos::temporal::api::failure::v1::Failure {
200
+ message: "Intentional test failure".to_string(),
201
+ ..Default::default()
202
+ },
203
+ ),
204
+ ..Default::default()
205
+ }
206
+ .into(),
207
+ )
208
+ }
209
+
210
+ pub(crate) fn mock_poller<T>() -> MockPoller<T>
211
+ where
212
+ T: Send + Sync + 'static,
213
+ {
214
+ let mut mock_poller = MockPoller::new();
215
+ mock_poller.expect_shutdown_box().return_const(());
216
+ mock_poller.expect_notify_shutdown().return_const(());
217
+ mock_poller
218
+ }
@@ -42,6 +42,7 @@ enum HeartbeatAction {
42
42
  Evict {
43
43
  token: TaskToken,
44
44
  on_complete: Arc<Notify>,
45
+ should_flush: bool,
45
46
  },
46
47
  CompleteReport(TaskToken),
47
48
  CompleteThrottle(TaskToken),
@@ -118,7 +119,7 @@ impl ActivityHeartbeatManager {
118
119
  HeartbeatAction::SendHeartbeat(hb) => hb_states.record(hb),
119
120
  HeartbeatAction::CompleteReport(tt) => hb_states.handle_report_completed(tt),
120
121
  HeartbeatAction::CompleteThrottle(tt) => hb_states.handle_throttle_completed(tt),
121
- HeartbeatAction::Evict{ token, on_complete } => hb_states.evict(token, on_complete),
122
+ HeartbeatAction::Evict{ token, on_complete, should_flush } => hb_states.evict(token, on_complete, should_flush),
122
123
  },
123
124
  hb_states,
124
125
  ))
@@ -230,13 +231,14 @@ impl ActivityHeartbeatManager {
230
231
  }
231
232
 
232
233
  /// Tell the heartbeat manager we are done forever with a certain task, so it may be forgotten.
233
- /// This will also force-flush the most recently provided details.
234
+ /// If should_flush is true, will also force-flush the most recently provided details.
234
235
  /// Record *should* not be called with the same TaskToken after calling this.
235
- pub(super) async fn evict(&self, task_token: TaskToken) {
236
+ pub(super) async fn evict(&self, task_token: TaskToken, should_flush: bool) {
236
237
  let completed = Arc::new(Notify::new());
237
238
  let _ = self.heartbeat_tx.send(HeartbeatAction::Evict {
238
239
  token: task_token,
239
240
  on_complete: completed.clone(),
241
+ should_flush,
240
242
  });
241
243
  completed.notified().await;
242
244
  }
@@ -397,12 +399,15 @@ impl HeartbeatStreamState {
397
399
  &mut self,
398
400
  tt: TaskToken,
399
401
  on_complete: Arc<Notify>,
402
+ should_flush: bool,
400
403
  ) -> Option<HeartbeatExecutorAction> {
401
404
  if let Some(state) = self.tt_to_state.remove(&tt) {
402
405
  if let Some(cancel_tok) = state.throttled_cancellation_token {
403
406
  cancel_tok.cancel();
404
407
  }
405
- if let Some(last_deets) = state.last_recorded_details {
408
+ if let Some(last_deets) = state.last_recorded_details
409
+ && should_flush
410
+ {
406
411
  self.tt_needs_flush.insert(tt.clone(), on_complete);
407
412
  return Some(HeartbeatExecutorAction::Report {
408
413
  task_token: tt,
@@ -524,7 +529,7 @@ mod test {
524
529
  record_heartbeat(&hm, fake_task_token.clone(), 0, Duration::from_millis(100));
525
530
  // Let it propagate
526
531
  sleep(Duration::from_millis(10)).await;
527
- hm.evict(fake_task_token.clone().into()).await;
532
+ hm.evict(fake_task_token.clone().into(), true).await;
528
533
  record_heartbeat(&hm, fake_task_token, 0, Duration::from_millis(100));
529
534
  // Let it propagate
530
535
  sleep(Duration::from_millis(10)).await;
@@ -543,7 +548,38 @@ mod test {
543
548
  let hm = ActivityHeartbeatManager::new(Arc::new(mock_client), cancel_tx);
544
549
  let fake_task_token = vec![1, 2, 3];
545
550
  record_heartbeat(&hm, fake_task_token.clone(), 0, Duration::from_millis(100));
546
- hm.evict(fake_task_token.clone().into()).await;
551
+ hm.evict(fake_task_token.clone().into(), true).await;
552
+ hm.shutdown().await;
553
+ }
554
+
555
+ #[tokio::test]
556
+ async fn no_flush_on_successful_completion() {
557
+ let mut mock_client = mock_worker_client();
558
+ // Should only expect 1 heartbeat call, not 2 (the second would be from evict flushing)
559
+ mock_client
560
+ .expect_record_activity_heartbeat()
561
+ .returning(|_, _| Ok(RecordActivityTaskHeartbeatResponse::default()))
562
+ .times(1);
563
+ let (cancel_tx, _cancel_rx) = unbounded_channel();
564
+ let hm = ActivityHeartbeatManager::new(Arc::new(mock_client), cancel_tx);
565
+ let fake_task_token = vec![1, 2, 3];
566
+
567
+ // Record initial heartbeat - this should be sent immediately
568
+ record_heartbeat(&hm, fake_task_token.clone(), 0, Duration::from_millis(100));
569
+
570
+ // Wait a bit for initial heartbeat to process and enter throttling phase
571
+ sleep(Duration::from_millis(50)).await;
572
+
573
+ // Record another heartbeat while throttled - this should be stored in last_recorded_details
574
+ record_heartbeat(&hm, fake_task_token.clone(), 1, Duration::from_millis(100));
575
+
576
+ // Wait a bit to ensure the second heartbeat is recorded but not sent
577
+ sleep(Duration::from_millis(10)).await;
578
+
579
+ // Evict the activity with should_flush false
580
+ // This should NOT send the stored heartbeat details since the activity completed successfully
581
+ hm.evict(fake_task_token.into(), false).await;
582
+
547
583
  hm.shutdown().await;
548
584
  }
549
585
 
@@ -2,7 +2,6 @@ use crate::{
2
2
  MetricsContext, TaskToken,
3
3
  abstractions::{MeteredPermitDealer, OwnedMeteredSemPermit, UsedMeteredSemPermit, dbg_panic},
4
4
  protosext::ValidScheduleLA,
5
- retry_logic::RetryPolicyExt,
6
5
  telemetry::metrics::{activity_type, should_record_failure_metric, workflow_type},
7
6
  worker::workflow::HeartbeatTimeoutMsg,
8
7
  };
@@ -13,6 +12,7 @@ use parking_lot::{Mutex, MutexGuard};
13
12
  use std::{
14
13
  collections::{HashMap, hash_map::Entry},
15
14
  fmt::{Debug, Formatter},
15
+ num::NonZero,
16
16
  pin::Pin,
17
17
  task::{Context, Poll},
18
18
  time::{Duration, Instant, SystemTime},
@@ -490,7 +490,7 @@ impl LocalActivityManager {
490
490
  dispatch_time: Instant::now(),
491
491
  attempt,
492
492
  _permit: permit.into_used(LocalActivitySlotInfo {
493
- activity_type: new_la.workflow_type.clone(),
493
+ activity_type: sa.activity_type.clone(),
494
494
  }),
495
495
  },
496
496
  );
@@ -525,7 +525,7 @@ impl LocalActivityManager {
525
525
  .or(schedule_to_close)
526
526
  .and_then(|t| t.try_into().ok()),
527
527
  heartbeat_timeout: None,
528
- retry_policy: Some(sa.retry_policy),
528
+ retry_policy: Some(sa.retry_policy.into()),
529
529
  priority: Some(Default::default()),
530
530
  is_local: true,
531
531
  })),
@@ -570,7 +570,7 @@ impl LocalActivityManager {
570
570
  macro_rules! calc_backoff {
571
571
  ($fail: ident) => {
572
572
  info.la_info.schedule_cmd.retry_policy.should_retry(
573
- info.attempt as usize,
573
+ info.attempt.try_into().unwrap_or(NonZero::<u32>::MIN),
574
574
  $fail
575
575
  .failure
576
576
  .as_ref()
@@ -976,7 +976,7 @@ impl Drop for TimeoutBag {
976
976
  #[cfg(test)]
977
977
  mod tests {
978
978
  use super::*;
979
- use crate::{prost_dur, protosext::LACloseTimeouts};
979
+ use crate::{prost_dur, protosext::LACloseTimeouts, retry_logic::ValidatedRetryPolicy};
980
980
  use futures_util::FutureExt;
981
981
  use temporal_sdk_core_protos::temporal::api::{
982
982
  common::v1::RetryPolicy,
@@ -1111,13 +1111,13 @@ mod tests {
1111
1111
  seq: 1,
1112
1112
  activity_id: 1.to_string(),
1113
1113
  attempt: 5,
1114
- retry_policy: RetryPolicy {
1114
+ retry_policy: ValidatedRetryPolicy::from_proto_with_defaults(RetryPolicy {
1115
1115
  initial_interval: Some(prost_dur!(from_secs(1))),
1116
1116
  backoff_coefficient: 10.0,
1117
1117
  maximum_interval: Some(prost_dur!(from_secs(10))),
1118
1118
  maximum_attempts: 10,
1119
1119
  non_retryable_error_types: vec![],
1120
- },
1120
+ }),
1121
1121
  local_retry_threshold: Duration::from_secs(5),
1122
1122
  ..Default::default()
1123
1123
  },
@@ -1146,13 +1146,13 @@ mod tests {
1146
1146
  seq: 1,
1147
1147
  activity_id: "1".to_string(),
1148
1148
  attempt: 1,
1149
- retry_policy: RetryPolicy {
1149
+ retry_policy: ValidatedRetryPolicy::from_proto_with_defaults(RetryPolicy {
1150
1150
  initial_interval: Some(prost_dur!(from_secs(1))),
1151
1151
  backoff_coefficient: 10.0,
1152
1152
  maximum_interval: Some(prost_dur!(from_secs(10))),
1153
1153
  maximum_attempts: 10,
1154
1154
  non_retryable_error_types: vec!["TestError".to_string()],
1155
- },
1155
+ }),
1156
1156
  local_retry_threshold: Duration::from_secs(5),
1157
1157
  ..Default::default()
1158
1158
  },
@@ -1190,13 +1190,13 @@ mod tests {
1190
1190
  seq: 1,
1191
1191
  activity_id: 1.to_string(),
1192
1192
  attempt: 5,
1193
- retry_policy: RetryPolicy {
1193
+ retry_policy: ValidatedRetryPolicy::from_proto_with_defaults(RetryPolicy {
1194
1194
  initial_interval: Some(prost_dur!(from_secs(10))),
1195
1195
  backoff_coefficient: 1.0,
1196
1196
  maximum_interval: Some(prost_dur!(from_secs(10))),
1197
1197
  maximum_attempts: 10,
1198
1198
  non_retryable_error_types: vec![],
1199
- },
1199
+ }),
1200
1200
  local_retry_threshold: Duration::from_secs(500),
1201
1201
  ..Default::default()
1202
1202
  },
@@ -1239,11 +1239,11 @@ mod tests {
1239
1239
  seq: 1,
1240
1240
  activity_id: 1.to_string(),
1241
1241
  attempt: 5,
1242
- retry_policy: RetryPolicy {
1242
+ retry_policy: ValidatedRetryPolicy::from_proto_with_defaults(RetryPolicy {
1243
1243
  initial_interval: Some(prost_dur!(from_millis(10))),
1244
1244
  backoff_coefficient: 1.0,
1245
1245
  ..Default::default()
1246
- },
1246
+ }),
1247
1247
  local_retry_threshold: Duration::from_secs(500),
1248
1248
  ..Default::default()
1249
1249
  },
@@ -1276,11 +1276,11 @@ mod tests {
1276
1276
  seq: 1,
1277
1277
  activity_id: 1.to_string(),
1278
1278
  attempt: 5,
1279
- retry_policy: RetryPolicy {
1279
+ retry_policy: ValidatedRetryPolicy::from_proto_with_defaults(RetryPolicy {
1280
1280
  initial_interval: Some(prost_dur!(from_millis(10))),
1281
1281
  backoff_coefficient: 1.0,
1282
1282
  ..Default::default()
1283
- },
1283
+ }),
1284
1284
  local_retry_threshold: Duration::from_secs(500),
1285
1285
  schedule_to_start_timeout: Some(timeout),
1286
1286
  ..Default::default()
@@ -1319,12 +1319,12 @@ mod tests {
1319
1319
  seq: 1,
1320
1320
  activity_id: 1.to_string(),
1321
1321
  attempt: 5,
1322
- retry_policy: RetryPolicy {
1322
+ retry_policy: ValidatedRetryPolicy::from_proto_with_defaults(RetryPolicy {
1323
1323
  initial_interval: Some(prost_dur!(from_millis(10))),
1324
1324
  backoff_coefficient: 1.0,
1325
1325
  maximum_attempts: 1,
1326
1326
  ..Default::default()
1327
- },
1327
+ }),
1328
1328
  local_retry_threshold: Duration::from_secs(500),
1329
1329
  close_timeouts,
1330
1330
  ..Default::default()
@@ -1400,11 +1400,11 @@ mod tests {
1400
1400
  schedule_cmd: ValidScheduleLA {
1401
1401
  seq: 1,
1402
1402
  activity_id: 1.to_string(),
1403
- retry_policy: RetryPolicy {
1403
+ retry_policy: ValidatedRetryPolicy::from_proto_with_defaults(RetryPolicy {
1404
1404
  initial_interval: Some(prost_dur!(from_millis(1))),
1405
1405
  backoff_coefficient: 1.0,
1406
1406
  ..Default::default()
1407
- },
1407
+ }),
1408
1408
  local_retry_threshold: Duration::from_secs(500),
1409
1409
  ..Default::default()
1410
1410
  },
@@ -333,7 +333,14 @@ impl WorkerActivityTasks {
333
333
  if let Some(jh) = act_info.local_timeouts_task {
334
334
  jh.abort()
335
335
  };
336
- self.heartbeat_manager.evict(task_token.clone()).await;
336
+ let should_flush = !known_not_found
337
+ && !matches!(
338
+ &status,
339
+ aer::Status::Completed(_) | aer::Status::WillCompleteAsync(_)
340
+ );
341
+ self.heartbeat_manager
342
+ .evict(task_token.clone(), should_flush)
343
+ .await;
337
344
 
338
345
  // No need to report activities which we already know the server doesn't care about
339
346
  if !known_not_found {
@@ -400,8 +407,6 @@ impl WorkerActivityTasks {
400
407
  }
401
408
  };
402
409
 
403
- self.complete_notify.notify_waiters();
404
-
405
410
  if let Some(e) = maybe_net_err {
406
411
  if e.code() == tonic::Code::NotFound {
407
412
  warn!(task_token=?task_token, details=?e, "Activity not found on \
@@ -418,6 +423,8 @@ impl WorkerActivityTasks {
418
423
  &task_token
419
424
  );
420
425
  }
426
+
427
+ self.complete_notify.notify_waiters();
421
428
  }
422
429
 
423
430
  /// Attempt to record an activity heartbeat
@@ -20,9 +20,9 @@ pub(crate) static DEFAULT_TEST_CAPABILITIES: &Capabilities = &Capabilities {
20
20
  nexus: false,
21
21
  };
22
22
 
23
- #[cfg(test)]
23
+ #[cfg(any(feature = "test-utilities", test))]
24
24
  /// Create a mock client primed with basic necessary expectations
25
- pub(crate) fn mock_worker_client() -> MockWorkerClient {
25
+ pub fn mock_worker_client() -> MockWorkerClient {
26
26
  let mut r = MockWorkerClient::new();
27
27
  r.expect_capabilities()
28
28
  .returning(|| Some(*DEFAULT_TEST_CAPABILITIES));
@@ -152,7 +152,7 @@ mockall::mock! {
152
152
 
153
153
  fn record_worker_heartbeat<'a, 'b>(&self, heartbeat: WorkerHeartbeat) -> impl Future<Output = Result<RecordWorkerHeartbeatResponse>> + Send + 'b where 'a: 'b, Self: 'b;
154
154
 
155
- fn replace_client(&self, new_client: RetryClient<Client>);
155
+ fn replace_client(&self, new_client: Client);
156
156
  fn capabilities(&self) -> Option<Capabilities>;
157
157
  fn workers(&self) -> Arc<SlotManager>;
158
158
  fn is_mock(&self) -> bool;