@temporalio/core-bridge 0.14.0 → 0.16.4

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 (75) hide show
  1. package/Cargo.lock +162 -38
  2. package/Cargo.toml +3 -3
  3. package/index.d.ts +14 -1
  4. package/index.node +0 -0
  5. package/package.json +8 -5
  6. package/releases/aarch64-apple-darwin/index.node +0 -0
  7. package/releases/{x86_64-pc-windows-gnu → aarch64-unknown-linux-gnu}/index.node +0 -0
  8. package/releases/x86_64-apple-darwin/index.node +0 -0
  9. package/releases/x86_64-pc-windows-msvc/index.node +0 -0
  10. package/releases/x86_64-unknown-linux-gnu/index.node +0 -0
  11. package/scripts/build.js +77 -34
  12. package/sdk-core/.buildkite/docker/Dockerfile +1 -1
  13. package/sdk-core/Cargo.toml +6 -5
  14. package/sdk-core/fsm/Cargo.toml +1 -1
  15. package/sdk-core/fsm/rustfsm_procmacro/Cargo.toml +2 -2
  16. package/sdk-core/fsm/rustfsm_procmacro/src/lib.rs +8 -9
  17. package/sdk-core/fsm/rustfsm_procmacro/tests/trybuild/no_handle_conversions_require_into_fail.stderr +13 -7
  18. package/sdk-core/fsm/rustfsm_trait/Cargo.toml +2 -2
  19. package/sdk-core/fsm/rustfsm_trait/src/lib.rs +1 -1
  20. package/sdk-core/protos/local/workflow_activation.proto +6 -3
  21. package/sdk-core/sdk-core-protos/Cargo.toml +4 -4
  22. package/sdk-core/sdk-core-protos/src/lib.rs +38 -50
  23. package/sdk-core/src/core_tests/activity_tasks.rs +5 -5
  24. package/sdk-core/src/core_tests/child_workflows.rs +55 -29
  25. package/sdk-core/src/core_tests/determinism.rs +19 -9
  26. package/sdk-core/src/core_tests/mod.rs +3 -3
  27. package/sdk-core/src/core_tests/retry.rs +14 -8
  28. package/sdk-core/src/core_tests/workers.rs +1 -1
  29. package/sdk-core/src/core_tests/workflow_tasks.rs +347 -4
  30. package/sdk-core/src/errors.rs +27 -44
  31. package/sdk-core/src/lib.rs +13 -3
  32. package/sdk-core/src/machines/activity_state_machine.rs +44 -5
  33. package/sdk-core/src/machines/child_workflow_state_machine.rs +31 -11
  34. package/sdk-core/src/machines/complete_workflow_state_machine.rs +1 -1
  35. package/sdk-core/src/machines/continue_as_new_workflow_state_machine.rs +1 -1
  36. package/sdk-core/src/machines/mod.rs +18 -23
  37. package/sdk-core/src/machines/patch_state_machine.rs +8 -8
  38. package/sdk-core/src/machines/signal_external_state_machine.rs +22 -1
  39. package/sdk-core/src/machines/timer_state_machine.rs +21 -3
  40. package/sdk-core/src/machines/transition_coverage.rs +3 -3
  41. package/sdk-core/src/machines/workflow_machines.rs +11 -11
  42. package/sdk-core/src/pending_activations.rs +27 -22
  43. package/sdk-core/src/pollers/gateway.rs +15 -7
  44. package/sdk-core/src/pollers/poll_buffer.rs +6 -5
  45. package/sdk-core/src/pollers/retry.rs +153 -120
  46. package/sdk-core/src/prototype_rust_sdk/workflow_context.rs +61 -46
  47. package/sdk-core/src/prototype_rust_sdk/workflow_future.rs +13 -12
  48. package/sdk-core/src/prototype_rust_sdk.rs +17 -23
  49. package/sdk-core/src/telemetry/metrics.rs +2 -4
  50. package/sdk-core/src/telemetry/mod.rs +6 -7
  51. package/sdk-core/src/test_help/canned_histories.rs +17 -93
  52. package/sdk-core/src/test_help/history_builder.rs +61 -2
  53. package/sdk-core/src/test_help/history_info.rs +21 -2
  54. package/sdk-core/src/test_help/mod.rs +26 -34
  55. package/sdk-core/src/worker/activities/activity_heartbeat_manager.rs +246 -138
  56. package/sdk-core/src/worker/activities.rs +46 -45
  57. package/sdk-core/src/worker/config.rs +11 -0
  58. package/sdk-core/src/worker/dispatcher.rs +5 -5
  59. package/sdk-core/src/worker/mod.rs +86 -56
  60. package/sdk-core/src/workflow/driven_workflow.rs +3 -3
  61. package/sdk-core/src/workflow/history_update.rs +1 -1
  62. package/sdk-core/src/workflow/mod.rs +2 -1
  63. package/sdk-core/src/workflow/workflow_tasks/cache_manager.rs +13 -17
  64. package/sdk-core/src/workflow/workflow_tasks/concurrency_manager.rs +10 -18
  65. package/sdk-core/src/workflow/workflow_tasks/mod.rs +72 -57
  66. package/sdk-core/test_utils/Cargo.toml +1 -1
  67. package/sdk-core/test_utils/src/lib.rs +2 -2
  68. package/sdk-core/tests/integ_tests/workflow_tests/activities.rs +61 -1
  69. package/sdk-core/tests/integ_tests/workflow_tests/child_workflows.rs +2 -2
  70. package/sdk-core/tests/integ_tests/workflow_tests/determinism.rs +49 -0
  71. package/sdk-core/tests/integ_tests/workflow_tests/signals.rs +2 -2
  72. package/sdk-core/tests/integ_tests/workflow_tests.rs +1 -0
  73. package/src/conversions.rs +17 -0
  74. package/src/errors.rs +0 -7
  75. package/src/lib.rs +0 -20
@@ -15,34 +15,45 @@ use temporal_sdk_core_protos::{
15
15
  use tokio::{
16
16
  sync::{
17
17
  mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
18
- watch, Mutex,
18
+ Mutex,
19
19
  },
20
20
  task::JoinHandle,
21
21
  };
22
+ use tokio_util::sync::CancellationToken;
22
23
 
23
24
  /// Used to supply new heartbeat events to the activity heartbeat manager, or to send a shutdown
24
25
  /// request.
25
26
  pub(crate) struct ActivityHeartbeatManager {
26
- heartbeat_tx: UnboundedSender<HBAction>,
27
27
  /// Cancellations that have been received when heartbeating are queued here and can be consumed
28
28
  /// by [fetch_cancellations]
29
29
  incoming_cancels: Mutex<UnboundedReceiver<PendingActivityCancel>>,
30
- shutting_down: watch::Sender<bool>,
30
+ cancellation_token: CancellationToken,
31
31
  /// Used during `shutdown` to await until all inflight requests are sent.
32
32
  join_handle: Mutex<Option<JoinHandle<()>>>,
33
+ heartbeat_tx: UnboundedSender<HeartbeatAction>,
33
34
  }
34
35
 
35
36
  #[derive(Debug)]
36
- enum HBAction {
37
- HB(ValidActivityHeartbeat),
37
+ enum HeartbeatAction {
38
+ SendHeartbeat(ValidActivityHeartbeat),
38
39
  Evict(TaskToken),
40
+ CompleteReport(TaskToken),
41
+ CompleteThrottle(TaskToken),
39
42
  }
40
43
 
41
44
  #[derive(Debug)]
42
45
  pub struct ValidActivityHeartbeat {
43
46
  pub task_token: TaskToken,
44
47
  pub details: Vec<common::Payload>,
45
- pub delay: time::Duration,
48
+ pub throttle_interval: time::Duration,
49
+ }
50
+
51
+ #[derive(Debug)]
52
+ enum HeartbeatExecutorAction {
53
+ /// Heartbeats are throttled for this task token, sleep until duration or wait to be cancelled
54
+ Sleep(TaskToken, Duration, CancellationToken),
55
+ /// Report heartbeat to the server
56
+ Report(TaskToken, Vec<common::Payload>),
46
57
  }
47
58
 
48
59
  /// Handle that is used by the core for all interactions with the manager, allows sending new
@@ -58,18 +69,17 @@ impl ActivityHeartbeatManager {
58
69
  /// no longer an efficient way to forget about that task token.
59
70
  pub(super) fn record(
60
71
  &self,
61
- details: ActivityHeartbeat,
62
- delay: Duration,
72
+ hb: ActivityHeartbeat,
73
+ throttle_interval: Duration,
63
74
  ) -> Result<(), ActivityHeartbeatError> {
64
- if *self.shutting_down.borrow() {
75
+ if self.cancellation_token.is_cancelled() {
65
76
  return Err(ActivityHeartbeatError::ShuttingDown);
66
77
  }
67
-
68
78
  self.heartbeat_tx
69
- .send(HBAction::HB(ValidActivityHeartbeat {
70
- task_token: TaskToken(details.task_token),
71
- details: details.details,
72
- delay,
79
+ .send(HeartbeatAction::SendHeartbeat(ValidActivityHeartbeat {
80
+ task_token: TaskToken(hb.task_token),
81
+ details: hb.details,
82
+ throttle_interval,
73
83
  }))
74
84
  .expect("Receive half of the heartbeats event channel must not be dropped");
75
85
 
@@ -77,9 +87,9 @@ impl ActivityHeartbeatManager {
77
87
  }
78
88
 
79
89
  /// Tell the heartbeat manager we are done forever with a certain task, so it may be forgotten.
80
- /// Record should *not* be called with the same TaskToken after calling this.
90
+ /// Record *should* not be called with the same TaskToken after calling this.
81
91
  pub(super) fn evict(&self, task_token: TaskToken) {
82
- let _ = self.heartbeat_tx.send(HBAction::Evict(task_token));
92
+ let _ = self.heartbeat_tx.send(HeartbeatAction::Evict(task_token));
83
93
  }
84
94
 
85
95
  /// Returns a future that resolves any time there is a new activity cancel that must be
@@ -88,15 +98,11 @@ impl ActivityHeartbeatManager {
88
98
  self.incoming_cancels.lock().await.recv().await
89
99
  }
90
100
 
91
- pub(super) fn notify_shutdown(&self) {
92
- let _ = self.shutting_down.send(true);
93
- }
94
-
95
101
  // TODO: Can own self now!
96
- /// Initiates shutdown procedure by stopping lifecycle loop and awaiting for all heartbeat
97
- /// processors to terminate gracefully.
102
+ /// Initiates shutdown procedure by stopping lifecycle loop and awaiting for all in-flight
103
+ /// heartbeat requests to be flushed to the server.
98
104
  pub(super) async fn shutdown(&self) {
99
- self.notify_shutdown();
105
+ let _ = self.cancellation_token.cancel();
100
106
  let mut handle = self.join_handle.lock().await;
101
107
  if let Some(h) = handle.take() {
102
108
  h.await.expect("shutdown should exit cleanly");
@@ -105,132 +111,212 @@ impl ActivityHeartbeatManager {
105
111
  }
106
112
 
107
113
  #[derive(Debug)]
108
- struct HbStreamState {
109
- last_sent: HashMap<TaskToken, ActivityHbState>,
110
- incoming_hbs: UnboundedReceiver<HBAction>,
114
+ struct ActivityHeartbeatState {
115
+ /// If None and throttle interval is over, untrack this task token
116
+ last_recorded_details: Option<Vec<common::Payload>>,
117
+ last_send_requested: Instant,
118
+ throttle_interval: Duration,
119
+ throttled_cancellation_token: Option<CancellationToken>,
111
120
  }
112
121
 
113
- impl HbStreamState {
114
- fn new(incoming_hbs: UnboundedReceiver<HBAction>) -> Self {
115
- Self {
116
- last_sent: Default::default(),
117
- incoming_hbs,
122
+ impl ActivityHeartbeatState {
123
+ /// Get duration to sleep by subtracting `throttle_interval` by elapsed time since
124
+ /// `last_send_requested`
125
+ fn get_throttle_sleep_duration(&self) -> Duration {
126
+ let time_since_last_sent = self.last_send_requested.elapsed();
127
+
128
+ if time_since_last_sent > Duration::ZERO && self.throttle_interval > time_since_last_sent {
129
+ self.throttle_interval - time_since_last_sent
130
+ } else {
131
+ Duration::ZERO
118
132
  }
119
133
  }
120
134
  }
121
135
 
122
136
  #[derive(Debug)]
123
- struct ActivityHbState {
124
- delay: Duration,
125
- last_sent: Instant,
137
+ struct HeartbeatStreamState {
138
+ tt_to_state: HashMap<TaskToken, ActivityHeartbeatState>,
139
+ incoming_hbs: UnboundedReceiver<HeartbeatAction>,
140
+ /// Token that can be used to cancel the entire stream.
141
+ /// Requests to the server are not cancelled with this token.
142
+ cancellation_token: CancellationToken,
143
+ }
144
+
145
+ impl HeartbeatStreamState {
146
+ fn new() -> (Self, UnboundedSender<HeartbeatAction>, CancellationToken) {
147
+ let (heartbeat_tx, incoming_hbs) = unbounded_channel();
148
+ let cancellation_token = CancellationToken::new();
149
+ (
150
+ Self {
151
+ cancellation_token: cancellation_token.clone(),
152
+ tt_to_state: Default::default(),
153
+ incoming_hbs,
154
+ },
155
+ heartbeat_tx,
156
+ cancellation_token,
157
+ )
158
+ }
159
+
160
+ /// Record a heartbeat received from lang
161
+ fn record(&mut self, hb: ValidActivityHeartbeat) -> Option<HeartbeatExecutorAction> {
162
+ match self.tt_to_state.entry(hb.task_token.clone()) {
163
+ Entry::Vacant(e) => {
164
+ let state = ActivityHeartbeatState {
165
+ throttle_interval: hb.throttle_interval,
166
+ last_send_requested: Instant::now(),
167
+ // Don't record here because we already flush out these details.
168
+ // None is used to mark that after throttling we can stop tracking this task
169
+ // token.
170
+ last_recorded_details: None,
171
+ throttled_cancellation_token: None,
172
+ };
173
+ e.insert(state);
174
+ Some(HeartbeatExecutorAction::Report(hb.task_token, hb.details))
175
+ }
176
+ Entry::Occupied(mut o) => {
177
+ let state = o.get_mut();
178
+ state.last_recorded_details = Some(hb.details);
179
+ None
180
+ }
181
+ }
182
+ }
183
+
184
+ /// Heartbeat report to server completed
185
+ fn handle_report_completed(&mut self, tt: TaskToken) -> Option<HeartbeatExecutorAction> {
186
+ if let Some(st) = self.tt_to_state.get_mut(&tt) {
187
+ let cancellation_token = self.cancellation_token.child_token();
188
+ st.throttled_cancellation_token = Some(cancellation_token.clone());
189
+ // Always sleep for simplicity even if the duration is 0
190
+ Some(HeartbeatExecutorAction::Sleep(
191
+ tt.clone(),
192
+ st.get_throttle_sleep_duration(),
193
+ cancellation_token,
194
+ ))
195
+ } else {
196
+ None
197
+ }
198
+ }
199
+
200
+ /// Throttling completed, report or stop tracking task token
201
+ fn handle_throttle_completed(&mut self, tt: TaskToken) -> Option<HeartbeatExecutorAction> {
202
+ match self.tt_to_state.entry(tt.clone()) {
203
+ Entry::Occupied(mut e) => {
204
+ let state = e.get_mut();
205
+ if let Some(details) = state.last_recorded_details.take() {
206
+ // Delete the recorded details before reporting
207
+ // Reset the cancellation token and schedule another report
208
+ state.throttled_cancellation_token = None;
209
+ state.last_send_requested = Instant::now();
210
+ Some(HeartbeatExecutorAction::Report(tt, details))
211
+ } else {
212
+ // Nothing to report, forget this task token
213
+ e.remove();
214
+ None
215
+ }
216
+ }
217
+ Entry::Vacant(_) => None,
218
+ }
219
+ }
220
+
221
+ /// Activity should not be tracked anymore, cancel throttle timer if running
222
+ fn evict(&mut self, tt: TaskToken) -> Option<HeartbeatExecutorAction> {
223
+ if let Some(token) = self
224
+ .tt_to_state
225
+ .remove(&tt)
226
+ .and_then(|st| st.throttled_cancellation_token)
227
+ {
228
+ let _ = token.cancel();
229
+ };
230
+ None
231
+ }
126
232
  }
127
233
 
128
234
  impl ActivityHeartbeatManager {
129
235
  /// Creates a new instance of an activity heartbeat manager and returns a handle to the user,
130
236
  /// which allows to send new heartbeats and initiate the shutdown.
131
237
  pub fn new(sg: Arc<impl ServerGatewayApis + Send + Sync + 'static + ?Sized>) -> Self {
132
- let (heartbeat_tx, heartbeat_rx) = unbounded_channel();
238
+ let (heartbeat_stream_state, heartbeat_tx_source, cancellation_token) =
239
+ HeartbeatStreamState::new();
133
240
  let (cancels_tx, cancels_rx) = unbounded_channel();
134
- let (shutdown_tx, shutdown_rx) = watch::channel(false);
241
+ let heartbeat_tx = heartbeat_tx_source.clone();
135
242
 
136
243
  let join_handle = tokio::spawn(
137
244
  // The stream of incoming heartbeats uses unfold to carry state across each item in the
138
245
  // stream The closure checks if, for any given activity, we should heartbeat or not
139
246
  // depending on its delay and when we last issued a heartbeat for it.
140
- futures::stream::unfold(HbStreamState::new(heartbeat_rx), move |mut hb_states| {
141
- let mut shutdown = shutdown_rx.clone();
247
+ futures::stream::unfold(heartbeat_stream_state, move |mut hb_states| {
142
248
  async move {
143
- let hb = loop {
144
- tokio::select! {
145
- biased;
146
-
147
- _ = shutdown.changed() => return None,
148
-
149
- hb = hb_states.incoming_hbs.recv() => {
150
- if let Some(hb) = hb {
151
- match hb {
152
- HBAction::HB(hb) => break hb,
153
- HBAction::Evict(tt) => {
154
- hb_states.last_sent.remove(&tt);
155
- continue
156
- }
157
- }
158
- } else {
159
- return None;
160
- }
161
- }
162
- }
163
- };
249
+ let hb = tokio::select! {
250
+ biased;
164
251
 
165
- let do_record = match hb_states.last_sent.entry(hb.task_token.clone()) {
166
- Entry::Vacant(e) => {
167
- e.insert(ActivityHbState {
168
- delay: hb.delay,
169
- last_sent: Instant::now(),
170
- });
171
- true
252
+ _ = hb_states.cancellation_token.cancelled() => {
253
+ return None
172
254
  }
173
- Entry::Occupied(mut o) => {
174
- let o = o.get_mut();
175
- let now = Instant::now();
176
- let elapsed = o.last_sent.elapsed();
177
- let do_rec = elapsed >= o.delay;
178
- if do_rec {
179
- o.last_sent = now;
180
- o.delay = hb.delay;
181
- }
182
- do_rec
255
+ hb = hb_states.incoming_hbs.recv() => match hb {
256
+ None => return None,
257
+ Some(hb) => hb,
183
258
  }
184
259
  };
185
260
 
186
- if *shutdown.borrow() {
187
- return None;
188
- }
189
-
190
- let maybe_send_hb = if do_record { Some(hb) } else { None };
191
- Some((maybe_send_hb, hb_states))
261
+ Some((
262
+ match hb {
263
+ HeartbeatAction::SendHeartbeat(hb) => hb_states.record(hb),
264
+ HeartbeatAction::CompleteReport(tt) => hb_states.handle_report_completed(tt),
265
+ HeartbeatAction::CompleteThrottle(tt) => hb_states.handle_throttle_completed(tt),
266
+ HeartbeatAction::Evict(tt) => hb_states.evict(tt),
267
+ },
268
+ hb_states,
269
+ ))
192
270
  }
193
271
  })
194
- .for_each_concurrent(None, move |hb| {
272
+ .filter_map(|opt| async { opt })
273
+ .for_each_concurrent(None, move |action| {
274
+ let heartbeat_tx = heartbeat_tx_source.clone();
195
275
  let sg = sg.clone();
196
276
  let cancels_tx = cancels_tx.clone();
197
277
  async move {
198
- let hb = if let Some(hb) = hb {
199
- hb
200
- } else {
201
- return;
202
- };
203
-
204
- match sg
205
- .record_activity_heartbeat(
206
- hb.task_token.clone(),
207
- hb.details.clone().into_payloads(),
208
- )
209
- .await
210
- {
211
- Ok(RecordActivityTaskHeartbeatResponse { cancel_requested }) => {
212
- if cancel_requested {
213
- cancels_tx
214
- .send(PendingActivityCancel::new(
215
- hb.task_token.clone(),
216
- ActivityCancelReason::Cancelled,
217
- ))
218
- .expect("Receive half of heartbeat cancels not blocked");
219
- }
278
+ match action {
279
+ HeartbeatExecutorAction::Sleep(tt, duration, cancellation_token) => {
280
+ tokio::select! {
281
+ _ = cancellation_token.cancelled() => (),
282
+ _ = tokio::time::sleep(duration) => {
283
+ let _ = heartbeat_tx.send(HeartbeatAction::CompleteThrottle(tt));
284
+ },
285
+ };
220
286
  }
221
- // Send cancels for any activity that learns its workflow already finished
222
- // (which is one thing not found implies - other reasons would seem equally
223
- // valid).
224
- Err(s) if s.code() == tonic::Code::NotFound => {
225
- cancels_tx
226
- .send(PendingActivityCancel::new(
227
- hb.task_token.clone(),
228
- ActivityCancelReason::NotFound,
229
- ))
230
- .expect("Receive half of heartbeat cancels not blocked");
231
- }
232
- Err(e) => {
233
- warn!("Error when recording heartbeat: {:?}", e)
287
+ HeartbeatExecutorAction::Report(tt, details) => {
288
+ match sg
289
+ .record_activity_heartbeat(tt.clone(), details.into_payloads())
290
+ .await
291
+ {
292
+ Ok(RecordActivityTaskHeartbeatResponse { cancel_requested }) => {
293
+ if cancel_requested {
294
+ cancels_tx
295
+ .send(PendingActivityCancel::new(
296
+ tt.clone(),
297
+ ActivityCancelReason::Cancelled,
298
+ ))
299
+ .expect(
300
+ "Receive half of heartbeat cancels not blocked",
301
+ );
302
+ }
303
+ }
304
+ // Send cancels for any activity that learns its workflow already finished
305
+ // (which is one thing not found implies - other reasons would seem equally
306
+ // valid).
307
+ Err(s) if s.code() == tonic::Code::NotFound => {
308
+ cancels_tx
309
+ .send(PendingActivityCancel::new(
310
+ tt.clone(),
311
+ ActivityCancelReason::NotFound,
312
+ ))
313
+ .expect("Receive half of heartbeat cancels not blocked");
314
+ }
315
+ Err(e) => {
316
+ warn!("Error when recording heartbeat: {:?}", e);
317
+ }
318
+ };
319
+ let _ = heartbeat_tx.send(HeartbeatAction::CompleteReport(tt));
234
320
  }
235
321
  }
236
322
  }
@@ -238,10 +324,10 @@ impl ActivityHeartbeatManager {
238
324
  );
239
325
 
240
326
  Self {
241
- heartbeat_tx,
242
327
  incoming_cancels: Mutex::new(cancels_rx),
243
- shutting_down: shutdown_tx,
244
328
  join_handle: Mutex::new(Some(join_handle)),
329
+ cancellation_token,
330
+ heartbeat_tx,
245
331
  }
246
332
  }
247
333
  }
@@ -257,7 +343,7 @@ mod test {
257
343
  };
258
344
  use tokio::time::sleep;
259
345
 
260
- /// Ensure that heartbeats that are sent with a small delay are aggregated and sent roughly once
346
+ /// Ensure that heartbeats that are sent with a small `throttle_interval` are aggregated and sent roughly once
261
347
  /// every 1/2 of the heartbeat timeout.
262
348
  #[tokio::test]
263
349
  async fn process_heartbeats_and_shutdown() {
@@ -268,18 +354,38 @@ mod test {
268
354
  .times(2);
269
355
  let hm = ActivityHeartbeatManager::new(Arc::new(mock_gateway));
270
356
  let fake_task_token = vec![1, 2, 3];
271
- // Sending heartbeat requests for 600ms, this should send first heartbeat right away, and
272
- // all other requests should be aggregated and last one should be sent to the server in
273
- // 500ms (1/2 of heartbeat timeout).
274
- for i in 0u8..60 {
275
- record_heartbeat(&hm, fake_task_token.clone(), i, Duration::from_millis(1000));
276
- sleep(Duration::from_millis(10)).await;
357
+ // Send 2 heartbeat requests for 20ms apart.
358
+ // The first heartbeat should be sent right away, and
359
+ // the second should be throttled until 50ms have passed.
360
+ for i in 0_u8..2 {
361
+ record_heartbeat(&hm, fake_task_token.clone(), i, Duration::from_millis(50));
362
+ sleep(Duration::from_millis(20)).await;
277
363
  }
364
+ // sleep again to let heartbeats be flushed
365
+ sleep(Duration::from_millis(20)).await;
278
366
  hm.shutdown().await;
279
367
  }
280
368
 
281
- /// Ensure that heartbeat can be called from a tight loop without any delays, resulting in two
282
- /// interactions with the server - one immediately and one after 500ms after the delay.
369
+ #[tokio::test]
370
+ async fn send_heartbeats_less_frequently_than_throttle_interval() {
371
+ let mut mock_gateway = MockServerGatewayApis::new();
372
+ mock_gateway
373
+ .expect_record_activity_heartbeat()
374
+ .returning(|_, _| Ok(RecordActivityTaskHeartbeatResponse::default()))
375
+ .times(3);
376
+ let hm = ActivityHeartbeatManager::new(Arc::new(mock_gateway));
377
+ let fake_task_token = vec![1, 2, 3];
378
+ // Heartbeats always get sent if recorded less frequently than the throttle intreval
379
+ for i in 0_u8..3 {
380
+ record_heartbeat(&hm, fake_task_token.clone(), i, Duration::from_millis(10));
381
+ sleep(Duration::from_millis(20)).await;
382
+ }
383
+ // sleep again to let heartbeats be flushed
384
+ hm.shutdown().await;
385
+ }
386
+
387
+ /// Ensure that heartbeat can be called from a tight loop without any throttle_interval, resulting in two
388
+ /// interactions with the server - one immediately and one after 500ms after the throttle_interval.
283
389
  #[tokio::test]
284
390
  async fn process_tight_loop_and_shutdown() {
285
391
  let mut mock_gateway = MockServerGatewayApis::new();
@@ -290,15 +396,15 @@ mod test {
290
396
  let hm = ActivityHeartbeatManager::new(Arc::new(mock_gateway));
291
397
  let fake_task_token = vec![1, 2, 3];
292
398
  // Send a whole bunch of heartbeats very fast. We should still only send one total.
293
- for i in 0u8..u8::MAX {
294
- record_heartbeat(&hm, fake_task_token.clone(), i, Duration::from_millis(1000));
399
+ for i in 0_u8..50 {
400
+ record_heartbeat(&hm, fake_task_token.clone(), i, Duration::from_millis(2000));
401
+ // Let it propagate
402
+ sleep(Duration::from_millis(10)).await;
295
403
  }
296
- // Let it propagate
297
- sleep(Duration::from_millis(50)).await;
298
404
  hm.shutdown().await;
299
405
  }
300
406
 
301
- /// This test reports one heartbeat and waits for the delay to elapse before sending another
407
+ /// This test reports one heartbeat and waits for the throttle_interval to elapse before sending another
302
408
  #[tokio::test]
303
409
  async fn report_heartbeat_after_timeout() {
304
410
  let mut mock_gateway = MockServerGatewayApis::new();
@@ -326,10 +432,12 @@ mod test {
326
432
  let hm = ActivityHeartbeatManager::new(Arc::new(mock_gateway));
327
433
  let fake_task_token = vec![1, 2, 3];
328
434
  record_heartbeat(&hm, fake_task_token.clone(), 0, Duration::from_millis(100));
435
+ // Let it propagate
436
+ sleep(Duration::from_millis(10)).await;
329
437
  hm.evict(fake_task_token.clone().into());
330
438
  record_heartbeat(&hm, fake_task_token, 0, Duration::from_millis(100));
331
439
  // Let it propagate
332
- sleep(Duration::from_millis(100)).await;
440
+ sleep(Duration::from_millis(10)).await;
333
441
  // We know it works b/c otherwise we would have only called record 1 time w/o sleep
334
442
  hm.shutdown().await;
335
443
  }
@@ -356,7 +464,7 @@ mod test {
356
464
  Duration::from_millis(1000),
357
465
  ) {
358
466
  Ok(_) => {
359
- unreachable!("heartbeat should not be recorded after the shutdown")
467
+ unreachable!("heartbeat should not be recorded after the shutdown");
360
468
  }
361
469
  Err(e) => {
362
470
  matches!(e, ActivityHeartbeatError::ShuttingDown);
@@ -368,7 +476,7 @@ mod test {
368
476
  hm: &ActivityHeartbeatManager,
369
477
  task_token: Vec<u8>,
370
478
  payload_data: u8,
371
- delay: Duration,
479
+ throttle_interval: Duration,
372
480
  ) {
373
481
  hm.record(
374
482
  ActivityHeartbeat {
@@ -380,7 +488,7 @@ mod test {
380
488
  }],
381
489
  },
382
490
  // Mimic the same delay we would apply in activity task manager
383
- delay / 2,
491
+ throttle_interval,
384
492
  )
385
493
  .expect("hearbeat recording should not fail");
386
494
  }