1#![warn(missing_docs)] #[macro_use]
8extern crate tracing;
9
10mod activity;
11mod async_activity_handle;
12pub mod callback_based;
13mod dns;
14#[cfg(feature = "envconfig")]
16pub mod envconfig;
17pub mod errors;
18pub mod grpc;
19pub mod interceptors;
21mod metrics;
22mod options_structs;
23pub mod plugins;
25#[doc(hidden)]
27pub mod proxy;
28mod replaceable;
29pub mod request_extensions;
30mod retry;
31mod rpc_options;
32pub mod schedules;
34#[cfg(test)]
35mod test_helpers;
36pub mod worker;
37mod workflow_handle;
38mod workflow_status;
39
40pub use crate::{
41 proxy::HttpConnectProxyOptions,
42 request_extensions::PayloadErrorLimits,
43 retry::{CallType, RETRYABLE_ERROR_CODES},
44};
45pub use activity::*;
46pub use async_activity_handle::{
47 ActivityHeartbeatResponse, ActivityIdentifier, AsyncActivityHandle,
48};
49#[doc(hidden)]
50pub use retry::jittered;
51
52pub use interceptors::{
53 BackfillScheduleInput, CancelWorkflowInput, ClientInterceptor, CompleteAsyncActivityInput,
54 CountWorkflowsInput, CountWorkflowsOutput, CreateScheduleInput, CreateScheduleOutput,
55 DeleteScheduleInput, DescribeScheduleInput, DescribeScheduleOutput, DescribeWorkflowInput,
56 DescribeWorkflowOutput, FailAsyncActivityInput, FetchWorkflowHistoryPageInput,
57 FetchWorkflowHistoryPageOutput, HasArgs, HeartbeatAsyncActivityInput, ListSchedulesPageInput,
58 ListSchedulesPageOutput, ListWorkflowsPageInput, ListWorkflowsPageOutput, Next,
59 PauseScheduleInput, PollWorkflowUpdateInput, PollWorkflowUpdateOutput, QueryWorkflowInput,
60 QueryWorkflowOutput, ReportAsyncActivityCancellationInput, SendScheduleUpdateInput,
61 SignalWorkflowInput, StartWorkflowInput, StartWorkflowOutput, StartWorkflowUpdateInput,
62 StartWorkflowUpdateOutput, TemporalClientValue, TerminateWorkflowInput, TriggerScheduleInput,
63 UnpauseScheduleInput, UpdateScheduleInput,
64};
65pub use metrics::{LONG_REQUEST_LATENCY_HISTOGRAM_NAME, REQUEST_LATENCY_HISTOGRAM_NAME};
66pub use options_structs::*;
67pub use plugins::{
68 ClientPlugin, ErasedClientPlugin, PluginApplyError, PluginError, PluginTarget, WorkerPluginData,
69};
70pub use replaceable::SharedReplaceableClient;
71pub use retry::RetryOptions;
72pub use rpc_options::{RpcMetadata, RpcMetadataError, RpcOptions};
73pub use temporalio_common::{Memo, RetryPolicy};
74pub use url::Url;
75pub mod danger {
77 pub use tokio_rustls::rustls::client::danger::ServerCertVerifier;
81}
82#[cfg(feature = "dynamic-tls")]
83pub use tokio_rustls::rustls::SignatureScheme;
86#[cfg(feature = "dynamic-tls")]
87pub use tokio_rustls::rustls::client::ResolvesClientCert;
96#[cfg(feature = "dynamic-tls")]
97pub use tokio_rustls::rustls::sign::CertifiedKey;
100pub use tonic;
101pub use workflow_handle::{
102 UntypedQuery, UntypedSignal, UntypedUpdate, UntypedWorkflow, UntypedWorkflowHandle,
103 WorkflowExecutionDescription, WorkflowExecutionInfo, WorkflowExecutionResult, WorkflowHandle,
104 WorkflowHistory, WorkflowHistoryJsonError, WorkflowResultDetails, WorkflowUpdateHandle,
105};
106pub use workflow_status::WorkflowExecutionStatus;
107
108use crate::{
109 grpc::{
110 AttachMetricLabels, CloudService, HealthService, OperatorService, TestService,
111 WorkflowService,
112 },
113 metrics::{ChannelOrGrpcOverride, GrpcMetricSvc, MetricsContext},
114 request_extensions::RequestExt,
115 worker::ClientWorkerSet,
116};
117use errors::*;
118use futures_util::{future::BoxFuture, stream, stream::Stream};
119use http::Uri;
120use parking_lot::RwLock;
121use std::{
122 collections::{HashMap, VecDeque},
123 error::Error,
124 fmt::Debug,
125 pin::Pin,
126 str::FromStr,
127 sync::{Arc, OnceLock},
128 task::{Context, Poll},
129 time::{Duration, SystemTime},
130};
131use temporalio_common::{
132 ActivityDefinition, HasWorkflowDefinition, UntypedActivity,
133 data_converters::{
134 DataConverter, GenericPayloadConverter, PayloadConverter, SerializationContext,
135 SerializationContextData,
136 },
137 payload_visitor::decode_payloads,
138 protos::{
139 coresdk::IntoPayloadsExt,
140 grpc::health::v1::health_client::HealthClient,
141 proto_ts_to_system_time,
142 temporal::api::{
143 cloud::cloudservice::v1::cloud_service_client::CloudServiceClient,
144 common::v1::{ActivityType, WorkflowType},
145 enums::v1::{
146 ActivityIdConflictPolicy as ProtoActivityIdConflictPolicy,
147 ActivityIdReusePolicy as ProtoActivityIdReusePolicy, TaskQueueKind,
148 },
149 errordetails::v1::WorkflowExecutionAlreadyStartedFailure,
150 operatorservice::v1::operator_service_client::OperatorServiceClient,
151 sdk::v1::UserMetadata,
152 taskqueue::v1::TaskQueue,
153 testservice::v1::test_service_client::TestServiceClient,
154 workflow::v1 as workflow,
155 workflowservice::v1::{
156 count_workflow_executions_response, workflow_service_client::WorkflowServiceClient,
157 *,
158 },
159 },
160 utilities::decode_status_detail,
161 },
162 search_attributes::{SearchAttributeError, SearchAttributeValue, SearchAttributes},
163};
164use tonic::{
165 Code, IntoRequest,
166 body::Body,
167 client::GrpcService,
168 codec::CompressionEncoding,
169 codegen::InterceptedService,
170 metadata::{
171 AsciiMetadataKey, AsciiMetadataValue, BinaryMetadataKey, BinaryMetadataValue, MetadataMap,
172 MetadataValue,
173 },
174 service::Interceptor,
175 transport::{Certificate, Endpoint, Identity},
176};
177use tower::ServiceBuilder;
178use uuid::Uuid;
179
180static CLIENT_NAME_HEADER_KEY: &str = "client-name";
181static CLIENT_VERSION_HEADER_KEY: &str = "client-version";
182static TEMPORAL_NAMESPACE_HEADER_KEY: &str = "temporal-namespace";
183
184#[doc(hidden)]
185pub static MESSAGE_TOO_LARGE_KEY: &str = "message-too-large";
187#[doc(hidden)]
188pub fn payload_limit_violation_from(
191 status: &tonic::Status,
192) -> Option<&temporalio_common::payload_limits::PayloadLimitViolation> {
193 std::error::Error::source(status).and_then(|src| src.downcast_ref())
194}
195#[doc(hidden)]
196pub static ERROR_RETURNED_DUE_TO_SHORT_CIRCUIT: &str = "short-circuit";
198
199const LONG_POLL_TIMEOUT: Duration = Duration::from_secs(70);
201const OTHER_CALL_TIMEOUT: Duration = Duration::from_secs(30);
202const VERSION: &str = env!("CARGO_PKG_VERSION");
203
204#[derive(Clone, Debug)]
209pub struct Connection {
210 inner: Arc<ConnectionInner>,
211}
212
213#[derive(Clone, derive_more::Debug)]
214struct ConnectionInner {
215 #[debug(skip)]
216 service: TemporalServiceClient,
217 retry_options: RetryOptions,
218 identity: String,
219 headers: Arc<RwLock<ClientHeaders>>,
220 client_name: String,
221 client_version: String,
222 capabilities: Option<get_system_info_response::Capabilities>,
224 workers: Arc<ClientWorkerSet>,
225 _dns_task: Option<Arc<dns::DnsReresolutionHandle>>,
226 payloads_warn_size: usize,
228 memo_warn_size: usize,
229}
230
231fn resolve_warn_threshold(option: &'static str, bytes: u64) -> usize {
236 usize::try_from(bytes).unwrap_or_else(|_| {
237 warn!(
238 option,
239 configured_bytes = bytes,
240 "Configured payload size warning threshold exceeds the maximum addressable size on this \
241 platform; disabling this warning"
242 );
243 0
244 })
245}
246
247impl Connection {
248 pub async fn connect(mut options: ConnectionOptions) -> Result<Self, ClientConnectError> {
250 if options.service_override.is_some() {
251 options.grpc_compression = GrpcCompression::None;
252 }
253
254 let first_result = Self::connect_once(&options).await;
255 if options.grpc_compression == GrpcCompression::Gzip
256 && let Err(ClientConnectError::SystemInfoCallError(status)) = &first_result
257 && status.code() == Code::Unimplemented
258 && {
259 let msg = status.message().to_lowercase();
260 msg.contains("decompress")
261 || msg.contains("grpc-encoding")
262 || msg.contains("compressor")
263 }
264 {
265 options.grpc_compression = GrpcCompression::None;
266 return Self::connect_once(&options).await;
267 }
268 first_result
269 }
270
271 async fn connect_once(options: &ConnectionOptions) -> Result<Self, ClientConnectError> {
272 let dns_lb_opts = dns::validate_and_get_dns_lb(options)?.cloned();
273 let (service, dns_task) = if let Some(service_override) = options.service_override.clone() {
274 (
275 GrpcMetricSvc {
276 inner: ChannelOrGrpcOverride::GrpcOverride(service_override),
277 metrics: options.metrics_meter.clone().map(MetricsContext::new),
278 disable_errcode_label: options.disable_error_code_metric_tags,
279 },
280 None,
281 )
282 } else if let Some(dns_opts) = &dns_lb_opts {
283 let (channel, sender) = dns::create_balanced_channel(options).await?;
284 let handle = dns::spawn_dns_reresolution(
285 sender,
286 options.target.clone(),
287 options.tls_options.clone(),
288 options.keep_alive.clone(),
289 options.override_origin.clone(),
290 dns_opts.resolution_interval,
291 options.connect_timeout,
292 );
293 (
294 ServiceBuilder::new()
295 .layer_fn(move |channel| GrpcMetricSvc {
296 inner: ChannelOrGrpcOverride::Channel(channel),
297 metrics: options.metrics_meter.clone().map(MetricsContext::new),
298 disable_errcode_label: options.disable_error_code_metric_tags,
299 })
300 .service(channel),
301 Some(handle),
302 )
303 } else {
304 let endpoint = Endpoint::from_shared(options.target.to_string())?;
305 let endpoint = if let Some(timeout) = options.connect_timeout {
306 endpoint.connect_timeout(timeout)
307 } else {
308 endpoint
309 };
310 let tls_result = add_tls_to_channel(options.tls_options.as_ref(), endpoint).await?;
311
312 #[cfg(feature = "dynamic-tls")]
313 let (channel, custom_connector_info) = match tls_result {
314 TlsConfigResult::Standard(ep) => (
315 ep,
316 None::<(Arc<tokio_rustls::rustls::ClientConfig>, String)>,
317 ),
318 TlsConfigResult::CustomConnector {
319 endpoint: ep,
320 rustls_config,
321 domain,
322 } => (ep, Some((rustls_config, domain))),
323 };
324 #[cfg(not(feature = "dynamic-tls"))]
325 let channel = match tls_result {
326 TlsConfigResult::Standard(ep) => ep,
327 };
328
329 let channel = if let Some(keep_alive) = options.keep_alive.as_ref() {
330 channel
331 .keep_alive_while_idle(true)
332 .http2_keep_alive_interval(keep_alive.interval)
333 .keep_alive_timeout(keep_alive.timeout)
334 } else {
335 channel
336 };
337 let channel = if let Some(origin) = options.override_origin.clone() {
338 channel.origin(origin)
339 } else {
340 channel
341 };
342 #[cfg(feature = "dynamic-tls")]
344 if options.http_connect_proxy.is_some() && custom_connector_info.is_some() {
345 return Err(ClientConnectError::InvalidConfig(
346 "client_cert_resolver is not yet supported with http_connect_proxy. \
347 Use static client_tls_options when using a proxy, or remove the proxy."
348 .to_owned(),
349 ));
350 }
351 let channel = if let Some(proxy) = options.http_connect_proxy.as_ref() {
353 proxy.connect_endpoint(&channel).await?
354 } else {
355 #[cfg(feature = "dynamic-tls")]
356 if let Some((rustls_config, domain)) = custom_connector_info {
357 let server_name =
358 tokio_rustls::rustls::pki_types::ServerName::try_from(domain.as_str())
359 .map_err(|e| {
360 ClientConnectError::InvalidConfig(format!(
361 "Invalid TLS domain name '{domain}': {e}"
362 ))
363 })?
364 .to_owned();
365 let connector = DynamicTlsConnector {
366 tls: tokio_rustls::TlsConnector::from(rustls_config),
367 domain: Arc::new(server_name),
368 };
369 channel.connect_with_connector(connector).await?
370 } else {
371 channel.connect().await?
372 }
373 #[cfg(not(feature = "dynamic-tls"))]
374 channel.connect().await?
375 };
376 (
377 ServiceBuilder::new()
378 .layer_fn(move |channel| GrpcMetricSvc {
379 inner: ChannelOrGrpcOverride::Channel(channel),
380 metrics: options.metrics_meter.clone().map(MetricsContext::new),
381 disable_errcode_label: options.disable_error_code_metric_tags,
382 })
383 .service(channel),
384 None,
385 )
386 };
387
388 let headers = Arc::new(RwLock::new(ClientHeaders {
389 user_headers: parse_ascii_headers(options.headers.clone().unwrap_or_default())?,
390 user_binary_headers: parse_binary_headers(
391 options.binary_headers.clone().unwrap_or_default(),
392 )?,
393 api_key: options.api_key.clone(),
394 }));
395 let interceptor = ServiceCallInterceptor {
396 client_name: options.client_name.clone(),
397 client_version: options.client_version.clone(),
398 headers: headers.clone(),
399 };
400 let svc = InterceptedService::new(service, interceptor);
401 let mut svc_client = TemporalServiceClient::new(svc, options.grpc_compression);
402
403 let capabilities = if !options.skip_get_system_info {
404 match svc_client
405 .get_system_info(GetSystemInfoRequest::default().into_request())
406 .await
407 {
408 Ok(sysinfo) => sysinfo.into_inner().capabilities,
409 Err(status) => match status.code() {
410 Code::Unimplemented
411 if {
412 let msg = status.message().to_lowercase();
413 msg.contains("unknown method")
414 || msg.contains("unknown service")
415 || msg.contains("method not found")
416 || (msg.contains("getsysteminfo")
417 && (msg.contains("is unimplemented")
418 || msg.contains("not implement")))
419 } =>
420 {
421 None
422 }
423 _ => return Err(ClientConnectError::SystemInfoCallError(status)),
424 },
425 }
426 } else {
427 None
428 };
429 Ok(Self {
430 inner: Arc::new(ConnectionInner {
431 service: svc_client,
432 retry_options: options.retry_options.clone(),
433 identity: options.identity.clone(),
434 headers,
435 client_name: options.client_name.clone(),
436 client_version: options.client_version.clone(),
437 capabilities,
438 workers: Arc::new(ClientWorkerSet::new()),
439 _dns_task: dns_task,
440 payloads_warn_size: resolve_warn_threshold(
441 "payloads_warn_size",
442 options.payload_limits.payloads_warn_size,
443 ),
444 memo_warn_size: resolve_warn_threshold(
445 "memo_warn_size",
446 options.payload_limits.memo_warn_size,
447 ),
448 }),
449 })
450 }
451
452 pub fn set_api_key(&self, api_key: Option<String>) {
454 self.inner.headers.write().api_key = api_key;
455 }
456
457 pub fn set_headers(&self, headers: HashMap<String, String>) -> Result<(), InvalidHeaderError> {
466 self.inner.headers.write().user_headers = parse_ascii_headers(headers)?;
467 Ok(())
468 }
469
470 pub fn set_binary_headers(
479 &self,
480 binary_headers: HashMap<String, Vec<u8>>,
481 ) -> Result<(), InvalidHeaderError> {
482 self.inner.headers.write().user_binary_headers = parse_binary_headers(binary_headers)?;
483 Ok(())
484 }
485
486 pub fn client_name(&self) -> &str {
488 &self.inner.client_name
489 }
490
491 pub fn client_version(&self) -> &str {
493 &self.inner.client_version
494 }
495
496 pub fn capabilities(&self) -> Option<&get_system_info_response::Capabilities> {
499 self.inner.capabilities.as_ref()
500 }
501
502 pub fn retry_options_mut(&mut self) -> &mut RetryOptions {
507 &mut Arc::make_mut(&mut self.inner).retry_options
508 }
509
510 pub fn identity(&self) -> &str {
512 &self.inner.identity
513 }
514
515 pub fn identity_mut(&mut self) -> &mut String {
520 &mut Arc::make_mut(&mut self.inner).identity
521 }
522
523 pub fn workers(&self) -> Arc<ClientWorkerSet> {
525 self.inner.workers.clone()
526 }
527
528 pub fn worker_grouping_key(&self) -> Uuid {
530 self.inner.workers.worker_grouping_key()
531 }
532
533 pub fn workflow_service(&self) -> Box<dyn WorkflowService> {
535 self.inner.service.workflow_service()
536 }
537
538 pub fn operator_service(&self) -> Box<dyn OperatorService> {
540 self.inner.service.operator_service()
541 }
542
543 pub fn cloud_service(&self) -> Box<dyn CloudService> {
545 self.inner.service.cloud_service()
546 }
547
548 pub fn test_service(&self) -> Box<dyn TestService> {
550 self.inner.service.test_service()
551 }
552
553 pub fn health_service(&self) -> Box<dyn HealthService> {
555 self.inner.service.health_service()
556 }
557}
558
559#[derive(Debug)]
560struct ClientHeaders {
561 user_headers: HashMap<AsciiMetadataKey, AsciiMetadataValue>,
562 user_binary_headers: HashMap<BinaryMetadataKey, BinaryMetadataValue>,
563 api_key: Option<String>,
564}
565
566impl ClientHeaders {
567 fn apply_to_metadata(&self, metadata: &mut MetadataMap) {
568 for (key, val) in self.user_headers.iter() {
569 if !metadata.contains_key(key) {
571 metadata.insert(key, val.clone());
572 }
573 }
574 for (key, val) in self.user_binary_headers.iter() {
575 if !metadata.contains_key(key) {
577 metadata.insert_bin(key, val.clone());
578 }
579 }
580 if let Some(api_key) = &self.api_key {
581 if !metadata.contains_key("authorization")
583 && let Ok(val) = format!("Bearer {api_key}").parse()
584 {
585 metadata.insert("authorization", val);
586 }
587 }
588 }
589}
590
591#[derive(Debug)]
594enum TlsConfigResult {
595 Standard(Endpoint),
597 #[cfg(feature = "dynamic-tls")]
602 CustomConnector {
603 endpoint: Endpoint,
604 rustls_config: Arc<tokio_rustls::rustls::ClientConfig>,
605 domain: String,
606 },
607}
608
609async fn add_tls_to_channel(
617 tls_options: Option<&TlsOptions>,
618 mut channel: Endpoint,
619) -> Result<TlsConfigResult, ClientConnectError> {
620 if let Some(tls_cfg) = tls_options {
621 if tls_cfg.server_cert_verifier.is_some() && tls_cfg.server_root_ca_cert.is_some() {
622 return Err(ClientConnectError::InvalidConfig(
623 "Cannot set both `server_root_ca_cert` and `server_cert_verifier`".to_owned(),
624 ));
625 }
626
627 #[cfg(feature = "dynamic-tls")]
628 if tls_cfg.client_tls_options.is_some() && tls_cfg.client_cert_resolver.is_some() {
629 return Err(ClientConnectError::InvalidConfig(
630 "Cannot set both `client_tls_options` and `client_cert_resolver`. \
631 Use `client_tls_options` for static certificates or \
632 `client_cert_resolver` for dynamic certificate resolution, but not both."
633 .to_owned(),
634 ));
635 }
636
637 let domain_override = tls_cfg.domain.clone();
639 if let Some(domain) = &domain_override {
640 let uri: Uri = format!("https://{domain}").parse()?;
641 channel = channel.origin(uri);
642 }
643
644 #[cfg(feature = "dynamic-tls")]
646 if let Some(resolver) = &tls_cfg.client_cert_resolver {
647 let rustls_config = build_custom_rustls_config(tls_cfg, Some(resolver.clone()))?;
648 let sni_domain = domain_override
651 .or_else(|| {
652 channel
653 .uri()
654 .host()
655 .map(|h| h.trim_matches(|c| c == '[' || c == ']').to_owned())
656 })
657 .ok_or_else(|| {
658 ClientConnectError::InvalidConfig(
659 "Cannot determine TLS server name for dynamic cert resolution: \
660 set 'domain' in TlsOptions or use a URL with a hostname"
661 .to_owned(),
662 )
663 })?;
664 return Ok(TlsConfigResult::CustomConnector {
665 endpoint: channel,
666 rustls_config: Arc::new(rustls_config),
667 domain: sni_domain,
668 });
669 }
670
671 let mut tls = tonic::transport::ClientTlsConfig::new();
673
674 if tls_cfg.server_cert_verifier.is_none() {
675 if let Some(root_cert) = &tls_cfg.server_root_ca_cert {
676 let server_root_ca_cert = Certificate::from_pem(root_cert);
677 tls = tls.ca_certificate(server_root_ca_cert);
678 } else {
679 tls = tls.with_native_roots();
680 }
681 }
682
683 if let Some(domain) = &tls_cfg.domain {
684 tls = tls.domain_name(domain);
685 }
686
687 if let Some(client_opts) = &tls_cfg.client_tls_options {
688 let client_identity =
689 Identity::from_pem(&client_opts.client_cert, &client_opts.client_private_key);
690 tls = tls.identity(client_identity);
691 }
692
693 let endpoint = if let Some(verifier) = &tls_cfg.server_cert_verifier {
694 channel
695 .tls_config_with_verifier(tls, verifier.clone())
696 .map_err(ClientConnectError::from)?
697 } else {
698 channel.tls_config(tls).map_err(ClientConnectError::from)?
699 };
700 return Ok(TlsConfigResult::Standard(endpoint));
701 }
702 Ok(TlsConfigResult::Standard(channel))
703}
704
705#[cfg(feature = "dynamic-tls")]
706fn build_custom_rustls_config(
711 tls_cfg: &TlsOptions,
712 client_cert_resolver: Option<Arc<dyn tokio_rustls::rustls::client::ResolvesClientCert>>,
713) -> Result<tokio_rustls::rustls::ClientConfig, ClientConnectError> {
714 use tokio_rustls::rustls::{ClientConfig, RootCertStore, crypto};
715
716 let provider = crypto::CryptoProvider::get_default()
718 .cloned()
719 .or_else(|| {
720 #[cfg(feature = "tls-ring")]
722 {
723 return Some(Arc::new(crypto::ring::default_provider()));
724 }
725 #[cfg(feature = "tls-aws-lc")]
726 #[allow(unreachable_code)]
727 {
728 return Some(Arc::new(crypto::aws_lc_rs::default_provider()));
729 }
730 #[allow(unreachable_code)]
731 None
732 })
733 .ok_or_else(|| {
734 ClientConnectError::InvalidConfig(
735 "No TLS crypto provider available. Enable the `tls-ring` or `tls-aws-lc` feature."
736 .to_owned(),
737 )
738 })?;
739
740 let builder = ClientConfig::builder_with_provider(provider)
741 .with_safe_default_protocol_versions()
742 .map_err(|e| {
743 ClientConnectError::InvalidConfig(format!("Failed to configure TLS protocols: {e}"))
744 })?;
745
746 let builder = if let Some(verifier) = &tls_cfg.server_cert_verifier {
748 builder
749 .dangerous()
750 .with_custom_certificate_verifier(verifier.clone())
751 } else {
752 use std::io::Cursor;
753 use tokio_rustls::rustls::pki_types::{CertificateDer, pem::PemObject as _};
754
755 let mut roots = RootCertStore::empty();
756 if let Some(ca_cert) = &tls_cfg.server_root_ca_cert {
757 let certs: Vec<CertificateDer<'static>> =
758 CertificateDer::pem_reader_iter(&mut Cursor::new(ca_cert))
759 .collect::<Result<Vec<_>, _>>()
760 .map_err(|e| {
761 ClientConnectError::InvalidConfig(format!(
762 "Failed to parse CA certificate PEM: {e}"
763 ))
764 })?;
765 roots.add_parsable_certificates(certs);
766 if roots.is_empty() {
767 return Err(ClientConnectError::InvalidConfig(
768 "None of the provided CA certificates could be parsed. \
769 Ensure the PEM data contains valid X.509 certificates."
770 .to_owned(),
771 ));
772 }
773 } else {
774 let native_result = rustls_native_certs::load_native_certs();
776 if !native_result.errors.is_empty() {
777 warn!(
778 "errors occurred when loading native certs: {:?}",
779 native_result.errors
780 );
781 }
782 if native_result.certs.is_empty() {
783 return Err(ClientConnectError::InvalidConfig(
784 "No native TLS root certificates found".to_owned(),
785 ));
786 }
787 roots.add_parsable_certificates(native_result.certs);
788 if roots.is_empty() {
789 return Err(ClientConnectError::InvalidConfig(
790 "Native TLS root certificates were found but none could be parsed".to_owned(),
791 ));
792 }
793 }
794 builder.with_root_certificates(roots)
795 };
796
797 let mut config = if let Some(resolver) = client_cert_resolver {
799 builder.with_client_cert_resolver(resolver)
800 } else {
801 builder.with_no_client_auth()
802 };
803
804 config.alpn_protocols.push(b"h2".to_vec());
806
807 Ok(config)
808}
809
810#[cfg(feature = "dynamic-tls")]
811const DYNAMIC_TLS_CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
815
816#[cfg(feature = "dynamic-tls")]
817#[derive(Clone)]
820struct DynamicTlsConnector {
821 tls: tokio_rustls::TlsConnector,
822 domain: Arc<tokio_rustls::rustls::pki_types::ServerName<'static>>,
823}
824
825#[cfg(feature = "dynamic-tls")]
826impl std::fmt::Debug for DynamicTlsConnector {
827 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
828 f.debug_struct("DynamicTlsConnector")
829 .field("domain", &self.domain)
830 .finish()
831 }
832}
833
834#[cfg(feature = "dynamic-tls")]
835impl tower::Service<Uri> for DynamicTlsConnector {
836 type Response = hyper_util::rt::TokioIo<tokio_rustls::client::TlsStream<tokio::net::TcpStream>>;
837 type Error = Box<dyn std::error::Error + Send + Sync>;
838 type Future =
839 Pin<Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + Send>>;
840
841 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
842 Poll::Ready(Ok(()))
843 }
844
845 fn call(&mut self, uri: Uri) -> Self::Future {
846 let tls = self.tls.clone();
847 let domain = self.domain.clone();
848
849 Box::pin(async move {
850 let host = uri
851 .host()
852 .ok_or_else(|| -> Box<dyn std::error::Error + Send + Sync> {
853 format!("URI has no host for TLS connection: {uri}").into()
854 })?;
855 let port = uri.port_u16().unwrap_or(443);
856 let addr_display = format!("{}:{}", host, port);
859
860 debug!(target: "temporal_client", %uri, addr = %addr_display, "DynamicTlsConnector: establishing TCP+TLS connection");
861
862 let tcp = tokio::time::timeout(
866 DYNAMIC_TLS_CONNECT_TIMEOUT,
867 tokio::net::TcpStream::connect((host, port)),
868 )
869 .await
870 .map_err(|_| -> Box<dyn std::error::Error + Send + Sync> {
871 format!(
872 "TCP connect to {addr_display} timed out after {}s",
873 DYNAMIC_TLS_CONNECT_TIMEOUT.as_secs()
874 )
875 .into()
876 })?
877 .map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
878 format!("TCP connect to {addr_display} failed: {e}").into()
879 })?;
880
881 tcp.set_nodelay(true)?;
883
884 let tls_stream = tls.connect(domain.as_ref().to_owned(), tcp).await?;
885 debug!(target: "temporal_client", addr = %addr_display, "DynamicTlsConnector: TLS handshake complete");
886 Ok(hyper_util::rt::TokioIo::new(tls_stream))
887 })
888 }
889}
890
891fn parse_ascii_headers(
892 headers: HashMap<String, String>,
893) -> Result<HashMap<AsciiMetadataKey, AsciiMetadataValue>, InvalidHeaderError> {
894 let mut parsed_headers = HashMap::with_capacity(headers.len());
895 for (k, v) in headers.into_iter() {
896 let key = match AsciiMetadataKey::from_str(&k) {
897 Ok(key) => key,
898 Err(err) => {
899 return Err(InvalidHeaderError::InvalidAsciiHeaderKey {
900 key: k,
901 source: err,
902 });
903 }
904 };
905 let value = match MetadataValue::from_str(&v) {
906 Ok(value) => value,
907 Err(err) => {
908 return Err(InvalidHeaderError::InvalidAsciiHeaderValue {
909 key: k,
910 value: v,
911 source: err,
912 });
913 }
914 };
915 parsed_headers.insert(key, value);
916 }
917
918 Ok(parsed_headers)
919}
920
921fn parse_binary_headers(
922 headers: HashMap<String, Vec<u8>>,
923) -> Result<HashMap<BinaryMetadataKey, BinaryMetadataValue>, InvalidHeaderError> {
924 let mut parsed_headers = HashMap::with_capacity(headers.len());
925 for (k, v) in headers.into_iter() {
926 let key = match BinaryMetadataKey::from_str(&k) {
927 Ok(key) => key,
928 Err(err) => {
929 return Err(InvalidHeaderError::InvalidBinaryHeaderKey {
930 key: k,
931 source: err,
932 });
933 }
934 };
935 let value = BinaryMetadataValue::from_bytes(&v);
936 parsed_headers.insert(key, value);
937 }
938
939 Ok(parsed_headers)
940}
941
942#[derive(Clone)]
944pub struct ServiceCallInterceptor {
945 client_name: String,
946 client_version: String,
947 headers: Arc<RwLock<ClientHeaders>>,
949}
950
951impl Interceptor for ServiceCallInterceptor {
952 fn call(
955 &mut self,
956 mut request: tonic::Request<()>,
957 ) -> Result<tonic::Request<()>, tonic::Status> {
958 let metadata = request.metadata_mut();
959 if !metadata.contains_key(CLIENT_NAME_HEADER_KEY) {
960 metadata.insert(
961 CLIENT_NAME_HEADER_KEY,
962 self.client_name
963 .parse()
964 .unwrap_or_else(|_| MetadataValue::from_static("")),
965 );
966 }
967 if !metadata.contains_key(CLIENT_VERSION_HEADER_KEY) {
968 metadata.insert(
969 CLIENT_VERSION_HEADER_KEY,
970 self.client_version
971 .parse()
972 .unwrap_or_else(|_| MetadataValue::from_static("")),
973 );
974 }
975 self.headers.read().apply_to_metadata(metadata);
976 request.set_default_timeout(OTHER_CALL_TIMEOUT);
977
978 Ok(request)
979 }
980}
981
982#[derive(Clone)]
984pub struct TemporalServiceClient {
985 workflow_svc_client: Box<dyn WorkflowService>,
986 operator_svc_client: Box<dyn OperatorService>,
987 cloud_svc_client: Box<dyn CloudService>,
988 test_svc_client: Box<dyn TestService>,
989 health_svc_client: Box<dyn HealthService>,
990}
991
992fn get_decode_max_size() -> usize {
995 static _DECODE_MAX_SIZE: OnceLock<usize> = OnceLock::new();
996 *_DECODE_MAX_SIZE.get_or_init(|| {
997 std::env::var("TEMPORAL_MAX_INCOMING_GRPC_BYTES")
998 .ok()
999 .and_then(|s| s.parse().ok())
1000 .unwrap_or(128 * 1024 * 1024)
1001 })
1002}
1003
1004impl TemporalServiceClient {
1005 fn new<T>(svc: T, compression: GrpcCompression) -> Self
1006 where
1007 T: GrpcService<Body> + Send + Sync + Clone + 'static,
1008 T::ResponseBody: tonic::codegen::Body<Data = tonic::codegen::Bytes> + Send + 'static,
1009 T::Error: Into<tonic::codegen::StdError>,
1010 <T::ResponseBody as tonic::codegen::Body>::Error: Into<tonic::codegen::StdError> + Send,
1011 <T as GrpcService<Body>>::Future: Send,
1012 {
1013 macro_rules! configure {
1016 ($client:expr) => {{
1017 let client = $client.max_decoding_message_size(get_decode_max_size());
1018 match compression {
1019 GrpcCompression::Gzip => client
1020 .send_compressed(CompressionEncoding::Gzip)
1021 .accept_compressed(CompressionEncoding::Gzip),
1022 GrpcCompression::None => client,
1023 }
1024 }};
1025 }
1026
1027 let workflow_svc_client = Box::new(configure!(WorkflowServiceClient::new(svc.clone())));
1028 let operator_svc_client = Box::new(configure!(OperatorServiceClient::new(svc.clone())));
1029 let cloud_svc_client = Box::new(configure!(CloudServiceClient::new(svc.clone())));
1030 let test_svc_client = Box::new(configure!(TestServiceClient::new(svc.clone())));
1031 let health_svc_client = Box::new(configure!(HealthClient::new(svc.clone())));
1032
1033 Self {
1034 workflow_svc_client,
1035 operator_svc_client,
1036 cloud_svc_client,
1037 test_svc_client,
1038 health_svc_client,
1039 }
1040 }
1041
1042 pub fn from_services(
1045 workflow: Box<dyn WorkflowService>,
1046 operator: Box<dyn OperatorService>,
1047 cloud: Box<dyn CloudService>,
1048 test: Box<dyn TestService>,
1049 health: Box<dyn HealthService>,
1050 ) -> Self {
1051 Self {
1052 workflow_svc_client: workflow,
1053 operator_svc_client: operator,
1054 cloud_svc_client: cloud,
1055 test_svc_client: test,
1056 health_svc_client: health,
1057 }
1058 }
1059
1060 pub fn workflow_service(&self) -> Box<dyn WorkflowService> {
1062 self.workflow_svc_client.clone()
1063 }
1064 pub fn operator_service(&self) -> Box<dyn OperatorService> {
1066 self.operator_svc_client.clone()
1067 }
1068 pub fn cloud_service(&self) -> Box<dyn CloudService> {
1070 self.cloud_svc_client.clone()
1071 }
1072 pub fn test_service(&self) -> Box<dyn TestService> {
1074 self.test_svc_client.clone()
1075 }
1076 pub fn health_service(&self) -> Box<dyn HealthService> {
1078 self.health_svc_client.clone()
1079 }
1080}
1081
1082#[derive(Clone, Debug)]
1085pub struct Client {
1086 connection: Connection,
1087 options: Arc<ClientOptions>,
1088}
1089
1090impl Client {
1091 pub async fn connect(
1094 mut connection_options: ConnectionOptions,
1095 client_options: ClientOptions,
1096 ) -> Result<Self, ClientConnectError> {
1097 plugins::apply_connection_plugins(&client_options, &mut connection_options)?;
1098 let connection = Connection::connect(connection_options).await?;
1099 Ok(Self::new(connection, client_options)?)
1100 }
1101
1102 pub fn new(connection: Connection, mut options: ClientOptions) -> Result<Self, ClientNewError> {
1107 plugins::apply_client_plugins(&mut options)?;
1108 Ok(Client {
1109 connection,
1110 options: Arc::new(options),
1111 })
1112 }
1113
1114 pub fn options(&self) -> &ClientOptions {
1116 &self.options
1117 }
1118
1119 pub fn options_mut(&mut self) -> &mut ClientOptions {
1124 Arc::make_mut(&mut self.options)
1125 }
1126
1127 pub fn connection(&self) -> &Connection {
1129 &self.connection
1130 }
1131
1132 pub fn connection_mut(&mut self) -> &mut Connection {
1134 &mut self.connection
1135 }
1136}
1137
1138impl Client {
1142 pub async fn start_workflow<W>(
1147 &self,
1148 workflow: W,
1149 input: W::Input,
1150 options: WorkflowStartOptions,
1151 ) -> Result<WorkflowHandle<Self, W>, WorkflowStartError>
1152 where
1153 W: HasWorkflowDefinition,
1154 W::Input: Send,
1155 {
1156 WorkflowClientTrait::start_workflow(self, workflow, input, options).await
1157 }
1158
1159 pub fn get_workflow_handle<W: HasWorkflowDefinition>(
1163 &self,
1164 workflow_id: impl Into<String>,
1165 ) -> WorkflowHandle<Self, W> {
1166 WorkflowClientTrait::get_workflow_handle(self, workflow_id)
1167 }
1168
1169 pub fn list_workflows(
1174 &self,
1175 query: impl Into<String>,
1176 opts: WorkflowListOptions,
1177 ) -> ListWorkflowsStream {
1178 WorkflowClientTrait::list_workflows(self, query, opts)
1179 }
1180
1181 pub async fn count_workflows(
1183 &self,
1184 query: impl Into<String>,
1185 opts: WorkflowCountOptions,
1186 ) -> Result<WorkflowExecutionCount, ClientError> {
1187 WorkflowClientTrait::count_workflows(self, query, opts).await
1188 }
1189
1190 pub fn get_async_activity_handle(
1197 &self,
1198 identifier: ActivityIdentifier,
1199 ) -> AsyncActivityHandle<Self> {
1200 WorkflowClientTrait::get_async_activity_handle(self, identifier)
1201 }
1202
1203 pub async fn start_activity<A>(
1208 &self,
1209 activity: A,
1210 input: A::Input,
1211 options: ActivityStartOptions,
1212 ) -> Result<ActivityHandle<Self, A>, StartActivityError>
1213 where
1214 A: ActivityDefinition,
1215 {
1216 WorkflowClientTrait::start_activity(self, activity, input, options).await
1217 }
1218
1219 pub fn get_activity_handle<A>(
1230 &self,
1231 activity: A,
1232 id: impl Into<String>,
1233 run_id: Option<String>,
1234 ) -> ActivityHandle<Self, A>
1235 where
1236 Self: Sized,
1237 A: ActivityDefinition,
1238 {
1239 WorkflowClientTrait::get_activity_handle(self, activity, id, run_id)
1240 }
1241
1242 pub fn get_untyped_activity_handle(
1253 &self,
1254 id: impl Into<String>,
1255 run_id: Option<String>,
1256 ) -> ActivityHandle<Self, UntypedActivity>
1257 where
1258 Self: Sized,
1259 {
1260 WorkflowClientTrait::get_untyped_activity_handle(self, id, run_id)
1261 }
1262
1263 pub fn list_activities(
1265 &self,
1266 query: impl Into<String>,
1267 options: ActivityListOptions,
1268 ) -> ListActivitiesStream {
1269 WorkflowClientTrait::list_activities(self, query, options)
1270 }
1271
1272 pub async fn count_activities(
1274 &self,
1275 query: impl Into<String>,
1276 options: ActivityCountOptions,
1277 ) -> Result<ActivityExecutionCount, ClientError> {
1278 WorkflowClientTrait::count_activities(self, query, options).await
1279 }
1280}
1281
1282impl NamespacedClient for Client {
1283 fn namespace(&self) -> String {
1284 self.options.namespace.clone()
1285 }
1286
1287 fn identity(&self) -> String {
1288 self.connection.identity().to_owned()
1289 }
1290
1291 fn data_converter(&self) -> &DataConverter {
1292 &self.options.data_converter
1293 }
1294
1295 fn client_interceptors(&self) -> &[Arc<dyn ClientInterceptor>] {
1296 &self.options.client_interceptors
1297 }
1298}
1299
1300#[derive(Clone)]
1302pub enum Namespace {
1303 Name(String),
1305 Id(String),
1307}
1308
1309pub(crate) trait WorkflowClientTrait: NamespacedClient {
1312 fn start_workflow<W>(
1314 &self,
1315 workflow: W,
1316 input: W::Input,
1317 options: WorkflowStartOptions,
1318 ) -> impl Future<Output = Result<WorkflowHandle<Self, W>, WorkflowStartError>>
1319 where
1320 Self: Sized,
1321 W: HasWorkflowDefinition,
1322 W::Input: Send;
1323
1324 fn get_workflow_handle<W: HasWorkflowDefinition>(
1331 &self,
1332 workflow_id: impl Into<String>,
1333 ) -> WorkflowHandle<Self, W>
1334 where
1335 Self: Sized;
1336
1337 fn list_workflows(
1341 &self,
1342 query: impl Into<String>,
1343 opts: WorkflowListOptions,
1344 ) -> ListWorkflowsStream;
1345
1346 fn count_workflows(
1348 &self,
1349 query: impl Into<String>,
1350 opts: WorkflowCountOptions,
1351 ) -> impl Future<Output = Result<WorkflowExecutionCount, ClientError>>;
1352
1353 fn get_async_activity_handle(
1357 &self,
1358 identifier: ActivityIdentifier,
1359 ) -> AsyncActivityHandle<Self>
1360 where
1361 Self: Sized;
1362
1363 fn start_activity<A>(
1365 &self,
1366 activity: A,
1367 input: A::Input,
1368 options: ActivityStartOptions,
1369 ) -> impl Future<Output = Result<ActivityHandle<Self, A>, StartActivityError>>
1370 where
1371 Self: Sized,
1372 A: ActivityDefinition;
1373
1374 fn get_activity_handle<A>(
1376 &self,
1377 activity: A,
1378 id: impl Into<String>,
1379 run_id: Option<String>,
1380 ) -> ActivityHandle<Self, A>
1381 where
1382 Self: Sized,
1383 A: ActivityDefinition;
1384
1385 fn get_untyped_activity_handle(
1387 &self,
1388 id: impl Into<String>,
1389 run_id: Option<String>,
1390 ) -> ActivityHandle<Self, UntypedActivity>
1391 where
1392 Self: Sized;
1393
1394 fn list_activities(
1396 &self,
1397 query: impl Into<String>,
1398 _options: ActivityListOptions,
1399 ) -> ListActivitiesStream;
1400
1401 fn count_activities(
1403 &self,
1404 query: impl Into<String>,
1405 _options: ActivityCountOptions,
1406 ) -> impl Future<Output = Result<ActivityExecutionCount, ClientError>>;
1407}
1408
1409pub trait NamespacedClient {
1411 fn namespace(&self) -> String;
1413 fn identity(&self) -> String;
1415 fn data_converter(&self) -> &DataConverter {
1418 static DEFAULT: OnceLock<DataConverter> = OnceLock::new();
1419 DEFAULT.get_or_init(DataConverter::default)
1420 }
1421 fn client_interceptors(&self) -> &[Arc<dyn ClientInterceptor>] {
1429 &[]
1430 }
1431}
1432
1433#[derive(Debug, Clone)]
1436pub struct WorkflowExecution {
1437 raw: workflow::WorkflowExecutionInfo,
1438 data_converter: DataConverter,
1439}
1440
1441impl WorkflowExecution {
1442 fn new_with_data_converter(
1443 raw: workflow::WorkflowExecutionInfo,
1444 data_converter: DataConverter,
1445 ) -> Self {
1446 Self {
1447 raw,
1448 data_converter,
1449 }
1450 }
1451
1452 pub fn id(&self) -> &str {
1454 self.raw
1455 .execution
1456 .as_ref()
1457 .map(|e| e.workflow_id.as_str())
1458 .unwrap_or("")
1459 }
1460
1461 pub fn run_id(&self) -> &str {
1463 self.raw
1464 .execution
1465 .as_ref()
1466 .map(|e| e.run_id.as_str())
1467 .unwrap_or("")
1468 }
1469
1470 pub fn workflow_type(&self) -> &str {
1472 self.raw
1473 .r#type
1474 .as_ref()
1475 .map(|t| t.name.as_str())
1476 .unwrap_or("")
1477 }
1478
1479 pub fn status(&self) -> WorkflowExecutionStatus {
1481 WorkflowExecutionStatus::from_raw(self.raw.status)
1482 }
1483
1484 pub fn start_time(&self) -> Option<SystemTime> {
1486 self.raw
1487 .start_time
1488 .as_ref()
1489 .and_then(proto_ts_to_system_time)
1490 }
1491
1492 pub fn execution_time(&self) -> Option<SystemTime> {
1494 self.raw
1495 .execution_time
1496 .as_ref()
1497 .and_then(proto_ts_to_system_time)
1498 }
1499
1500 pub fn close_time(&self) -> Option<SystemTime> {
1502 self.raw
1503 .close_time
1504 .as_ref()
1505 .and_then(proto_ts_to_system_time)
1506 }
1507
1508 pub fn task_queue(&self) -> &str {
1510 &self.raw.task_queue
1511 }
1512
1513 pub fn history_length(&self) -> i64 {
1515 self.raw.history_length
1516 }
1517
1518 pub fn memo(&self) -> Memo {
1520 Memo::from_raw(
1521 self.raw.memo.clone(),
1522 self.data_converter.payload_converter().clone(),
1523 SerializationContextData::Workflow,
1524 )
1525 }
1526
1527 pub fn parent_id(&self) -> Option<&str> {
1529 self.raw
1530 .parent_execution
1531 .as_ref()
1532 .map(|e| e.workflow_id.as_str())
1533 }
1534
1535 pub fn parent_run_id(&self) -> Option<&str> {
1537 self.raw
1538 .parent_execution
1539 .as_ref()
1540 .map(|e| e.run_id.as_str())
1541 }
1542
1543 pub fn search_attributes(&self) -> SearchAttributes {
1545 self.raw
1546 .search_attributes
1547 .as_ref()
1548 .map(SearchAttributes::from_proto)
1549 .unwrap_or_default()
1550 }
1551
1552 pub fn raw(&self) -> &workflow::WorkflowExecutionInfo {
1554 &self.raw
1555 }
1556
1557 pub fn into_raw(self) -> workflow::WorkflowExecutionInfo {
1559 self.raw
1560 }
1561}
1562
1563pub struct ListWorkflowsStream {
1566 inner: Pin<Box<dyn Stream<Item = Result<WorkflowExecution, ClientError>> + Send>>,
1567}
1568
1569impl ListWorkflowsStream {
1570 fn new(
1571 inner: Pin<Box<dyn Stream<Item = Result<WorkflowExecution, ClientError>> + Send>>,
1572 ) -> Self {
1573 Self { inner }
1574 }
1575}
1576
1577impl Stream for ListWorkflowsStream {
1578 type Item = Result<WorkflowExecution, ClientError>;
1579
1580 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
1581 self.inner.as_mut().poll_next(cx)
1582 }
1583}
1584
1585#[derive(Debug, Clone)]
1590pub struct WorkflowExecutionCount {
1591 count: usize,
1592 groups: Vec<WorkflowCountAggregationGroup>,
1593}
1594
1595impl WorkflowExecutionCount {
1596 pub(crate) fn from_response(resp: CountWorkflowExecutionsResponse) -> Self {
1597 Self {
1598 count: resp.count as usize,
1599 groups: resp
1600 .groups
1601 .into_iter()
1602 .map(WorkflowCountAggregationGroup::from_proto)
1603 .collect(),
1604 }
1605 }
1606
1607 pub fn count(&self) -> usize {
1610 self.count
1611 }
1612
1613 pub fn groups(&self) -> &[WorkflowCountAggregationGroup] {
1615 &self.groups
1616 }
1617}
1618
1619#[derive(Debug, Clone)]
1621pub struct WorkflowCountAggregationGroup {
1622 raw: count_workflow_executions_response::AggregationGroup,
1623}
1624
1625impl WorkflowCountAggregationGroup {
1626 fn from_proto(proto: count_workflow_executions_response::AggregationGroup) -> Self {
1627 Self { raw: proto }
1628 }
1629
1630 pub fn get<T: SearchAttributeValue>(&self, index: usize) -> Option<T> {
1635 self.try_get(index).ok().flatten()
1636 }
1637
1638 pub fn try_get<T: SearchAttributeValue>(
1644 &self,
1645 index: usize,
1646 ) -> Result<Option<T>, SearchAttributeError> {
1647 match self.raw.group_values.get(index) {
1648 Some(payload) => T::from_search_attribute_payload(payload).map(Some),
1649 None => Ok(None),
1650 }
1651 }
1652
1653 pub fn count(&self) -> usize {
1655 self.raw.count as usize
1656 }
1657}
1658
1659impl<T> WorkflowClientTrait for T
1660where
1661 T: WorkflowService + NamespacedClient + Clone + Send + Sync + 'static,
1662{
1663 async fn start_workflow<W>(
1664 &self,
1665 workflow: W,
1666 input: W::Input,
1667 options: WorkflowStartOptions,
1668 ) -> Result<WorkflowHandle<Self, W>, WorkflowStartError>
1669 where
1670 W: HasWorkflowDefinition,
1671 W::Input: Send,
1672 {
1673 let namespace = self.namespace();
1674 let interceptor_output = interceptors::call_start_workflow(
1675 self.client_interceptors(),
1676 StartWorkflowInput::new(workflow.name().to_owned(), input, options),
1677 Next::new({
1678 let client = (*self).clone();
1679 move |input: StartWorkflowInput| -> BoxFuture<
1680 '_,
1681 Result<StartWorkflowOutput, WorkflowStartError>,
1682 > {
1683 let mut client = client;
1684 Box::pin(async move {
1685 let (workflow_type, args, options, rpc_options) = input.into_parts();
1686 let data_converter = client.data_converter().clone();
1687 let unencoded_payloads = {
1688 let payload_converter = data_converter.payload_converter();
1689 let context = SerializationContext {
1690 data: &SerializationContextData::Workflow,
1691 converter: payload_converter,
1692 };
1693 args.serialize_payloads(&context)
1694 };
1695 drop(args);
1696
1697 let payloads = data_converter
1698 .codec()
1699 .encode(&SerializationContextData::Workflow, unencoded_payloads?)
1700 .await?;
1701 let namespace = client.namespace();
1702 let workflow_id = options.workflow_id.clone();
1703 let task_queue_name = options.task_queue.clone();
1704
1705 let user_metadata = if options.static_summary.is_some()
1706 || options.static_details.is_some()
1707 {
1708 let payload_converter = PayloadConverter::default();
1709 let context = SerializationContext {
1710 data: &SerializationContextData::Workflow,
1711 converter: &payload_converter,
1712 };
1713 Some(UserMetadata {
1714 summary: options.static_summary.map(|summary| {
1715 payload_converter.to_payload(&context, &summary).expect(
1716 "String-to-JSON payload serialization is infallible",
1717 )
1718 }),
1719 details: options.static_details.map(|details| {
1720 payload_converter.to_payload(&context, &details).expect(
1721 "String-to-JSON payload serialization is infallible",
1722 )
1723 }),
1724 })
1725 } else {
1726 None
1727 };
1728
1729 let run_id = if let Some(start_signal) = options.start_signal {
1730 let mut request = SignalWithStartWorkflowExecutionRequest {
1731 namespace,
1732 workflow_id: workflow_id.clone(),
1733 workflow_type: Some(WorkflowType {
1734 name: workflow_type,
1735 }),
1736 task_queue: Some(TaskQueue {
1737 name: task_queue_name,
1738 kind: TaskQueueKind::Normal as i32,
1739 normal_name: String::new(),
1740 }),
1741 input: payloads.into_payloads(),
1742 signal_name: start_signal.signal_name,
1743 signal_input: start_signal.input,
1744 identity: client.identity(),
1745 request_id: Uuid::new_v4().to_string(),
1746 workflow_id_reuse_policy: options.id_reuse_policy as i32,
1747 workflow_id_conflict_policy: options.id_conflict_policy as i32,
1748 workflow_execution_timeout: options
1749 .execution_timeout
1750 .and_then(|duration| duration.try_into().ok()),
1751 workflow_run_timeout: options
1752 .run_timeout
1753 .and_then(|duration| duration.try_into().ok()),
1754 workflow_task_timeout: options
1755 .task_timeout
1756 .and_then(|duration| duration.try_into().ok()),
1757 search_attributes: options
1758 .search_attributes
1759 .map(|attributes| attributes.into_proto()),
1760 cron_schedule: options.cron_schedule.unwrap_or_default(),
1761 retry_policy: options.retry_policy.map(Into::into),
1762 header: options.header.or(start_signal.header),
1763 user_metadata,
1764 ..Default::default()
1765 }
1766 .into_request();
1767 rpc_options.apply_to(&mut request);
1768 WorkflowService::signal_with_start_workflow_execution(
1769 &mut client,
1770 request,
1771 )
1772 .await?
1773 .into_inner()
1774 .run_id
1775 } else {
1776 let mut request = StartWorkflowExecutionRequest {
1777 namespace,
1778 input: payloads.into_payloads(),
1779 workflow_id: workflow_id.clone(),
1780 workflow_type: Some(WorkflowType {
1781 name: workflow_type,
1782 }),
1783 task_queue: Some(TaskQueue {
1784 name: task_queue_name,
1785 kind: TaskQueueKind::Unspecified as i32,
1786 normal_name: String::new(),
1787 }),
1788 request_id: Uuid::new_v4().to_string(),
1789 workflow_id_reuse_policy: options.id_reuse_policy as i32,
1790 workflow_id_conflict_policy: options.id_conflict_policy as i32,
1791 workflow_execution_timeout: options
1792 .execution_timeout
1793 .and_then(|duration| duration.try_into().ok()),
1794 workflow_run_timeout: options
1795 .run_timeout
1796 .and_then(|duration| duration.try_into().ok()),
1797 workflow_task_timeout: options
1798 .task_timeout
1799 .and_then(|duration| duration.try_into().ok()),
1800 search_attributes: options
1801 .search_attributes
1802 .map(|attributes| attributes.into_proto()),
1803 cron_schedule: options.cron_schedule.unwrap_or_default(),
1804 request_eager_execution: options.enable_eager_workflow_start,
1805 retry_policy: options.retry_policy.map(Into::into),
1806 links: options.links,
1807 completion_callbacks: options.completion_callbacks,
1808 priority: Some(options.priority.into()),
1809 header: options.header,
1810 user_metadata,
1811 ..Default::default()
1812 }
1813 .into_request();
1814 rpc_options.apply_to(&mut request);
1815 client
1816 .start_workflow_execution(request)
1817 .await
1818 .map_err(|status| {
1819 if status.code() == Code::AlreadyExists {
1820 let run_id = decode_status_detail::<
1821 WorkflowExecutionAlreadyStartedFailure,
1822 >(
1823 status.details()
1824 )
1825 .map(|failure| failure.run_id);
1826 WorkflowStartError::AlreadyStarted {
1827 run_id,
1828 source: status,
1829 }
1830 } else {
1831 WorkflowStartError::Rpc(status)
1832 }
1833 })?
1834 .into_inner()
1835 .run_id
1836 };
1837
1838 Ok(StartWorkflowOutput::new(workflow_id, run_id))
1839 })
1840 }
1841 }),
1842 )
1843 .await?;
1844 let StartWorkflowOutput {
1845 workflow_id,
1846 run_id,
1847 } = interceptor_output;
1848
1849 Ok(WorkflowHandle::new(
1850 self.clone(),
1851 WorkflowExecutionInfo {
1852 namespace,
1853 workflow_id,
1854 run_id: Some(run_id.clone()),
1855 first_execution_run_id: Some(run_id),
1856 },
1857 ))
1858 }
1859
1860 fn get_workflow_handle<W: HasWorkflowDefinition>(
1861 &self,
1862 workflow_id: impl Into<String>,
1863 ) -> WorkflowHandle<Self, W>
1864 where
1865 Self: Sized,
1866 {
1867 WorkflowHandle::new(
1868 self.clone(),
1869 WorkflowExecutionInfo {
1870 namespace: self.namespace(),
1871 workflow_id: workflow_id.into(),
1872 run_id: None,
1873 first_execution_run_id: None,
1874 },
1875 )
1876 }
1877
1878 fn list_workflows(
1879 &self,
1880 query: impl Into<String>,
1881 opts: WorkflowListOptions,
1882 ) -> ListWorkflowsStream {
1883 let client = self.clone();
1884 let namespace = self.namespace();
1885 let query = query.into();
1886 let limit = opts.limit;
1887 let rpc_options = opts.rpc_options;
1888
1889 let initial_state = (Vec::new(), VecDeque::new(), 0, false);
1891
1892 let stream = stream::unfold(
1893 initial_state,
1894 move |(next_page_token, mut buffer, mut yielded, exhausted)| {
1895 let client = client.clone();
1896 let namespace = namespace.clone();
1897 let query = query.clone();
1898 let rpc_options = rpc_options.clone();
1899
1900 async move {
1901 if let Some(l) = limit
1902 && yielded >= l
1903 {
1904 return None;
1905 }
1906
1907 if let Some(exec) = buffer.pop_front() {
1908 yielded += 1;
1909 return Some((Ok(exec), (next_page_token, buffer, yielded, exhausted)));
1910 }
1911
1912 if exhausted {
1913 return None;
1914 }
1915
1916 let response = interceptors::call_list_workflows_page(
1917 client.client_interceptors(),
1918 ListWorkflowsPageInput {
1919 query,
1920 next_page_token: next_page_token.clone(),
1921 rpc_options,
1922 },
1923 Next::new({
1924 let mut rpc_client = client.clone();
1925 move |input: ListWorkflowsPageInput| -> BoxFuture<
1926 '_,
1927 Result<ListWorkflowsPageOutput, ClientError>,
1928 > {
1929 Box::pin(async move {
1930 let mut request = ListWorkflowExecutionsRequest {
1931 namespace,
1932 page_size: 0,
1933 next_page_token: input.next_page_token,
1934 query: input.query,
1935 }
1936 .into_request();
1937 input.rpc_options.apply_to(&mut request);
1938 let response = WorkflowService::list_workflow_executions(
1939 &mut rpc_client,
1940 request,
1941 )
1942 .await?
1943 .into_inner();
1944 Ok(ListWorkflowsPageOutput::new(
1945 response.executions,
1946 response.next_page_token,
1947 ))
1948 })
1949 }
1950 }),
1951 )
1952 .await;
1953
1954 match response {
1955 Ok(mut output) => {
1956 let new_exhausted = output.next_page_token.is_empty();
1957 let new_token = output.next_page_token;
1958
1959 let data_converter = client.data_converter().clone();
1960 for execution in &mut output.executions {
1961 if let Some(memo) = execution.memo.as_mut()
1962 && let Err(err) = decode_payloads(
1963 memo,
1964 data_converter.codec(),
1965 &SerializationContextData::Workflow,
1966 )
1967 .await
1968 {
1969 return Some((
1970 Err(ClientError::from(err)),
1971 (new_token, buffer, yielded, true),
1972 ));
1973 }
1974 }
1975 buffer = output
1976 .executions
1977 .into_iter()
1978 .map(|raw| {
1979 WorkflowExecution::new_with_data_converter(
1980 raw,
1981 data_converter.clone(),
1982 )
1983 })
1984 .collect();
1985
1986 if let Some(exec) = buffer.pop_front() {
1987 yielded += 1;
1988 Some((Ok(exec), (new_token, buffer, yielded, new_exhausted)))
1989 } else {
1990 None
1991 }
1992 }
1993 Err(e) => Some((Err(e), (next_page_token, buffer, yielded, true))),
1994 }
1995 }
1996 },
1997 );
1998
1999 ListWorkflowsStream::new(Box::pin(stream))
2000 }
2001
2002 async fn count_workflows(
2003 &self,
2004 query: impl Into<String>,
2005 opts: WorkflowCountOptions,
2006 ) -> Result<WorkflowExecutionCount, ClientError> {
2007 let output = interceptors::call_count_workflows(
2008 self.client_interceptors(),
2009 CountWorkflowsInput {
2010 query: query.into(),
2011 options: opts,
2012 },
2013 Next::new({
2014 let mut client = (*self).clone();
2015 move |input: CountWorkflowsInput| -> BoxFuture<
2016 '_,
2017 Result<CountWorkflowsOutput, ClientError>,
2018 > {
2019 Box::pin(async move {
2020 let mut request = CountWorkflowExecutionsRequest {
2021 namespace: client.namespace(),
2022 query: input.query,
2023 }
2024 .into_request();
2025 input.options.rpc_options.apply_to(&mut request);
2026 let response = WorkflowService::count_workflow_executions(
2027 &mut client,
2028 request,
2029 )
2030 .await?
2031 .into_inner();
2032 Ok(CountWorkflowsOutput::new(response))
2033 })
2034 }
2035 }),
2036 )
2037 .await?;
2038
2039 Ok(WorkflowExecutionCount::from_response(output.response))
2040 }
2041
2042 fn get_async_activity_handle(&self, identifier: ActivityIdentifier) -> AsyncActivityHandle<Self>
2043 where
2044 Self: Sized,
2045 {
2046 AsyncActivityHandle::new(self.clone(), identifier)
2047 }
2048
2049 async fn start_activity<A>(
2050 &self,
2051 activity: A,
2052 input: A::Input,
2053 options: ActivityStartOptions,
2054 ) -> Result<ActivityHandle<Self, A>, StartActivityError>
2055 where
2056 Self: Sized,
2057 A: ActivityDefinition,
2058 {
2059 let mut client = self.clone();
2060 let dc = client.data_converter();
2061 let sc = &SerializationContextData::Activity;
2062
2063 let user_metadata = {
2064 let summary = match &options.summary {
2065 Some(summary) => Some(dc.to_payload(sc, summary).await?),
2066 None => None,
2067 };
2068 let details = match &options.static_details {
2069 Some(details) => Some(dc.to_payload(sc, details).await?),
2070 None => None,
2071 };
2072 (summary.is_some() || details.is_some()).then_some(UserMetadata { summary, details })
2073 };
2074
2075 let resp = client
2076 .start_activity_execution(
2077 StartActivityExecutionRequest {
2078 namespace: client.namespace(),
2079 identity: client.identity(),
2080 request_id: Uuid::new_v4().to_string(),
2081 activity_id: options.id.clone(),
2082 activity_type: Some(ActivityType {
2083 name: activity.name().to_string(),
2084 }),
2085 task_queue: Some(TaskQueue {
2086 name: options.task_queue,
2087 kind: TaskQueueKind::Normal.into(),
2088 normal_name: "".to_string(),
2089 }),
2090 schedule_to_close_timeout: try_into_or_box_err(
2091 options.close_timeouts.schedule_to_close(),
2092 StartActivityError::Other,
2093 )?,
2094 schedule_to_start_timeout: try_into_or_box_err(
2095 options.schedule_to_start_timeout,
2096 StartActivityError::Other,
2097 )?,
2098 start_to_close_timeout: try_into_or_box_err(
2099 options.close_timeouts.start_to_close(),
2100 StartActivityError::Other,
2101 )?,
2102 heartbeat_timeout: try_into_or_box_err(
2103 options.heartbeat_timeout,
2104 StartActivityError::Other,
2105 )?,
2106 retry_policy: options.retry_policy.map(Into::into),
2107 input: dc.to_payloads(sc, &input).await?.into_payloads(),
2108 id_reuse_policy: ProtoActivityIdReusePolicy::from(options.id_reuse_policy)
2109 .into(),
2110 id_conflict_policy: ProtoActivityIdConflictPolicy::from(
2111 options.id_conflict_policy,
2112 )
2113 .into(),
2114 search_attributes: options.search_attributes.map(SearchAttributes::into_proto),
2115 header: options.header,
2116 user_metadata,
2117 priority: Some(options.priority.into()),
2118 start_delay: try_into_or_box_err(
2119 options.start_delay,
2120 StartActivityError::Other,
2121 )?,
2122 ..Default::default()
2123 }
2124 .into_request(),
2125 )
2126 .await?
2127 .into_inner();
2128
2129 Ok(ActivityHandle::new(
2130 client,
2131 options.id,
2132 (!resp.run_id.is_empty()).then_some(resp.run_id),
2133 ))
2134 }
2135
2136 fn get_activity_handle<A>(
2137 &self,
2138 _activity: A,
2139 id: impl Into<String>,
2140 run_id: Option<String>,
2141 ) -> ActivityHandle<Self, A>
2142 where
2143 Self: Sized,
2144 A: ActivityDefinition,
2145 {
2146 ActivityHandle::new(self.clone(), id.into(), run_id)
2147 }
2148
2149 fn get_untyped_activity_handle(
2150 &self,
2151 id: impl Into<String>,
2152 run_id: Option<String>,
2153 ) -> ActivityHandle<Self, UntypedActivity>
2154 where
2155 Self: Sized,
2156 {
2157 ActivityHandle::new(self.clone(), id.into(), run_id)
2158 }
2159
2160 fn list_activities(
2161 &self,
2162 query: impl Into<String>,
2163 _options: ActivityListOptions,
2164 ) -> ListActivitiesStream {
2165 let client = self.clone();
2166 let namespace = client.namespace();
2167 let query = query.into();
2168
2169 ListActivitiesStream::new(stream::unfold(
2170 Some(vec![]), move |next_page_token| {
2172 let mut client = client.clone();
2173 let namespace = namespace.clone();
2174 let query = query.clone();
2175
2176 async move {
2177 #[allow(clippy::question_mark)]
2179 let Some(token): Option<Vec<u8>> = next_page_token else {
2180 return None;
2181 };
2182
2183 match WorkflowService::list_activity_executions(
2184 &mut client,
2185 ListActivityExecutionsRequest {
2186 namespace,
2187 page_size: 0, next_page_token: token.clone(),
2189 query,
2190 }
2191 .into_request(),
2192 )
2193 .await
2194 .map(|r| r.into_inner())
2195 {
2196 Ok(resp) => Some((
2197 Ok(resp.executions),
2198 (!resp.next_page_token.is_empty()).then_some(resp.next_page_token),
2199 )),
2200 Err(e) => Some((Err(e.into()), Some(token))),
2201 }
2202 }
2203 },
2204 ))
2205 }
2206
2207 async fn count_activities(
2208 &self,
2209 query: impl Into<String>,
2210 _options: ActivityCountOptions,
2211 ) -> Result<ActivityExecutionCount, ClientError> {
2212 let mut client = self.clone();
2213 let resp = client
2214 .count_activity_executions(
2215 CountActivityExecutionsRequest {
2216 namespace: client.namespace(),
2217 query: query.into(),
2218 }
2219 .into_request(),
2220 )
2221 .await?
2222 .into_inner();
2223 Ok(ActivityExecutionCount::from_response(resp))
2224 }
2225}
2226
2227macro_rules! dbg_panic {
2228 ($($arg:tt)*) => {
2229 use tracing::error;
2230 error!($($arg)*);
2231 debug_assert!(false, $($arg)*);
2232 };
2233}
2234pub(crate) use dbg_panic;
2235
2236fn try_into_or_box_err<A, B, E, MapErr>(val: Option<A>, map_err: MapErr) -> Result<Option<B>, E>
2237where
2238 A: TryInto<B>,
2239 <A as TryInto<B>>::Error: Error + Send + Sync + 'static,
2240 MapErr: FnOnce(Box<dyn Error + Send + Sync + 'static>) -> E,
2241{
2242 val.map(TryInto::try_into)
2243 .transpose()
2244 .map_err(|e| map_err(Box::from(e)))
2245}
2246
2247#[cfg(test)]
2248mod tests {
2249 use super::*;
2250 use crate::callback_based::CallbackBasedGrpcService;
2251 use std::{
2252 sync::atomic::{AtomicUsize, Ordering},
2253 time::Instant,
2254 };
2255 use temporalio_common::search_attributes::SearchAttributeKey;
2256 use tonic::{Status, metadata::Ascii};
2257 use url::Url;
2258
2259 #[test]
2260 fn count_aggregation_group_gets_typed_value() {
2261 let attrs = SearchAttributes::new([SearchAttributeKey::int("group").value_set(42)]);
2262 let group = WorkflowCountAggregationGroup {
2263 raw: count_workflow_executions_response::AggregationGroup {
2264 group_values: vec![attrs.raw_payload("group").unwrap().clone()],
2265 count: 1,
2266 },
2267 };
2268
2269 assert_eq!(group.get::<i64>(0), Some(42));
2270 assert_eq!(group.get::<i64>(1), None);
2271 assert!(group.try_get::<String>(0).is_err());
2272 assert_eq!(group.try_get::<i64>(1).unwrap(), None);
2273 }
2274
2275 fn connection_options_for_system_info_test(
2276 service_override: CallbackBasedGrpcService,
2277 ) -> ConnectionOptions {
2278 ConnectionOptions::new(Url::parse("http://localhost:7233").unwrap())
2279 .service_override(service_override)
2280 .dns_load_balancing(None)
2281 .build()
2282 }
2283
2284 #[test]
2285 fn applies_headers() {
2286 let headers = Arc::new(RwLock::new(ClientHeaders {
2288 user_headers: HashMap::new(),
2289 user_binary_headers: HashMap::new(),
2290 api_key: Some("my-api-key".to_owned()),
2291 }));
2292 headers.clone().write().user_headers.insert(
2293 "my-meta-key".parse().unwrap(),
2294 "my-meta-val".parse().unwrap(),
2295 );
2296 headers.clone().write().user_binary_headers.insert(
2297 "my-bin-meta-key-bin".parse().unwrap(),
2298 vec![1, 2, 3].try_into().unwrap(),
2299 );
2300 let mut interceptor = ServiceCallInterceptor {
2301 client_name: "cute-kitty".to_string(),
2302 client_version: "0.1.0".to_string(),
2303 headers: headers.clone(),
2304 };
2305
2306 let req = interceptor.call(tonic::Request::new(())).unwrap();
2308 assert_eq!(req.metadata().get("my-meta-key").unwrap(), "my-meta-val");
2309 assert_eq!(
2310 req.metadata().get("authorization").unwrap(),
2311 "Bearer my-api-key"
2312 );
2313 assert_eq!(
2314 req.metadata().get_bin("my-bin-meta-key-bin").unwrap(),
2315 vec![1, 2, 3].as_slice()
2316 );
2317
2318 let mut req = tonic::Request::new(());
2320 req.metadata_mut()
2321 .insert("my-meta-key", "my-meta-val2".parse().unwrap());
2322 req.metadata_mut()
2323 .insert("authorization", "my-api-key2".parse().unwrap());
2324 req.metadata_mut()
2325 .insert_bin("my-bin-meta-key-bin", vec![4, 5, 6].try_into().unwrap());
2326 let req = interceptor.call(req).unwrap();
2327 assert_eq!(req.metadata().get("my-meta-key").unwrap(), "my-meta-val2");
2328 assert_eq!(req.metadata().get("authorization").unwrap(), "my-api-key2");
2329 assert_eq!(
2330 req.metadata().get_bin("my-bin-meta-key-bin").unwrap(),
2331 vec![4, 5, 6].as_slice()
2332 );
2333
2334 headers.clone().write().user_headers.insert(
2336 "authorization".parse().unwrap(),
2337 "my-api-key3".parse().unwrap(),
2338 );
2339 let req = interceptor.call(tonic::Request::new(())).unwrap();
2340 assert_eq!(req.metadata().get("my-meta-key").unwrap(), "my-meta-val");
2341 assert_eq!(req.metadata().get("authorization").unwrap(), "my-api-key3");
2342
2343 headers.clone().write().user_headers.clear();
2345 headers.clone().write().user_binary_headers.clear();
2346 headers.clone().write().api_key.take();
2347 let req = interceptor.call(tonic::Request::new(())).unwrap();
2348 assert!(!req.metadata().contains_key("my-meta-key"));
2349 assert!(!req.metadata().contains_key("authorization"));
2350 assert!(!req.metadata().contains_key("my-bin-meta-key-bin"));
2351
2352 let mut req = tonic::Request::new(());
2354 req.metadata_mut()
2355 .insert("grpc-timeout", "1S".parse().unwrap());
2356 let req = interceptor.call(req).unwrap();
2357 assert_eq!(
2358 req.metadata().get("grpc-timeout").unwrap(),
2359 "1S".parse::<MetadataValue<Ascii>>().unwrap()
2360 );
2361 }
2362
2363 #[test]
2364 fn invalid_ascii_header_key() {
2365 let invalid_headers = {
2366 let mut h = HashMap::new();
2367 h.insert("x-binary-key-bin".to_owned(), "value".to_owned());
2368 h
2369 };
2370
2371 let result = parse_ascii_headers(invalid_headers);
2372 assert!(result.is_err());
2373 assert_eq!(
2374 result.err().unwrap().to_string(),
2375 "Invalid ASCII header key 'x-binary-key-bin': invalid gRPC metadata key name"
2376 );
2377 }
2378
2379 #[test]
2380 fn invalid_ascii_header_value() {
2381 let invalid_headers = {
2382 let mut h = HashMap::new();
2383 h.insert("x-ascii-key".to_owned(), "\x00value".to_owned());
2385 h
2386 };
2387
2388 let result = parse_ascii_headers(invalid_headers);
2389 assert!(result.is_err());
2390 assert_eq!(
2391 result.err().unwrap().to_string(),
2392 "Invalid ASCII header value for key 'x-ascii-key': failed to parse metadata value"
2393 );
2394 }
2395
2396 #[test]
2397 fn invalid_binary_header_key() {
2398 let invalid_headers = {
2399 let mut h = HashMap::new();
2400 h.insert("x-ascii-key".to_owned(), vec![1, 2, 3]);
2401 h
2402 };
2403
2404 let result = parse_binary_headers(invalid_headers);
2405 assert!(result.is_err());
2406 assert_eq!(
2407 result.err().unwrap().to_string(),
2408 "Invalid binary header key 'x-ascii-key': invalid gRPC metadata key name"
2409 );
2410 }
2411
2412 #[test]
2413 fn keep_alive_defaults() {
2414 let opts = ConnectionOptions::new(Url::parse("https://smolkitty").unwrap())
2415 .identity("enchicat".to_string())
2416 .client_name("cute-kitty".to_string())
2417 .client_version("0.1.0".to_string())
2418 .build();
2419 assert_eq!(
2420 opts.keep_alive.clone().unwrap().interval,
2421 ClientKeepAliveOptions::default().interval
2422 );
2423 assert_eq!(
2424 opts.keep_alive.clone().unwrap().timeout,
2425 ClientKeepAliveOptions::default().timeout
2426 );
2427
2428 let opts = ConnectionOptions::new(Url::parse("https://smolkitty").unwrap())
2430 .identity("enchicat".to_string())
2431 .client_name("cute-kitty".to_string())
2432 .client_version("0.1.0".to_string())
2433 .keep_alive(None)
2434 .build();
2435 dbg!(&opts.keep_alive);
2436 assert!(opts.keep_alive.is_none());
2437 }
2438
2439 #[rstest::rstest]
2440 #[case(
2441 "unknown method GetSystemInfo for service temporal.api.workflowservice.v1.WorkflowService"
2442 )]
2443 #[case("Method temporal.api.workflowservice.v1.WorkflowService/GetSystemInfo is unimplemented")]
2444 #[case(
2445 "The server does not implement the method /temporal.api.workflowservice.v1.WorkflowService/GetSystemInfo"
2446 )]
2447 #[tokio::test]
2448 async fn get_system_info_missing_method_falls_back_to_empty_capabilities(
2449 #[case] message: &'static str,
2450 ) {
2451 let attempts = Arc::new(AtomicUsize::new(0));
2452 let attempts_clone = attempts.clone();
2453 let service_override = CallbackBasedGrpcService {
2454 callback: Arc::new(move |req| {
2455 let attempts = attempts_clone.clone();
2456 Box::pin(async move {
2457 assert_eq!(req.rpc, "GetSystemInfo");
2458 attempts.fetch_add(1, Ordering::SeqCst);
2459 Err(Status::unimplemented(message))
2460 })
2461 }),
2462 };
2463
2464 let connection =
2465 Connection::connect(connection_options_for_system_info_test(service_override))
2466 .await
2467 .unwrap();
2468
2469 assert!(connection.capabilities().is_none());
2470 assert_eq!(attempts.load(Ordering::SeqCst), 1);
2471 }
2472
2473 #[tokio::test]
2474 async fn get_system_info_non_missing_unimplemented_fails_connect() {
2475 let attempts = Arc::new(AtomicUsize::new(0));
2476 let attempts_clone = attempts.clone();
2477 let service_override = CallbackBasedGrpcService {
2478 callback: Arc::new(move |req| {
2479 let attempts = attempts_clone.clone();
2480 Box::pin(async move {
2481 assert_eq!(req.rpc, "GetSystemInfo");
2482 attempts.fetch_add(1, Ordering::SeqCst);
2483 Err(Status::unimplemented("backend temporarily unimplemented"))
2484 })
2485 }),
2486 };
2487
2488 let err =
2489 match Connection::connect(connection_options_for_system_info_test(service_override))
2490 .await
2491 {
2492 Ok(_) => panic!("connection should fail"),
2493 Err(err) => err,
2494 };
2495
2496 assert!(matches!(
2497 err,
2498 ClientConnectError::SystemInfoCallError(status)
2499 if status.code() == Code::Unimplemented
2500 && status.message() == "backend temporarily unimplemented"
2501 ));
2502 assert_eq!(attempts.load(Ordering::SeqCst), 1);
2503 }
2504
2505 #[tokio::test]
2506 async fn connect_timeout_bounds_connection_attempt() {
2507 let url = Url::parse("http://10.255.255.1:7233").unwrap();
2508 let opts = ConnectionOptions::new(url)
2509 .connect_timeout(Duration::from_millis(500))
2510 .build();
2511 let start = Instant::now();
2512 let result = Connection::connect(opts).await;
2513 assert!(result.is_err(), "connection should fail");
2514 assert!(start.elapsed() < Duration::from_secs(2));
2515 }
2516
2517 mod tls_custom_verifier_tests {
2518 use super::*;
2519 use tokio_rustls::rustls::{
2520 DigitallySignedStruct, Error as RustlsError, SignatureScheme,
2521 client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
2522 pki_types::{CertificateDer, ServerName, UnixTime},
2523 };
2524
2525 #[derive(Debug)]
2528 struct MockVerifier;
2529
2530 impl ServerCertVerifier for MockVerifier {
2531 fn verify_server_cert(
2532 &self,
2533 _end_entity: &CertificateDer<'_>,
2534 _intermediates: &[CertificateDer<'_>],
2535 _server_name: &ServerName<'_>,
2536 _ocsp_response: &[u8],
2537 _now: UnixTime,
2538 ) -> Result<ServerCertVerified, RustlsError> {
2539 Ok(ServerCertVerified::assertion())
2540 }
2541
2542 fn verify_tls12_signature(
2543 &self,
2544 _message: &[u8],
2545 _cert: &CertificateDer<'_>,
2546 _dss: &DigitallySignedStruct,
2547 ) -> Result<HandshakeSignatureValid, RustlsError> {
2548 Ok(HandshakeSignatureValid::assertion())
2549 }
2550
2551 fn verify_tls13_signature(
2552 &self,
2553 _message: &[u8],
2554 _cert: &CertificateDer<'_>,
2555 _dss: &DigitallySignedStruct,
2556 ) -> Result<HandshakeSignatureValid, RustlsError> {
2557 Ok(HandshakeSignatureValid::assertion())
2558 }
2559
2560 fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
2561 vec![
2562 SignatureScheme::ECDSA_NISTP256_SHA256,
2563 SignatureScheme::RSA_PSS_SHA256,
2564 ]
2565 }
2566 }
2567
2568 #[tokio::test]
2569 async fn add_tls_to_channel_with_custom_verifier() {
2570 let tls_opts = TlsOptions::builder()
2571 .server_cert_verifier(Arc::new(MockVerifier))
2572 .domain("test.temporal.io".to_string())
2573 .build();
2574 let endpoint = tonic::transport::Channel::from_static("https://test.temporal.io:7233");
2575 let result = add_tls_to_channel(Some(&tls_opts), endpoint).await;
2576 assert!(
2577 matches!(&result, Ok(TlsConfigResult::Standard(_))),
2578 "add_tls_to_channel should succeed with a custom verifier: {:?}",
2579 result.err()
2580 );
2581 }
2582
2583 #[tokio::test]
2584 async fn add_tls_to_channel_with_verifier_and_ca_cert_fails() {
2585 let tls_opts = TlsOptions::builder()
2588 .server_root_ca_cert(b"some-ca-cert-bytes".to_vec())
2589 .server_cert_verifier(Arc::new(MockVerifier))
2590 .domain("test.temporal.io".to_string())
2591 .build();
2592 let endpoint = tonic::transport::Channel::from_static("https://test.temporal.io:7233");
2593 let result = add_tls_to_channel(Some(&tls_opts), endpoint).await;
2594 assert!(
2595 matches!(result, Err(ClientConnectError::InvalidConfig(_))),
2596 "add_tls_to_channel should fail with InvalidConfig when both CA cert and verifier are set: {:?}",
2597 result
2598 );
2599 }
2600
2601 #[tokio::test]
2602 async fn add_tls_to_channel_without_verifier_still_works() {
2603 let tls_opts = TlsOptions::builder()
2605 .domain("test.temporal.io".to_string())
2606 .build();
2607 let endpoint = tonic::transport::Channel::from_static("https://test.temporal.io:7233");
2608 let result = add_tls_to_channel(Some(&tls_opts), endpoint).await;
2609 assert!(
2610 matches!(&result, Ok(TlsConfigResult::Standard(_))),
2611 "add_tls_to_channel should succeed without a verifier (native roots): {:?}",
2612 result.err()
2613 );
2614 }
2615
2616 #[cfg(feature = "dynamic-tls")]
2619 mod dynamic_cert_tests {
2620 use super::*;
2621
2622 #[derive(Debug)]
2625 struct MockClientCertResolver;
2626
2627 impl tokio_rustls::rustls::client::ResolvesClientCert for MockClientCertResolver {
2628 fn resolve(
2629 &self,
2630 _acceptable_issuers: &[&[u8]],
2631 _sigschemes: &[tokio_rustls::rustls::SignatureScheme],
2632 ) -> Option<Arc<tokio_rustls::rustls::sign::CertifiedKey>> {
2633 None }
2635
2636 fn has_certs(&self) -> bool {
2637 false
2638 }
2639 }
2640
2641 #[tokio::test]
2642 async fn add_tls_with_client_cert_resolver_returns_custom_connector() {
2643 let resolver = Arc::new(MockClientCertResolver);
2644 let tls_opts = TlsOptions {
2645 client_cert_resolver: Some(resolver),
2646 domain: Some("test.temporal.io".to_string()),
2647 ..Default::default()
2648 };
2649 let endpoint =
2650 tonic::transport::Channel::from_static("https://test.temporal.io:7233");
2651 let result = add_tls_to_channel(Some(&tls_opts), endpoint).await;
2652 match result {
2653 Ok(TlsConfigResult::CustomConnector {
2654 domain,
2655 rustls_config,
2656 ..
2657 }) => {
2658 assert_eq!(domain, "test.temporal.io");
2659 assert_eq!(rustls_config.alpn_protocols, vec![b"h2".to_vec()]);
2661 }
2662 other => panic!(
2663 "Expected TlsConfigResult::CustomConnector, got {:?}",
2664 other.err()
2665 ),
2666 }
2667 }
2668
2669 #[tokio::test]
2670 async fn add_tls_with_client_cert_resolver_inherits_domain_from_endpoint() {
2671 let resolver = Arc::new(MockClientCertResolver);
2672 let tls_opts = TlsOptions {
2673 client_cert_resolver: Some(resolver),
2674 ..Default::default()
2676 };
2677 let endpoint =
2678 tonic::transport::Channel::from_static("https://my-server.example.com:7233");
2679 let result = add_tls_to_channel(Some(&tls_opts), endpoint).await;
2680 match result {
2681 Ok(TlsConfigResult::CustomConnector { domain, .. }) => {
2682 assert_eq!(domain, "my-server.example.com");
2683 }
2684 other => panic!(
2685 "Expected TlsConfigResult::CustomConnector, got {:?}",
2686 other.err()
2687 ),
2688 }
2689 }
2690
2691 #[tokio::test]
2692 async fn add_tls_with_resolver_and_custom_verifier() {
2693 let resolver = Arc::new(MockClientCertResolver);
2694 let tls_opts = TlsOptions {
2695 client_cert_resolver: Some(resolver),
2696 server_cert_verifier: Some(Arc::new(MockVerifier)),
2697 domain: Some("test.temporal.io".to_string()),
2698 ..Default::default()
2699 };
2700 let endpoint =
2701 tonic::transport::Channel::from_static("https://test.temporal.io:7233");
2702 let result = add_tls_to_channel(Some(&tls_opts), endpoint).await;
2703 assert!(
2704 matches!(&result, Ok(TlsConfigResult::CustomConnector { .. })),
2705 "Should succeed when combining cert resolver with custom server verifier: {:?}",
2706 result.err()
2707 );
2708 }
2709
2710 #[tokio::test]
2711 async fn add_tls_with_resolver_and_custom_ca_cert() {
2712 let ca_pem = include_bytes!("../tests/testdata/ca.pem");
2714 let resolver = Arc::new(MockClientCertResolver);
2715 let tls_opts = TlsOptions {
2716 client_cert_resolver: Some(resolver),
2717 server_root_ca_cert: Some(ca_pem.to_vec()),
2718 domain: Some("test.temporal.io".to_string()),
2719 ..Default::default()
2720 };
2721 let endpoint =
2722 tonic::transport::Channel::from_static("https://test.temporal.io:7233");
2723 let result = add_tls_to_channel(Some(&tls_opts), endpoint).await;
2724 assert!(
2725 matches!(&result, Ok(TlsConfigResult::CustomConnector { .. })),
2726 "Should succeed when combining cert resolver with custom CA cert: {:?}",
2727 result.err()
2728 );
2729 }
2730
2731 #[tokio::test]
2732 async fn add_tls_both_static_and_dynamic_client_cert_fails() {
2733 let resolver = Arc::new(MockClientCertResolver);
2734 let tls_opts = TlsOptions {
2735 client_tls_options: Some(ClientTlsOptions {
2736 client_cert: b"some-cert".to_vec(),
2737 client_private_key: b"some-key".to_vec(),
2738 }),
2739 client_cert_resolver: Some(resolver),
2740 domain: Some("test.temporal.io".to_string()),
2741 ..Default::default()
2742 };
2743 let endpoint =
2744 tonic::transport::Channel::from_static("https://test.temporal.io:7233");
2745 let result = add_tls_to_channel(Some(&tls_opts), endpoint).await;
2746 assert!(
2747 matches!(result, Err(ClientConnectError::InvalidConfig(msg)) if msg.contains("client_tls_options") && msg.contains("client_cert_resolver")),
2748 "Should fail with InvalidConfig when both static and dynamic client certs are set"
2749 );
2750 }
2751
2752 #[tokio::test]
2753 async fn add_tls_no_options_returns_standard_passthrough() {
2754 let endpoint = tonic::transport::Channel::from_static("http://localhost:7233");
2755 let result = add_tls_to_channel(None, endpoint).await;
2756 assert!(
2757 matches!(&result, Ok(TlsConfigResult::Standard(_))),
2758 "Should return Standard when no TLS options are set"
2759 );
2760 }
2761
2762 #[test]
2763 fn build_custom_rustls_config_with_resolver() {
2764 let resolver = Arc::new(MockClientCertResolver);
2765 let tls_opts = TlsOptions {
2766 domain: Some("test.temporal.io".to_string()),
2767 ..Default::default()
2768 };
2769 let config = build_custom_rustls_config(&tls_opts, Some(resolver));
2770 assert!(config.is_ok(), "Should build config: {:?}", config.err());
2771 let config = config.unwrap();
2772 assert_eq!(config.alpn_protocols, vec![b"h2".to_vec()]);
2773 }
2774
2775 #[test]
2776 fn build_custom_rustls_config_without_resolver() {
2777 let tls_opts = TlsOptions {
2778 domain: Some("test.temporal.io".to_string()),
2779 ..Default::default()
2780 };
2781 let config = build_custom_rustls_config(&tls_opts, None);
2782 assert!(config.is_ok(), "Should build config: {:?}", config.err());
2783 }
2784
2785 #[test]
2786 fn build_custom_rustls_config_with_custom_verifier_and_resolver() {
2787 let resolver = Arc::new(MockClientCertResolver);
2788 let tls_opts = TlsOptions {
2789 server_cert_verifier: Some(Arc::new(MockVerifier)),
2790 domain: Some("test.temporal.io".to_string()),
2791 ..Default::default()
2792 };
2793 let config = build_custom_rustls_config(&tls_opts, Some(resolver));
2794 assert!(
2795 config.is_ok(),
2796 "Should build config with custom verifier + resolver: {:?}",
2797 config.err()
2798 );
2799 }
2800
2801 #[test]
2802 fn tls_options_debug_shows_custom_for_resolver() {
2803 let resolver = Arc::new(MockClientCertResolver);
2804 let tls_opts = TlsOptions {
2805 client_cert_resolver: Some(resolver),
2806 ..Default::default()
2807 };
2808 let debug_str = format!("{:?}", tls_opts);
2809 assert!(
2810 debug_str.contains("\"<custom>\""),
2811 "Debug should show <custom> for client_cert_resolver: {debug_str}"
2812 );
2813 assert!(
2814 debug_str.contains("client_cert_resolver"),
2815 "Debug should contain field name: {debug_str}"
2816 );
2817 }
2818
2819 #[test]
2820 fn tls_options_default_has_no_resolver() {
2821 let tls_opts = TlsOptions::default();
2822 assert!(tls_opts.client_cert_resolver.is_none());
2823 assert!(tls_opts.client_tls_options.is_none());
2824 assert!(tls_opts.server_cert_verifier.is_none());
2825 }
2826
2827 #[tokio::test]
2828 async fn add_tls_resolver_with_ip_host_uses_ip_as_domain() {
2829 let resolver = Arc::new(MockClientCertResolver);
2832 let tls_opts = TlsOptions {
2833 client_cert_resolver: Some(resolver),
2834 ..Default::default()
2836 };
2837 let endpoint = tonic::transport::Channel::from_static("https://192.168.1.100:7233");
2838 let result = add_tls_to_channel(Some(&tls_opts), endpoint).await;
2839 match result {
2840 Ok(TlsConfigResult::CustomConnector { domain, .. }) => {
2841 assert_eq!(domain, "192.168.1.100");
2842 }
2843 other => panic!(
2844 "Expected CustomConnector with IP domain, got {:?}",
2845 other.err()
2846 ),
2847 }
2848 }
2849 }
2850 }
2851
2852 mod start_workflow_interceptor_tests {
2853 use super::*;
2854 use crate::request_extensions::RetryConfigForCall;
2855 use parking_lot::Mutex;
2856 use std::sync::atomic::{AtomicUsize, Ordering};
2857 use temporalio_common::{
2858 HasWorkflowDefinition, WorkflowDefinition,
2859 data_converters::{
2860 DefaultFailureConverter, PayloadCodec, PayloadConversionError,
2861 SerializationContext, SerializationContextData, TemporalSerializable,
2862 },
2863 protos::temporal::api::common::v1::Payload,
2864 };
2865 use tonic::{Request, Response};
2866
2867 struct TestWorkflow;
2868
2869 impl WorkflowDefinition for TestWorkflow {
2870 type Input = Vec<String>;
2871 type Output = ();
2872
2873 fn name(&self) -> &str {
2874 "test-workflow"
2875 }
2876 }
2877
2878 impl HasWorkflowDefinition for TestWorkflow {
2879 type Run = Self;
2880 }
2881
2882 #[derive(Default)]
2883 struct RecordedStart {
2884 calls: usize,
2885 workflow_type: String,
2886 payloads: Vec<Payload>,
2887 ascii_metadata: Option<String>,
2888 binary_metadata: Option<Vec<u8>>,
2889 grpc_timeout: Option<String>,
2890 retry_options: Option<RetryOptions>,
2891 }
2892
2893 struct CountingCodec {
2894 encode_calls: Arc<AtomicUsize>,
2895 }
2896
2897 impl PayloadCodec for CountingCodec {
2898 fn encode(
2899 &self,
2900 _context: &SerializationContextData,
2901 payloads: Vec<Payload>,
2902 ) -> futures_util::future::BoxFuture<
2903 'static,
2904 Result<Vec<Payload>, PayloadConversionError>,
2905 > {
2906 self.encode_calls.fetch_add(1, Ordering::SeqCst);
2907 Box::pin(async move { Ok(payloads) })
2908 }
2909
2910 fn decode(
2911 &self,
2912 _context: &SerializationContextData,
2913 payloads: Vec<Payload>,
2914 ) -> futures_util::future::BoxFuture<
2915 'static,
2916 Result<Vec<Payload>, PayloadConversionError>,
2917 > {
2918 Box::pin(async move { Ok(payloads) })
2919 }
2920 }
2921
2922 #[derive(Clone)]
2923 struct MockStartWorkflowClient {
2924 recorded: Arc<Mutex<RecordedStart>>,
2925 data_converter: DataConverter,
2926 }
2927
2928 impl NamespacedClient for MockStartWorkflowClient {
2929 fn namespace(&self) -> String {
2930 "test-namespace".to_owned()
2931 }
2932
2933 fn identity(&self) -> String {
2934 "test-identity".to_owned()
2935 }
2936
2937 fn data_converter(&self) -> &DataConverter {
2938 &self.data_converter
2939 }
2940 }
2941
2942 impl WorkflowService for MockStartWorkflowClient {
2943 fn start_workflow_execution(
2944 &mut self,
2945 request: Request<StartWorkflowExecutionRequest>,
2946 ) -> futures_util::future::BoxFuture<
2947 '_,
2948 Result<Response<StartWorkflowExecutionResponse>, tonic::Status>,
2949 > {
2950 let ascii_metadata = request
2951 .metadata()
2952 .get("call-meta")
2953 .map(|value| value.to_str().unwrap().to_owned());
2954 let binary_metadata = request
2955 .metadata()
2956 .get_bin("call-meta-bin")
2957 .map(|value| value.to_bytes().unwrap().to_vec());
2958 let grpc_timeout = request
2959 .metadata()
2960 .get("grpc-timeout")
2961 .map(|value| value.to_str().unwrap().to_owned());
2962 let retry_options = request
2963 .extensions()
2964 .get::<RetryConfigForCall>()
2965 .map(|config| config.0.clone());
2966 let request = request.into_inner();
2967 let mut recorded = self.recorded.lock();
2968 recorded.calls += 1;
2969 recorded.workflow_type = request.workflow_type.unwrap().name;
2970 recorded.payloads = request.input.unwrap_or_default().payloads;
2971 recorded.ascii_metadata = ascii_metadata;
2972 recorded.binary_metadata = binary_metadata;
2973 recorded.grpc_timeout = grpc_timeout;
2974 recorded.retry_options = retry_options;
2975
2976 Box::pin(async {
2977 Ok(Response::new(StartWorkflowExecutionResponse {
2978 run_id: "server-run-id".to_owned(),
2979 ..Default::default()
2980 }))
2981 })
2982 }
2983
2984 fn signal_with_start_workflow_execution(
2985 &mut self,
2986 request: Request<SignalWithStartWorkflowExecutionRequest>,
2987 ) -> futures_util::future::BoxFuture<
2988 '_,
2989 Result<Response<SignalWithStartWorkflowExecutionResponse>, tonic::Status>,
2990 > {
2991 let ascii_metadata = request
2992 .metadata()
2993 .get("call-meta")
2994 .map(|value| value.to_str().unwrap().to_owned());
2995 let binary_metadata = request
2996 .metadata()
2997 .get_bin("call-meta-bin")
2998 .map(|value| value.to_bytes().unwrap().to_vec());
2999 let grpc_timeout = request
3000 .metadata()
3001 .get("grpc-timeout")
3002 .map(|value| value.to_str().unwrap().to_owned());
3003 let retry_options = request
3004 .extensions()
3005 .get::<RetryConfigForCall>()
3006 .map(|config| config.0.clone());
3007 let request = request.into_inner();
3008 let mut recorded = self.recorded.lock();
3009 recorded.calls += 1;
3010 recorded.workflow_type = request.workflow_type.unwrap().name;
3011 recorded.payloads = request.input.unwrap_or_default().payloads;
3012 recorded.ascii_metadata = ascii_metadata;
3013 recorded.binary_metadata = binary_metadata;
3014 recorded.grpc_timeout = grpc_timeout;
3015 recorded.retry_options = retry_options;
3016
3017 Box::pin(async {
3018 Ok(Response::new(SignalWithStartWorkflowExecutionResponse {
3019 run_id: "signal-server-run-id".to_owned(),
3020 ..Default::default()
3021 }))
3022 })
3023 }
3024 }
3025
3026 #[derive(Clone)]
3027 struct InterceptedClient {
3028 inner: MockStartWorkflowClient,
3029 interceptors: Vec<Arc<dyn ClientInterceptor>>,
3030 }
3031
3032 impl NamespacedClient for InterceptedClient {
3033 fn namespace(&self) -> String {
3034 self.inner.namespace()
3035 }
3036
3037 fn identity(&self) -> String {
3038 self.inner.identity()
3039 }
3040
3041 fn data_converter(&self) -> &DataConverter {
3042 self.inner.data_converter()
3043 }
3044
3045 fn client_interceptors(&self) -> &[Arc<dyn ClientInterceptor>] {
3046 &self.interceptors
3047 }
3048 }
3049
3050 impl WorkflowService for InterceptedClient {
3051 fn start_workflow_execution(
3052 &mut self,
3053 request: Request<StartWorkflowExecutionRequest>,
3054 ) -> futures_util::future::BoxFuture<
3055 '_,
3056 Result<Response<StartWorkflowExecutionResponse>, tonic::Status>,
3057 > {
3058 self.inner.start_workflow_execution(request)
3059 }
3060
3061 fn signal_with_start_workflow_execution(
3062 &mut self,
3063 request: Request<SignalWithStartWorkflowExecutionRequest>,
3064 ) -> futures_util::future::BoxFuture<
3065 '_,
3066 Result<Response<SignalWithStartWorkflowExecutionResponse>, tonic::Status>,
3067 > {
3068 self.inner.signal_with_start_workflow_execution(request)
3069 }
3070 }
3071
3072 struct OrderedInterceptor {
3073 name: &'static str,
3074 events: Arc<Mutex<Vec<String>>>,
3075 encode_calls: Arc<AtomicUsize>,
3076 }
3077
3078 impl ClientInterceptor for OrderedInterceptor {
3079 fn start_workflow<'a>(
3080 &'a self,
3081 mut input: StartWorkflowInput,
3082 next: Next<
3083 'a,
3084 StartWorkflowInput,
3085 BoxFuture<'a, Result<StartWorkflowOutput, WorkflowStartError>>,
3086 >,
3087 ) -> BoxFuture<'a, Result<StartWorkflowOutput, WorkflowStartError>> {
3088 Box::pin(async move {
3089 assert_eq!(self.encode_calls.load(Ordering::SeqCst), 0);
3090 self.events.lock().push(format!("{}-pre", self.name));
3091 tokio::task::yield_now().await;
3092 if self.name == "outer" {
3093 input
3094 .args_mut::<Vec<String>>()
3095 .unwrap()
3096 .push("mutated".to_owned());
3097 } else {
3098 assert_eq!(
3099 input.args_ref::<Vec<String>>().unwrap(),
3100 &["initial".to_owned(), "mutated".to_owned()]
3101 );
3102 input.replace_args("replacement".to_owned());
3103 input.workflow_type = "replacement-workflow".to_owned();
3104 }
3105 let result = next.run(input).await;
3106 tokio::task::yield_now().await;
3107 self.events.lock().push(format!("{}-post", self.name));
3108 result
3109 })
3110 }
3111 }
3112
3113 struct ShortCircuitInterceptor;
3114
3115 impl ClientInterceptor for ShortCircuitInterceptor {
3116 fn start_workflow<'a>(
3117 &'a self,
3118 input: StartWorkflowInput,
3119 _next: Next<
3120 'a,
3121 StartWorkflowInput,
3122 BoxFuture<'a, Result<StartWorkflowOutput, WorkflowStartError>>,
3123 >,
3124 ) -> BoxFuture<'a, Result<StartWorkflowOutput, WorkflowStartError>> {
3125 assert_eq!(
3126 input.args_ref::<Vec<String>>().unwrap(),
3127 &["initial".to_owned()]
3128 );
3129 Box::pin(async {
3130 Ok(StartWorkflowOutput::new(
3131 "short-circuit-workflow-id",
3132 "short-circuit-run-id",
3133 ))
3134 })
3135 }
3136 }
3137
3138 struct CountingInput {
3139 conversion_calls: Arc<AtomicUsize>,
3140 }
3141
3142 impl TemporalSerializable for CountingInput {
3143 fn to_payloads(
3144 &self,
3145 _context: &SerializationContext<'_>,
3146 ) -> Result<Vec<Payload>, PayloadConversionError> {
3147 self.conversion_calls.fetch_add(1, Ordering::SeqCst);
3148 Ok(vec![Payload::default()])
3149 }
3150 }
3151
3152 struct ConversionTimingInterceptor {
3153 conversion_calls: Arc<AtomicUsize>,
3154 }
3155
3156 impl ClientInterceptor for ConversionTimingInterceptor {
3157 fn start_workflow<'a>(
3158 &'a self,
3159 mut input: StartWorkflowInput,
3160 next: Next<
3161 'a,
3162 StartWorkflowInput,
3163 BoxFuture<'a, Result<StartWorkflowOutput, WorkflowStartError>>,
3164 >,
3165 ) -> BoxFuture<'a, Result<StartWorkflowOutput, WorkflowStartError>> {
3166 input.replace_args(CountingInput {
3167 conversion_calls: self.conversion_calls.clone(),
3168 });
3169 let future = next.run(input);
3170 assert_eq!(self.conversion_calls.load(Ordering::SeqCst), 0);
3171 future
3172 }
3173 }
3174
3175 fn mock_client(
3176 interceptors: Vec<Arc<dyn ClientInterceptor>>,
3177 encode_calls: Arc<AtomicUsize>,
3178 ) -> (InterceptedClient, Arc<Mutex<RecordedStart>>) {
3179 let recorded = Arc::new(Mutex::new(RecordedStart::default()));
3180 let data_converter = DataConverter::new(
3181 PayloadConverter::default(),
3182 DefaultFailureConverter,
3183 CountingCodec {
3184 encode_calls: encode_calls.clone(),
3185 },
3186 );
3187 (
3188 InterceptedClient {
3189 inner: MockStartWorkflowClient {
3190 recorded: recorded.clone(),
3191 data_converter,
3192 },
3193 interceptors,
3194 },
3195 recorded,
3196 )
3197 }
3198
3199 #[tokio::test]
3200 async fn interceptors_order_mutate_replace_and_defer_conversion() {
3201 let events = Arc::new(Mutex::new(Vec::new()));
3202 let encode_calls = Arc::new(AtomicUsize::new(0));
3203 let interceptors: Vec<Arc<dyn ClientInterceptor>> = vec![
3204 Arc::new(OrderedInterceptor {
3205 name: "outer",
3206 events: events.clone(),
3207 encode_calls: encode_calls.clone(),
3208 }),
3209 Arc::new(OrderedInterceptor {
3210 name: "inner",
3211 events: events.clone(),
3212 encode_calls: encode_calls.clone(),
3213 }),
3214 ];
3215 let (client, recorded) = mock_client(interceptors, encode_calls.clone());
3216
3217 let handle = client
3218 .start_workflow(
3219 TestWorkflow,
3220 vec!["initial".to_owned()],
3221 WorkflowStartOptions::new("task-queue", "workflow-id").build(),
3222 )
3223 .await
3224 .unwrap();
3225
3226 assert_eq!(
3227 events.lock().as_slice(),
3228 ["outer-pre", "inner-pre", "inner-post", "outer-post"]
3229 );
3230 assert_eq!(encode_calls.load(Ordering::SeqCst), 1);
3231 assert_eq!(handle.run_id(), Some("server-run-id"));
3232 let payloads = {
3233 let recorded = recorded.lock();
3234 assert_eq!(recorded.calls, 1);
3235 assert_eq!(recorded.workflow_type, "replacement-workflow");
3236 recorded.payloads.clone()
3237 };
3238 let replacement: String = client
3239 .data_converter()
3240 .from_payloads(&SerializationContextData::Workflow, payloads)
3241 .await
3242 .unwrap();
3243 assert_eq!(replacement, "replacement");
3244 }
3245
3246 #[tokio::test]
3247 async fn interceptor_can_short_circuit() {
3248 let encode_calls = Arc::new(AtomicUsize::new(0));
3249 let (client, recorded) = mock_client(
3250 vec![Arc::new(ShortCircuitInterceptor)],
3251 encode_calls.clone(),
3252 );
3253 let handle = client
3254 .start_workflow(
3255 TestWorkflow,
3256 vec!["initial".to_owned()],
3257 WorkflowStartOptions::new("task-queue", "ignored-workflow-id").build(),
3258 )
3259 .await
3260 .unwrap();
3261
3262 assert_eq!(handle.info().workflow_id, "short-circuit-workflow-id");
3263 assert_eq!(handle.run_id(), Some("short-circuit-run-id"));
3264 assert_eq!(recorded.lock().calls, 0);
3265 assert_eq!(encode_calls.load(Ordering::SeqCst), 0);
3266 }
3267
3268 #[tokio::test]
3269 async fn payload_conversion_waits_for_next_future_poll() {
3270 let conversion_calls = Arc::new(AtomicUsize::new(0));
3271 let encode_calls = Arc::new(AtomicUsize::new(0));
3272 let recorded = Arc::new(Mutex::new(RecordedStart::default()));
3273 let data_converter = DataConverter::new(
3274 PayloadConverter::UseWrappers,
3275 DefaultFailureConverter,
3276 CountingCodec {
3277 encode_calls: encode_calls.clone(),
3278 },
3279 );
3280 let client = InterceptedClient {
3281 inner: MockStartWorkflowClient {
3282 recorded: recorded.clone(),
3283 data_converter,
3284 },
3285 interceptors: vec![Arc::new(ConversionTimingInterceptor {
3286 conversion_calls: conversion_calls.clone(),
3287 })],
3288 };
3289
3290 client
3291 .start_workflow(
3292 TestWorkflow,
3293 vec!["initial".to_owned()],
3294 WorkflowStartOptions::new("task-queue", "workflow-id").build(),
3295 )
3296 .await
3297 .unwrap();
3298
3299 assert_eq!(conversion_calls.load(Ordering::SeqCst), 1);
3300 assert_eq!(encode_calls.load(Ordering::SeqCst), 1);
3301 assert_eq!(recorded.lock().calls, 1);
3302 }
3303
3304 #[tokio::test]
3305 async fn custom_client_defaults_to_empty_chain() {
3306 let recorded = Arc::new(Mutex::new(RecordedStart::default()));
3307 let client = MockStartWorkflowClient {
3308 recorded: recorded.clone(),
3309 data_converter: DataConverter::default(),
3310 };
3311 assert!(client.client_interceptors().is_empty());
3312
3313 client
3314 .start_workflow(
3315 TestWorkflow,
3316 vec!["initial".to_owned()],
3317 WorkflowStartOptions::new("task-queue", "workflow-id").build(),
3318 )
3319 .await
3320 .unwrap();
3321 assert_eq!(recorded.lock().calls, 1);
3322 }
3323
3324 #[tokio::test]
3325 async fn rpc_options_reach_the_request() {
3326 let (client, recorded) = mock_client(Vec::new(), Arc::new(AtomicUsize::new(0)));
3327 let mut metadata = RpcMetadata::new();
3328 metadata.insert("call-meta", "call-value").unwrap();
3329 metadata
3330 .insert_binary("call-meta-bin", vec![0, 255])
3331 .unwrap();
3332 let rpc_options = RpcOptions::builder()
3333 .metadata(metadata)
3334 .timeout(Duration::from_millis(250))
3335 .retry_options(RetryOptions::no_retries())
3336 .build();
3337 let mut options = WorkflowStartOptions::new("task-queue", "workflow-id").build();
3338 options.rpc_options = rpc_options.clone();
3339
3340 client
3341 .start_workflow(TestWorkflow, vec!["initial".to_owned()], options)
3342 .await
3343 .unwrap();
3344
3345 {
3346 let recorded = recorded.lock();
3347 assert_eq!(recorded.ascii_metadata.as_deref(), Some("call-value"));
3348 assert_eq!(recorded.binary_metadata.as_deref(), Some(&[0, 255][..]));
3349 assert_eq!(recorded.grpc_timeout.as_deref(), Some("250000u"));
3350 assert_eq!(recorded.retry_options, Some(RetryOptions::no_retries()));
3351 }
3352
3353 let mut options = WorkflowStartOptions::new("task-queue", "signal-workflow-id").build();
3354 options.start_signal = Some(WorkflowStartSignal::new("signal-name").build());
3355 options.rpc_options = rpc_options;
3356 let handle = client
3357 .start_workflow(TestWorkflow, vec!["initial".to_owned()], options)
3358 .await
3359 .unwrap();
3360
3361 let recorded = recorded.lock();
3362 assert_eq!(recorded.calls, 2);
3363 assert_eq!(recorded.ascii_metadata.as_deref(), Some("call-value"));
3364 assert_eq!(recorded.binary_metadata.as_deref(), Some(&[0, 255][..]));
3365 assert_eq!(recorded.grpc_timeout.as_deref(), Some("250000u"));
3366 assert_eq!(recorded.retry_options, Some(RetryOptions::no_retries()));
3367 assert_eq!(handle.run_id(), Some("signal-server-run-id"));
3368 }
3369
3370 #[test]
3371 fn rpc_metadata_combines_with_and_overrides_connection_defaults() {
3372 let headers = Arc::new(RwLock::new(ClientHeaders {
3373 user_headers: HashMap::from([
3374 (
3375 "shared-meta".parse().unwrap(),
3376 "connection-value".parse().unwrap(),
3377 ),
3378 (
3379 "connection-meta".parse().unwrap(),
3380 "connection-only".parse().unwrap(),
3381 ),
3382 ]),
3383 user_binary_headers: HashMap::from([
3384 (
3385 "shared-meta-bin".parse().unwrap(),
3386 BinaryMetadataValue::from_bytes(&[1]),
3387 ),
3388 (
3389 "connection-meta-bin".parse().unwrap(),
3390 BinaryMetadataValue::from_bytes(&[2]),
3391 ),
3392 ]),
3393 api_key: None,
3394 }));
3395 let mut service_interceptor = ServiceCallInterceptor {
3396 client_name: "test-client".to_owned(),
3397 client_version: "test-version".to_owned(),
3398 headers,
3399 };
3400 let mut rpc_options = RpcOptions::default();
3401 rpc_options
3402 .metadata
3403 .insert("shared-meta", "call-value")
3404 .unwrap();
3405 rpc_options
3406 .metadata
3407 .insert("call-meta", "call-only")
3408 .unwrap();
3409 rpc_options
3410 .metadata
3411 .insert_binary("shared-meta-bin", vec![3])
3412 .unwrap();
3413 rpc_options
3414 .metadata
3415 .insert_binary("call-meta-bin", vec![4])
3416 .unwrap();
3417 let mut request = Request::new(());
3418 rpc_options.apply_to(&mut request);
3419
3420 let request = service_interceptor.call(request).unwrap();
3421 assert_eq!(request.metadata().get("shared-meta").unwrap(), "call-value");
3422 assert_eq!(request.metadata().get("call-meta").unwrap(), "call-only");
3423 assert_eq!(
3424 request.metadata().get("connection-meta").unwrap(),
3425 "connection-only"
3426 );
3427 assert_eq!(
3428 request.metadata().get_bin("shared-meta-bin").unwrap(),
3429 &[3][..]
3430 );
3431 assert_eq!(
3432 request.metadata().get_bin("call-meta-bin").unwrap(),
3433 &[4][..]
3434 );
3435 assert_eq!(
3436 request.metadata().get_bin("connection-meta-bin").unwrap(),
3437 &[2][..]
3438 );
3439 }
3440 }
3441
3442 mod list_workflows_tests {
3443 use super::*;
3444 use crate::test_helpers::{FailingCodec, XorCodec};
3445 use futures_util::{FutureExt, StreamExt};
3446 use std::sync::atomic::{AtomicUsize, Ordering};
3447 use temporalio_common::{
3448 data_converters::DefaultFailureConverter,
3449 protos::temporal::api::common::v1::{
3450 Memo as ProtoMemo, Payload, WorkflowExecution as ProtoWorkflowExecution,
3451 },
3452 };
3453 use tonic::{Request, Response};
3454
3455 #[derive(Clone)]
3456 struct MockListWorkflowsClient {
3457 call_count: Arc<AtomicUsize>,
3458 page_size: usize,
3460 total_workflows: usize,
3462 data_converter: DataConverter,
3463 memo_payload: Option<Payload>,
3464 interceptors: Vec<Arc<dyn ClientInterceptor>>,
3465 }
3466
3467 impl NamespacedClient for MockListWorkflowsClient {
3468 fn namespace(&self) -> String {
3469 "test-namespace".to_string()
3470 }
3471 fn identity(&self) -> String {
3472 "test-identity".to_string()
3473 }
3474 fn data_converter(&self) -> &DataConverter {
3475 &self.data_converter
3476 }
3477 fn client_interceptors(&self) -> &[Arc<dyn ClientInterceptor>] {
3478 &self.interceptors
3479 }
3480 }
3481
3482 struct CountingListInterceptor {
3483 calls: Arc<AtomicUsize>,
3484 }
3485
3486 impl ClientInterceptor for CountingListInterceptor {
3487 fn list_workflows_page<'a>(
3488 &'a self,
3489 input: ListWorkflowsPageInput,
3490 next: Next<
3491 'a,
3492 ListWorkflowsPageInput,
3493 BoxFuture<'a, Result<ListWorkflowsPageOutput, ClientError>>,
3494 >,
3495 ) -> BoxFuture<'a, Result<ListWorkflowsPageOutput, ClientError>> {
3496 self.calls.fetch_add(1, Ordering::SeqCst);
3497 next.run(input)
3498 }
3499 }
3500
3501 impl WorkflowService for MockListWorkflowsClient {
3502 fn list_workflow_executions(
3503 &mut self,
3504 request: Request<ListWorkflowExecutionsRequest>,
3505 ) -> futures_util::future::BoxFuture<
3506 '_,
3507 Result<Response<ListWorkflowExecutionsResponse>, tonic::Status>,
3508 > {
3509 self.call_count.fetch_add(1, Ordering::SeqCst);
3510 let req = request.into_inner();
3511
3512 let offset: usize = if req.next_page_token.is_empty() {
3514 0
3515 } else {
3516 String::from_utf8(req.next_page_token)
3517 .unwrap()
3518 .parse()
3519 .unwrap()
3520 };
3521
3522 let remaining = self.total_workflows.saturating_sub(offset);
3523 let count = remaining.min(self.page_size);
3524 let new_offset = offset + count;
3525
3526 let executions: Vec<_> = (offset..offset + count)
3527 .map(|i| workflow::WorkflowExecutionInfo {
3528 execution: Some(ProtoWorkflowExecution {
3529 workflow_id: format!("wf-{i}"),
3530 run_id: format!("run-{i}"),
3531 }),
3532 r#type: Some(WorkflowType {
3533 name: "TestWorkflow".to_string(),
3534 }),
3535 task_queue: "test-queue".to_string(),
3536 memo: self.memo_payload.clone().map(|payload| ProtoMemo {
3537 fields: HashMap::from([("memo-key".to_owned(), payload)]),
3538 }),
3539 ..Default::default()
3540 })
3541 .collect();
3542
3543 let next_page_token = if new_offset < self.total_workflows {
3544 new_offset.to_string().into_bytes()
3545 } else {
3546 vec![]
3547 };
3548
3549 async move {
3550 Ok(Response::new(ListWorkflowExecutionsResponse {
3551 executions,
3552 next_page_token,
3553 }))
3554 }
3555 .boxed()
3556 }
3557 }
3558
3559 #[tokio::test]
3560 async fn list_workflows_paginates_through_all_results() {
3561 let call_count = Arc::new(AtomicUsize::new(0));
3562 let interceptor_calls = Arc::new(AtomicUsize::new(0));
3563 let client = MockListWorkflowsClient {
3564 call_count: call_count.clone(),
3565 page_size: 3,
3566 total_workflows: 10,
3567 data_converter: DataConverter::default(),
3568 memo_payload: None,
3569 interceptors: vec![Arc::new(CountingListInterceptor {
3570 calls: interceptor_calls.clone(),
3571 })],
3572 };
3573
3574 let stream = client.list_workflows("", WorkflowListOptions::default());
3575 let results: Vec<_> = stream.collect().await;
3576
3577 assert_eq!(results.len(), 10);
3578 for (i, result) in results.iter().enumerate() {
3579 let wf = result.as_ref().unwrap();
3580 assert_eq!(wf.id(), format!("wf-{i}"));
3581 assert_eq!(wf.run_id(), format!("run-{i}"));
3582 }
3583 assert_eq!(call_count.load(Ordering::SeqCst), 4);
3585 assert_eq!(interceptor_calls.load(Ordering::SeqCst), 4);
3586 }
3587
3588 #[tokio::test]
3589 async fn list_workflows_respects_limit() {
3590 let call_count = Arc::new(AtomicUsize::new(0));
3591 let client = MockListWorkflowsClient {
3592 call_count: call_count.clone(),
3593 page_size: 3,
3594 total_workflows: 10,
3595 data_converter: DataConverter::default(),
3596 memo_payload: None,
3597 interceptors: Vec::new(),
3598 };
3599
3600 let opts = WorkflowListOptions::builder().limit(5).build();
3601 let stream = client.list_workflows("", opts);
3602 let results: Vec<_> = stream.collect().await;
3603
3604 assert_eq!(results.len(), 5);
3605 for (i, result) in results.iter().enumerate() {
3606 let wf = result.as_ref().unwrap();
3607 assert_eq!(wf.id(), format!("wf-{i}"));
3608 }
3609 assert_eq!(call_count.load(Ordering::SeqCst), 2);
3611 }
3612
3613 #[tokio::test]
3614 async fn list_workflows_limit_less_than_page_size() {
3615 let call_count = Arc::new(AtomicUsize::new(0));
3616 let client = MockListWorkflowsClient {
3617 call_count: call_count.clone(),
3618 page_size: 10,
3619 total_workflows: 100,
3620 data_converter: DataConverter::default(),
3621 memo_payload: None,
3622 interceptors: Vec::new(),
3623 };
3624
3625 let opts = WorkflowListOptions::builder().limit(3).build();
3626 let stream = client.list_workflows("", opts);
3627 let results: Vec<_> = stream.collect().await;
3628
3629 assert_eq!(results.len(), 3);
3630 assert_eq!(call_count.load(Ordering::SeqCst), 1);
3632 }
3633
3634 #[tokio::test]
3635 async fn list_workflows_empty_results() {
3636 let call_count = Arc::new(AtomicUsize::new(0));
3637 let client = MockListWorkflowsClient {
3638 call_count: call_count.clone(),
3639 page_size: 10,
3640 total_workflows: 0,
3641 data_converter: DataConverter::default(),
3642 memo_payload: None,
3643 interceptors: Vec::new(),
3644 };
3645
3646 let stream = client.list_workflows("", WorkflowListOptions::default());
3647 let results: Vec<_> = stream.collect().await;
3648
3649 assert_eq!(results.len(), 0);
3650 assert_eq!(call_count.load(Ordering::SeqCst), 1);
3651 }
3652
3653 #[tokio::test]
3654 async fn list_workflows_exposes_typed_memo() {
3655 let data_converter = DataConverter::new(
3656 PayloadConverter::default(),
3657 DefaultFailureConverter,
3658 XorCodec,
3659 );
3660 let memo_payload = data_converter
3661 .to_payload(
3662 &SerializationContextData::Workflow,
3663 &"memo-value".to_owned(),
3664 )
3665 .await
3666 .unwrap();
3667 let client = MockListWorkflowsClient {
3668 call_count: Arc::new(AtomicUsize::new(0)),
3669 page_size: 1,
3670 total_workflows: 1,
3671 data_converter,
3672 memo_payload: Some(memo_payload),
3673 interceptors: Vec::new(),
3674 };
3675
3676 let workflow = client
3677 .list_workflows("", WorkflowListOptions::default())
3678 .next()
3679 .await
3680 .unwrap()
3681 .unwrap();
3682
3683 assert_eq!(
3684 workflow.memo().get::<String>("memo-key").unwrap(),
3685 Some("memo-value".to_owned())
3686 );
3687 }
3688
3689 #[tokio::test]
3690 async fn list_workflows_yields_codec_error_then_ends() {
3691 let client = MockListWorkflowsClient {
3692 call_count: Arc::new(AtomicUsize::new(0)),
3693 page_size: 1,
3694 total_workflows: 1,
3695 data_converter: DataConverter::new(
3696 PayloadConverter::default(),
3697 DefaultFailureConverter,
3698 FailingCodec,
3699 ),
3700 memo_payload: Some(Payload::default()),
3701 interceptors: Vec::new(),
3702 };
3703 let mut stream = client.list_workflows("", WorkflowListOptions::default());
3704
3705 let err = stream.next().await.unwrap().unwrap_err();
3706
3707 assert!(matches!(err, ClientError::PayloadConversion(_)));
3708 assert!(stream.next().await.is_none());
3709 }
3710 }
3711}