@temporalio/core-bridge 0.16.0 → 0.17.1

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 (58) hide show
  1. package/Cargo.lock +1 -0
  2. package/index.d.ts +14 -0
  3. package/index.node +0 -0
  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.toml +1 -0
  11. package/sdk-core/fsm/rustfsm_procmacro/Cargo.toml +1 -1
  12. package/sdk-core/fsm/rustfsm_procmacro/src/lib.rs +8 -9
  13. package/sdk-core/fsm/rustfsm_trait/Cargo.toml +1 -1
  14. package/sdk-core/fsm/rustfsm_trait/src/lib.rs +1 -1
  15. package/sdk-core/sdk-core-protos/src/lib.rs +43 -48
  16. package/sdk-core/src/core_tests/activity_tasks.rs +5 -5
  17. package/sdk-core/src/core_tests/mod.rs +2 -2
  18. package/sdk-core/src/core_tests/queries.rs +9 -2
  19. package/sdk-core/src/core_tests/workflow_tasks.rs +87 -8
  20. package/sdk-core/src/errors.rs +13 -13
  21. package/sdk-core/src/lib.rs +2 -2
  22. package/sdk-core/src/machines/activity_state_machine.rs +3 -3
  23. package/sdk-core/src/machines/child_workflow_state_machine.rs +6 -15
  24. package/sdk-core/src/machines/complete_workflow_state_machine.rs +1 -1
  25. package/sdk-core/src/machines/continue_as_new_workflow_state_machine.rs +1 -1
  26. package/sdk-core/src/machines/mod.rs +16 -22
  27. package/sdk-core/src/machines/patch_state_machine.rs +8 -8
  28. package/sdk-core/src/machines/signal_external_state_machine.rs +2 -2
  29. package/sdk-core/src/machines/timer_state_machine.rs +4 -4
  30. package/sdk-core/src/machines/transition_coverage.rs +3 -3
  31. package/sdk-core/src/machines/workflow_machines.rs +26 -24
  32. package/sdk-core/src/pending_activations.rs +19 -20
  33. package/sdk-core/src/pollers/gateway.rs +3 -3
  34. package/sdk-core/src/pollers/poll_buffer.rs +2 -2
  35. package/sdk-core/src/pollers/retry.rs +4 -4
  36. package/sdk-core/src/prototype_rust_sdk/workflow_context.rs +3 -3
  37. package/sdk-core/src/prototype_rust_sdk/workflow_future.rs +4 -4
  38. package/sdk-core/src/prototype_rust_sdk.rs +3 -11
  39. package/sdk-core/src/telemetry/metrics.rs +2 -4
  40. package/sdk-core/src/telemetry/mod.rs +6 -7
  41. package/sdk-core/src/test_help/canned_histories.rs +8 -5
  42. package/sdk-core/src/test_help/history_builder.rs +12 -2
  43. package/sdk-core/src/test_help/history_info.rs +23 -3
  44. package/sdk-core/src/test_help/mod.rs +24 -40
  45. package/sdk-core/src/worker/activities/activity_heartbeat_manager.rs +246 -138
  46. package/sdk-core/src/worker/activities.rs +46 -45
  47. package/sdk-core/src/worker/config.rs +11 -0
  48. package/sdk-core/src/worker/dispatcher.rs +5 -5
  49. package/sdk-core/src/worker/mod.rs +71 -52
  50. package/sdk-core/src/workflow/driven_workflow.rs +3 -3
  51. package/sdk-core/src/workflow/history_update.rs +1 -1
  52. package/sdk-core/src/workflow/mod.rs +1 -1
  53. package/sdk-core/src/workflow/workflow_tasks/cache_manager.rs +13 -17
  54. package/sdk-core/src/workflow/workflow_tasks/concurrency_manager.rs +4 -8
  55. package/sdk-core/src/workflow/workflow_tasks/mod.rs +46 -53
  56. package/sdk-core/test_utils/src/lib.rs +2 -2
  57. package/sdk-core/tests/integ_tests/workflow_tests/activities.rs +61 -1
  58. package/src/conversions.rs +17 -0
@@ -21,7 +21,7 @@ use crate::{
21
21
  use crossbeam::queue::SegQueue;
22
22
  use futures::FutureExt;
23
23
  use parking_lot::Mutex;
24
- use std::{fmt::Debug, ops::DerefMut, time::Instant};
24
+ use std::{fmt::Debug, time::Instant};
25
25
  use temporal_sdk_core_protos::coresdk::{
26
26
  workflow_activation::{
27
27
  create_evict_activation, create_query_activation, wf_activation_job, QueryWorkflow,
@@ -77,9 +77,9 @@ pub(crate) enum OutstandingActivation {
77
77
  }
78
78
 
79
79
  impl OutstandingActivation {
80
- fn has_eviction(&self) -> bool {
80
+ const fn has_eviction(self) -> bool {
81
81
  matches!(
82
- &self,
82
+ self,
83
83
  OutstandingActivation::Normal {
84
84
  contains_eviction: true
85
85
  }
@@ -135,7 +135,7 @@ pub enum ActivationAction {
135
135
  }
136
136
 
137
137
  macro_rules! machine_mut {
138
- ($myself:ident, $run_id:ident, $task_token:ident, $clos:expr) => {{
138
+ ($myself:ident, $run_id:ident, $clos:expr) => {{
139
139
  $myself
140
140
  .workflow_machines
141
141
  .access($run_id, $clos)
@@ -143,7 +143,6 @@ macro_rules! machine_mut {
143
143
  .map_err(|source| WorkflowUpdateError {
144
144
  source,
145
145
  run_id: $run_id.to_owned(),
146
- task_token: Some($task_token.clone()),
147
146
  })
148
147
  }};
149
148
  }
@@ -256,6 +255,7 @@ impl WorkflowTaskManager {
256
255
  debug!(
257
256
  task_token = %&work.task_token,
258
257
  history_length = %work.history.events.len(),
258
+ attempt = %work.attempt,
259
259
  "Applying new workflow task from server"
260
260
  );
261
261
  let task_start_time = Instant::now();
@@ -303,13 +303,13 @@ impl WorkflowTaskManager {
303
303
  )
304
304
  .expect("Workflow machines must exist, we just created/updated them");
305
305
 
306
- if !next_activation.jobs.is_empty() {
306
+ if next_activation.jobs.is_empty() {
307
+ NewWfTaskOutcome::Autocomplete
308
+ } else {
307
309
  if let Err(wme) = self.insert_outstanding_activation(&next_activation) {
308
310
  return NewWfTaskOutcome::Evict(wme.into());
309
311
  }
310
312
  NewWfTaskOutcome::IssueActivation(next_activation)
311
- } else {
312
- NewWfTaskOutcome::Autocomplete
313
313
  }
314
314
  }
315
315
 
@@ -325,19 +325,20 @@ impl WorkflowTaskManager {
325
325
  return Ok(None);
326
326
  }
327
327
 
328
- let task_token = if let Some(entry) = self.workflow_machines.get_task(run_id) {
329
- entry.info.task_token.clone()
330
- } else {
331
- if !self.activation_has_eviction(run_id) {
332
- // Don't bother warning if this was an eviction, since it's normal to issue
333
- // eviction activations without an associated workflow task in that case.
334
- warn!(
335
- run_id,
336
- "Attempted to complete activation for nonexistent run"
337
- );
338
- }
339
- return Ok(None);
340
- };
328
+ let (task_token, is_leg_query_task) =
329
+ if let Some(entry) = self.workflow_machines.get_task(run_id) {
330
+ (entry.info.task_token.clone(), entry.legacy_query.is_some())
331
+ } else {
332
+ if !self.activation_has_eviction(run_id) {
333
+ // Don't bother warning if this was an eviction, since it's normal to issue
334
+ // eviction activations without an associated workflow task in that case.
335
+ warn!(
336
+ run_id,
337
+ "Attempted to complete activation for run without associated workflow task"
338
+ );
339
+ }
340
+ return Ok(None);
341
+ };
341
342
 
342
343
  // If the only command in the activation is a legacy query response, that means we need
343
344
  // to respond differently than a typical activation.
@@ -364,7 +365,6 @@ impl WorkflowTaskManager {
364
365
  return Err(WorkflowUpdateError {
365
366
  source: WFMachinesError::Fatal("Legacy query activation response included other commands, this is not allowed and constitutes an error in the lang SDK".to_string()),
366
367
  run_id: run_id.to_string(),
367
- task_token: Some(task_token)
368
368
  });
369
369
  }
370
370
  query_responses.push(qr);
@@ -375,30 +375,32 @@ impl WorkflowTaskManager {
375
375
  }
376
376
 
377
377
  // Send commands from lang into the machines
378
- machine_mut!(self, run_id, task_token, |wfm: &mut WorkflowManager| {
378
+ machine_mut!(self, run_id, |wfm: &mut WorkflowManager| {
379
379
  wfm.push_commands(commands).boxed()
380
380
  })?;
381
381
  // Check if the workflow run needs another activation and queue it up if there is one
382
382
  // by pushing it into the pending activations list
383
- let next_activation = machine_mut!(
384
- self,
385
- run_id,
386
- task_token,
387
- move |mgr: &mut WorkflowManager| mgr.get_next_activation().boxed()
388
- )?;
383
+ let next_activation = machine_mut!(self, run_id, move |mgr: &mut WorkflowManager| mgr
384
+ .get_next_activation()
385
+ .boxed())?;
389
386
  if !next_activation.jobs.is_empty() {
390
387
  self.pending_activations.push(next_activation);
391
388
  let _ = self.pending_activations_notifier.send(true);
392
389
  }
393
390
  // We want to fetch the outgoing commands only after any new activation has been queued,
394
391
  // as doing so may have altered the outgoing commands.
395
- let server_cmds =
396
- machine_mut!(self, run_id, task_token, |wfm: &mut WorkflowManager| {
397
- async move { Ok(wfm.get_server_commands()) }.boxed()
398
- })?;
392
+ let server_cmds = machine_mut!(self, run_id, |wfm: &mut WorkflowManager| {
393
+ async move { Ok(wfm.get_server_commands()) }.boxed()
394
+ })?;
395
+ let is_query_playback = is_leg_query_task && query_responses.is_empty();
399
396
  // We only actually want to send commands back to the server if there are no more
400
- // pending activations and we are caught up on replay.
401
- if !self.pending_activations.has_pending(run_id) && !server_cmds.replaying {
397
+ // pending activations and we are caught up on replay. We don't want to complete a wft
398
+ // if we already saw the final event in the workflow, or if we are playing back for the
399
+ // express purpose of fulfilling a query
400
+ if !self.pending_activations.has_pending(run_id)
401
+ && !server_cmds.replaying
402
+ && !is_query_playback
403
+ {
402
404
  Some(ServerCommandsWithWorkflowInfo {
403
405
  task_token,
404
406
  action: ActivationAction::WftComplete {
@@ -406,7 +408,9 @@ impl WorkflowTaskManager {
406
408
  query_responses,
407
409
  },
408
410
  })
409
- } else if !query_responses.is_empty() {
411
+ } else if query_responses.is_empty() {
412
+ None
413
+ } else {
410
414
  Some(ServerCommandsWithWorkflowInfo {
411
415
  task_token,
412
416
  action: ActivationAction::WftComplete {
@@ -414,8 +418,6 @@ impl WorkflowTaskManager {
414
418
  query_responses,
415
419
  },
416
420
  })
417
- } else {
418
- None
419
421
  }
420
422
  };
421
423
  Ok(ret)
@@ -447,13 +449,9 @@ impl WorkflowTaskManager {
447
449
  FailedActivationOutcome::ReportLegacyQueryFailure(tt)
448
450
  } else {
449
451
  // Blow up any cached data associated with the workflow
450
- let should_report =
451
- if let Some(attempt) = self.request_eviction(run_id, "Activation failed by lang") {
452
- // Only report to server if the last task wasn't also a failure (avoid spam)
453
- attempt <= 1
454
- } else {
455
- true
456
- };
452
+ let should_report = self
453
+ .request_eviction(run_id, "Activation failed")
454
+ .map_or(true, |attempt| attempt <= 1);
457
455
  if should_report {
458
456
  FailedActivationOutcome::Report(tt)
459
457
  } else {
@@ -511,11 +509,7 @@ impl WorkflowTaskManager {
511
509
 
512
510
  Ok((wft_info, activation))
513
511
  }
514
- Err(source) => Err(WorkflowUpdateError {
515
- source,
516
- run_id,
517
- task_token: Some(wft_info.task_token),
518
- }),
512
+ Err(source) => Err(WorkflowUpdateError { source, run_id }),
519
513
  }
520
514
  }
521
515
 
@@ -541,11 +535,10 @@ impl WorkflowTaskManager {
541
535
  if !just_evicted {
542
536
  // Check if there was a legacy query which must be fulfilled, and if there is create
543
537
  // a new pending activation for it.
544
- if let Some(ref mut ot) = self
538
+ if let Some(ref mut ot) = &mut *self
545
539
  .workflow_machines
546
540
  .get_task_mut(run_id)
547
541
  .expect("Machine must exist")
548
- .deref_mut()
549
542
  {
550
543
  if let Some(query) = ot.legacy_query.take() {
551
544
  let na = create_query_activation(run_id.to_string(), [query]);
@@ -623,7 +616,7 @@ impl WorkflowTaskManager {
623
616
  fn activation_has_eviction(&self, run_id: &str) -> bool {
624
617
  self.workflow_machines
625
618
  .get_activation(run_id)
626
- .map(|oa| oa.has_eviction())
619
+ .map(OutstandingActivation::has_eviction)
627
620
  .unwrap_or_default()
628
621
  }
629
622
  }
@@ -46,7 +46,7 @@ impl CoreWfStarter {
46
46
  .unwrap(),
47
47
  worker_config: WorkerConfigBuilder::default()
48
48
  .task_queue(task_queue)
49
- .max_cached_workflows(1000usize)
49
+ .max_cached_workflows(1000_usize)
50
50
  .build()
51
51
  .unwrap(),
52
52
  wft_timeout: None,
@@ -63,7 +63,7 @@ impl CoreWfStarter {
63
63
  }
64
64
 
65
65
  pub async fn shutdown(&mut self) {
66
- self.get_core().await.shutdown().await
66
+ self.get_core().await.shutdown().await;
67
67
  }
68
68
 
69
69
  pub async fn get_core(&mut self) -> Arc<dyn Core> {
@@ -8,7 +8,7 @@ use temporal_sdk_core_protos::{
8
8
  workflow_activation::{wf_activation_job, FireTimer, ResolveActivity, WfActivationJob},
9
9
  workflow_commands::{ActivityCancellationType, RequestCancelActivity, StartTimer},
10
10
  workflow_completion::WfActivationCompletion,
11
- ActivityTaskCompletion, IntoCompletion,
11
+ ActivityHeartbeat, ActivityTaskCompletion, IntoCompletion,
12
12
  },
13
13
  temporal::api::{
14
14
  common::v1::{ActivityType, Payloads},
@@ -647,3 +647,63 @@ async fn async_activity_completion_workflow() {
647
647
  );
648
648
  core.complete_execution(&task_q, &task.run_id).await;
649
649
  }
650
+
651
+ #[tokio::test]
652
+ async fn activity_cancelled_after_heartbeat_times_out() {
653
+ let test_name = "activity_cancelled_after_heartbeat_times_out";
654
+ let (core, task_q) = init_core_and_create_wf(test_name).await;
655
+ let activity_id = "act-1";
656
+ let task = core.poll_workflow_activation(&task_q).await.unwrap();
657
+ // Complete workflow task and schedule activity
658
+ core.complete_workflow_activation(
659
+ schedule_activity_cmd(
660
+ 0,
661
+ &task_q,
662
+ activity_id,
663
+ ActivityCancellationType::WaitCancellationCompleted,
664
+ Duration::from_secs(60),
665
+ Duration::from_secs(1),
666
+ )
667
+ .into_completion(task_q.clone(), task.run_id),
668
+ )
669
+ .await
670
+ .unwrap();
671
+ // Poll activity and verify that it's been scheduled with correct parameters
672
+ let task = core.poll_activity_task(&task_q).await.unwrap();
673
+ assert_matches!(
674
+ task.variant,
675
+ Some(act_task::Variant::Start(start_activity)) => {
676
+ assert_eq!(start_activity.activity_type, "test_activity".to_string())
677
+ }
678
+ );
679
+ // Delay the heartbeat
680
+ sleep(Duration::from_secs(2)).await;
681
+ core.record_activity_heartbeat(ActivityHeartbeat {
682
+ task_token: task.task_token.clone(),
683
+ task_queue: task_q.to_string(),
684
+ details: vec![],
685
+ });
686
+
687
+ // Verify activity got cancelled
688
+ let cancel_task = core.poll_activity_task(&task_q).await.unwrap();
689
+ assert_eq!(cancel_task.task_token, task.task_token.clone());
690
+ assert_matches!(cancel_task.variant, Some(act_task::Variant::Cancel(_)));
691
+
692
+ // Complete activity with cancelled result
693
+ core.complete_activity_task(ActivityTaskCompletion {
694
+ task_token: task.task_token.clone(),
695
+ task_queue: task_q.to_string(),
696
+ result: Some(ActivityResult::cancel_from_details(None)),
697
+ })
698
+ .await
699
+ .unwrap();
700
+
701
+ // Verify shutdown completes
702
+ core.shutdown_worker(task_q.as_str()).await;
703
+ core.shutdown().await;
704
+ // Cleanup just in case
705
+ core.server_gateway()
706
+ .terminate_workflow_execution(test_name.to_string(), None)
707
+ .await
708
+ .unwrap();
709
+ }
@@ -276,6 +276,21 @@ impl ObjectHandleConversionsExt for Handle<'_, JsObject> {
276
276
  ) as u64);
277
277
  let max_cached_workflows =
278
278
  js_value_getter!(cx, self, "maxCachedWorkflows", JsNumber) as usize;
279
+
280
+ let max_heartbeat_throttle_interval = Duration::from_millis(js_value_getter!(
281
+ cx,
282
+ self,
283
+ "maxHeartbeatThrottleIntervalMs",
284
+ JsNumber
285
+ ) as u64);
286
+
287
+ let default_heartbeat_throttle_interval = Duration::from_millis(js_value_getter!(
288
+ cx,
289
+ self,
290
+ "defaultHeartbeatThrottleIntervalMs",
291
+ JsNumber
292
+ ) as u64);
293
+
279
294
  Ok(WorkerConfig {
280
295
  no_remote_activities: false, // TODO: make this configurable once Core implements local activities
281
296
  max_concurrent_at_polls,
@@ -286,6 +301,8 @@ impl ObjectHandleConversionsExt for Handle<'_, JsObject> {
286
301
  nonsticky_to_sticky_poll_ratio,
287
302
  sticky_queue_schedule_to_start_timeout,
288
303
  task_queue,
304
+ max_heartbeat_throttle_interval,
305
+ default_heartbeat_throttle_interval,
289
306
  })
290
307
  }
291
308
  }