@temporalio/core-bridge 1.1.0 → 1.3.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 (114) hide show
  1. package/Cargo.lock +786 -54
  2. package/Cargo.toml +2 -2
  3. package/common.js +7 -3
  4. package/index.d.ts +110 -3
  5. package/index.js +2 -6
  6. package/package.json +3 -3
  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/scripts/build.js +4 -3
  13. package/sdk-core/.buildkite/docker/Dockerfile +2 -1
  14. package/sdk-core/ARCHITECTURE.md +2 -2
  15. package/sdk-core/README.md +12 -0
  16. package/sdk-core/bridge-ffi/Cargo.toml +2 -2
  17. package/sdk-core/client/Cargo.toml +6 -4
  18. package/sdk-core/client/src/lib.rs +338 -215
  19. package/sdk-core/client/src/raw.rs +352 -106
  20. package/sdk-core/client/src/retry.rs +159 -133
  21. package/sdk-core/client/src/workflow_handle/mod.rs +1 -1
  22. package/sdk-core/core/Cargo.toml +18 -9
  23. package/sdk-core/core/src/core_tests/activity_tasks.rs +63 -23
  24. package/sdk-core/core/src/core_tests/child_workflows.rs +125 -3
  25. package/sdk-core/core/src/core_tests/local_activities.rs +6 -6
  26. package/sdk-core/core/src/core_tests/workers.rs +3 -2
  27. package/sdk-core/core/src/core_tests/workflow_tasks.rs +70 -2
  28. package/sdk-core/core/src/ephemeral_server/mod.rs +499 -0
  29. package/sdk-core/core/src/lib.rs +60 -26
  30. package/sdk-core/core/src/pollers/poll_buffer.rs +4 -4
  31. package/sdk-core/core/src/replay/mod.rs +3 -3
  32. package/sdk-core/core/src/retry_logic.rs +10 -9
  33. package/sdk-core/core/src/telemetry/mod.rs +10 -7
  34. package/sdk-core/core/src/test_help/mod.rs +18 -8
  35. package/sdk-core/core/src/worker/activities/activity_heartbeat_manager.rs +10 -10
  36. package/sdk-core/core/src/worker/activities/local_activities.rs +13 -13
  37. package/sdk-core/core/src/worker/activities.rs +6 -12
  38. package/sdk-core/core/src/worker/client.rs +193 -64
  39. package/sdk-core/core/src/worker/mod.rs +14 -19
  40. package/sdk-core/core/src/worker/workflow/driven_workflow.rs +3 -0
  41. package/sdk-core/core/src/worker/workflow/history_update.rs +5 -5
  42. package/sdk-core/core/src/worker/workflow/machines/child_workflow_state_machine.rs +133 -85
  43. package/sdk-core/core/src/worker/workflow/machines/mod.rs +3 -2
  44. package/sdk-core/core/src/worker/workflow/machines/workflow_machines.rs +160 -105
  45. package/sdk-core/core/src/worker/workflow/managed_run.rs +2 -1
  46. package/sdk-core/core/src/worker/workflow/mod.rs +59 -58
  47. package/sdk-core/core/src/worker/workflow/run_cache.rs +5 -3
  48. package/sdk-core/core/src/worker/workflow/workflow_stream.rs +7 -5
  49. package/sdk-core/core-api/Cargo.toml +2 -2
  50. package/sdk-core/core-api/src/errors.rs +3 -11
  51. package/sdk-core/core-api/src/worker.rs +7 -0
  52. package/sdk-core/protos/api_upstream/.buildkite/Dockerfile +1 -1
  53. package/sdk-core/protos/api_upstream/.github/CODEOWNERS +1 -1
  54. package/sdk-core/protos/api_upstream/.github/PULL_REQUEST_TEMPLATE.md +2 -6
  55. package/sdk-core/protos/api_upstream/.github/workflows/trigger-api-go-update.yml +29 -0
  56. package/sdk-core/protos/api_upstream/Makefile +2 -2
  57. package/sdk-core/protos/api_upstream/buf.yaml +1 -0
  58. package/sdk-core/protos/api_upstream/temporal/api/batch/v1/message.proto +86 -0
  59. package/sdk-core/protos/api_upstream/temporal/api/command/v1/message.proto +26 -0
  60. package/sdk-core/protos/api_upstream/temporal/api/enums/v1/batch_operation.proto +46 -0
  61. package/sdk-core/protos/api_upstream/temporal/api/enums/v1/command_type.proto +7 -0
  62. package/sdk-core/protos/api_upstream/temporal/api/enums/v1/event_type.proto +14 -0
  63. package/sdk-core/protos/api_upstream/temporal/api/enums/v1/update.proto +51 -0
  64. package/sdk-core/protos/api_upstream/temporal/api/failure/v1/message.proto +18 -0
  65. package/sdk-core/protos/api_upstream/temporal/api/history/v1/message.proto +57 -1
  66. package/sdk-core/protos/api_upstream/temporal/api/operatorservice/v1/request_response.proto +1 -3
  67. package/sdk-core/protos/api_upstream/temporal/api/operatorservice/v1/service.proto +4 -2
  68. package/sdk-core/protos/api_upstream/temporal/api/replication/v1/message.proto +11 -0
  69. package/sdk-core/protos/api_upstream/temporal/api/taskqueue/v1/message.proto +23 -0
  70. package/sdk-core/protos/api_upstream/temporal/api/update/v1/message.proto +46 -0
  71. package/sdk-core/protos/api_upstream/temporal/api/workflow/v1/message.proto +1 -0
  72. package/sdk-core/protos/api_upstream/temporal/api/workflowservice/v1/request_response.proto +172 -0
  73. package/sdk-core/protos/api_upstream/temporal/api/workflowservice/v1/service.proto +30 -0
  74. package/sdk-core/protos/grpc/health/v1/health.proto +63 -0
  75. package/sdk-core/protos/local/temporal/sdk/core/workflow_commands/workflow_commands.proto +18 -15
  76. package/sdk-core/protos/testsrv_upstream/Makefile +80 -0
  77. package/sdk-core/protos/testsrv_upstream/api-linter.yaml +38 -0
  78. package/sdk-core/protos/testsrv_upstream/buf.yaml +13 -0
  79. package/sdk-core/protos/testsrv_upstream/dependencies/gogoproto/gogo.proto +141 -0
  80. package/sdk-core/protos/testsrv_upstream/temporal/api/testservice/v1/request_response.proto +63 -0
  81. package/sdk-core/protos/testsrv_upstream/temporal/api/testservice/v1/service.proto +90 -0
  82. package/sdk-core/sdk/Cargo.toml +2 -2
  83. package/sdk-core/sdk/src/lib.rs +2 -2
  84. package/sdk-core/sdk/src/workflow_context/options.rs +36 -8
  85. package/sdk-core/sdk/src/workflow_context.rs +30 -6
  86. package/sdk-core/sdk/src/workflow_future.rs +4 -4
  87. package/sdk-core/sdk-core-protos/Cargo.toml +5 -5
  88. package/sdk-core/sdk-core-protos/build.rs +9 -1
  89. package/sdk-core/sdk-core-protos/src/history_builder.rs +6 -1
  90. package/sdk-core/sdk-core-protos/src/lib.rs +93 -32
  91. package/sdk-core/test-utils/Cargo.toml +3 -3
  92. package/sdk-core/test-utils/src/canned_histories.rs +58 -0
  93. package/sdk-core/test-utils/src/lib.rs +14 -10
  94. package/sdk-core/tests/integ_tests/ephemeral_server_tests.rs +141 -0
  95. package/sdk-core/tests/integ_tests/heartbeat_tests.rs +55 -5
  96. package/sdk-core/tests/integ_tests/polling_tests.rs +1 -1
  97. package/sdk-core/tests/integ_tests/queries_tests.rs +4 -4
  98. package/sdk-core/tests/integ_tests/visibility_tests.rs +93 -0
  99. package/sdk-core/tests/integ_tests/workflow_tests/activities.rs +93 -10
  100. package/sdk-core/tests/integ_tests/workflow_tests/cancel_wf.rs +1 -1
  101. package/sdk-core/tests/integ_tests/workflow_tests/local_activities.rs +14 -14
  102. package/sdk-core/tests/integ_tests/workflow_tests/replay.rs +1 -1
  103. package/sdk-core/tests/integ_tests/workflow_tests/resets.rs +12 -12
  104. package/sdk-core/tests/integ_tests/workflow_tests/signals.rs +12 -1
  105. package/sdk-core/tests/integ_tests/workflow_tests/timers.rs +3 -3
  106. package/sdk-core/tests/integ_tests/workflow_tests.rs +19 -4
  107. package/sdk-core/tests/load_tests.rs +2 -1
  108. package/sdk-core/tests/main.rs +10 -0
  109. package/src/conversions.rs +138 -91
  110. package/src/helpers.rs +190 -0
  111. package/src/lib.rs +10 -912
  112. package/src/runtime.rs +436 -0
  113. package/src/testing.rs +67 -0
  114. package/src/worker.rs +465 -0
@@ -0,0 +1,499 @@
1
+ //! This module implements support for downloading and running ephemeral test
2
+ //! servers useful for testing.
3
+
4
+ use anyhow::anyhow;
5
+ use flate2::read::GzDecoder;
6
+ use futures::StreamExt;
7
+ use serde::Deserialize;
8
+ use std::{
9
+ fs::OpenOptions,
10
+ path::{Path, PathBuf},
11
+ };
12
+ use temporal_client::ClientOptionsBuilder;
13
+ use tokio::{
14
+ task::spawn_blocking,
15
+ time::{sleep, Duration},
16
+ };
17
+ use tokio_util::io::{StreamReader, SyncIoBridge};
18
+ use url::Url;
19
+ use zip::read::read_zipfile_from_stream;
20
+
21
+ #[cfg(target_family = "unix")]
22
+ use std::os::unix::fs::OpenOptionsExt;
23
+
24
+ /// Configuration for Temporalite.
25
+ #[derive(Debug, Clone, derive_builder::Builder)]
26
+ pub struct TemporaliteConfig {
27
+ /// Required path to executable or download info.
28
+ pub exe: EphemeralExe,
29
+ /// Namespace to use.
30
+ #[builder(default = "\"default\".to_owned()")]
31
+ pub namespace: String,
32
+ /// IP to bind to.
33
+ #[builder(default = "\"127.0.0.1\".to_owned()")]
34
+ pub ip: String,
35
+ /// Port to use or obtains a free one if none given.
36
+ #[builder(default)]
37
+ pub port: Option<u16>,
38
+ /// Sqlite DB filename if persisting or non-persistent if none.
39
+ #[builder(default)]
40
+ pub db_filename: Option<String>,
41
+ /// Whether to enable the UI.
42
+ #[builder(default)]
43
+ pub ui: bool,
44
+ /// Log format and level
45
+ #[builder(default = "(\"pretty\".to_owned(), \"warn\".to_owned())")]
46
+ pub log: (String, String),
47
+ /// Additional arguments to Temporalite.
48
+ #[builder(default)]
49
+ pub extra_args: Vec<String>,
50
+ }
51
+
52
+ impl TemporaliteConfig {
53
+ /// Start a Temporalite server.
54
+ pub async fn start_server(&self) -> anyhow::Result<EphemeralServer> {
55
+ // Get exe path
56
+ let exe_path = self.exe.get_or_download("temporalite").await?;
57
+
58
+ // Get free port if not already given
59
+ let port = self.port.unwrap_or_else(|| get_free_port(&self.ip));
60
+
61
+ // Build arg set
62
+ let mut args = vec![
63
+ "start".to_owned(),
64
+ "--port".to_owned(),
65
+ port.to_string(),
66
+ "--namespace".to_owned(),
67
+ self.namespace.clone(),
68
+ "--ip".to_owned(),
69
+ self.ip.clone(),
70
+ "--log-format".to_owned(),
71
+ self.log.0.clone(),
72
+ "--log-level".to_owned(),
73
+ self.log.1.clone(),
74
+ ];
75
+ if let Some(db_filename) = &self.db_filename {
76
+ args.push("--filename".to_owned());
77
+ args.push(db_filename.clone());
78
+ } else {
79
+ args.push("--ephemeral".to_owned());
80
+ }
81
+ if !self.ui {
82
+ args.push("--headless".to_owned());
83
+ }
84
+ args.extend(self.extra_args.clone());
85
+
86
+ // Start
87
+ EphemeralServer::start(EphemeralServerConfig {
88
+ exe_path,
89
+ port,
90
+ args,
91
+ has_test_service: false,
92
+ })
93
+ .await
94
+ }
95
+ }
96
+
97
+ /// Configuration for the test server.
98
+ #[derive(Debug, Clone, derive_builder::Builder)]
99
+ pub struct TestServerConfig {
100
+ /// Required path to executable or download info.
101
+ pub exe: EphemeralExe,
102
+ /// Port to use or obtains a free one if none given.
103
+ #[builder(default)]
104
+ pub port: Option<u16>,
105
+ /// Additional arguments to the test server.
106
+ #[builder(default)]
107
+ pub extra_args: Vec<String>,
108
+ }
109
+
110
+ impl TestServerConfig {
111
+ /// Start a test server.
112
+ pub async fn start_server(&self) -> anyhow::Result<EphemeralServer> {
113
+ // Get exe path
114
+ let exe_path = self.exe.get_or_download("temporal-test-server").await?;
115
+
116
+ // Get free port if not already given
117
+ let port = self.port.unwrap_or_else(|| get_free_port("0.0.0.0"));
118
+
119
+ // Build arg set
120
+ let mut args = vec![port.to_string()];
121
+ args.extend(self.extra_args.clone());
122
+
123
+ // Start
124
+ EphemeralServer::start(EphemeralServerConfig {
125
+ exe_path,
126
+ port,
127
+ args,
128
+ has_test_service: true,
129
+ })
130
+ .await
131
+ }
132
+ }
133
+
134
+ struct EphemeralServerConfig {
135
+ exe_path: PathBuf,
136
+ port: u16,
137
+ args: Vec<String>,
138
+ has_test_service: bool,
139
+ }
140
+
141
+ /// Server that will be stopped when dropped.
142
+ pub struct EphemeralServer {
143
+ /// gRPC target host:port for the server frontend.
144
+ pub target: String,
145
+ /// Whether the target implements the gRPC TestService
146
+ pub has_test_service: bool,
147
+ child: tokio::process::Child,
148
+ }
149
+
150
+ impl EphemeralServer {
151
+ async fn start(config: EphemeralServerConfig) -> anyhow::Result<EphemeralServer> {
152
+ // Start process
153
+ // TODO(cretz): Offer stdio suppression?
154
+ let child = tokio::process::Command::new(config.exe_path)
155
+ .args(config.args)
156
+ .stdin(std::process::Stdio::null())
157
+ .spawn()?;
158
+ let target = format!("127.0.0.1:{}", config.port);
159
+ let target_url = format!("http://{}", target);
160
+ let success = Ok(EphemeralServer {
161
+ target,
162
+ has_test_service: config.has_test_service,
163
+ child,
164
+ });
165
+
166
+ // Try to connect every 100ms for 5s
167
+ // TODO(cretz): Some other way, e.g. via stdout, to know whether the
168
+ // server is up?
169
+ let client_options = ClientOptionsBuilder::default()
170
+ .identity("online_checker".to_owned())
171
+ .target_url(Url::parse(&target_url)?)
172
+ .client_name("online-checker".to_owned())
173
+ .client_version("0.1.0".to_owned())
174
+ .build()?;
175
+ for _ in 0..50 {
176
+ sleep(Duration::from_millis(100)).await;
177
+ if client_options
178
+ .connect_no_namespace(None, None)
179
+ .await
180
+ .is_ok()
181
+ {
182
+ return success;
183
+ }
184
+ }
185
+ Err(anyhow!("Failed connecting to test server after 5 seconds"))
186
+ }
187
+
188
+ /// Shutdown the server (i.e. kill the child process). This does not attempt
189
+ /// a kill if the child process appears completed, but such a check is not
190
+ /// atomic so a kill could still fail as completed if completed just before
191
+ /// kill.
192
+ #[cfg(not(target_family = "unix"))]
193
+ pub async fn shutdown(&mut self) -> anyhow::Result<()> {
194
+ // Only kill if there is a PID
195
+ if self.child.id().is_some() {
196
+ Ok(self.child.kill().await?)
197
+ } else {
198
+ Ok(())
199
+ }
200
+ }
201
+
202
+ /// Shutdown the server (i.e. kill the child process). This does not attempt
203
+ /// a kill if the child process appears completed, but such a check is not
204
+ /// atomic so a kill could still fail as completed if completed just before
205
+ /// kill.
206
+ #[cfg(target_family = "unix")]
207
+ pub async fn shutdown(&mut self) -> anyhow::Result<()> {
208
+ // For whatever reason, Tokio is not properly waiting on result
209
+ // after sending kill in some cases which is causing defunct zombie
210
+ // processes to remain and kill() to hang. Therefore, we are sending
211
+ // SIGINT and waiting on the process ourselves using a low-level call.
212
+ //
213
+ // WARNING: This is based on empirical evidence starting a Python test
214
+ // run on Linux with Python 3.7 (does not happen on Python 3.10 nor does
215
+ // it happen on Temporalite nor does it happen in Rust integration
216
+ // tests). Do not consider this fixed without running that scenario.
217
+ if let Some(pid) = self.child.id() {
218
+ let nix_pid = nix::unistd::Pid::from_raw(pid as i32);
219
+ Ok(spawn_blocking(move || {
220
+ nix::sys::signal::kill(nix_pid, nix::sys::signal::Signal::SIGINT)?;
221
+ nix::sys::wait::waitpid(Some(nix_pid), None)
222
+ })
223
+ .await?
224
+ .map(|_| ())?)
225
+ } else {
226
+ Ok(())
227
+ }
228
+ }
229
+ }
230
+
231
+ /// Where to find an executable. Can be a path or download.
232
+ #[derive(Debug, Clone)]
233
+ pub enum EphemeralExe {
234
+ /// Existing path on the filesystem for the executable.
235
+ ExistingPath(String),
236
+ /// Download the executable if not already there.
237
+ CachedDownload {
238
+ /// Which version to download.
239
+ version: EphemeralExeVersion,
240
+ /// Destination directory or the user temp directory if none set.
241
+ dest_dir: Option<String>,
242
+ },
243
+ }
244
+
245
+ /// Which version of the exe to download.
246
+ #[derive(Debug, Clone)]
247
+ pub enum EphemeralExeVersion {
248
+ /// Use a default version for the given SDK name and version.
249
+ Default {
250
+ /// Name of the SDK to get the default for.
251
+ sdk_name: String,
252
+ /// Version of the SDK to get the default for.
253
+ sdk_version: String,
254
+ },
255
+ /// Specific version.
256
+ Fixed(String),
257
+ }
258
+
259
+ #[derive(Deserialize, Debug)]
260
+ #[serde(rename_all = "camelCase")]
261
+ struct DownloadInfo {
262
+ archive_url: String,
263
+ file_to_extract: String,
264
+ }
265
+
266
+ impl EphemeralExe {
267
+ async fn get_or_download(&self, artifact_name: &str) -> anyhow::Result<PathBuf> {
268
+ match self {
269
+ EphemeralExe::ExistingPath(exe_path) => {
270
+ let path = PathBuf::from(exe_path);
271
+ if !path.exists() {
272
+ return Err(anyhow!("Exe path does not exist"));
273
+ }
274
+ Ok(path)
275
+ }
276
+ EphemeralExe::CachedDownload { version, dest_dir } => {
277
+ let dest_dir = dest_dir
278
+ .as_ref()
279
+ .map(PathBuf::from)
280
+ .unwrap_or_else(std::env::temp_dir);
281
+ let (platform, out_ext) = match std::env::consts::OS {
282
+ "windows" => ("windows", ".exe"),
283
+ "macos" => ("darwin", ""),
284
+ _ => ("linux", ""),
285
+ };
286
+ // Create dest file based on SDK name/version or fixed version
287
+ let dest = dest_dir.join(match version {
288
+ EphemeralExeVersion::Default {
289
+ sdk_name,
290
+ sdk_version,
291
+ } => format!("{}-{}-{}{}", artifact_name, sdk_name, sdk_version, out_ext),
292
+ EphemeralExeVersion::Fixed(version) => {
293
+ format!("{}-{}{}", artifact_name, version, out_ext)
294
+ }
295
+ });
296
+ debug!(
297
+ "Lazily downloading or using existing exe at {}",
298
+ dest.display()
299
+ );
300
+
301
+ // If it already exists, skip
302
+ if dest.exists() {
303
+ return Ok(dest);
304
+ }
305
+
306
+ // Get info about the proper archive and in-archive file
307
+ let arch = match std::env::consts::ARCH {
308
+ "x86_64" => "amd64",
309
+ "arm" | "aarch64" => "arm64",
310
+ other => return Err(anyhow!("Unsupported arch: {}", other)),
311
+ };
312
+ let mut get_info_params = vec![("arch", arch), ("platform", platform)];
313
+ let version_name = match version {
314
+ EphemeralExeVersion::Default {
315
+ sdk_name,
316
+ sdk_version,
317
+ } => {
318
+ get_info_params.push(("sdk-name", sdk_name.as_str()));
319
+ get_info_params.push(("sdk-version", sdk_version.as_str()));
320
+ "default"
321
+ }
322
+ EphemeralExeVersion::Fixed(version) => version,
323
+ };
324
+ let client = reqwest::Client::new();
325
+ let info: DownloadInfo = client
326
+ .get(format!(
327
+ "https://temporal.download/{}/{}",
328
+ artifact_name, version_name
329
+ ))
330
+ .query(&get_info_params)
331
+ .send()
332
+ .await?
333
+ .json()
334
+ .await?;
335
+
336
+ // Attempt download, looping because it could have waited for
337
+ // concurrent one to finish
338
+ loop {
339
+ if lazy_download_exe(
340
+ &client,
341
+ &info.archive_url,
342
+ Path::new(&info.file_to_extract),
343
+ &dest,
344
+ )
345
+ .await?
346
+ {
347
+ return Ok(dest);
348
+ }
349
+ }
350
+ }
351
+ }
352
+ }
353
+ }
354
+
355
+ fn get_free_port(bind_ip: &str) -> u16 {
356
+ // Can just ask OS to give us a port then close socket. OS's don't give that
357
+ // port back to anyone else anytime soon.
358
+ std::net::TcpListener::bind(format!("{}:0", bind_ip))
359
+ .unwrap()
360
+ .local_addr()
361
+ .unwrap()
362
+ .port()
363
+ }
364
+
365
+ /// Returns false if we successfully waited for another download to complete, or
366
+ /// true if the destination is known to exist. Should call again if false is
367
+ /// returned.
368
+ async fn lazy_download_exe(
369
+ client: &reqwest::Client,
370
+ uri: &str,
371
+ file_to_extract: &Path,
372
+ dest: &Path,
373
+ ) -> anyhow::Result<bool> {
374
+ // If it already exists, do not extract
375
+ if dest.exists() {
376
+ return Ok(true);
377
+ }
378
+
379
+ // We only want to download if we're not already downloading. To avoid some
380
+ // kind of global lock, we'll just create the file eagerly w/ a temp
381
+ // filename and delete it on failure or move it on success. If the temp file
382
+ // already exists, we'll wait a bit and re-run this.
383
+ let temp_dest_str = format!("{}{}", dest.to_str().unwrap(), ".downloading");
384
+ let temp_dest = Path::new(&temp_dest_str);
385
+ // Try to open file, using a file mode on unix families
386
+ #[cfg(target_family = "unix")]
387
+ let file = OpenOptions::new()
388
+ .create_new(true)
389
+ .write(true)
390
+ .mode(0o755)
391
+ .open(temp_dest);
392
+ #[cfg(not(target_family = "unix"))]
393
+ let file = OpenOptions::new()
394
+ .create_new(true)
395
+ .write(true)
396
+ .open(temp_dest);
397
+ // This match only gets Ok if the file was downloaded and extracted to the
398
+ // temporary path
399
+ match file {
400
+ Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
401
+ // Since it already exists, we'll try once a second for 20 seconds
402
+ // to wait for it to be done, then return false so the caller can
403
+ // try again.
404
+ for _ in 0..20 {
405
+ sleep(Duration::from_secs(1)).await;
406
+ if !temp_dest.exists() {
407
+ return Ok(false);
408
+ }
409
+ }
410
+ Err(anyhow!(
411
+ "Temp download file at {} not complete after 20 seconds. \
412
+ Make sure another download isn't running for too long and delete the temp file.",
413
+ temp_dest.display()
414
+ ))
415
+ }
416
+ Err(err) => Err(err.into()),
417
+ // If the dest was added since, just remove temp file
418
+ Ok(_) if dest.exists() => {
419
+ std::fs::remove_file(temp_dest)?;
420
+ return Ok(true);
421
+ }
422
+ // Download and extract the binary
423
+ Ok(mut temp_file) => {
424
+ info!("Downloading {} to {}", uri, dest.display());
425
+ download_and_extract(client, uri, file_to_extract, &mut temp_file)
426
+ .await
427
+ .map_err(|err| {
428
+ // Failed to download, just remove file
429
+ if let Err(err) = std::fs::remove_file(temp_dest) {
430
+ warn!(
431
+ "Failed removing temp file at {}: {:?}",
432
+ temp_dest.display(),
433
+ err
434
+ );
435
+ }
436
+ err
437
+ })
438
+ }
439
+ }?;
440
+ // Now that file should be dropped, we can rename
441
+ std::fs::rename(temp_dest, dest)?;
442
+ Ok(true)
443
+ }
444
+
445
+ async fn download_and_extract(
446
+ client: &reqwest::Client,
447
+ uri: &str,
448
+ file_to_extract: &Path,
449
+ dest: &mut std::fs::File,
450
+ ) -> anyhow::Result<()> {
451
+ // Start download. We are using streaming here to extract the file from the
452
+ // tarball or zip instead of loading into memory for Cursor/Seek.
453
+ let resp = client.get(uri).send().await?;
454
+ // We have to map the error type to an io error
455
+ let stream = resp
456
+ .bytes_stream()
457
+ .map(|item| item.map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err)));
458
+
459
+ // Since our tar/zip impls use sync IO, we have to create a bridge and run
460
+ // in a blocking closure.
461
+ let mut reader = SyncIoBridge::new(StreamReader::new(stream));
462
+ let tarball = if uri.ends_with(".tar.gz") {
463
+ true
464
+ } else if uri.ends_with(".zip") {
465
+ false
466
+ } else {
467
+ return Err(anyhow!("URI not .tar.gz or .zip"));
468
+ };
469
+ let file_to_extract = file_to_extract.to_path_buf();
470
+ let mut dest = dest.try_clone()?;
471
+
472
+ spawn_blocking(move || {
473
+ if tarball {
474
+ for entry in tar::Archive::new(GzDecoder::new(reader)).entries()? {
475
+ let mut entry = entry?;
476
+ if entry.path()? == file_to_extract {
477
+ std::io::copy(&mut entry, &mut dest)?;
478
+ return Ok(());
479
+ }
480
+ }
481
+ Err(anyhow!("Unable to find file in tarball"))
482
+ } else {
483
+ loop {
484
+ // This is the way to stream a zip file without creating an archive
485
+ // that requires Seek.
486
+ if let Some(mut file) = read_zipfile_from_stream(&mut reader)? {
487
+ // If this is the file we're expecting, extract it
488
+ if file.enclosed_name() == Some(&file_to_extract) {
489
+ std::io::copy(&mut file, &mut dest)?;
490
+ return Ok(());
491
+ }
492
+ } else {
493
+ return Err(anyhow!("Unable to find file in zip"));
494
+ }
495
+ }
496
+ }
497
+ })
498
+ .await?
499
+ }
@@ -12,6 +12,7 @@ extern crate tracing;
12
12
  extern crate core;
13
13
 
14
14
  mod abstractions;
15
+ pub mod ephemeral_server;
15
16
  mod log_export;
16
17
  mod pollers;
17
18
  mod protosext;
@@ -48,7 +49,7 @@ use crate::{
48
49
  worker::client::WorkerClientBag,
49
50
  };
50
51
  use std::sync::Arc;
51
- use temporal_client::AnyClient;
52
+ use temporal_client::{ConfiguredClient, TemporalServiceClientWithMetrics};
52
53
  use temporal_sdk_core_api::{
53
54
  errors::{CompleteActivityError, PollActivityError, PollWfError},
54
55
  CoreLog, Worker as WorkerTrait,
@@ -58,46 +59,43 @@ use temporal_sdk_core_protos::{coresdk::ActivityHeartbeat, temporal::api::histor
58
59
  lazy_static::lazy_static! {
59
60
  /// A process-wide unique string, which will be different on every startup
60
61
  static ref PROCCESS_UNIQ_ID: String = {
61
- uuid::Uuid::new_v4().to_simple().to_string()
62
+ uuid::Uuid::new_v4().simple().to_string()
62
63
  };
63
64
  }
64
65
 
65
66
  /// Initialize a worker bound to a task queue.
66
67
  ///
67
- /// Lang implementations should pass in a a [temporal_client::ConfiguredClient] directly (or a
68
- /// [RetryClient] wrapping one). When they do so, this function will always overwrite the client
69
- /// retry configuration, force the client to use the namespace defined in the worker config, and set
70
- /// the client identity appropriately. IE: Use [ClientOptions::connect_no_namespace], not
71
- /// [ClientOptions::connect].
72
- ///
73
- /// It is also possible to pass in a [WorkflowClientTrait] implementor, but this largely exists to
74
- /// support testing and mocking. Lang impls should not operate that way, as it may result in
75
- /// improper retry behavior for a worker.
68
+ /// Lang implementations may pass in a [temporal_client::ConfiguredClient] directly (or a
69
+ /// [RetryClient] wrapping one, or a handful of other variants of the same idea). When they do so,
70
+ /// this function will always overwrite the client retry configuration, force the client to use the
71
+ /// namespace defined in the worker config, and set the client identity appropriately. IE: Use
72
+ /// [ClientOptions::connect_no_namespace], not [ClientOptions::connect].
76
73
  pub fn init_worker<CT>(worker_config: WorkerConfig, client: CT) -> Worker
77
74
  where
78
- CT: Into<AnyClient>,
75
+ CT: Into<sealed::AnyClient>,
79
76
  {
80
- let as_enum = client.into();
81
- let client = match as_enum {
82
- AnyClient::HighLevel(ac) => ac,
83
- AnyClient::LowLevel(ll) => {
84
- let mut client = Client::new(*ll, worker_config.namespace.clone());
85
- client.set_worker_build_id(worker_config.worker_build_id.clone());
86
- if let Some(ref id_override) = worker_config.client_identity_override {
87
- client.options_mut().identity = id_override.clone();
88
- }
89
- let retry_client = RetryClient::new(client, Default::default());
90
- Arc::new(retry_client)
77
+ let client = {
78
+ let ll = client.into().into_inner();
79
+ let mut client = Client::new(*ll, worker_config.namespace.clone());
80
+ client.set_worker_build_id(worker_config.worker_build_id.clone());
81
+ if let Some(ref id_override) = worker_config.client_identity_override {
82
+ client.options_mut().identity = id_override.clone();
91
83
  }
84
+ RetryClient::new(client, RetryConfig::default())
92
85
  };
93
86
  if client.namespace() != worker_config.namespace {
94
87
  panic!("Passed in client is not bound to the same namespace as the worker");
95
88
  }
96
- let sticky_q = sticky_q_name_for_worker(&client.get_options().identity, &worker_config);
89
+ let client_ident = client.get_options().identity.clone();
90
+ let sticky_q = sticky_q_name_for_worker(&client_ident, &worker_config);
97
91
  let client_bag = Arc::new(WorkerClientBag::new(
98
- Box::new(client),
92
+ client,
99
93
  worker_config.namespace.clone(),
94
+ client_ident,
95
+ worker_config.worker_build_id.clone(),
96
+ worker_config.use_worker_versioning,
100
97
  ));
98
+
101
99
  let metrics = MetricsContext::top_level(worker_config.namespace.clone())
102
100
  .with_task_q(worker_config.task_queue.clone());
103
101
  Worker::new(worker_config, sticky_q, client_bag, metrics)
@@ -118,7 +116,7 @@ pub fn init_replay_worker(
118
116
  config.max_concurrent_wft_polls = 1;
119
117
  config.no_remote_activities = true;
120
118
  // Could possibly just use mocked pollers here?
121
- let client = mock_client_from_history(history, &config.task_queue);
119
+ let client = mock_client_from_history(history, config.task_queue.clone());
122
120
  let run_id = history.extract_run_id_from_start()?.to_string();
123
121
  let last_event = history.last_event_id();
124
122
  let mut worker = Worker::new(config, None, Arc::new(client), MetricsContext::default());
@@ -139,3 +137,39 @@ pub(crate) fn sticky_q_name_for_worker(
139
137
  None
140
138
  }
141
139
  }
140
+
141
+ mod sealed {
142
+ use super::*;
143
+
144
+ /// Allows passing different kinds of clients into things that want to be flexible. Motivating
145
+ /// use-case was worker initialization.
146
+ ///
147
+ /// Needs to exist in this crate to avoid blanket impl conflicts.
148
+ pub struct AnyClient(Box<ConfiguredClient<TemporalServiceClientWithMetrics>>);
149
+ impl AnyClient {
150
+ pub(crate) fn into_inner(self) -> Box<ConfiguredClient<TemporalServiceClientWithMetrics>> {
151
+ self.0
152
+ }
153
+ }
154
+
155
+ impl From<RetryClient<ConfiguredClient<TemporalServiceClientWithMetrics>>> for AnyClient {
156
+ fn from(c: RetryClient<ConfiguredClient<TemporalServiceClientWithMetrics>>) -> Self {
157
+ Self(Box::new(c.into_inner()))
158
+ }
159
+ }
160
+ impl From<RetryClient<Client>> for AnyClient {
161
+ fn from(c: RetryClient<Client>) -> Self {
162
+ Self(Box::new(c.into_inner().into_inner()))
163
+ }
164
+ }
165
+ impl From<Arc<RetryClient<Client>>> for AnyClient {
166
+ fn from(c: Arc<RetryClient<Client>>) -> Self {
167
+ Self(Box::new(c.get_client().inner().clone()))
168
+ }
169
+ }
170
+ impl From<ConfiguredClient<TemporalServiceClientWithMetrics>> for AnyClient {
171
+ fn from(c: ConfiguredClient<TemporalServiceClientWithMetrics>) -> Self {
172
+ Self(Box::new(c))
173
+ }
174
+ }
175
+ }
@@ -1,6 +1,6 @@
1
1
  use crate::{
2
2
  pollers::{self, Poller},
3
- worker::client::WorkerClientBag,
3
+ worker::client::WorkerClient,
4
4
  };
5
5
  use futures::{prelude::stream::FuturesUnordered, StreamExt};
6
6
  use std::{
@@ -199,7 +199,7 @@ impl Poller<PollWorkflowTaskQueueResponse> for WorkflowTaskPoller {
199
199
 
200
200
  pub type PollWorkflowTaskBuffer = LongPollBuffer<PollWorkflowTaskQueueResponse>;
201
201
  pub(crate) fn new_workflow_task_buffer(
202
- client: Arc<WorkerClientBag>,
202
+ client: Arc<dyn WorkerClient>,
203
203
  task_queue: String,
204
204
  is_sticky: bool,
205
205
  concurrent_pollers: usize,
@@ -220,7 +220,7 @@ pub(crate) fn new_workflow_task_buffer(
220
220
 
221
221
  pub type PollActivityTaskBuffer = LongPollBuffer<PollActivityTaskQueueResponse>;
222
222
  pub(crate) fn new_activity_task_buffer(
223
- client: Arc<WorkerClientBag>,
223
+ client: Arc<dyn WorkerClient>,
224
224
  task_queue: String,
225
225
  concurrent_pollers: usize,
226
226
  buffer_size: usize,
@@ -262,7 +262,7 @@ mod tests {
262
262
  });
263
263
 
264
264
  let pb = new_workflow_task_buffer(
265
- Arc::new(mock_client.into()),
265
+ Arc::new(mock_client),
266
266
  "someq".to_string(),
267
267
  false,
268
268
  1,
@@ -2,7 +2,7 @@
2
2
  //! to replay canned histories. It should be used by Lang SDKs to provide replay capabilities to
3
3
  //! users during testing.
4
4
 
5
- use crate::{worker::client::mocks::mock_manual_workflow_client, WorkerClientBag};
5
+ use crate::worker::client::{mocks::mock_manual_workflow_client, WorkerClient};
6
6
  use futures::FutureExt;
7
7
  use std::{
8
8
  sync::{
@@ -29,7 +29,7 @@ pub use temporal_sdk_core_protos::{
29
29
  pub(crate) fn mock_client_from_history(
30
30
  history: &History,
31
31
  task_queue: impl Into<String>,
32
- ) -> WorkerClientBag {
32
+ ) -> impl WorkerClient {
33
33
  let mut mg = mock_manual_workflow_client();
34
34
 
35
35
  let hist_info = HistoryInfo::new_from_history(history, None).unwrap();
@@ -67,5 +67,5 @@ pub(crate) fn mock_client_from_history(
67
67
  async move { Ok(RespondWorkflowTaskFailedResponse {}) }.boxed()
68
68
  });
69
69
 
70
- WorkerClientBag::new(Box::new(mg), "fake_namespace".to_string())
70
+ mg
71
71
  }