@temporalio/core-bridge 0.21.0 → 0.23.0

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 (63) hide show
  1. package/Cargo.lock +139 -127
  2. package/Cargo.toml +2 -1
  3. package/index.d.ts +93 -14
  4. package/package.json +3 -3
  5. package/releases/aarch64-apple-darwin/index.node +0 -0
  6. package/releases/x86_64-apple-darwin/index.node +0 -0
  7. package/releases/x86_64-pc-windows-msvc/index.node +0 -0
  8. package/releases/x86_64-unknown-linux-gnu/index.node +0 -0
  9. package/sdk-core/.buildkite/docker/docker-compose.yaml +5 -4
  10. package/sdk-core/bridge-ffi/src/lib.rs +1 -1
  11. package/sdk-core/bridge-ffi/src/wrappers.rs +60 -34
  12. package/sdk-core/client/Cargo.toml +2 -0
  13. package/sdk-core/client/src/lib.rs +76 -16
  14. package/sdk-core/client/src/raw.rs +10 -2
  15. package/sdk-core/client/src/retry.rs +12 -1
  16. package/sdk-core/client/src/workflow_handle/mod.rs +183 -0
  17. package/sdk-core/core/benches/workflow_replay.rs +1 -7
  18. package/sdk-core/core/src/abstractions.rs +10 -3
  19. package/sdk-core/core/src/core_tests/child_workflows.rs +7 -9
  20. package/sdk-core/core/src/core_tests/determinism.rs +8 -19
  21. package/sdk-core/core/src/core_tests/local_activities.rs +22 -32
  22. package/sdk-core/core/src/core_tests/queries.rs +181 -20
  23. package/sdk-core/core/src/core_tests/workers.rs +4 -34
  24. package/sdk-core/core/src/core_tests/workflow_tasks.rs +197 -41
  25. package/sdk-core/core/src/lib.rs +2 -1
  26. package/sdk-core/core/src/pending_activations.rs +11 -0
  27. package/sdk-core/core/src/protosext/mod.rs +1 -3
  28. package/sdk-core/core/src/retry_logic.rs +73 -16
  29. package/sdk-core/core/src/telemetry/mod.rs +168 -110
  30. package/sdk-core/core/src/test_help/mod.rs +57 -7
  31. package/sdk-core/core/src/worker/activities/local_activities.rs +6 -12
  32. package/sdk-core/core/src/worker/mod.rs +64 -15
  33. package/sdk-core/core/src/workflow/machines/mod.rs +1 -1
  34. package/sdk-core/core/src/workflow/machines/timer_state_machine.rs +2 -2
  35. package/sdk-core/core/src/workflow/machines/workflow_machines.rs +14 -3
  36. package/sdk-core/core/src/workflow/mod.rs +5 -2
  37. package/sdk-core/core/src/workflow/workflow_tasks/cache_manager.rs +47 -2
  38. package/sdk-core/core/src/workflow/workflow_tasks/concurrency_manager.rs +16 -2
  39. package/sdk-core/core/src/workflow/workflow_tasks/mod.rs +202 -101
  40. package/sdk-core/core-api/src/worker.rs +9 -0
  41. package/sdk-core/protos/local/temporal/sdk/core/bridge/bridge.proto +27 -5
  42. package/sdk-core/sdk/Cargo.toml +1 -0
  43. package/sdk-core/sdk/src/activity_context.rs +223 -0
  44. package/sdk-core/sdk/src/interceptors.rs +8 -2
  45. package/sdk-core/sdk/src/lib.rs +168 -123
  46. package/sdk-core/sdk-core-protos/src/history_info.rs +3 -7
  47. package/sdk-core/sdk-core-protos/src/lib.rs +13 -0
  48. package/sdk-core/test-utils/Cargo.toml +1 -0
  49. package/sdk-core/test-utils/src/histfetch.rs +1 -1
  50. package/sdk-core/test-utils/src/lib.rs +88 -44
  51. package/sdk-core/tests/integ_tests/client_tests.rs +2 -2
  52. package/sdk-core/tests/integ_tests/workflow_tests/activities.rs +11 -4
  53. package/sdk-core/tests/integ_tests/workflow_tests/child_workflows.rs +0 -1
  54. package/sdk-core/tests/integ_tests/workflow_tests/continue_as_new.rs +0 -3
  55. package/sdk-core/tests/integ_tests/workflow_tests/local_activities.rs +33 -17
  56. package/sdk-core/tests/integ_tests/workflow_tests/resets.rs +10 -1
  57. package/sdk-core/tests/integ_tests/workflow_tests/signals.rs +0 -1
  58. package/sdk-core/tests/integ_tests/workflow_tests.rs +71 -3
  59. package/sdk-core/tests/load_tests.rs +80 -6
  60. package/sdk-core/tests/main.rs +5 -2
  61. package/src/conversions.rs +104 -33
  62. package/src/lib.rs +70 -6
  63. package/releases/aarch64-unknown-linux-gnu/index.node +0 -0
@@ -11,8 +11,13 @@ use std::{
11
11
  use temporal_sdk_core_api::Worker as WorkerTrait;
12
12
  use temporal_sdk_core_protos::{
13
13
  coresdk::{
14
- workflow_activation::{workflow_activation_job, WorkflowActivationJob},
15
- workflow_commands::{CompleteWorkflowExecution, QueryResult, QuerySuccess},
14
+ workflow_activation::{
15
+ remove_from_cache::EvictionReason, workflow_activation_job, WorkflowActivationJob,
16
+ },
17
+ workflow_commands::{
18
+ ActivityCancellationType, CompleteWorkflowExecution, ContinueAsNewWorkflowExecution,
19
+ QueryResult, QuerySuccess, RequestCancelActivity,
20
+ },
16
21
  workflow_completion::WorkflowActivationCompletion,
17
22
  },
18
23
  temporal::api::{
@@ -26,7 +31,7 @@ use temporal_sdk_core_protos::{
26
31
  },
27
32
  },
28
33
  };
29
- use temporal_sdk_core_test_utils::start_timer_cmd;
34
+ use temporal_sdk_core_test_utils::{schedule_activity_cmd, start_timer_cmd};
30
35
 
31
36
  #[rstest::rstest]
32
37
  #[case::with_history(true)]
@@ -81,6 +86,10 @@ async fn legacy_query(#[case] include_history: bool) {
81
86
  };
82
87
  let clear_eviction = || async {
83
88
  let t = worker.poll_workflow_activation().await.unwrap();
89
+ assert_matches!(
90
+ t.jobs[0].variant,
91
+ Some(workflow_activation_job::Variant::RemoveFromCache(_))
92
+ );
84
93
  worker
85
94
  .complete_workflow_activation(WorkflowActivationCompletion::empty(t.run_id))
86
95
  .await
@@ -156,7 +165,12 @@ async fn new_queries(#[case] num_queries: usize) {
156
165
  let tasks = VecDeque::from(vec![
157
166
  hist_to_poll_resp(&t, wfid.to_owned(), 1.into(), TEST_Q.to_string()),
158
167
  {
159
- let mut pr = hist_to_poll_resp(&t, wfid.to_owned(), 2.into(), TEST_Q.to_string());
168
+ let mut pr = hist_to_poll_resp(
169
+ &t,
170
+ wfid.to_owned(),
171
+ ResponseType::OneTask(2),
172
+ TEST_Q.to_string(),
173
+ );
160
174
  pr.queries = HashMap::new();
161
175
  for i in 1..=num_queries {
162
176
  pr.queries.insert(
@@ -383,20 +397,50 @@ async fn legacy_query_after_complete(#[values(false, true)] full_history: bool)
383
397
  core.shutdown().await;
384
398
  }
385
399
 
400
+ enum QueryHists {
401
+ Empty,
402
+ Full,
403
+ Partial,
404
+ }
405
+ #[rstest::rstest]
386
406
  #[tokio::test]
387
- async fn query_cache_miss_causes_page_fetch_dont_reply_wft_too_early() {
407
+ async fn query_cache_miss_causes_page_fetch_dont_reply_wft_too_early(
408
+ #[values(QueryHists::Empty, QueryHists::Full, QueryHists::Partial)] hist_type: QueryHists,
409
+ ) {
388
410
  let wfid = "fake_wf_id";
389
411
  let query_resp = "response";
390
412
  let t = canned_histories::single_timer("1");
391
413
  let full_hist = t.get_full_history_info().unwrap();
392
414
  let tasks = VecDeque::from(vec![{
393
- // Create a partial task
394
- let mut pr = hist_to_poll_resp(
395
- &t,
396
- wfid.to_owned(),
397
- ResponseType::OneTask(2),
398
- TEST_Q.to_string(),
399
- );
415
+ let mut pr = match hist_type {
416
+ QueryHists::Empty => {
417
+ // Create a no-history poll response. This happens to be easiest to do by just ripping
418
+ // out the history after making a normal one.
419
+ let mut pr = hist_to_poll_resp(
420
+ &t,
421
+ wfid.to_owned(),
422
+ ResponseType::AllHistory,
423
+ TEST_Q.to_string(),
424
+ );
425
+ pr.history = Some(Default::default());
426
+ pr
427
+ }
428
+ QueryHists::Full => hist_to_poll_resp(
429
+ &t,
430
+ wfid.to_owned(),
431
+ ResponseType::AllHistory,
432
+ TEST_Q.to_string(),
433
+ ),
434
+ QueryHists::Partial => {
435
+ // Create a partial task
436
+ hist_to_poll_resp(
437
+ &t,
438
+ wfid.to_owned(),
439
+ ResponseType::OneTask(2),
440
+ TEST_Q.to_string(),
441
+ )
442
+ }
443
+ };
400
444
  pr.queries = HashMap::new();
401
445
  pr.queries.insert(
402
446
  "the-query".to_string(),
@@ -409,14 +453,16 @@ async fn query_cache_miss_causes_page_fetch_dont_reply_wft_too_early() {
409
453
  pr
410
454
  }]);
411
455
  let mut mock_client = mock_workflow_client();
412
- mock_client
413
- .expect_get_workflow_execution_history()
414
- .returning(move |_, _, _| {
415
- Ok(GetWorkflowExecutionHistoryResponse {
416
- history: Some(full_hist.clone().into()),
417
- ..Default::default()
418
- })
419
- });
456
+ if !matches!(hist_type, QueryHists::Full) {
457
+ mock_client
458
+ .expect_get_workflow_execution_history()
459
+ .returning(move |_, _, _| {
460
+ Ok(GetWorkflowExecutionHistoryResponse {
461
+ history: Some(full_hist.clone().into()),
462
+ ..Default::default()
463
+ })
464
+ });
465
+ }
420
466
  mock_client
421
467
  .expect_complete_workflow_task()
422
468
  .times(1)
@@ -487,3 +533,118 @@ async fn query_cache_miss_causes_page_fetch_dont_reply_wft_too_early() {
487
533
 
488
534
  core.shutdown().await;
489
535
  }
536
+
537
+ #[tokio::test]
538
+ async fn query_replay_with_continue_as_new_doesnt_reply_empty_command() {
539
+ let wfid = "fake_wf_id";
540
+ let t = canned_histories::single_timer("1");
541
+ let query_with_hist_task = {
542
+ let mut pr = hist_to_poll_resp(
543
+ &t,
544
+ wfid.to_owned(),
545
+ ResponseType::ToTaskNum(1),
546
+ TEST_Q.to_string(),
547
+ );
548
+ pr.queries = HashMap::new();
549
+ pr.queries.insert(
550
+ "the-query".to_string(),
551
+ WorkflowQuery {
552
+ query_type: "query-type".to_string(),
553
+ query_args: Some(b"hi".into()),
554
+ header: None,
555
+ },
556
+ );
557
+ pr
558
+ };
559
+ let tasks = VecDeque::from(vec![query_with_hist_task]);
560
+ let mut mock_client = mock_workflow_client();
561
+ mock_client
562
+ .expect_complete_workflow_task()
563
+ .times(1)
564
+ .returning(|resp| {
565
+ // Verify both the complete command and the query response are sent
566
+ assert_eq!(resp.commands.len(), 1);
567
+ assert_eq!(resp.query_responses.len(), 1);
568
+ Ok(RespondWorkflowTaskCompletedResponse::default())
569
+ });
570
+
571
+ let mut mock = MocksHolder::from_client_with_responses(mock_client, tasks, vec![]);
572
+ mock.worker_cfg(|wc| wc.max_cached_workflows = 10);
573
+ let core = mock_worker(mock);
574
+
575
+ let task = core.poll_workflow_activation().await.unwrap();
576
+ // Scheduling and immediately canceling an activity produces another activation which is
577
+ // important in this repro
578
+ core.complete_workflow_activation(WorkflowActivationCompletion::from_cmds(
579
+ task.run_id,
580
+ vec![
581
+ schedule_activity_cmd(
582
+ 0,
583
+ "whatever",
584
+ "act-id",
585
+ ActivityCancellationType::TryCancel,
586
+ Duration::from_secs(60),
587
+ Duration::from_secs(60),
588
+ ),
589
+ RequestCancelActivity { seq: 0 }.into(),
590
+ ContinueAsNewWorkflowExecution {
591
+ ..Default::default()
592
+ }
593
+ .into(),
594
+ ],
595
+ ))
596
+ .await
597
+ .unwrap();
598
+
599
+ // Activity unblocked
600
+ let task = core.poll_workflow_activation().await.unwrap();
601
+ assert_matches!(
602
+ task.jobs.as_slice(),
603
+ [WorkflowActivationJob {
604
+ variant: Some(workflow_activation_job::Variant::ResolveActivity(_)),
605
+ }]
606
+ );
607
+ core.complete_workflow_activation(WorkflowActivationCompletion::empty(task.run_id))
608
+ .await
609
+ .unwrap();
610
+
611
+ let task = core.poll_workflow_activation().await.unwrap();
612
+ let query = assert_matches!(
613
+ task.jobs.as_slice(),
614
+ [WorkflowActivationJob {
615
+ variant: Some(workflow_activation_job::Variant::QueryWorkflow(q)),
616
+ }] => q
617
+ );
618
+ // Throw an evict in there. Repro required a pending eviction during complete.
619
+ core.request_wf_eviction(&task.run_id, "I said so", EvictionReason::LangRequested);
620
+
621
+ core.complete_workflow_activation(WorkflowActivationCompletion::from_cmd(
622
+ task.run_id,
623
+ QueryResult {
624
+ query_id: query.query_id.clone(),
625
+ variant: Some(
626
+ QuerySuccess {
627
+ response: Some("whatever".into()),
628
+ }
629
+ .into(),
630
+ ),
631
+ }
632
+ .into(),
633
+ ))
634
+ .await
635
+ .unwrap();
636
+
637
+ // Need to complete the eviction to finally send commands and finish
638
+ let task = core.poll_workflow_activation().await.unwrap();
639
+ assert_matches!(
640
+ task.jobs.as_slice(),
641
+ [WorkflowActivationJob {
642
+ variant: Some(workflow_activation_job::Variant::RemoveFromCache(_))
643
+ }]
644
+ );
645
+ core.complete_workflow_activation(WorkflowActivationCompletion::empty(task.run_id))
646
+ .await
647
+ .unwrap();
648
+
649
+ core.shutdown().await;
650
+ }
@@ -20,38 +20,6 @@ use temporal_sdk_core_protos::{
20
20
  use temporal_sdk_core_test_utils::start_timer_cmd;
21
21
  use tokio::sync::{watch, Barrier};
22
22
 
23
- #[tokio::test]
24
- async fn multi_workers() {
25
- // TODO: Turn this into a test with multiple independent workers
26
- // Make histories for 5 different workflows on 5 different task queues
27
- // let hists = (0..5).into_iter().map(|i| {
28
- // let wf_id = format!("fake-wf-{}", i);
29
- // let hist = canned_histories::single_timer("1");
30
- // FakeWfResponses {
31
- // wf_id,
32
- // hist,
33
- // response_batches: vec![1.into(), 2.into()],
34
- // task_q: format!("q-{}", i),
35
- // }
36
- // });
37
- // let mock = build_multihist_mock_sg(hists, false, None);
38
- //
39
- // let core = &mock_worker(mock);
40
- //
41
- // for i in 0..5 {
42
- // let tq = format!("q-{}", i);
43
- // let res = core.poll_workflow_activation().await.unwrap();
44
- // assert_matches!(
45
- // res.jobs[0].variant,
46
- // Some(workflow_activation_job::Variant::StartWorkflow(_))
47
- // );
48
- // core.complete_workflow_activation(WorkflowActivationCompletion::empty(res.run_id))
49
- // .await
50
- // .unwrap();
51
- // }
52
- // core.shutdown().await;
53
- }
54
-
55
23
  #[tokio::test]
56
24
  async fn after_shutdown_of_worker_get_shutdown_err() {
57
25
  let t = canned_histories::single_timer("1");
@@ -195,7 +163,7 @@ async fn can_shutdown_local_act_only_worker_when_act_polling() {
195
163
  }
196
164
 
197
165
  #[tokio::test]
198
- async fn complete_with_task_not_found_during_shutdwn() {
166
+ async fn complete_with_task_not_found_during_shutdown() {
199
167
  let t = canned_histories::single_timer("1");
200
168
  let mut mock = mock_workflow_client();
201
169
  mock.expect_complete_workflow_task()
@@ -236,5 +204,7 @@ async fn complete_with_task_not_found_during_shutdwn() {
236
204
  complete_order.borrow_mut().push(1);
237
205
  };
238
206
  tokio::join!(shutdown_fut, poll_fut, complete_fut);
239
- assert_eq!(&complete_order.into_inner(), &[1, 2, 3])
207
+ // Shutdown will currently complete first before the actual eviction reply since the
208
+ // workflow task is marked complete as soon as we get not found back from the server.
209
+ assert_eq!(&complete_order.into_inner(), &[1, 3, 2])
240
210
  }
@@ -71,7 +71,7 @@ fn single_activity_failure_setup(hist_batches: &'static [usize]) -> Worker {
71
71
  #[case::incremental(single_timer_setup(&[1, 2]), NonSticky)]
72
72
  #[case::replay(single_timer_setup(&[2]), NonSticky)]
73
73
  #[case::incremental_evict(single_timer_setup(&[1, 2]), AfterEveryReply)]
74
- #[case::replay_evict(single_timer_setup(&[2, 2]), AfterEveryReply)]
74
+ #[case::replay_evict(single_timer_setup(&[2]), AfterEveryReply)]
75
75
  #[tokio::test]
76
76
  async fn single_timer(#[case] worker: Worker, #[case] evict: WorkflowCachingPolicy) {
77
77
  poll_and_reply(
@@ -1114,14 +1114,9 @@ async fn complete_after_eviction() {
1114
1114
  let eviction_activation = core.poll_workflow_activation().await.unwrap();
1115
1115
  assert_matches!(
1116
1116
  eviction_activation.jobs.as_slice(),
1117
- [
1118
- WorkflowActivationJob {
1119
- variant: Some(workflow_activation_job::Variant::FireTimer(_)),
1120
- },
1121
- WorkflowActivationJob {
1122
- variant: Some(workflow_activation_job::Variant::RemoveFromCache(_)),
1123
- }
1124
- ]
1117
+ [WorkflowActivationJob {
1118
+ variant: Some(workflow_activation_job::Variant::FireTimer(_)),
1119
+ },]
1125
1120
  );
1126
1121
  // Complete the activation containing the eviction, the way we normally would have
1127
1122
  core.complete_workflow_activation(WorkflowActivationCompletion::from_cmds(
@@ -1130,6 +1125,13 @@ async fn complete_after_eviction() {
1130
1125
  ))
1131
1126
  .await
1132
1127
  .unwrap();
1128
+ let eviction = core.poll_workflow_activation().await.unwrap();
1129
+ assert_matches!(
1130
+ eviction.jobs.as_slice(),
1131
+ [WorkflowActivationJob {
1132
+ variant: Some(workflow_activation_job::Variant::RemoveFromCache(_)),
1133
+ }]
1134
+ );
1133
1135
  core.shutdown().await;
1134
1136
  }
1135
1137
 
@@ -1260,38 +1262,6 @@ async fn buffered_work_drained_on_shutdown() {
1260
1262
  });
1261
1263
  }
1262
1264
 
1263
- #[tokio::test]
1264
- async fn buffering_tasks_doesnt_count_toward_outstanding_max() {
1265
- let wfid = "fake_wf_id";
1266
- let t = canned_histories::single_timer("1");
1267
- let mock = mock_workflow_client();
1268
- let mut tasks = VecDeque::new();
1269
- // A way bigger task list than allowed outstanding tasks
1270
- tasks.extend(
1271
- std::iter::repeat(hist_to_poll_resp(
1272
- &t,
1273
- wfid.to_owned(),
1274
- 2.into(),
1275
- TEST_Q.to_string(),
1276
- ))
1277
- .take(20),
1278
- );
1279
- let mut mock = MocksHolder::from_client_with_responses(mock, tasks, []);
1280
- mock.worker_cfg(|wc| {
1281
- wc.max_cached_workflows = 10;
1282
- wc.max_outstanding_workflow_tasks = 5;
1283
- });
1284
- let core = mock_worker(mock);
1285
- // Poll for first WFT
1286
- core.poll_workflow_activation().await.unwrap();
1287
- // This will error out when the mock runs out of responses. Otherwise it would hang when we
1288
- // hit the max
1289
- assert_matches!(
1290
- core.poll_workflow_activation().await.unwrap_err(),
1291
- PollWfError::TonicError(_)
1292
- );
1293
- }
1294
-
1295
1265
  #[tokio::test]
1296
1266
  async fn fail_wft_then_recover() {
1297
1267
  let t = canned_histories::long_sequential_timers(1);
@@ -1737,3 +1707,189 @@ async fn evict_missing_wf_during_poll_doesnt_eat_permit() {
1737
1707
 
1738
1708
  core.shutdown().await;
1739
1709
  }
1710
+
1711
+ #[tokio::test]
1712
+ async fn poll_faster_than_complete_wont_overflow_cache() {
1713
+ // Make workflow tasks for 5 different runs
1714
+ let tasks: Vec<_> = (1..=5)
1715
+ .map(|i| {
1716
+ hist_to_poll_resp(
1717
+ // New hist each time for new run ids
1718
+ &canned_histories::single_timer("1"),
1719
+ format!("wf-{}", i),
1720
+ ResponseType::ToTaskNum(1),
1721
+ TEST_Q.to_string(),
1722
+ )
1723
+ })
1724
+ .collect();
1725
+ let mut mock = mock_workflow_client();
1726
+ mock.expect_complete_workflow_task()
1727
+ .times(3)
1728
+ .returning(|_| Ok(Default::default()));
1729
+ let mut mock = MocksHolder::from_client_with_responses(mock, tasks, []);
1730
+ mock.worker_cfg(|wc| {
1731
+ wc.max_cached_workflows = 3;
1732
+ wc.max_outstanding_workflow_tasks = 3;
1733
+ });
1734
+ let core = mock_worker(mock);
1735
+ // Poll 4 times, completing once, such that max tasks are never exceeded
1736
+ let p1 = core.poll_workflow_activation().await.unwrap();
1737
+ let p2 = core.poll_workflow_activation().await.unwrap();
1738
+ let p3 = core.poll_workflow_activation().await.unwrap();
1739
+ for (i, p_res) in [&p1, &p2, &p3].into_iter().enumerate() {
1740
+ assert_matches!(
1741
+ &p_res.jobs[0].variant,
1742
+ Some(workflow_activation_job::Variant::StartWorkflow(sw))
1743
+ if sw.workflow_id == format!("wf-{}", i + 1)
1744
+ );
1745
+ }
1746
+ // Complete first task to free a wft slot. Cache size is at 3
1747
+ core.complete_workflow_activation(WorkflowActivationCompletion::from_cmd(
1748
+ p1.run_id,
1749
+ start_timer_cmd(1, Duration::from_secs(1)),
1750
+ ))
1751
+ .await
1752
+ .unwrap();
1753
+ // Now we're at cache limit. We will poll for a task, discover it is for a new run, issue
1754
+ // an eviction, and buffer the new run task. However, the run we're trying to evict has pending
1755
+ // activations! Thus, we must complete them first before this poll will unblock, and then it
1756
+ // will unblock with the eviciton.
1757
+ let p4 = async {
1758
+ let p4 = core.poll_workflow_activation().await.unwrap();
1759
+ assert_matches!(
1760
+ &p4.jobs.as_slice(),
1761
+ [WorkflowActivationJob {
1762
+ variant: Some(workflow_activation_job::Variant::RemoveFromCache(_)),
1763
+ }]
1764
+ );
1765
+ p4
1766
+ };
1767
+ let p2_pending_completer = async {
1768
+ // Sleep needed because otherwise the complete unblocks waiting for the cache to free a slot
1769
+ // before we have a chance to actually... wait for it.
1770
+ tokio::time::sleep(Duration::from_millis(100)).await;
1771
+ core.complete_workflow_activation(WorkflowActivationCompletion::from_cmd(
1772
+ p2.run_id,
1773
+ start_timer_cmd(1, Duration::from_secs(1)),
1774
+ ))
1775
+ .await
1776
+ .unwrap();
1777
+ };
1778
+ let (p4, _) = tokio::join!(p4, p2_pending_completer);
1779
+ assert_eq!(core.cached_workflows(), 3);
1780
+
1781
+ // This poll should also block until the eviction is actually completed
1782
+ let blocking_poll = async {
1783
+ let res = core.poll_workflow_activation().await.unwrap();
1784
+ assert_matches!(
1785
+ &res.jobs[0].variant,
1786
+ Some(workflow_activation_job::Variant::StartWorkflow(sw))
1787
+ if sw.workflow_id == format!("wf-{}", 4)
1788
+ );
1789
+ res
1790
+ };
1791
+ let complete_evict = async {
1792
+ core.complete_workflow_activation(WorkflowActivationCompletion::empty(p4.run_id))
1793
+ .await
1794
+ .unwrap();
1795
+ };
1796
+
1797
+ let (_p5, _) = tokio::join!(blocking_poll, complete_evict);
1798
+ assert_eq!(core.cached_workflows(), 3);
1799
+ // The next poll will get an buffer a task for a new run, and generate an eviction for p3 but
1800
+ // that eviction cannot be obtained until we complete the existing outstanding task.
1801
+ let p6 = async {
1802
+ let p6 = core.poll_workflow_activation().await.unwrap();
1803
+ assert_matches!(
1804
+ p6.jobs.as_slice(),
1805
+ [WorkflowActivationJob {
1806
+ variant: Some(workflow_activation_job::Variant::RemoveFromCache(_)),
1807
+ }]
1808
+ );
1809
+ p6
1810
+ };
1811
+ let completer = async {
1812
+ core.complete_workflow_activation(WorkflowActivationCompletion::from_cmd(
1813
+ p3.run_id,
1814
+ start_timer_cmd(1, Duration::from_secs(1)),
1815
+ ))
1816
+ .await
1817
+ .unwrap();
1818
+ };
1819
+ let (p6, _) = tokio::join!(p6, completer);
1820
+ let complete_evict = async {
1821
+ core.complete_workflow_activation(WorkflowActivationCompletion::empty(p6.run_id))
1822
+ .await
1823
+ .unwrap();
1824
+ };
1825
+ let blocking_poll = async {
1826
+ // This poll will also block until the last eviction goes through, and when it does it'll
1827
+ // produce the final start workflow task
1828
+ let res = core.poll_workflow_activation().await.unwrap();
1829
+ assert_matches!(
1830
+ &res.jobs[0].variant,
1831
+ Some(workflow_activation_job::Variant::StartWorkflow(sw))
1832
+ if sw.workflow_id == "wf-5"
1833
+ );
1834
+ };
1835
+
1836
+ tokio::join!(blocking_poll, complete_evict);
1837
+ // p5 outstanding and final poll outstanding -- hence one permit available
1838
+ assert_eq!(core.available_wft_permits(), 1);
1839
+ assert_eq!(core.cached_workflows(), 3);
1840
+ }
1841
+
1842
+ #[tokio::test]
1843
+ async fn eviction_waits_until_replay_finished() {
1844
+ let wfid = "fake_wf_id";
1845
+ let t = canned_histories::long_sequential_timers(3);
1846
+ let mock = mock_workflow_client();
1847
+ let mock = single_hist_mock_sg(wfid, t, &[3], mock, true);
1848
+ let core = mock_worker(mock);
1849
+
1850
+ let activation = core.poll_workflow_activation().await.unwrap();
1851
+ // Immediately request eviction after getting start workflow
1852
+ core.request_workflow_eviction(&activation.run_id);
1853
+ core.complete_workflow_activation(WorkflowActivationCompletion::from_cmd(
1854
+ activation.run_id,
1855
+ start_timer_cmd(1, Duration::from_secs(1)),
1856
+ ))
1857
+ .await
1858
+ .unwrap();
1859
+ let t1_fired = core.poll_workflow_activation().await.unwrap();
1860
+ assert_matches!(
1861
+ t1_fired.jobs.as_slice(),
1862
+ [WorkflowActivationJob {
1863
+ variant: Some(workflow_activation_job::Variant::FireTimer(_)),
1864
+ }]
1865
+ );
1866
+ core.complete_workflow_activation(WorkflowActivationCompletion::from_cmd(
1867
+ t1_fired.run_id,
1868
+ start_timer_cmd(2, Duration::from_secs(1)),
1869
+ ))
1870
+ .await
1871
+ .unwrap();
1872
+ let t2_fired = core.poll_workflow_activation().await.unwrap();
1873
+ assert_matches!(
1874
+ t2_fired.jobs.as_slice(),
1875
+ [WorkflowActivationJob {
1876
+ variant: Some(workflow_activation_job::Variant::FireTimer(_)),
1877
+ }]
1878
+ );
1879
+ core.complete_workflow_activation(WorkflowActivationCompletion::from_cmds(
1880
+ t2_fired.run_id,
1881
+ vec![CompleteWorkflowExecution { result: None }.into()],
1882
+ ))
1883
+ .await
1884
+ .unwrap();
1885
+ // The first two WFTs were replay, and now that we've caught up, the eviction will be sent
1886
+ let eviction = core.poll_workflow_activation().await.unwrap();
1887
+ assert_matches!(
1888
+ eviction.jobs.as_slice(),
1889
+ [WorkflowActivationJob {
1890
+ variant: Some(workflow_activation_job::Variant::RemoveFromCache(_)),
1891
+ }]
1892
+ );
1893
+
1894
+ core.shutdown().await;
1895
+ }
@@ -34,7 +34,8 @@ pub use pollers::{
34
34
  TlsConfig, WorkflowClientTrait,
35
35
  };
36
36
  pub use telemetry::{
37
- fetch_global_buffered_logs, telemetry_init, TelemetryOptions, TelemetryOptionsBuilder,
37
+ fetch_global_buffered_logs, telemetry_init, Logger, MetricsExporter, OtelCollectorOptions,
38
+ TelemetryOptions, TelemetryOptionsBuilder, TraceExporter,
38
39
  };
39
40
  pub use temporal_sdk_core_api as api;
40
41
  pub use temporal_sdk_core_protos as protos;
@@ -22,6 +22,7 @@ struct PaInner {
22
22
  queue: VecDeque<ActivationKey>,
23
23
  }
24
24
 
25
+ #[derive(Debug)]
25
26
  pub struct PendingActInfo {
26
27
  pub needs_eviction: Option<RemoveFromCache>,
27
28
  pub run_id: String,
@@ -41,6 +42,7 @@ impl PendingActivations {
41
42
  inner.queue.push_back(key);
42
43
  };
43
44
  }
45
+
44
46
  pub fn notify_needs_eviction(&self, run_id: &str, message: String, reason: EvictionReason) {
45
47
  let mut inner = self.inner.write();
46
48
 
@@ -106,6 +108,15 @@ impl PendingActivations {
106
108
  inner.activations.remove(k);
107
109
  }
108
110
  }
111
+
112
+ /// Returns true if any pending activation contains an eviction
113
+ pub fn is_some_eviction(&self) -> bool {
114
+ self.inner
115
+ .read()
116
+ .activations
117
+ .values()
118
+ .any(|act| act.needs_eviction.is_some())
119
+ }
109
120
  }
110
121
 
111
122
  #[cfg(test)]
@@ -359,9 +359,7 @@ impl ValidScheduleLA {
359
359
  ))
360
360
  }
361
361
  };
362
- let retry_policy = v
363
- .retry_policy
364
- .ok_or_else(|| anyhow!("Retry policy must be defined!"))?;
362
+ let retry_policy = v.retry_policy.unwrap_or_default();
365
363
  let local_retry_threshold = v
366
364
  .local_retry_threshold
367
365
  .clone()