@temporalio/core-bridge 1.12.2 → 1.12.3

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 (57) hide show
  1. package/package.json +3 -3
  2. package/releases/aarch64-apple-darwin/index.node +0 -0
  3. package/releases/aarch64-unknown-linux-gnu/index.node +0 -0
  4. package/releases/x86_64-apple-darwin/index.node +0 -0
  5. package/releases/x86_64-pc-windows-msvc/index.node +0 -0
  6. package/releases/x86_64-unknown-linux-gnu/index.node +0 -0
  7. package/sdk-core/.cargo/config.toml +1 -1
  8. package/sdk-core/client/src/callback_based.rs +123 -0
  9. package/sdk-core/client/src/lib.rs +96 -28
  10. package/sdk-core/client/src/metrics.rs +33 -5
  11. package/sdk-core/client/src/raw.rs +40 -1
  12. package/sdk-core/client/src/retry.rs +12 -3
  13. package/sdk-core/core/src/lib.rs +4 -2
  14. package/sdk-core/core/src/pollers/poll_buffer.rs +62 -14
  15. package/sdk-core/core/src/worker/client.rs +9 -5
  16. package/sdk-core/core/src/worker/heartbeat.rs +3 -1
  17. package/sdk-core/core-api/src/worker.rs +2 -2
  18. package/sdk-core/core-c-bridge/Cargo.toml +2 -0
  19. package/sdk-core/core-c-bridge/include/temporal-sdk-core-c-bridge.h +105 -0
  20. package/sdk-core/core-c-bridge/src/client.rs +265 -8
  21. package/sdk-core/core-c-bridge/src/tests/context.rs +11 -0
  22. package/sdk-core/core-c-bridge/src/tests/mod.rs +179 -3
  23. package/sdk-core/sdk-core-protos/protos/api_cloud_upstream/CODEOWNERS +1 -1
  24. package/sdk-core/sdk-core-protos/protos/api_cloud_upstream/README.md +1 -1
  25. package/sdk-core/sdk-core-protos/protos/api_cloud_upstream/VERSION +1 -1
  26. package/sdk-core/sdk-core-protos/protos/api_cloud_upstream/buf.yaml +1 -0
  27. package/sdk-core/sdk-core-protos/protos/api_cloud_upstream/temporal/api/cloud/cloudservice/v1/request_response.proto +83 -0
  28. package/sdk-core/sdk-core-protos/protos/api_cloud_upstream/temporal/api/cloud/cloudservice/v1/service.proto +37 -0
  29. package/sdk-core/sdk-core-protos/protos/api_cloud_upstream/temporal/api/cloud/connectivityrule/v1/message.proto +64 -0
  30. package/sdk-core/sdk-core-protos/protos/api_cloud_upstream/temporal/api/cloud/identity/v1/message.proto +3 -1
  31. package/sdk-core/sdk-core-protos/protos/api_cloud_upstream/temporal/api/cloud/namespace/v1/message.proto +10 -0
  32. package/sdk-core/sdk-core-protos/protos/api_cloud_upstream/temporal/api/cloud/operation/v1/message.proto +1 -0
  33. package/sdk-core/sdk-core-protos/protos/api_upstream/openapi/openapiv2.json +644 -9
  34. package/sdk-core/sdk-core-protos/protos/api_upstream/openapi/openapiv3.yaml +635 -21
  35. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/batch/v1/message.proto +60 -2
  36. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/common/v1/message.proto +84 -15
  37. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/enums/v1/batch_operation.proto +3 -0
  38. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/enums/v1/task_queue.proto +11 -0
  39. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/history/v1/message.proto +5 -0
  40. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/sdk/v1/task_complete_metadata.proto +1 -1
  41. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/sdk/v1/worker_config.proto +36 -0
  42. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/taskqueue/v1/message.proto +29 -0
  43. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/worker/v1/message.proto +11 -1
  44. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/workflowservice/v1/request_response.proto +122 -4
  45. package/sdk-core/sdk-core-protos/protos/api_upstream/temporal/api/workflowservice/v1/service.proto +41 -0
  46. package/sdk-core/sdk-core-protos/src/lib.rs +5 -1
  47. package/sdk-core/test-utils/Cargo.toml +1 -0
  48. package/sdk-core/test-utils/src/lib.rs +90 -3
  49. package/sdk-core/tests/cloud_tests.rs +11 -74
  50. package/sdk-core/tests/integ_tests/client_tests.rs +14 -10
  51. package/sdk-core/tests/integ_tests/worker_tests.rs +8 -2
  52. package/sdk-core/tests/integ_tests/workflow_tests/activities.rs +13 -0
  53. package/sdk-core/tests/integ_tests/workflow_tests/priority.rs +2 -108
  54. package/sdk-core/tests/main.rs +3 -0
  55. package/sdk-core/tests/shared_tests/mod.rs +43 -0
  56. package/sdk-core/tests/shared_tests/priority.rs +155 -0
  57. package/src/client.rs +5 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@temporalio/core-bridge",
3
- "version": "1.12.2",
3
+ "version": "1.12.3",
4
4
  "description": "Temporal.io SDK Core<>Node bridge",
5
5
  "main": "index.js",
6
6
  "types": "lib/index.d.ts",
@@ -23,7 +23,7 @@
23
23
  "license": "MIT",
24
24
  "dependencies": {
25
25
  "@grpc/grpc-js": "^1.12.4",
26
- "@temporalio/common": "1.12.2",
26
+ "@temporalio/common": "1.12.3",
27
27
  "arg": "^5.0.2",
28
28
  "cargo-cp-artifact": "^0.1.8",
29
29
  "which": "^4.0.0"
@@ -56,5 +56,5 @@
56
56
  "publishConfig": {
57
57
  "access": "public"
58
58
  },
59
- "gitHead": "98393e00b714b8d44a3dc25714d313d3366f4c50"
59
+ "gitHead": "e25f1d5ddaf0b5c755457b1cc1dde7c6e089a63b"
60
60
  }
@@ -1,6 +1,6 @@
1
1
  [env]
2
2
  # This temporarily overrides the version of the CLI used for integration tests, locally and in CI
3
- #CLI_VERSION_OVERRIDE = "v1.3.1-priority.0"
3
+ CLI_VERSION_OVERRIDE = "v1.4.1-cloud-v1-29-0-139-2.0"
4
4
 
5
5
  [alias]
6
6
  integ-test = ["test", "--features", "temporal-sdk-core-protos/serde_serialize", "--package", "temporal-sdk-core", "--test", "integ_runner", "--"]
@@ -0,0 +1,123 @@
1
+ //! This module implements support for callback-based gRPC service that has a callback invoked for
2
+ //! every gRPC call instead of directly using the network.
3
+
4
+ use anyhow::anyhow;
5
+ use bytes::{BufMut, BytesMut};
6
+ use futures_util::future::BoxFuture;
7
+ use futures_util::stream;
8
+ use http::{HeaderMap, Request, Response};
9
+ use http_body_util::{BodyExt, StreamBody, combinators::BoxBody};
10
+ use hyper::body::{Bytes, Frame};
11
+ use std::{
12
+ sync::Arc,
13
+ task::{Context, Poll},
14
+ };
15
+ use tonic::{Status, metadata::GRPC_CONTENT_TYPE};
16
+ use tower::Service;
17
+
18
+ /// gRPC request for use by a callback.
19
+ pub struct GrpcRequest {
20
+ /// Fully qualified gRPC service name.
21
+ pub service: String,
22
+ /// RPC name.
23
+ pub rpc: String,
24
+ /// Request headers.
25
+ pub headers: HeaderMap,
26
+ /// Protobuf bytes of the request.
27
+ pub proto: Bytes,
28
+ }
29
+
30
+ /// Successful gRPC response returned by a callback.
31
+ pub struct GrpcSuccessResponse {
32
+ /// Response headers.
33
+ pub headers: HeaderMap,
34
+
35
+ /// Response proto bytes.
36
+ pub proto: Vec<u8>,
37
+ }
38
+
39
+ /// gRPC service that invokes the given callback on each call.
40
+ #[derive(Clone)]
41
+ pub struct CallbackBasedGrpcService {
42
+ /// Callback to invoke on each RPC call.
43
+ #[allow(clippy::type_complexity)] // Signature is not that complex
44
+ pub callback: Arc<
45
+ dyn Fn(GrpcRequest) -> BoxFuture<'static, Result<GrpcSuccessResponse, Status>>
46
+ + Send
47
+ + Sync,
48
+ >,
49
+ }
50
+
51
+ impl Service<Request<tonic::body::Body>> for CallbackBasedGrpcService {
52
+ type Response = http::Response<tonic::body::Body>;
53
+ type Error = anyhow::Error;
54
+ type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
55
+
56
+ fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
57
+ Poll::Ready(Ok(()))
58
+ }
59
+
60
+ fn call(&mut self, req: Request<tonic::body::Body>) -> Self::Future {
61
+ let callback = self.callback.clone();
62
+
63
+ Box::pin(async move {
64
+ // Build req
65
+ let (parts, body) = req.into_parts();
66
+ let mut path_parts = parts.uri.path().trim_start_matches('/').split('/');
67
+ let req_body = body.collect().await.map_err(|e| anyhow!(e))?.to_bytes();
68
+ // Body is flag saying whether compressed (we do not support that), then 32-bit length,
69
+ // then the actual proto.
70
+ if req_body.len() < 5 {
71
+ return Err(anyhow!("Too few request bytes: {}", req_body.len()));
72
+ } else if req_body[0] != 0 {
73
+ return Err(anyhow!("Compression not supported"));
74
+ }
75
+ let req_proto_len =
76
+ u32::from_be_bytes([req_body[1], req_body[2], req_body[3], req_body[4]]) as usize;
77
+ if req_body.len() < 5 + req_proto_len {
78
+ return Err(anyhow!(
79
+ "Expected request body length at least {}, got {}",
80
+ 5 + req_proto_len,
81
+ req_body.len()
82
+ ));
83
+ }
84
+ let req = GrpcRequest {
85
+ service: path_parts.next().unwrap_or_default().to_owned(),
86
+ rpc: path_parts.next().unwrap_or_default().to_owned(),
87
+ headers: parts.headers,
88
+ proto: req_body.slice(5..5 + req_proto_len),
89
+ };
90
+
91
+ // Invoke and handle response
92
+ match (callback)(req).await {
93
+ Ok(success) => {
94
+ // Create body bytes which requires a flag saying whether compressed, then
95
+ // message len, then actual message. So we create a Bytes for those 5 prepend
96
+ // parts, then stream it alongside the user-provided Vec. This allows us to
97
+ // avoid copying the vec
98
+ let mut body_prepend = BytesMut::with_capacity(5);
99
+ body_prepend.put_u8(0); // 0 means no compression
100
+ body_prepend.put_u32(success.proto.len() as u32);
101
+ let stream = stream::iter(vec![
102
+ Ok::<_, Status>(Frame::data(Bytes::from(body_prepend))),
103
+ Ok::<_, Status>(Frame::data(Bytes::from(success.proto))),
104
+ ]);
105
+ let stream_body = StreamBody::new(stream);
106
+ let full_body = BoxBody::new(stream_body).boxed();
107
+
108
+ // Build response appending headers
109
+ let mut resp_builder = Response::builder()
110
+ .status(200)
111
+ .header(http::header::CONTENT_TYPE, GRPC_CONTENT_TYPE);
112
+ for (key, value) in success.headers.iter() {
113
+ resp_builder = resp_builder.header(key, value);
114
+ }
115
+ Ok(resp_builder
116
+ .body(tonic::body::Body::new(full_body))
117
+ .map_err(|e| anyhow!(e))?)
118
+ }
119
+ Err(status) => Ok(status.into_http()),
120
+ }
121
+ })
122
+ }
123
+ }
@@ -7,6 +7,7 @@
7
7
  #[macro_use]
8
8
  extern crate tracing;
9
9
 
10
+ pub mod callback_based;
10
11
  mod metrics;
11
12
  mod proxy;
12
13
  mod raw;
@@ -35,7 +36,7 @@ pub use workflow_handle::{
35
36
  };
36
37
 
37
38
  use crate::{
38
- metrics::{GrpcMetricSvc, MetricsContext},
39
+ metrics::{ChannelOrGrpcOverride, GrpcMetricSvc, MetricsContext},
39
40
  raw::{AttachMetricLabels, sealed::RawClientLike},
40
41
  sealed::WfHandleClient,
41
42
  workflow_handle::UntypedWorkflowHandle,
@@ -89,6 +90,8 @@ static TEMPORAL_NAMESPACE_HEADER_KEY: &str = "temporal-namespace";
89
90
 
90
91
  /// Key used to communicate when a GRPC message is too large
91
92
  pub static MESSAGE_TOO_LARGE_KEY: &str = "message-too-large";
93
+ /// Key used to indicate a error was returned by the retryer because of the short-circuit predicate
94
+ pub static ERROR_RETURNED_DUE_TO_SHORT_CIRCUIT: &str = "short-circuit";
92
95
 
93
96
  /// The server times out polls after 60 seconds. Set our timeout to be slightly beyond that.
94
97
  const LONG_POLL_TIMEOUT: Duration = Duration::from_secs(70);
@@ -432,34 +435,59 @@ impl ClientOptions {
432
435
  metrics_meter: Option<TemporalMeter>,
433
436
  ) -> Result<RetryClient<ConfiguredClient<TemporalServiceClientWithMetrics>>, ClientInitError>
434
437
  {
435
- let channel = Channel::from_shared(self.target_url.to_string())?;
436
- let channel = self.add_tls_to_channel(channel).await?;
437
- let channel = if let Some(keep_alive) = self.keep_alive.as_ref() {
438
- channel
439
- .keep_alive_while_idle(true)
440
- .http2_keep_alive_interval(keep_alive.interval)
441
- .keep_alive_timeout(keep_alive.timeout)
442
- } else {
443
- channel
444
- };
445
- let channel = if let Some(origin) = self.override_origin.clone() {
446
- channel.origin(origin)
447
- } else {
448
- channel
449
- };
450
- // If there is a proxy, we have to connect that way
451
- let channel = if let Some(proxy) = self.http_connect_proxy.as_ref() {
452
- proxy.connect_endpoint(&channel).await?
453
- } else {
454
- channel.connect().await?
455
- };
456
- let service = ServiceBuilder::new()
457
- .layer_fn(move |channel| GrpcMetricSvc {
458
- inner: channel,
438
+ self.connect_no_namespace_with_service_override(metrics_meter, None)
439
+ .await
440
+ }
441
+
442
+ /// Attempt to establish a connection to the Temporal server and return a gRPC client which is
443
+ /// intercepted with retry, default headers functionality, and metrics if provided. If a
444
+ /// service_override is present, network-specific options are ignored and the callback is
445
+ /// invoked for each gRPC call.
446
+ ///
447
+ /// See [RetryClient] for more
448
+ pub async fn connect_no_namespace_with_service_override(
449
+ &self,
450
+ metrics_meter: Option<TemporalMeter>,
451
+ service_override: Option<callback_based::CallbackBasedGrpcService>,
452
+ ) -> Result<RetryClient<ConfiguredClient<TemporalServiceClientWithMetrics>>, ClientInitError>
453
+ {
454
+ let service = if let Some(service_override) = service_override {
455
+ GrpcMetricSvc {
456
+ inner: ChannelOrGrpcOverride::GrpcOverride(service_override),
459
457
  metrics: metrics_meter.clone().map(MetricsContext::new),
460
458
  disable_errcode_label: self.disable_error_code_metric_tags,
461
- })
462
- .service(channel);
459
+ }
460
+ } else {
461
+ let channel = Channel::from_shared(self.target_url.to_string())?;
462
+ let channel = self.add_tls_to_channel(channel).await?;
463
+ let channel = if let Some(keep_alive) = self.keep_alive.as_ref() {
464
+ channel
465
+ .keep_alive_while_idle(true)
466
+ .http2_keep_alive_interval(keep_alive.interval)
467
+ .keep_alive_timeout(keep_alive.timeout)
468
+ } else {
469
+ channel
470
+ };
471
+ let channel = if let Some(origin) = self.override_origin.clone() {
472
+ channel.origin(origin)
473
+ } else {
474
+ channel
475
+ };
476
+ // If there is a proxy, we have to connect that way
477
+ let channel = if let Some(proxy) = self.http_connect_proxy.as_ref() {
478
+ proxy.connect_endpoint(&channel).await?
479
+ } else {
480
+ channel.connect().await?
481
+ };
482
+ ServiceBuilder::new()
483
+ .layer_fn(move |channel| GrpcMetricSvc {
484
+ inner: ChannelOrGrpcOverride::Channel(channel),
485
+ metrics: metrics_meter.clone().map(MetricsContext::new),
486
+ disable_errcode_label: self.disable_error_code_metric_tags,
487
+ })
488
+ .service(channel)
489
+ };
490
+
463
491
  let headers = Arc::new(RwLock::new(ClientHeaders {
464
492
  user_headers: self.headers.clone().unwrap_or_default(),
465
493
  api_key: self.api_key.clone(),
@@ -1140,7 +1168,7 @@ pub struct WorkflowOptions {
1140
1168
  /// The overall semantics of Priority are:
1141
1169
  /// (more will be added here later)
1142
1170
  /// 1. First, consider "priority_key": lower number goes first.
1143
- #[derive(Debug, Clone, Default, PartialEq, Eq)]
1171
+ #[derive(Debug, Clone, Default, PartialEq)]
1144
1172
  pub struct Priority {
1145
1173
  /// Priority key is a positive integer from 1 to n, where smaller integers
1146
1174
  /// correspond to higher priorities (tasks run sooner). In general, tasks in
@@ -1153,12 +1181,50 @@ pub struct Priority {
1153
1181
  /// The default priority is (min+max)/2. With the default max of 5 and min of
1154
1182
  /// 1, that comes out to 3.
1155
1183
  pub priority_key: u32,
1184
+
1185
+ /// Fairness key is a short string that's used as a key for a fairness
1186
+ /// balancing mechanism. It may correspond to a tenant id, or to a fixed
1187
+ /// string like "high" or "low". The default is the empty string.
1188
+ ///
1189
+ /// The fairness mechanism attempts to dispatch tasks for a given key in
1190
+ /// proportion to its weight. For example, using a thousand distinct tenant
1191
+ /// ids, each with a weight of 1.0 (the default) will result in each tenant
1192
+ /// getting a roughly equal share of task dispatch throughput.
1193
+ ///
1194
+ /// (Note: this does not imply equal share of worker capacity! Fairness
1195
+ /// decisions are made based on queue statistics, not
1196
+ /// current worker load.)
1197
+ ///
1198
+ /// As another example, using keys "high" and "low" with weight 9.0 and 1.0
1199
+ /// respectively will prefer dispatching "high" tasks over "low" tasks at a
1200
+ /// 9:1 ratio, while allowing either key to use all worker capacity if the
1201
+ /// other is not present.
1202
+ ///
1203
+ /// All fairness mechanisms, including rate limits, are best-effort and
1204
+ /// probabilistic. The results may not match what a "perfect" algorithm with
1205
+ /// infinite resources would produce. The more unique keys are used, the less
1206
+ /// accurate the results will be.
1207
+ ///
1208
+ /// Fairness keys are limited to 64 bytes.
1209
+ pub fairness_key: String,
1210
+
1211
+ /// Fairness weight for a task can come from multiple sources for
1212
+ /// flexibility. From highest to lowest precedence:
1213
+ /// 1. Weights for a small set of keys can be overridden in task queue
1214
+ /// configuration with an API.
1215
+ /// 2. It can be attached to the workflow/activity in this field.
1216
+ /// 3. The default weight of 1.0 will be used.
1217
+ ///
1218
+ /// Weight values are clamped by the server to the range [0.001, 1000].
1219
+ pub fairness_weight: f32,
1156
1220
  }
1157
1221
 
1158
1222
  impl From<Priority> for common::v1::Priority {
1159
1223
  fn from(priority: Priority) -> Self {
1160
1224
  common::v1::Priority {
1161
1225
  priority_key: priority.priority_key as i32,
1226
+ fairness_key: priority.fairness_key,
1227
+ fairness_weight: priority.fairness_weight,
1162
1228
  }
1163
1229
  }
1164
1230
  }
@@ -1167,6 +1233,8 @@ impl From<common::v1::Priority> for Priority {
1167
1233
  fn from(priority: common::v1::Priority) -> Self {
1168
1234
  Self {
1169
1235
  priority_key: priority.priority_key as u32,
1236
+ fairness_key: priority.fairness_key,
1237
+ fairness_weight: priority.fairness_weight,
1170
1238
  }
1171
1239
  }
1172
1240
  }
@@ -1,6 +1,9 @@
1
- use crate::{AttachMetricLabels, CallType, dbg_panic};
1
+ use crate::{AttachMetricLabels, CallType, callback_based, dbg_panic};
2
+ use futures_util::TryFutureExt;
3
+ use futures_util::future::Either;
2
4
  use futures_util::{FutureExt, future::BoxFuture};
3
5
  use std::{
6
+ fmt,
4
7
  sync::Arc,
5
8
  task::{Context, Poll},
6
9
  time::{Duration, Instant},
@@ -205,19 +208,37 @@ fn code_as_screaming_snake(code: &Code) -> &'static str {
205
208
  /// Implements metrics functionality for gRPC (really, any http) calls
206
209
  #[derive(Debug, Clone)]
207
210
  pub struct GrpcMetricSvc {
208
- pub(crate) inner: Channel,
211
+ pub(crate) inner: ChannelOrGrpcOverride,
209
212
  // If set to none, metrics are a no-op
210
213
  pub(crate) metrics: Option<MetricsContext>,
211
214
  pub(crate) disable_errcode_label: bool,
212
215
  }
213
216
 
217
+ #[derive(Clone)]
218
+ pub(crate) enum ChannelOrGrpcOverride {
219
+ Channel(Channel),
220
+ GrpcOverride(callback_based::CallbackBasedGrpcService),
221
+ }
222
+
223
+ impl fmt::Debug for ChannelOrGrpcOverride {
224
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225
+ match self {
226
+ ChannelOrGrpcOverride::Channel(inner) => fmt::Debug::fmt(inner, f),
227
+ ChannelOrGrpcOverride::GrpcOverride(_) => f.write_str("<callback-based-grpc-service>"),
228
+ }
229
+ }
230
+ }
231
+
214
232
  impl Service<http::Request<Body>> for GrpcMetricSvc {
215
233
  type Response = http::Response<Body>;
216
- type Error = tonic::transport::Error;
234
+ type Error = Box<dyn std::error::Error + Send + Sync>;
217
235
  type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;
218
236
 
219
237
  fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
220
- self.inner.poll_ready(cx).map_err(Into::into)
238
+ match &mut self.inner {
239
+ ChannelOrGrpcOverride::Channel(inner) => inner.poll_ready(cx).map_err(Into::into),
240
+ ChannelOrGrpcOverride::GrpcOverride(inner) => inner.poll_ready(cx).map_err(Into::into),
241
+ }
221
242
  }
222
243
 
223
244
  fn call(&mut self, mut req: http::Request<Body>) -> Self::Future {
@@ -245,7 +266,14 @@ impl Service<http::Request<Body>> for GrpcMetricSvc {
245
266
  metrics
246
267
  })
247
268
  });
248
- let callfut = self.inner.call(req);
269
+ let callfut = match &mut self.inner {
270
+ ChannelOrGrpcOverride::Channel(inner) => {
271
+ Either::Left(inner.call(req).map_err(Into::into))
272
+ }
273
+ ChannelOrGrpcOverride::GrpcOverride(inner) => {
274
+ Either::Right(inner.call(req).map_err(Into::into))
275
+ }
276
+ };
249
277
  let errcode_label_disabled = self.disable_errcode_label;
250
278
  async move {
251
279
  let started = Instant::now();
@@ -1354,6 +1354,34 @@ proxier! {
1354
1354
  r.extensions_mut().insert(labels);
1355
1355
  }
1356
1356
  );
1357
+ (
1358
+ update_task_queue_config,
1359
+ UpdateTaskQueueConfigRequest,
1360
+ UpdateTaskQueueConfigResponse,
1361
+ |r| {
1362
+ let mut labels = namespaced_request!(r);
1363
+ labels.task_q_str(r.get_ref().task_queue.clone());
1364
+ r.extensions_mut().insert(labels);
1365
+ }
1366
+ );
1367
+ (
1368
+ fetch_worker_config,
1369
+ FetchWorkerConfigRequest,
1370
+ FetchWorkerConfigResponse,
1371
+ |r| {
1372
+ let labels = namespaced_request!(r);
1373
+ r.extensions_mut().insert(labels);
1374
+ }
1375
+ );
1376
+ (
1377
+ update_worker_config,
1378
+ UpdateWorkerConfigRequest,
1379
+ UpdateWorkerConfigResponse,
1380
+ |r| {
1381
+ let labels = namespaced_request!(r);
1382
+ r.extensions_mut().insert(labels);
1383
+ }
1384
+ );
1357
1385
  }
1358
1386
 
1359
1387
  proxier! {
@@ -1445,6 +1473,11 @@ proxier! {
1445
1473
  (update_namespace_export_sink, cloudreq::UpdateNamespaceExportSinkRequest, cloudreq::UpdateNamespaceExportSinkResponse);
1446
1474
  (delete_namespace_export_sink, cloudreq::DeleteNamespaceExportSinkRequest, cloudreq::DeleteNamespaceExportSinkResponse);
1447
1475
  (validate_namespace_export_sink, cloudreq::ValidateNamespaceExportSinkRequest, cloudreq::ValidateNamespaceExportSinkResponse);
1476
+ (update_namespace_tags, cloudreq::UpdateNamespaceTagsRequest, cloudreq::UpdateNamespaceTagsResponse);
1477
+ (create_connectivity_rule, cloudreq::CreateConnectivityRuleRequest, cloudreq::CreateConnectivityRuleResponse);
1478
+ (get_connectivity_rule, cloudreq::GetConnectivityRuleRequest, cloudreq::GetConnectivityRuleResponse);
1479
+ (get_connectivity_rules, cloudreq::GetConnectivityRulesRequest, cloudreq::GetConnectivityRulesResponse);
1480
+ (delete_connectivity_rule, cloudreq::DeleteConnectivityRuleRequest, cloudreq::DeleteConnectivityRuleResponse);
1448
1481
  }
1449
1482
 
1450
1483
  proxier! {
@@ -1538,11 +1571,17 @@ mod tests {
1538
1571
  })
1539
1572
  .collect();
1540
1573
  let no_underscores: HashSet<_> = impl_list.iter().map(|x| x.replace('_', "")).collect();
1574
+ let mut not_implemented = vec![];
1541
1575
  for method in methods {
1542
1576
  if !no_underscores.contains(&method.to_lowercase()) {
1543
- panic!("RPC method {method} is not implemented by raw client")
1577
+ not_implemented.push(method);
1544
1578
  }
1545
1579
  }
1580
+ if !not_implemented.is_empty() {
1581
+ panic!(
1582
+ "The following RPC methods are not implemented by raw client: {not_implemented:?}"
1583
+ );
1584
+ }
1546
1585
  }
1547
1586
  #[test]
1548
1587
  fn verify_all_workflow_service_methods_implemented() {
@@ -1,6 +1,6 @@
1
1
  use crate::{
2
- Client, IsWorkerTaskLongPoll, MESSAGE_TOO_LARGE_KEY, NamespacedClient, NoRetryOnMatching,
3
- Result, RetryConfig, raw::IsUserLongPoll,
2
+ Client, ERROR_RETURNED_DUE_TO_SHORT_CIRCUIT, IsWorkerTaskLongPoll, MESSAGE_TOO_LARGE_KEY,
3
+ NamespacedClient, NoRetryOnMatching, Result, RetryConfig, raw::IsUserLongPoll,
4
4
  };
5
5
  use backoff::{Clock, SystemClock, backoff::Backoff, exponential::ExponentialBackoff};
6
6
  use futures_retry::{ErrorHandler, FutureRetry, RetryPolicy};
@@ -214,6 +214,10 @@ where
214
214
  if let Some(sc) = self.retry_short_circuit.as_ref()
215
215
  && (sc.predicate)(&e)
216
216
  {
217
+ e.metadata_mut().insert(
218
+ ERROR_RETURNED_DUE_TO_SHORT_CIRCUIT,
219
+ tonic::metadata::MetadataValue::from(0),
220
+ );
217
221
  return RetryPolicy::ForwardError(e);
218
222
  }
219
223
 
@@ -441,7 +445,12 @@ mod tests {
441
445
  FixedClock(Instant::now()),
442
446
  );
443
447
  let result = err_handler.handle(1, Status::new(Code::ResourceExhausted, "leave me alone"));
444
- assert_matches!(result, RetryPolicy::ForwardError(_))
448
+ let e = assert_matches!(result, RetryPolicy::ForwardError(e) => e);
449
+ assert!(
450
+ e.metadata()
451
+ .get(ERROR_RETURNED_DUE_TO_SHORT_CIRCUIT)
452
+ .is_some()
453
+ );
445
454
  }
446
455
 
447
456
  #[tokio::test]
@@ -103,7 +103,9 @@ where
103
103
  bail!("Client identity cannot be empty. Either lang or user should be setting this value");
104
104
  }
105
105
 
106
- let heartbeat_fn = Arc::new(OnceLock::new());
106
+ let heartbeat_fn = worker_config
107
+ .heartbeat_interval
108
+ .map(|_| Arc::new(OnceLock::new()));
107
109
 
108
110
  let client_bag = Arc::new(WorkerClientBag::new(
109
111
  client,
@@ -118,7 +120,7 @@ where
118
120
  sticky_q,
119
121
  client_bag,
120
122
  Some(&runtime.telemetry),
121
- Some(heartbeat_fn),
123
+ heartbeat_fn,
122
124
  ))
123
125
  }
124
126
 
@@ -18,7 +18,7 @@ use std::{
18
18
  },
19
19
  time::Duration,
20
20
  };
21
- use temporal_client::NoRetryOnMatching;
21
+ use temporal_client::{ERROR_RETURNED_DUE_TO_SHORT_CIRCUIT, NoRetryOnMatching};
22
22
  use temporal_sdk_core_api::worker::{
23
23
  ActivitySlotKind, NexusSlotKind, PollerBehavior, SlotKind, WorkflowSlotKind,
24
24
  };
@@ -538,20 +538,27 @@ impl PollScalerReportHandle {
538
538
  }
539
539
  }
540
540
  Err(e) => {
541
- // We should only see (and react to) errors in autoscaling mode
542
- if matches!(self.behavior, PollerBehavior::Autoscaling { .. })
543
- && self.ever_saw_scaling_decision.load(Ordering::Relaxed)
544
- {
545
- debug!("Got error from server while polling: {:?}", e);
546
- if e.code() == Code::ResourceExhausted {
547
- // Scale down significantly for resource exhaustion
548
- self.change_target(usize::saturating_div, 2);
549
- } else {
550
- // Other codes that would normally have made us back off briefly can
551
- // reclaim this poller
552
- self.change_target(usize::saturating_sub, 1);
541
+ if matches!(self.behavior, PollerBehavior::Autoscaling { .. }) {
542
+ // We should only react to errors in autoscaling mode if we saw a scaling
543
+ // decision
544
+ if self.ever_saw_scaling_decision.load(Ordering::Relaxed) {
545
+ debug!("Got error from server while polling: {:?}", e);
546
+ if e.code() == Code::ResourceExhausted {
547
+ // Scale down significantly for resource exhaustion
548
+ self.change_target(usize::saturating_div, 2);
549
+ } else {
550
+ // Other codes that would normally have made us back off briefly can
551
+ // reclaim this poller
552
+ self.change_target(usize::saturating_sub, 1);
553
+ }
553
554
  }
554
- return false;
555
+ // Only propagate errors out if they weren't because of the short-circuiting
556
+ // logic. IE: We don't want to fail callers because we said we wanted to know
557
+ // about ResourceExhausted errors, but we haven't seen a scaling decision yet,
558
+ // so we're not reacting to errors, only propagating them.
559
+ return !e
560
+ .metadata()
561
+ .contains_key(ERROR_RETURNED_DUE_TO_SHORT_CIRCUIT);
555
562
  }
556
563
  }
557
564
  }
@@ -748,4 +755,45 @@ mod tests {
748
755
  pb.poll().await.unwrap().unwrap();
749
756
  pb.shutdown().await;
750
757
  }
758
+
759
+ #[tokio::test]
760
+ async fn autoscale_wont_fail_caller_on_short_circuited_error() {
761
+ let mut mock_client = mock_manual_worker_client();
762
+ mock_client
763
+ .expect_poll_workflow_task()
764
+ .times(1)
765
+ .returning(move |_, _| {
766
+ async {
767
+ let mut st = tonic::Status::cancelled("whatever");
768
+ st.metadata_mut()
769
+ .insert(ERROR_RETURNED_DUE_TO_SHORT_CIRCUIT, 1.into());
770
+ Err(st)
771
+ }
772
+ .boxed()
773
+ });
774
+ mock_client
775
+ .expect_poll_workflow_task()
776
+ .returning(move |_, _| async { Ok(Default::default()) }.boxed());
777
+
778
+ let pb = LongPollBuffer::new_workflow_task(
779
+ Arc::new(mock_client),
780
+ "sometq".to_string(),
781
+ None,
782
+ PollerBehavior::Autoscaling {
783
+ minimum: 1,
784
+ maximum: 1,
785
+ initial: 1,
786
+ },
787
+ fixed_size_permit_dealer(1),
788
+ CancellationToken::new(),
789
+ None::<fn(usize)>,
790
+ WorkflowTaskOptions {
791
+ wft_poller_shared: Some(Arc::new(WFTPollerShared::new(Some(1)))),
792
+ },
793
+ );
794
+
795
+ // Should not see error, unwraps should get empty response
796
+ pb.poll().await.unwrap().unwrap();
797
+ pb.shutdown().await;
798
+ }
751
799
  }
@@ -50,7 +50,7 @@ pub(crate) struct WorkerClientBag {
50
50
  namespace: String,
51
51
  identity: String,
52
52
  worker_versioning_strategy: WorkerVersioningStrategy,
53
- heartbeat_data: Arc<OnceLock<HeartbeatFn>>,
53
+ heartbeat_data: Option<Arc<OnceLock<HeartbeatFn>>>,
54
54
  }
55
55
 
56
56
  impl WorkerClientBag {
@@ -59,7 +59,7 @@ impl WorkerClientBag {
59
59
  namespace: String,
60
60
  identity: String,
61
61
  worker_versioning_strategy: WorkerVersioningStrategy,
62
- heartbeat_data: Arc<OnceLock<HeartbeatFn>>,
62
+ heartbeat_data: Option<Arc<OnceLock<HeartbeatFn>>>,
63
63
  ) -> Self {
64
64
  Self {
65
65
  replaceable_client: RwLock::new(client),
@@ -129,10 +129,14 @@ impl WorkerClientBag {
129
129
  }
130
130
 
131
131
  fn capture_heartbeat(&self) -> Option<WorkerHeartbeat> {
132
- if let Some(hb) = self.heartbeat_data.get() {
133
- hb()
132
+ if let Some(heartbeat_data) = self.heartbeat_data.as_ref() {
133
+ if let Some(hb) = heartbeat_data.get() {
134
+ hb()
135
+ } else {
136
+ dbg_panic!("Heartbeat function never set");
137
+ None
138
+ }
134
139
  } else {
135
- dbg_panic!("Heartbeat function never set");
136
140
  None
137
141
  }
138
142
  }