@temporalio/core-bridge 0.23.0 → 1.0.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 (135) hide show
  1. package/Cargo.lock +118 -15
  2. package/Cargo.toml +2 -1
  3. package/LICENSE.md +1 -1
  4. package/README.md +1 -1
  5. package/index.d.ts +47 -18
  6. package/package.json +7 -7
  7. package/releases/aarch64-apple-darwin/index.node +0 -0
  8. package/releases/aarch64-unknown-linux-gnu/index.node +0 -0
  9. package/releases/x86_64-apple-darwin/index.node +0 -0
  10. package/releases/x86_64-pc-windows-msvc/index.node +0 -0
  11. package/releases/x86_64-unknown-linux-gnu/index.node +0 -0
  12. package/sdk-core/.buildkite/docker/docker-compose.yaml +4 -2
  13. package/sdk-core/ARCHITECTURE.md +9 -7
  14. package/sdk-core/README.md +5 -1
  15. package/sdk-core/arch_docs/diagrams/workflow_internals.svg +1 -0
  16. package/sdk-core/bridge-ffi/src/wrappers.rs +0 -3
  17. package/sdk-core/client/src/lib.rs +26 -8
  18. package/sdk-core/client/src/raw.rs +166 -54
  19. package/sdk-core/client/src/retry.rs +9 -4
  20. package/sdk-core/client/src/workflow_handle/mod.rs +4 -2
  21. package/sdk-core/core/Cargo.toml +2 -0
  22. package/sdk-core/core/src/abstractions.rs +137 -16
  23. package/sdk-core/core/src/core_tests/activity_tasks.rs +258 -63
  24. package/sdk-core/core/src/core_tests/child_workflows.rs +1 -2
  25. package/sdk-core/core/src/core_tests/determinism.rs +2 -2
  26. package/sdk-core/core/src/core_tests/local_activities.rs +8 -7
  27. package/sdk-core/core/src/core_tests/queries.rs +146 -60
  28. package/sdk-core/core/src/core_tests/replay_flag.rs +1 -1
  29. package/sdk-core/core/src/core_tests/workers.rs +39 -23
  30. package/sdk-core/core/src/core_tests/workflow_cancels.rs +1 -1
  31. package/sdk-core/core/src/core_tests/workflow_tasks.rs +387 -280
  32. package/sdk-core/core/src/lib.rs +6 -4
  33. package/sdk-core/core/src/pollers/poll_buffer.rs +16 -10
  34. package/sdk-core/core/src/protosext/mod.rs +6 -6
  35. package/sdk-core/core/src/retry_logic.rs +1 -1
  36. package/sdk-core/core/src/telemetry/metrics.rs +21 -7
  37. package/sdk-core/core/src/telemetry/mod.rs +18 -4
  38. package/sdk-core/core/src/test_help/mod.rs +341 -109
  39. package/sdk-core/core/src/worker/activities/activity_heartbeat_manager.rs +18 -9
  40. package/sdk-core/core/src/worker/activities/local_activities.rs +19 -16
  41. package/sdk-core/core/src/worker/activities.rs +156 -29
  42. package/sdk-core/core/src/worker/client.rs +1 -0
  43. package/sdk-core/core/src/worker/mod.rs +132 -659
  44. package/sdk-core/core/src/{workflow → worker/workflow}/bridge.rs +1 -1
  45. package/sdk-core/core/src/{workflow → worker/workflow}/driven_workflow.rs +1 -1
  46. package/sdk-core/core/src/{workflow → worker/workflow}/history_update.rs +16 -2
  47. package/sdk-core/core/src/{workflow → worker/workflow}/machines/activity_state_machine.rs +39 -4
  48. package/sdk-core/core/src/{workflow → worker/workflow}/machines/cancel_external_state_machine.rs +5 -2
  49. package/sdk-core/core/src/{workflow → worker/workflow}/machines/cancel_workflow_state_machine.rs +1 -1
  50. package/sdk-core/core/src/{workflow → worker/workflow}/machines/child_workflow_state_machine.rs +2 -4
  51. package/sdk-core/core/src/{workflow → worker/workflow}/machines/complete_workflow_state_machine.rs +0 -0
  52. package/sdk-core/core/src/{workflow → worker/workflow}/machines/continue_as_new_workflow_state_machine.rs +1 -1
  53. package/sdk-core/core/src/{workflow → worker/workflow}/machines/fail_workflow_state_machine.rs +0 -0
  54. package/sdk-core/core/src/{workflow → worker/workflow}/machines/local_activity_state_machine.rs +2 -5
  55. package/sdk-core/core/src/{workflow → worker/workflow}/machines/mod.rs +1 -1
  56. package/sdk-core/core/src/{workflow → worker/workflow}/machines/mutable_side_effect_state_machine.rs +0 -0
  57. package/sdk-core/core/src/{workflow → worker/workflow}/machines/patch_state_machine.rs +1 -1
  58. package/sdk-core/core/src/{workflow → worker/workflow}/machines/side_effect_state_machine.rs +0 -0
  59. package/sdk-core/core/src/{workflow → worker/workflow}/machines/signal_external_state_machine.rs +4 -2
  60. package/sdk-core/core/src/{workflow → worker/workflow}/machines/timer_state_machine.rs +1 -2
  61. package/sdk-core/core/src/{workflow → worker/workflow}/machines/transition_coverage.rs +1 -1
  62. package/sdk-core/core/src/{workflow → worker/workflow}/machines/upsert_search_attributes_state_machine.rs +5 -7
  63. package/sdk-core/core/src/{workflow → worker/workflow}/machines/workflow_machines/local_acts.rs +2 -2
  64. package/sdk-core/core/src/{workflow → worker/workflow}/machines/workflow_machines.rs +40 -16
  65. package/sdk-core/core/src/{workflow → worker/workflow}/machines/workflow_task_state_machine.rs +0 -0
  66. package/sdk-core/core/src/worker/workflow/managed_run/managed_wf_test.rs +198 -0
  67. package/sdk-core/core/src/worker/workflow/managed_run.rs +627 -0
  68. package/sdk-core/core/src/worker/workflow/mod.rs +1115 -0
  69. package/sdk-core/core/src/worker/workflow/run_cache.rs +143 -0
  70. package/sdk-core/core/src/worker/workflow/wft_poller.rs +88 -0
  71. package/sdk-core/core/src/worker/workflow/workflow_stream.rs +936 -0
  72. package/sdk-core/core-api/src/errors.rs +3 -10
  73. package/sdk-core/core-api/src/lib.rs +2 -1
  74. package/sdk-core/core-api/src/worker.rs +26 -2
  75. package/sdk-core/etc/dynamic-config.yaml +2 -0
  76. package/sdk-core/integ-with-otel.sh +1 -1
  77. package/sdk-core/protos/api_upstream/Makefile +4 -4
  78. package/sdk-core/protos/api_upstream/api-linter.yaml +2 -0
  79. package/sdk-core/protos/api_upstream/buf.yaml +8 -9
  80. package/sdk-core/protos/api_upstream/temporal/api/cluster/v1/message.proto +83 -0
  81. package/sdk-core/protos/api_upstream/temporal/api/command/v1/message.proto +7 -1
  82. package/sdk-core/protos/api_upstream/temporal/api/enums/v1/cluster.proto +40 -0
  83. package/sdk-core/protos/api_upstream/temporal/api/enums/v1/failed_cause.proto +3 -0
  84. package/sdk-core/protos/api_upstream/temporal/api/enums/v1/reset.proto +3 -1
  85. package/sdk-core/protos/api_upstream/temporal/api/enums/v1/schedule.proto +60 -0
  86. package/sdk-core/protos/api_upstream/temporal/api/enums/v1/workflow.proto +3 -0
  87. package/sdk-core/protos/api_upstream/temporal/api/errordetails/v1/message.proto +32 -4
  88. package/sdk-core/protos/api_upstream/temporal/api/history/v1/message.proto +69 -19
  89. package/sdk-core/protos/api_upstream/temporal/api/namespace/v1/message.proto +13 -0
  90. package/sdk-core/protos/api_upstream/temporal/api/operatorservice/v1/request_response.proto +163 -0
  91. package/sdk-core/protos/api_upstream/temporal/api/operatorservice/v1/service.proto +97 -0
  92. package/sdk-core/protos/api_upstream/temporal/api/schedule/v1/message.proto +300 -0
  93. package/sdk-core/protos/api_upstream/temporal/api/workflow/v1/message.proto +25 -0
  94. package/sdk-core/protos/api_upstream/temporal/api/workflowservice/v1/request_response.proto +180 -3
  95. package/sdk-core/protos/api_upstream/temporal/api/workflowservice/v1/service.proto +53 -3
  96. package/sdk-core/protos/local/temporal/sdk/core/activity_result/activity_result.proto +2 -2
  97. package/sdk-core/protos/local/temporal/sdk/core/activity_task/activity_task.proto +6 -5
  98. package/sdk-core/protos/local/temporal/sdk/core/bridge/bridge.proto +0 -1
  99. package/sdk-core/protos/local/temporal/sdk/core/child_workflow/child_workflow.proto +2 -1
  100. package/sdk-core/protos/local/temporal/sdk/core/common/common.proto +0 -64
  101. package/sdk-core/protos/local/temporal/sdk/core/core_interface.proto +2 -1
  102. package/sdk-core/protos/local/temporal/sdk/core/workflow_activation/workflow_activation.proto +11 -8
  103. package/sdk-core/protos/local/temporal/sdk/core/workflow_commands/workflow_commands.proto +30 -25
  104. package/sdk-core/sdk/src/activity_context.rs +12 -5
  105. package/sdk-core/sdk/src/app_data.rs +37 -0
  106. package/sdk-core/sdk/src/lib.rs +76 -43
  107. package/sdk-core/sdk/src/workflow_context/options.rs +8 -6
  108. package/sdk-core/sdk/src/workflow_context.rs +14 -19
  109. package/sdk-core/sdk/src/workflow_future.rs +11 -6
  110. package/sdk-core/sdk-core-protos/src/history_builder.rs +19 -5
  111. package/sdk-core/sdk-core-protos/src/history_info.rs +11 -6
  112. package/sdk-core/sdk-core-protos/src/lib.rs +74 -176
  113. package/sdk-core/test-utils/src/lib.rs +85 -72
  114. package/sdk-core/tests/integ_tests/heartbeat_tests.rs +11 -9
  115. package/sdk-core/tests/integ_tests/polling_tests.rs +12 -0
  116. package/sdk-core/tests/integ_tests/queries_tests.rs +39 -22
  117. package/sdk-core/tests/integ_tests/workflow_tests/activities.rs +49 -4
  118. package/sdk-core/tests/integ_tests/workflow_tests/appdata_propagation.rs +61 -0
  119. package/sdk-core/tests/integ_tests/workflow_tests/cancel_wf.rs +1 -1
  120. package/sdk-core/tests/integ_tests/workflow_tests/local_activities.rs +74 -13
  121. package/sdk-core/tests/integ_tests/workflow_tests/replay.rs +19 -0
  122. package/sdk-core/tests/integ_tests/workflow_tests/resets.rs +1 -1
  123. package/sdk-core/tests/integ_tests/workflow_tests/upsert_search_attrs.rs +6 -3
  124. package/sdk-core/tests/integ_tests/workflow_tests.rs +10 -23
  125. package/sdk-core/tests/load_tests.rs +8 -3
  126. package/sdk-core/tests/main.rs +2 -1
  127. package/src/conversions.rs +47 -39
  128. package/src/errors.rs +10 -21
  129. package/src/lib.rs +342 -325
  130. package/sdk-core/core/src/pending_activations.rs +0 -173
  131. package/sdk-core/core/src/worker/wft_delivery.rs +0 -81
  132. package/sdk-core/core/src/workflow/mod.rs +0 -478
  133. package/sdk-core/core/src/workflow/workflow_tasks/cache_manager.rs +0 -194
  134. package/sdk-core/core/src/workflow/workflow_tasks/concurrency_manager.rs +0 -418
  135. package/sdk-core/core/src/workflow/workflow_tasks/mod.rs +0 -989
@@ -2,22 +2,31 @@ pub(crate) use temporal_sdk_core_test_utils::canned_histories;
2
2
 
3
3
  use crate::{
4
4
  pollers::{BoxedActPoller, BoxedPoller, BoxedWFPoller, MockManualPoller, MockPoller},
5
+ protosext::ValidPollWFTQResponse,
5
6
  replay::TestHistoryBuilder,
6
7
  sticky_q_name_for_worker,
7
- worker::client::{mocks::mock_workflow_client, MockWorkerClient, WorkerClient},
8
- workflow::WorkflowCachingPolicy,
8
+ worker::{
9
+ client::{mocks::mock_workflow_client, MockWorkerClient, WorkerClient},
10
+ new_wft_poller,
11
+ },
9
12
  TaskToken, Worker, WorkerClientBag, WorkerConfig, WorkerConfigBuilder,
10
13
  };
11
14
  use bimap::BiMap;
12
- use futures::FutureExt;
15
+ use futures::{future::BoxFuture, stream, stream::BoxStream, FutureExt, Stream, StreamExt};
13
16
  use mockall::TimesRange;
14
17
  use parking_lot::RwLock;
15
18
  use std::{
16
19
  collections::{BTreeMap, HashMap, HashSet, VecDeque},
17
- ops::RangeFull,
18
- sync::Arc,
20
+ ops::{Deref, DerefMut},
21
+ pin::Pin,
22
+ sync::{
23
+ atomic::{AtomicBool, Ordering},
24
+ Arc,
25
+ },
26
+ task::{Context, Poll},
19
27
  time::Duration,
20
28
  };
29
+ use temporal_client::WorkflowTaskCompletion;
21
30
  use temporal_sdk_core_api::Worker as WorkerTrait;
22
31
  use temporal_sdk_core_protos::{
23
32
  coresdk::{
@@ -36,39 +45,61 @@ use temporal_sdk_core_protos::{
36
45
  },
37
46
  };
38
47
  use temporal_sdk_core_test_utils::TestWorker;
39
- use tokio::sync::Notify;
48
+ use tokio::sync::{mpsc::unbounded_channel, Notify};
49
+ use tokio_stream::wrappers::UnboundedReceiverStream;
50
+ use tokio_util::sync::CancellationToken;
40
51
 
41
52
  pub const TEST_Q: &str = "q";
42
53
  pub static NO_MORE_WORK_ERROR_MSG: &str = "No more work to do";
43
54
 
44
55
  pub fn test_worker_cfg() -> WorkerConfigBuilder {
45
56
  let mut wcb = WorkerConfigBuilder::default();
46
- wcb.namespace("default").task_queue(TEST_Q);
57
+ wcb.namespace("default")
58
+ .task_queue(TEST_Q)
59
+ .worker_build_id("test_bin_id")
60
+ // Serial polling since it makes mocking much easier.
61
+ .max_concurrent_wft_polls(1_usize);
47
62
  wcb
48
63
  }
49
64
 
50
65
  /// When constructing responses for mocks, indicates how a given response should be built
51
- #[derive(derive_more::From, Debug, Clone, Copy, Eq, PartialEq, Hash)]
66
+ #[derive(derive_more::From)]
67
+ #[allow(clippy::large_enum_variant)] // Test only code, whatever.
52
68
  pub enum ResponseType {
53
69
  ToTaskNum(usize),
54
70
  /// Returns just the history after the WFT completed of the provided task number - 1, through to
55
71
  /// the next WFT started. Simulating the incremental history for just the provided task number
56
72
  #[from(ignore)]
57
73
  OneTask(usize),
74
+ /// Waits until the future resolves before responding as `ToTaskNum` with the provided number
75
+ UntilResolved(BoxFuture<'static, ()>, usize),
58
76
  AllHistory,
77
+ Raw(PollWorkflowTaskQueueResponse),
78
+ }
79
+ #[derive(Eq, PartialEq, Hash)]
80
+ pub enum HashableResponseType {
81
+ ToTaskNum(usize),
82
+ OneTask(usize),
83
+ UntilResolved(usize),
84
+ AllHistory,
85
+ Raw(TaskToken),
86
+ }
87
+ impl ResponseType {
88
+ pub fn hashable(&self) -> HashableResponseType {
89
+ match self {
90
+ ResponseType::ToTaskNum(x) => HashableResponseType::ToTaskNum(*x),
91
+ ResponseType::OneTask(x) => HashableResponseType::OneTask(*x),
92
+ ResponseType::AllHistory => HashableResponseType::AllHistory,
93
+ ResponseType::Raw(r) => HashableResponseType::Raw(r.task_token.clone().into()),
94
+ ResponseType::UntilResolved(_, x) => HashableResponseType::UntilResolved(*x),
95
+ }
96
+ }
59
97
  }
60
-
61
98
  impl From<&usize> for ResponseType {
62
99
  fn from(u: &usize) -> Self {
63
100
  Self::ToTaskNum(*u)
64
101
  }
65
102
  }
66
- // :shrug:
67
- impl From<&Self> for ResponseType {
68
- fn from(r: &Self) -> Self {
69
- *r
70
- }
71
- }
72
103
 
73
104
  /// Given identifiers for a workflow/run, and a test history builder, construct an instance of
74
105
  /// the a worker with a mock server client that will produce the responses as appropriate.
@@ -89,20 +120,21 @@ pub(crate) fn build_fake_worker(
89
120
  response_batches,
90
121
  }],
91
122
  true,
92
- None,
123
+ 0,
93
124
  );
94
125
  mock_worker(mock_holder)
95
126
  }
96
127
 
97
128
  pub(crate) fn mock_worker(mocks: MocksHolder) -> Worker {
98
- let sticky_q = sticky_q_name_for_worker("unit-test", &mocks.mock_worker.config);
129
+ let sticky_q = sticky_q_name_for_worker("unit-test", &mocks.inputs.config);
99
130
  Worker::new_with_pollers(
100
- mocks.mock_worker.config,
131
+ mocks.inputs.config,
101
132
  sticky_q,
102
133
  Arc::new(mocks.client_bag),
103
- mocks.mock_worker.wf_poller,
104
- mocks.mock_worker.act_poller,
134
+ mocks.inputs.wft_stream,
135
+ mocks.inputs.act_poller,
105
136
  Default::default(),
137
+ CancellationToken::new(),
106
138
  )
107
139
  }
108
140
 
@@ -126,35 +158,52 @@ pub struct FakeWfResponses {
126
158
  pub response_batches: Vec<ResponseType>,
127
159
  }
128
160
 
161
+ // TODO: Should be all-internal to this module
129
162
  pub struct MocksHolder {
130
163
  client_bag: WorkerClientBag,
131
- mock_worker: MockWorker,
132
- // bidirectional mapping of run id / task token
133
- pub outstanding_task_map: Option<Arc<RwLock<BiMap<String, TaskToken>>>>,
164
+ inputs: MockWorkerInputs,
165
+ pub outstanding_task_map: Option<OutstandingWFTMap>,
134
166
  }
135
167
 
136
168
  impl MocksHolder {
137
169
  pub fn worker_cfg(&mut self, mutator: impl FnOnce(&mut WorkerConfig)) {
138
- mutator(&mut self.mock_worker.config);
170
+ mutator(&mut self.inputs.config);
171
+ }
172
+ pub fn set_act_poller(&mut self, poller: BoxedActPoller) {
173
+ self.inputs.act_poller = Some(poller);
174
+ }
175
+ /// Can be used for tests that need to avoid auto-shutdown due to running out of mock responses
176
+ pub fn make_wft_stream_interminable(&mut self) {
177
+ let old_stream = std::mem::replace(&mut self.inputs.wft_stream, stream::pending().boxed());
178
+ self.inputs.wft_stream = old_stream.chain(stream::pending()).boxed();
139
179
  }
140
180
  }
141
181
 
142
- pub struct MockWorker {
143
- pub wf_poller: BoxedWFPoller,
182
+ pub struct MockWorkerInputs {
183
+ pub wft_stream: BoxStream<'static, Result<ValidPollWFTQResponse, tonic::Status>>,
144
184
  pub act_poller: Option<BoxedActPoller>,
145
185
  pub config: WorkerConfig,
146
186
  }
147
187
 
148
- impl Default for MockWorker {
188
+ impl Default for MockWorkerInputs {
149
189
  fn default() -> Self {
150
- Self::new(Box::from(mock_poller()))
190
+ Self::new_from_poller(Box::from(mock_poller()))
151
191
  }
152
192
  }
153
193
 
154
- impl MockWorker {
155
- pub fn new(wf_poller: BoxedWFPoller) -> Self {
194
+ impl MockWorkerInputs {
195
+ pub fn new(
196
+ wft_stream: BoxStream<'static, Result<ValidPollWFTQResponse, tonic::Status>>,
197
+ ) -> Self {
198
+ Self {
199
+ wft_stream,
200
+ act_poller: None,
201
+ config: test_worker_cfg().build().unwrap(),
202
+ }
203
+ }
204
+ pub fn new_from_poller(wf_poller: BoxedWFPoller) -> Self {
156
205
  Self {
157
- wf_poller,
206
+ wft_stream: new_wft_poller(wf_poller, Default::default()).boxed(),
158
207
  act_poller: None,
159
208
  config: test_worker_cfg().build().unwrap(),
160
209
  }
@@ -162,54 +211,83 @@ impl MockWorker {
162
211
  }
163
212
 
164
213
  impl MocksHolder {
165
- pub(crate) fn from_mock_worker(client_bag: WorkerClientBag, mock_worker: MockWorker) -> Self {
214
+ pub(crate) fn from_mock_worker(
215
+ client_bag: WorkerClientBag,
216
+ mock_worker: MockWorkerInputs,
217
+ ) -> Self {
166
218
  Self {
167
219
  client_bag,
168
- mock_worker,
220
+ inputs: mock_worker,
169
221
  outstanding_task_map: None,
170
222
  }
171
223
  }
172
224
 
173
225
  /// Uses the provided list of tasks to create a mock poller for the `TEST_Q`
174
- pub(crate) fn from_client_with_responses<WFT, ACT>(
226
+ pub(crate) fn from_client_with_activities<ACT>(
175
227
  client: impl WorkerClient + 'static,
176
- wf_tasks: WFT,
177
228
  act_tasks: ACT,
178
229
  ) -> Self
179
230
  where
180
- WFT: IntoIterator<Item = PollWorkflowTaskQueueResponse>,
181
- ACT: IntoIterator<Item = PollActivityTaskQueueResponse>,
182
- <WFT as IntoIterator>::IntoIter: Send + 'static,
231
+ ACT: IntoIterator<Item = QueueResponse<PollActivityTaskQueueResponse>>,
183
232
  <ACT as IntoIterator>::IntoIter: Send + 'static,
184
233
  {
185
- let mock_poller = mock_poller_from_resps(wf_tasks);
234
+ let wft_stream = stream::pending().boxed();
186
235
  let mock_act_poller = mock_poller_from_resps(act_tasks);
187
- let mock_worker = MockWorker {
188
- wf_poller: mock_poller,
236
+ let mock_worker = MockWorkerInputs {
237
+ wft_stream,
189
238
  act_poller: Some(mock_act_poller),
190
239
  config: test_worker_cfg().build().unwrap(),
191
240
  };
192
241
  Self {
193
242
  client_bag: client.into(),
194
- mock_worker,
243
+ inputs: mock_worker,
244
+ outstanding_task_map: None,
245
+ }
246
+ }
247
+
248
+ /// Uses the provided task responses and delivers them as quickly as possible when polled.
249
+ /// This is only useful to test buffering, as typically you do not want to pretend that
250
+ /// the server is delivering WFTs super fast for the same run.
251
+ pub(crate) fn from_wft_stream(
252
+ client: impl WorkerClient + 'static,
253
+ stream: impl Stream<Item = PollWorkflowTaskQueueResponse> + Send + 'static,
254
+ ) -> Self {
255
+ let wft_stream = stream
256
+ .map(|r| Ok(r.try_into().expect("Mock responses must be valid work")))
257
+ .boxed();
258
+ let mock_worker = MockWorkerInputs {
259
+ wft_stream,
260
+ act_poller: None,
261
+ config: test_worker_cfg().build().unwrap(),
262
+ };
263
+ Self {
264
+ client_bag: client.into(),
265
+ inputs: mock_worker,
195
266
  outstanding_task_map: None,
196
267
  }
197
268
  }
198
269
  }
199
270
 
271
+ // TODO: Un-pub ideally
200
272
  pub(crate) fn mock_poller_from_resps<T, I>(tasks: I) -> BoxedPoller<T>
201
273
  where
202
274
  T: Send + Sync + 'static,
203
- I: IntoIterator<Item = T>,
275
+ I: IntoIterator<Item = QueueResponse<T>>,
204
276
  <I as IntoIterator>::IntoIter: Send + 'static,
205
277
  {
206
- let mut mock_poller = mock_poller();
278
+ let mut mock_poller = mock_manual_poller();
207
279
  let mut tasks = tasks.into_iter();
208
280
  mock_poller.expect_poll().returning(move || {
209
281
  if let Some(t) = tasks.next() {
210
- Some(Ok(t))
282
+ async move {
283
+ if let Some(f) = t.delay_until {
284
+ f.await;
285
+ }
286
+ Some(Ok(t.resp))
287
+ }
288
+ .boxed()
211
289
  } else {
212
- Some(Err(tonic::Status::cancelled(NO_MORE_WORK_ERROR_MSG)))
290
+ async { Some(Err(tonic::Status::cancelled(NO_MORE_WORK_ERROR_MSG))) }.boxed()
213
291
  }
214
292
  });
215
293
  Box::new(mock_poller) as BoxedPoller<T>
@@ -249,7 +327,7 @@ where
249
327
  pub(crate) fn build_multihist_mock_sg(
250
328
  hists: impl IntoIterator<Item = FakeWfResponses>,
251
329
  enforce_correct_number_of_polls: bool,
252
- num_expected_fails: Option<usize>,
330
+ num_expected_fails: usize,
253
331
  ) -> MocksHolder {
254
332
  let mh = MockPollCfg::new(
255
333
  hists.into_iter().collect(),
@@ -275,11 +353,13 @@ pub(crate) fn single_hist_mock_sg(
275
353
  pub(crate) struct MockPollCfg {
276
354
  pub hists: Vec<FakeWfResponses>,
277
355
  pub enforce_correct_number_of_polls: bool,
278
- pub num_expected_fails: Option<usize>,
356
+ pub num_expected_fails: usize,
357
+ pub num_expected_legacy_query_resps: usize,
279
358
  pub mock_client: MockWorkerClient,
280
359
  /// All calls to fail WFTs must match this predicate
281
360
  pub expect_fail_wft_matcher:
282
361
  Box<dyn Fn(&TaskToken, &WorkflowTaskFailedCause, &Option<Failure>) -> bool + Send>,
362
+ pub completion_asserts: Option<Box<dyn Fn(&WorkflowTaskCompletion) + Send>>,
283
363
  /// If being used with the Rust SDK, this is set true. It ensures pollers will not error out
284
364
  /// early with no work, since we cannot know the exact number of times polling will happen.
285
365
  /// Instead, they will just block forever.
@@ -290,14 +370,16 @@ impl MockPollCfg {
290
370
  pub fn new(
291
371
  hists: Vec<FakeWfResponses>,
292
372
  enforce_correct_number_of_polls: bool,
293
- num_expected_fails: Option<usize>,
373
+ num_expected_fails: usize,
294
374
  ) -> Self {
295
375
  Self {
296
376
  hists,
297
377
  enforce_correct_number_of_polls,
298
378
  num_expected_fails,
379
+ num_expected_legacy_query_resps: 0,
299
380
  mock_client: mock_workflow_client(),
300
381
  expect_fail_wft_matcher: Box::new(|_, _, _| true),
382
+ completion_asserts: None,
301
383
  using_rust_sdk: false,
302
384
  }
303
385
  }
@@ -314,19 +396,75 @@ impl MockPollCfg {
314
396
  response_batches: resps.into_iter().map(Into::into).collect(),
315
397
  }],
316
398
  enforce_correct_number_of_polls: true,
317
- num_expected_fails: None,
399
+ num_expected_fails: 0,
400
+ num_expected_legacy_query_resps: 0,
318
401
  mock_client,
319
402
  expect_fail_wft_matcher: Box::new(|_, _, _| true),
403
+ completion_asserts: None,
320
404
  using_rust_sdk: false,
321
405
  }
322
406
  }
323
407
  }
324
408
 
409
+ #[derive(Default, Clone)]
410
+ pub struct OutstandingWFTMap {
411
+ map: Arc<RwLock<BiMap<String, TaskToken>>>,
412
+ waker: Arc<Notify>,
413
+ all_work_delivered: Arc<AtomicBool>,
414
+ }
415
+ impl OutstandingWFTMap {
416
+ fn has_run(&self, run_id: &str) -> bool {
417
+ self.map.read().contains_left(run_id)
418
+ }
419
+ fn put_token(&self, run_id: String, token: TaskToken) {
420
+ self.map.write().insert(run_id, token);
421
+ }
422
+ fn release_token(&self, token: &TaskToken) {
423
+ self.map.write().remove_by_right(token);
424
+ self.waker.notify_one();
425
+ }
426
+ pub fn release_run(&self, run_id: &str) {
427
+ self.map.write().remove_by_left(run_id);
428
+ self.waker.notify_waiters();
429
+ }
430
+ pub fn all_work_delivered(&self) -> bool {
431
+ self.all_work_delivered.load(Ordering::Acquire)
432
+ }
433
+ }
434
+
435
+ struct EnsuresWorkDoneWFTStream {
436
+ inner: UnboundedReceiverStream<ValidPollWFTQResponse>,
437
+ all_work_was_completed: Arc<AtomicBool>,
438
+ }
439
+ impl Stream for EnsuresWorkDoneWFTStream {
440
+ type Item = ValidPollWFTQResponse;
441
+
442
+ fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
443
+ self.inner.poll_next_unpin(cx)
444
+ }
445
+ }
446
+ impl Drop for EnsuresWorkDoneWFTStream {
447
+ fn drop(&mut self) {
448
+ if !self.all_work_was_completed.load(Ordering::Acquire) && !std::thread::panicking() {
449
+ panic!("Not all workflow tasks were taken from mock!");
450
+ }
451
+ }
452
+ }
453
+
325
454
  /// Given an iterable of fake responses, return the mocks & associated data to work with them
326
455
  pub(crate) fn build_mock_pollers(mut cfg: MockPollCfg) -> MocksHolder {
327
456
  let mut task_q_resps: BTreeMap<String, VecDeque<_>> = BTreeMap::new();
328
- let outstanding_wf_task_tokens = Arc::new(RwLock::new(BiMap::new()));
329
- let mut correct_num_polls = None;
457
+ let all_work_delivered = if cfg.enforce_correct_number_of_polls && !cfg.using_rust_sdk {
458
+ Arc::new(AtomicBool::new(false))
459
+ } else {
460
+ Arc::new(AtomicBool::new(true))
461
+ };
462
+
463
+ let outstanding_wf_task_tokens = OutstandingWFTMap {
464
+ map: Arc::new(Default::default()),
465
+ waker: Arc::new(Default::default()),
466
+ all_work_delivered: all_work_delivered.clone(),
467
+ };
330
468
 
331
469
  for hist in cfg.hists {
332
470
  let full_hist_info = hist.hist.get_full_history_info().unwrap();
@@ -342,10 +480,6 @@ pub(crate) fn build_mock_pollers(mut cfg: MockPollCfg) -> MocksHolder {
342
480
  }
343
481
  }
344
482
 
345
- if cfg.enforce_correct_number_of_polls && !cfg.using_rust_sdk {
346
- *correct_num_polls.get_or_insert(0) += hist.response_batches.len();
347
- }
348
-
349
483
  // Convert history batches into poll responses, while also tracking how many times a given
350
484
  // history has been returned so we can increment the associated attempt number on the WFT.
351
485
  // NOTE: This is hard to use properly with the `AfterEveryReply` testing eviction mode.
@@ -354,10 +488,10 @@ pub(crate) fn build_mock_pollers(mut cfg: MockPollCfg) -> MocksHolder {
354
488
  let mut attempts_at_task_num = HashMap::new();
355
489
  let responses: Vec<_> = hist
356
490
  .response_batches
357
- .iter()
358
- .map(|to_task_num| {
359
- let cur_attempt = attempts_at_task_num.entry(to_task_num).or_insert(1);
360
- let mut r = hist_to_poll_resp(&hist.hist, hist.wf_id.clone(), *to_task_num, TEST_Q);
491
+ .into_iter()
492
+ .map(|response| {
493
+ let cur_attempt = attempts_at_task_num.entry(response.hashable()).or_insert(1);
494
+ let mut r = hist_to_poll_resp(&hist.hist, hist.wf_id.clone(), response, TEST_Q);
361
495
  r.attempt = *cur_attempt;
362
496
  *cur_attempt += 1;
363
497
  r
@@ -368,103 +502,168 @@ pub(crate) fn build_mock_pollers(mut cfg: MockPollCfg) -> MocksHolder {
368
502
  task_q_resps.insert(hist.wf_id, tasks);
369
503
  }
370
504
 
371
- let mut mock_poller = mock_manual_poller();
372
505
  // The poller will return history from any workflow runs that do not have currently
373
506
  // outstanding tasks.
374
507
  let outstanding = outstanding_wf_task_tokens.clone();
375
- let outstanding_wakeup_orig = Arc::new(Notify::new());
376
- let outstanding_wakeup = outstanding_wakeup_orig.clone();
377
- mock_poller
378
- .expect_poll()
379
- .times(correct_num_polls.map_or_else(|| RangeFull.into(), Into::<TimesRange>::into))
380
- .returning(move || {
508
+ let outstanding_wakeup = outstanding.waker.clone();
509
+ let (wft_tx, wft_rx) = unbounded_channel();
510
+ tokio::task::spawn(async move {
511
+ loop {
381
512
  let mut resp = None;
382
- for (_, tasks) in task_q_resps.iter_mut() {
513
+ let mut resp_iter = task_q_resps.iter_mut();
514
+ for (_, tasks) in &mut resp_iter {
383
515
  // Must extract run id from a workflow task associated with this workflow
384
516
  // TODO: Case where run id changes for same workflow id is not handled here
385
517
  if let Some(t) = tasks.get(0) {
386
518
  let rid = t.workflow_execution.as_ref().unwrap().run_id.clone();
387
- if !outstanding.read().contains_left(&rid) {
519
+ if !outstanding.has_run(&rid) {
388
520
  let t = tasks.pop_front().unwrap();
389
- outstanding
390
- .write()
391
- .insert(rid, TaskToken(t.task_token.clone()));
392
- resp = Some(Ok(t));
521
+ outstanding.put_token(rid, TaskToken(t.task_token.clone()));
522
+ resp = Some(t);
393
523
  break;
394
524
  }
395
525
  }
396
526
  }
397
- let outstanding_wakeup = outstanding_wakeup.clone();
398
- async move {
399
- if resp.is_some() {
400
- return resp;
401
- }
527
+ let no_tasks_for_anyone = resp_iter.next().is_none();
402
528
 
403
- if cfg.using_rust_sdk {
404
- // Simulate poll timeout, or just send an empty response and then try again
405
- // if we're told a new one might be ready.
406
- tokio::select! {
407
- _ = outstanding_wakeup.notified() => {}
408
- _ = tokio::time::sleep(Duration::from_secs(60)) => {}
409
- };
410
- Some(Ok(Default::default()))
411
- } else {
412
- Some(Err(tonic::Status::cancelled(NO_MORE_WORK_ERROR_MSG)))
529
+ if let Some(resp) = resp {
530
+ if let Some(d) = resp.delay_until {
531
+ d.await;
532
+ }
533
+ if wft_tx
534
+ .send(
535
+ resp.resp
536
+ .try_into()
537
+ .expect("Mock responses must be valid work"),
538
+ )
539
+ .is_err()
540
+ {
541
+ dbg!("Exiting mock WFT task because rcv half of stream was dropped");
542
+ break;
413
543
  }
414
544
  }
415
- .boxed()
416
- });
417
- let mock_worker = MockWorker::new(Box::from(mock_poller));
545
+
546
+ // No more work to do
547
+ if task_q_resps.values().all(|q| q.is_empty()) {
548
+ outstanding
549
+ .all_work_delivered
550
+ .store(true, Ordering::Release);
551
+ break;
552
+ }
553
+
554
+ if no_tasks_for_anyone {
555
+ tokio::select! {
556
+ _ = outstanding_wakeup.notified() => {}
557
+ _ = tokio::time::sleep(Duration::from_secs(60)) => {}
558
+ };
559
+ }
560
+ }
561
+ });
562
+ let mock_worker = MockWorkerInputs::new(
563
+ EnsuresWorkDoneWFTStream {
564
+ inner: UnboundedReceiverStream::new(wft_rx),
565
+ all_work_was_completed: all_work_delivered,
566
+ }
567
+ .map(Ok)
568
+ .boxed(),
569
+ );
418
570
 
419
571
  let outstanding = outstanding_wf_task_tokens.clone();
420
- let outstanding_wakeup = outstanding_wakeup_orig.clone();
421
572
  cfg.mock_client
422
573
  .expect_complete_workflow_task()
423
574
  .returning(move |comp| {
424
- outstanding.write().remove_by_right(&comp.task_token);
425
- outstanding_wakeup.notify_one();
575
+ if let Some(ass) = cfg.completion_asserts.as_ref() {
576
+ // tee hee
577
+ ass(&comp)
578
+ }
579
+ outstanding.release_token(&comp.task_token);
426
580
  Ok(RespondWorkflowTaskCompletedResponse::default())
427
581
  });
428
582
  let outstanding = outstanding_wf_task_tokens.clone();
429
583
  cfg.mock_client
430
584
  .expect_fail_workflow_task()
431
585
  .withf(cfg.expect_fail_wft_matcher)
432
- .times(
433
- cfg.num_expected_fails
434
- .map_or_else(|| RangeFull.into(), Into::<TimesRange>::into),
435
- )
586
+ .times::<TimesRange>(cfg.num_expected_fails.into())
436
587
  .returning(move |tt, _, _| {
437
- outstanding.write().remove_by_right(&tt);
438
- outstanding_wakeup_orig.notify_one();
588
+ outstanding.release_token(&tt);
589
+ Ok(Default::default())
590
+ });
591
+ let outstanding = outstanding_wf_task_tokens.clone();
592
+ cfg.mock_client
593
+ .expect_respond_legacy_query()
594
+ .times::<TimesRange>(cfg.num_expected_legacy_query_resps.into())
595
+ .returning(move |tt, _| {
596
+ outstanding.release_token(&tt);
439
597
  Ok(Default::default())
440
598
  });
441
599
 
442
600
  MocksHolder {
443
601
  client_bag: cfg.mock_client.into(),
444
- mock_worker,
602
+ inputs: mock_worker,
445
603
  outstanding_task_map: Some(outstanding_wf_task_tokens),
446
604
  }
447
605
  }
448
606
 
607
+ pub struct QueueResponse<T> {
608
+ pub resp: T,
609
+ pub delay_until: Option<BoxFuture<'static, ()>>,
610
+ }
611
+ impl<T> From<T> for QueueResponse<T> {
612
+ fn from(resp: T) -> Self {
613
+ QueueResponse {
614
+ resp,
615
+ delay_until: None,
616
+ }
617
+ }
618
+ }
619
+ impl From<QueueResponse<PollWorkflowTaskQueueResponse>> for ResponseType {
620
+ fn from(qr: QueueResponse<PollWorkflowTaskQueueResponse>) -> Self {
621
+ ResponseType::Raw(qr.resp)
622
+ }
623
+ }
624
+ impl<T> Deref for QueueResponse<T> {
625
+ type Target = T;
626
+
627
+ fn deref(&self) -> &Self::Target {
628
+ &self.resp
629
+ }
630
+ }
631
+ impl<T> DerefMut for QueueResponse<T> {
632
+ fn deref_mut(&mut self) -> &mut Self::Target {
633
+ &mut self.resp
634
+ }
635
+ }
636
+
449
637
  pub fn hist_to_poll_resp(
450
638
  t: &TestHistoryBuilder,
451
639
  wf_id: String,
452
640
  response_type: ResponseType,
453
641
  task_queue: impl Into<String>,
454
- ) -> PollWorkflowTaskQueueResponse {
642
+ ) -> QueueResponse<PollWorkflowTaskQueueResponse> {
455
643
  let run_id = t.get_orig_run_id();
456
644
  let wf = WorkflowExecution {
457
645
  workflow_id: wf_id,
458
646
  run_id: run_id.to_string(),
459
647
  };
648
+ let mut delay_until = None;
460
649
  let hist_info = match response_type {
461
650
  ResponseType::ToTaskNum(tn) => t.get_history_info(tn).unwrap(),
462
651
  ResponseType::OneTask(tn) => t.get_one_wft(tn).unwrap(),
463
652
  ResponseType::AllHistory => t.get_full_history_info().unwrap(),
653
+ ResponseType::Raw(r) => {
654
+ return QueueResponse {
655
+ resp: r,
656
+ delay_until: None,
657
+ }
658
+ }
659
+ ResponseType::UntilResolved(fut, tn) => {
660
+ delay_until = Some(fut);
661
+ t.get_history_info(tn).unwrap()
662
+ }
464
663
  };
465
664
  let mut resp = hist_info.as_poll_wft_response(task_queue);
466
665
  resp.workflow_execution = Some(wf);
467
- resp
666
+ QueueResponse { resp, delay_until }
468
667
  }
469
668
 
470
669
  type AsserterWithReply<'a> = (
@@ -472,6 +671,19 @@ type AsserterWithReply<'a> = (
472
671
  workflow_activation_completion::Status,
473
672
  );
474
673
 
674
+ /// Determines when workflows are kept in the cache or evicted for [poll_and_reply] type tests
675
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
676
+ pub(crate) enum WorkflowCachingPolicy {
677
+ /// Workflows are evicted after each workflow task completion. Note that this is *not* after
678
+ /// each workflow activation - there are often multiple activations per workflow task.
679
+ NonSticky,
680
+
681
+ /// Not a real mode, but good for imitating crashes. Evict workflows after *every* reply,
682
+ /// even if there are pending activations
683
+ #[cfg(test)]
684
+ AfterEveryReply,
685
+ }
686
+
475
687
  /// This function accepts a list of asserts and replies to workflow activations to run against the
476
688
  /// provided instance of fake core.
477
689
  ///
@@ -489,7 +701,7 @@ pub(crate) async fn poll_and_reply<'a>(
489
701
 
490
702
  pub(crate) async fn poll_and_reply_clears_outstanding_evicts<'a>(
491
703
  worker: &'a Worker,
492
- outstanding_map: Option<Arc<RwLock<BiMap<String, TaskToken>>>>,
704
+ outstanding_map: Option<OutstandingWFTMap>,
493
705
  eviction_mode: WorkflowCachingPolicy,
494
706
  expect_and_reply: &'a [AsserterWithReply<'a>],
495
707
  ) {
@@ -515,6 +727,7 @@ pub(crate) async fn poll_and_reply_clears_outstanding_evicts<'a>(
515
727
  let mut res = worker.poll_workflow_activation().await.unwrap();
516
728
  let contains_eviction = res.eviction_index();
517
729
 
730
+ let mut do_release = false;
518
731
  if let Some(eviction_job_ix) = contains_eviction {
519
732
  // If the job list has an eviction, make sure it was the last item in the list
520
733
  // then remove it, since in the tests we don't explicitly specify evict assertions
@@ -524,9 +737,7 @@ pub(crate) async fn poll_and_reply_clears_outstanding_evicts<'a>(
524
737
  "Eviction job was not last job in job list"
525
738
  );
526
739
  res.jobs.remove(eviction_job_ix);
527
- if let Some(omap) = outstanding_map.as_ref() {
528
- omap.write().remove_by_left(&res.run_id);
529
- }
740
+ do_release = true;
530
741
  }
531
742
 
532
743
  // TODO: Can remove this if?
@@ -549,6 +760,11 @@ pub(crate) async fn poll_and_reply_clears_outstanding_evicts<'a>(
549
760
 
550
761
  worker.complete_workflow_activation(reply).await.unwrap();
551
762
 
763
+ if do_release {
764
+ if let Some(omap) = outstanding_map.as_ref() {
765
+ omap.release_run(&res.run_id);
766
+ }
767
+ }
552
768
  // Restart assertions from the beginning if it was an eviction (and workflow execution
553
769
  // isn't over)
554
770
  if contains_eviction.is_some() && !ends_execution {
@@ -560,7 +776,6 @@ pub(crate) async fn poll_and_reply_clears_outstanding_evicts<'a>(
560
776
  }
561
777
 
562
778
  match eviction_mode {
563
- WorkflowCachingPolicy::Sticky { .. } => unimplemented!(),
564
779
  WorkflowCachingPolicy::NonSticky => (),
565
780
  WorkflowCachingPolicy::AfterEveryReply => {
566
781
  if evictions < expected_evictions {
@@ -575,7 +790,7 @@ pub(crate) async fn poll_and_reply_clears_outstanding_evicts<'a>(
575
790
  }
576
791
 
577
792
  assert_eq!(expected_fail_count, executed_failures.len());
578
- assert_eq!(worker.outstanding_workflow_tasks(), 0);
793
+ assert_eq!(worker.outstanding_workflow_tasks().await, 0);
579
794
  }
580
795
 
581
796
  pub(crate) fn gen_assert_and_reply(
@@ -615,3 +830,20 @@ macro_rules! job_assert {
615
830
  }
616
831
  };
617
832
  }
833
+
834
+ /// Forcibly drive a future a number of times, enforcing it is always returning Pending. This is
835
+ /// useful for ensuring some future has proceeded "enough" before racing it against another future.
836
+ #[macro_export]
837
+ macro_rules! advance_fut {
838
+ ($fut:ident) => {
839
+ ::futures::pin_mut!($fut);
840
+ {
841
+ let waker = ::futures::task::noop_waker();
842
+ let mut cx = core::task::Context::from_waker(&waker);
843
+ for _ in 0..10 {
844
+ assert_matches!($fut.poll_unpin(&mut cx), core::task::Poll::Pending);
845
+ ::tokio::task::yield_now().await;
846
+ }
847
+ }
848
+ };
849
+ }