1#![warn(missing_docs)] #[macro_use]
8extern crate tracing;
9
10mod async_activity_handle;
11pub mod callback_based;
12mod dns;
13#[cfg(feature = "envconfig")]
15pub mod envconfig;
16pub mod errors;
17pub mod grpc;
18pub mod interceptors;
20mod metrics;
21mod options_structs;
22#[doc(hidden)]
24pub mod proxy;
25mod replaceable;
26pub mod request_extensions;
27mod retry;
28mod rpc_options;
29pub mod schedules;
31#[cfg(test)]
32mod test_helpers;
33pub mod worker;
34mod workflow_handle;
35mod workflow_status;
36
37pub use crate::{
38 proxy::HttpConnectProxyOptions,
39 request_extensions::PayloadErrorLimits,
40 retry::{CallType, RETRYABLE_ERROR_CODES},
41};
42pub use async_activity_handle::{
43 ActivityHeartbeatResponse, ActivityIdentifier, AsyncActivityHandle,
44};
45#[doc(hidden)]
46pub use retry::jittered;
47
48pub use interceptors::{
49 BackfillScheduleInput, CancelWorkflowInput, ClientInterceptor, CompleteAsyncActivityInput,
50 CountWorkflowsInput, CountWorkflowsOutput, CreateScheduleInput, CreateScheduleOutput,
51 DeleteScheduleInput, DescribeScheduleInput, DescribeScheduleOutput, DescribeWorkflowInput,
52 DescribeWorkflowOutput, FailAsyncActivityInput, FetchWorkflowHistoryPageInput,
53 FetchWorkflowHistoryPageOutput, HasArgs, HeartbeatAsyncActivityInput, ListSchedulesPageInput,
54 ListSchedulesPageOutput, ListWorkflowsPageInput, ListWorkflowsPageOutput, Next,
55 PauseScheduleInput, PollWorkflowUpdateInput, PollWorkflowUpdateOutput, QueryWorkflowInput,
56 QueryWorkflowOutput, ReportAsyncActivityCancellationInput, SendScheduleUpdateInput,
57 SignalWorkflowInput, StartWorkflowInput, StartWorkflowOutput, StartWorkflowUpdateInput,
58 StartWorkflowUpdateOutput, TemporalClientValue, TerminateWorkflowInput, TriggerScheduleInput,
59 UnpauseScheduleInput, UpdateScheduleInput,
60};
61pub use metrics::{LONG_REQUEST_LATENCY_HISTOGRAM_NAME, REQUEST_LATENCY_HISTOGRAM_NAME};
62pub use options_structs::*;
63pub use replaceable::SharedReplaceableClient;
64pub use retry::RetryOptions;
65pub use rpc_options::{RpcMetadata, RpcMetadataError, RpcOptions};
66pub use temporalio_common::{Memo, RetryPolicy};
67pub use url::Url;
68pub mod danger {
70 pub use tokio_rustls::rustls::client::danger::ServerCertVerifier;
74}
75pub use tonic;
76pub use workflow_handle::{
77 UntypedQuery, UntypedSignal, UntypedUpdate, UntypedWorkflow, UntypedWorkflowHandle,
78 WorkflowExecutionDescription, WorkflowExecutionInfo, WorkflowExecutionResult, WorkflowHandle,
79 WorkflowHistory, WorkflowResultDetails, WorkflowUpdateHandle,
80};
81pub use workflow_status::WorkflowExecutionStatus;
82
83use crate::{
84 grpc::{
85 AttachMetricLabels, CloudService, HealthService, OperatorService, TestService,
86 WorkflowService,
87 },
88 metrics::{ChannelOrGrpcOverride, GrpcMetricSvc, MetricsContext},
89 request_extensions::RequestExt,
90 worker::ClientWorkerSet,
91};
92use errors::*;
93use futures_util::{future::BoxFuture, stream, stream::Stream};
94use http::Uri;
95use parking_lot::RwLock;
96use std::{
97 collections::{HashMap, VecDeque},
98 fmt::Debug,
99 pin::Pin,
100 str::FromStr,
101 sync::{Arc, OnceLock},
102 task::{Context, Poll},
103 time::{Duration, SystemTime},
104};
105use temporalio_common::{
106 HasWorkflowDefinition,
107 data_converters::{
108 DataConverter, GenericPayloadConverter, PayloadConverter, SerializationContext,
109 SerializationContextData,
110 },
111 payload_visitor::decode_payloads,
112 protos::{
113 coresdk::IntoPayloadsExt,
114 grpc::health::v1::health_client::HealthClient,
115 proto_ts_to_system_time,
116 temporal::api::{
117 cloud::cloudservice::v1::cloud_service_client::CloudServiceClient,
118 common::v1::WorkflowType,
119 enums::v1::TaskQueueKind,
120 errordetails::v1::WorkflowExecutionAlreadyStartedFailure,
121 operatorservice::v1::operator_service_client::OperatorServiceClient,
122 sdk::v1::UserMetadata,
123 taskqueue::v1::TaskQueue,
124 testservice::v1::test_service_client::TestServiceClient,
125 workflow::v1 as workflow,
126 workflowservice::v1::{
127 count_workflow_executions_response, workflow_service_client::WorkflowServiceClient,
128 *,
129 },
130 },
131 utilities::decode_status_detail,
132 },
133 search_attributes::{SearchAttributeError, SearchAttributeValue, SearchAttributes},
134};
135use tonic::{
136 Code, IntoRequest,
137 body::Body,
138 client::GrpcService,
139 codec::CompressionEncoding,
140 codegen::InterceptedService,
141 metadata::{
142 AsciiMetadataKey, AsciiMetadataValue, BinaryMetadataKey, BinaryMetadataValue, MetadataMap,
143 MetadataValue,
144 },
145 service::Interceptor,
146 transport::{Certificate, Endpoint, Identity},
147};
148use tower::ServiceBuilder;
149use uuid::Uuid;
150
151static CLIENT_NAME_HEADER_KEY: &str = "client-name";
152static CLIENT_VERSION_HEADER_KEY: &str = "client-version";
153static TEMPORAL_NAMESPACE_HEADER_KEY: &str = "temporal-namespace";
154
155#[doc(hidden)]
156pub static MESSAGE_TOO_LARGE_KEY: &str = "message-too-large";
158#[doc(hidden)]
159pub fn payload_limit_violation_from(
162 status: &tonic::Status,
163) -> Option<&temporalio_common::payload_limits::PayloadLimitViolation> {
164 std::error::Error::source(status).and_then(|src| src.downcast_ref())
165}
166#[doc(hidden)]
167pub static ERROR_RETURNED_DUE_TO_SHORT_CIRCUIT: &str = "short-circuit";
169
170const LONG_POLL_TIMEOUT: Duration = Duration::from_secs(70);
172const OTHER_CALL_TIMEOUT: Duration = Duration::from_secs(30);
173const VERSION: &str = env!("CARGO_PKG_VERSION");
174
175#[derive(Clone)]
180pub struct Connection {
181 inner: Arc<ConnectionInner>,
182}
183
184#[derive(Clone)]
185struct ConnectionInner {
186 service: TemporalServiceClient,
187 retry_options: RetryOptions,
188 identity: String,
189 headers: Arc<RwLock<ClientHeaders>>,
190 client_name: String,
191 client_version: String,
192 capabilities: Option<get_system_info_response::Capabilities>,
194 workers: Arc<ClientWorkerSet>,
195 _dns_task: Option<Arc<dns::DnsReresolutionHandle>>,
196 payloads_warn_size: usize,
198 memo_warn_size: usize,
199}
200
201fn resolve_warn_threshold(option: &'static str, bytes: u64) -> usize {
206 usize::try_from(bytes).unwrap_or_else(|_| {
207 warn!(
208 option,
209 configured_bytes = bytes,
210 "Configured payload size warning threshold exceeds the maximum addressable size on this \
211 platform; disabling this warning"
212 );
213 0
214 })
215}
216
217impl Connection {
218 pub async fn connect(mut options: ConnectionOptions) -> Result<Self, ClientConnectError> {
220 if options.service_override.is_some() {
221 options.grpc_compression = GrpcCompression::None;
222 }
223
224 let first_result = Self::connect_once(&options).await;
225 if options.grpc_compression == GrpcCompression::Gzip
226 && let Err(ClientConnectError::SystemInfoCallError(status)) = &first_result
227 && status.code() == Code::Unimplemented
228 && {
229 let msg = status.message().to_lowercase();
230 msg.contains("decompress")
231 || msg.contains("grpc-encoding")
232 || msg.contains("compressor")
233 }
234 {
235 options.grpc_compression = GrpcCompression::None;
236 return Self::connect_once(&options).await;
237 }
238 first_result
239 }
240
241 async fn connect_once(options: &ConnectionOptions) -> Result<Self, ClientConnectError> {
242 let dns_lb_opts = dns::validate_and_get_dns_lb(options)?.cloned();
243 let (service, dns_task) = if let Some(service_override) = options.service_override.clone() {
244 (
245 GrpcMetricSvc {
246 inner: ChannelOrGrpcOverride::GrpcOverride(service_override),
247 metrics: options.metrics_meter.clone().map(MetricsContext::new),
248 disable_errcode_label: options.disable_error_code_metric_tags,
249 },
250 None,
251 )
252 } else if let Some(dns_opts) = &dns_lb_opts {
253 let (channel, sender) = dns::create_balanced_channel(options).await?;
254 let handle = dns::spawn_dns_reresolution(
255 sender,
256 options.target.clone(),
257 options.tls_options.clone(),
258 options.keep_alive.clone(),
259 options.override_origin.clone(),
260 dns_opts.resolution_interval,
261 options.connect_timeout,
262 );
263 (
264 ServiceBuilder::new()
265 .layer_fn(move |channel| GrpcMetricSvc {
266 inner: ChannelOrGrpcOverride::Channel(channel),
267 metrics: options.metrics_meter.clone().map(MetricsContext::new),
268 disable_errcode_label: options.disable_error_code_metric_tags,
269 })
270 .service(channel),
271 Some(handle),
272 )
273 } else {
274 let channel = Endpoint::from_shared(options.target.to_string())?;
275 let channel = if let Some(timeout) = options.connect_timeout {
276 channel.connect_timeout(timeout)
277 } else {
278 channel
279 };
280 let channel = add_tls_to_channel(options.tls_options.as_ref(), channel).await?;
281 let channel = if let Some(keep_alive) = options.keep_alive.as_ref() {
282 channel
283 .keep_alive_while_idle(true)
284 .http2_keep_alive_interval(keep_alive.interval)
285 .keep_alive_timeout(keep_alive.timeout)
286 } else {
287 channel
288 };
289 let channel = if let Some(origin) = options.override_origin.clone() {
290 channel.origin(origin)
291 } else {
292 channel
293 };
294 let channel = if let Some(proxy) = options.http_connect_proxy.as_ref() {
296 proxy.connect_endpoint(&channel).await?
297 } else {
298 channel.connect().await?
299 };
300 (
301 ServiceBuilder::new()
302 .layer_fn(move |channel| GrpcMetricSvc {
303 inner: ChannelOrGrpcOverride::Channel(channel),
304 metrics: options.metrics_meter.clone().map(MetricsContext::new),
305 disable_errcode_label: options.disable_error_code_metric_tags,
306 })
307 .service(channel),
308 None,
309 )
310 };
311
312 let headers = Arc::new(RwLock::new(ClientHeaders {
313 user_headers: parse_ascii_headers(options.headers.clone().unwrap_or_default())?,
314 user_binary_headers: parse_binary_headers(
315 options.binary_headers.clone().unwrap_or_default(),
316 )?,
317 api_key: options.api_key.clone(),
318 }));
319 let interceptor = ServiceCallInterceptor {
320 client_name: options.client_name.clone(),
321 client_version: options.client_version.clone(),
322 headers: headers.clone(),
323 };
324 let svc = InterceptedService::new(service, interceptor);
325 let mut svc_client = TemporalServiceClient::new(svc, options.grpc_compression);
326
327 let capabilities = if !options.skip_get_system_info {
328 match svc_client
329 .get_system_info(GetSystemInfoRequest::default().into_request())
330 .await
331 {
332 Ok(sysinfo) => sysinfo.into_inner().capabilities,
333 Err(status) => match status.code() {
334 Code::Unimplemented
335 if {
336 let msg = status.message().to_lowercase();
337 msg.contains("unknown method")
338 || msg.contains("unknown service")
339 || msg.contains("method not found")
340 || (msg.contains("getsysteminfo")
341 && (msg.contains("is unimplemented")
342 || msg.contains("not implement")))
343 } =>
344 {
345 None
346 }
347 _ => return Err(ClientConnectError::SystemInfoCallError(status)),
348 },
349 }
350 } else {
351 None
352 };
353 Ok(Self {
354 inner: Arc::new(ConnectionInner {
355 service: svc_client,
356 retry_options: options.retry_options.clone(),
357 identity: options.identity.clone(),
358 headers,
359 client_name: options.client_name.clone(),
360 client_version: options.client_version.clone(),
361 capabilities,
362 workers: Arc::new(ClientWorkerSet::new()),
363 _dns_task: dns_task,
364 payloads_warn_size: resolve_warn_threshold(
365 "payloads_warn_size",
366 options.payload_limits.payloads_warn_size,
367 ),
368 memo_warn_size: resolve_warn_threshold(
369 "memo_warn_size",
370 options.payload_limits.memo_warn_size,
371 ),
372 }),
373 })
374 }
375
376 pub fn set_api_key(&self, api_key: Option<String>) {
378 self.inner.headers.write().api_key = api_key;
379 }
380
381 pub fn set_headers(&self, headers: HashMap<String, String>) -> Result<(), InvalidHeaderError> {
390 self.inner.headers.write().user_headers = parse_ascii_headers(headers)?;
391 Ok(())
392 }
393
394 pub fn set_binary_headers(
403 &self,
404 binary_headers: HashMap<String, Vec<u8>>,
405 ) -> Result<(), InvalidHeaderError> {
406 self.inner.headers.write().user_binary_headers = parse_binary_headers(binary_headers)?;
407 Ok(())
408 }
409
410 pub fn client_name(&self) -> &str {
412 &self.inner.client_name
413 }
414
415 pub fn client_version(&self) -> &str {
417 &self.inner.client_version
418 }
419
420 pub fn capabilities(&self) -> Option<&get_system_info_response::Capabilities> {
423 self.inner.capabilities.as_ref()
424 }
425
426 pub fn retry_options_mut(&mut self) -> &mut RetryOptions {
431 &mut Arc::make_mut(&mut self.inner).retry_options
432 }
433
434 pub fn identity(&self) -> &str {
436 &self.inner.identity
437 }
438
439 pub fn identity_mut(&mut self) -> &mut String {
444 &mut Arc::make_mut(&mut self.inner).identity
445 }
446
447 pub fn workers(&self) -> Arc<ClientWorkerSet> {
449 self.inner.workers.clone()
450 }
451
452 pub fn worker_grouping_key(&self) -> Uuid {
454 self.inner.workers.worker_grouping_key()
455 }
456
457 pub fn workflow_service(&self) -> Box<dyn WorkflowService> {
459 self.inner.service.workflow_service()
460 }
461
462 pub fn operator_service(&self) -> Box<dyn OperatorService> {
464 self.inner.service.operator_service()
465 }
466
467 pub fn cloud_service(&self) -> Box<dyn CloudService> {
469 self.inner.service.cloud_service()
470 }
471
472 pub fn test_service(&self) -> Box<dyn TestService> {
474 self.inner.service.test_service()
475 }
476
477 pub fn health_service(&self) -> Box<dyn HealthService> {
479 self.inner.service.health_service()
480 }
481}
482
483#[derive(Debug)]
484struct ClientHeaders {
485 user_headers: HashMap<AsciiMetadataKey, AsciiMetadataValue>,
486 user_binary_headers: HashMap<BinaryMetadataKey, BinaryMetadataValue>,
487 api_key: Option<String>,
488}
489
490impl ClientHeaders {
491 fn apply_to_metadata(&self, metadata: &mut MetadataMap) {
492 for (key, val) in self.user_headers.iter() {
493 if !metadata.contains_key(key) {
495 metadata.insert(key, val.clone());
496 }
497 }
498 for (key, val) in self.user_binary_headers.iter() {
499 if !metadata.contains_key(key) {
501 metadata.insert_bin(key, val.clone());
502 }
503 }
504 if let Some(api_key) = &self.api_key {
505 if !metadata.contains_key("authorization")
507 && let Ok(val) = format!("Bearer {api_key}").parse()
508 {
509 metadata.insert("authorization", val);
510 }
511 }
512 }
513}
514
515async fn add_tls_to_channel(
518 tls_options: Option<&TlsOptions>,
519 mut channel: Endpoint,
520) -> Result<Endpoint, ClientConnectError> {
521 if let Some(tls_cfg) = tls_options {
522 if tls_cfg.server_cert_verifier.is_some() && tls_cfg.server_root_ca_cert.is_some() {
523 return Err(ClientConnectError::InvalidConfig(
524 "Cannot set both `server_root_ca_cert` and `server_cert_verifier`".to_owned(),
525 ));
526 }
527
528 let mut tls = tonic::transport::ClientTlsConfig::new();
529
530 if tls_cfg.server_cert_verifier.is_none() {
531 if let Some(root_cert) = &tls_cfg.server_root_ca_cert {
532 let server_root_ca_cert = Certificate::from_pem(root_cert);
533 tls = tls.ca_certificate(server_root_ca_cert);
534 } else {
535 tls = tls.with_native_roots();
536 }
537 }
538
539 if let Some(domain) = &tls_cfg.domain {
540 tls = tls.domain_name(domain);
541
542 let uri: Uri = format!("https://{domain}").parse()?;
547 channel = channel.origin(uri);
548 }
549
550 if let Some(client_opts) = &tls_cfg.client_tls_options {
551 let client_identity =
552 Identity::from_pem(&client_opts.client_cert, &client_opts.client_private_key);
553 tls = tls.identity(client_identity);
554 }
555
556 return if let Some(verifier) = &tls_cfg.server_cert_verifier {
557 channel
558 .tls_config_with_verifier(tls, verifier.clone())
559 .map_err(Into::into)
560 } else {
561 channel.tls_config(tls).map_err(Into::into)
562 };
563 }
564 Ok(channel)
565}
566
567fn parse_ascii_headers(
568 headers: HashMap<String, String>,
569) -> Result<HashMap<AsciiMetadataKey, AsciiMetadataValue>, InvalidHeaderError> {
570 let mut parsed_headers = HashMap::with_capacity(headers.len());
571 for (k, v) in headers.into_iter() {
572 let key = match AsciiMetadataKey::from_str(&k) {
573 Ok(key) => key,
574 Err(err) => {
575 return Err(InvalidHeaderError::InvalidAsciiHeaderKey {
576 key: k,
577 source: err,
578 });
579 }
580 };
581 let value = match MetadataValue::from_str(&v) {
582 Ok(value) => value,
583 Err(err) => {
584 return Err(InvalidHeaderError::InvalidAsciiHeaderValue {
585 key: k,
586 value: v,
587 source: err,
588 });
589 }
590 };
591 parsed_headers.insert(key, value);
592 }
593
594 Ok(parsed_headers)
595}
596
597fn parse_binary_headers(
598 headers: HashMap<String, Vec<u8>>,
599) -> Result<HashMap<BinaryMetadataKey, BinaryMetadataValue>, InvalidHeaderError> {
600 let mut parsed_headers = HashMap::with_capacity(headers.len());
601 for (k, v) in headers.into_iter() {
602 let key = match BinaryMetadataKey::from_str(&k) {
603 Ok(key) => key,
604 Err(err) => {
605 return Err(InvalidHeaderError::InvalidBinaryHeaderKey {
606 key: k,
607 source: err,
608 });
609 }
610 };
611 let value = BinaryMetadataValue::from_bytes(&v);
612 parsed_headers.insert(key, value);
613 }
614
615 Ok(parsed_headers)
616}
617
618#[derive(Clone)]
620pub struct ServiceCallInterceptor {
621 client_name: String,
622 client_version: String,
623 headers: Arc<RwLock<ClientHeaders>>,
625}
626
627impl Interceptor for ServiceCallInterceptor {
628 fn call(
631 &mut self,
632 mut request: tonic::Request<()>,
633 ) -> Result<tonic::Request<()>, tonic::Status> {
634 let metadata = request.metadata_mut();
635 if !metadata.contains_key(CLIENT_NAME_HEADER_KEY) {
636 metadata.insert(
637 CLIENT_NAME_HEADER_KEY,
638 self.client_name
639 .parse()
640 .unwrap_or_else(|_| MetadataValue::from_static("")),
641 );
642 }
643 if !metadata.contains_key(CLIENT_VERSION_HEADER_KEY) {
644 metadata.insert(
645 CLIENT_VERSION_HEADER_KEY,
646 self.client_version
647 .parse()
648 .unwrap_or_else(|_| MetadataValue::from_static("")),
649 );
650 }
651 self.headers.read().apply_to_metadata(metadata);
652 request.set_default_timeout(OTHER_CALL_TIMEOUT);
653
654 Ok(request)
655 }
656}
657
658#[derive(Clone)]
660pub struct TemporalServiceClient {
661 workflow_svc_client: Box<dyn WorkflowService>,
662 operator_svc_client: Box<dyn OperatorService>,
663 cloud_svc_client: Box<dyn CloudService>,
664 test_svc_client: Box<dyn TestService>,
665 health_svc_client: Box<dyn HealthService>,
666}
667
668fn get_decode_max_size() -> usize {
671 static _DECODE_MAX_SIZE: OnceLock<usize> = OnceLock::new();
672 *_DECODE_MAX_SIZE.get_or_init(|| {
673 std::env::var("TEMPORAL_MAX_INCOMING_GRPC_BYTES")
674 .ok()
675 .and_then(|s| s.parse().ok())
676 .unwrap_or(128 * 1024 * 1024)
677 })
678}
679
680impl TemporalServiceClient {
681 fn new<T>(svc: T, compression: GrpcCompression) -> Self
682 where
683 T: GrpcService<Body> + Send + Sync + Clone + 'static,
684 T::ResponseBody: tonic::codegen::Body<Data = tonic::codegen::Bytes> + Send + 'static,
685 T::Error: Into<tonic::codegen::StdError>,
686 <T::ResponseBody as tonic::codegen::Body>::Error: Into<tonic::codegen::StdError> + Send,
687 <T as GrpcService<Body>>::Future: Send,
688 {
689 macro_rules! configure {
692 ($client:expr) => {{
693 let client = $client.max_decoding_message_size(get_decode_max_size());
694 match compression {
695 GrpcCompression::Gzip => client
696 .send_compressed(CompressionEncoding::Gzip)
697 .accept_compressed(CompressionEncoding::Gzip),
698 GrpcCompression::None => client,
699 }
700 }};
701 }
702
703 let workflow_svc_client = Box::new(configure!(WorkflowServiceClient::new(svc.clone())));
704 let operator_svc_client = Box::new(configure!(OperatorServiceClient::new(svc.clone())));
705 let cloud_svc_client = Box::new(configure!(CloudServiceClient::new(svc.clone())));
706 let test_svc_client = Box::new(configure!(TestServiceClient::new(svc.clone())));
707 let health_svc_client = Box::new(configure!(HealthClient::new(svc.clone())));
708
709 Self {
710 workflow_svc_client,
711 operator_svc_client,
712 cloud_svc_client,
713 test_svc_client,
714 health_svc_client,
715 }
716 }
717
718 pub fn from_services(
721 workflow: Box<dyn WorkflowService>,
722 operator: Box<dyn OperatorService>,
723 cloud: Box<dyn CloudService>,
724 test: Box<dyn TestService>,
725 health: Box<dyn HealthService>,
726 ) -> Self {
727 Self {
728 workflow_svc_client: workflow,
729 operator_svc_client: operator,
730 cloud_svc_client: cloud,
731 test_svc_client: test,
732 health_svc_client: health,
733 }
734 }
735
736 pub fn workflow_service(&self) -> Box<dyn WorkflowService> {
738 self.workflow_svc_client.clone()
739 }
740 pub fn operator_service(&self) -> Box<dyn OperatorService> {
742 self.operator_svc_client.clone()
743 }
744 pub fn cloud_service(&self) -> Box<dyn CloudService> {
746 self.cloud_svc_client.clone()
747 }
748 pub fn test_service(&self) -> Box<dyn TestService> {
750 self.test_svc_client.clone()
751 }
752 pub fn health_service(&self) -> Box<dyn HealthService> {
754 self.health_svc_client.clone()
755 }
756}
757
758#[derive(Clone)]
761pub struct Client {
762 connection: Connection,
763 options: Arc<ClientOptions>,
764}
765
766impl Client {
767 pub fn new(connection: Connection, options: ClientOptions) -> Result<Self, ClientNewError> {
772 Ok(Client {
773 connection,
774 options: Arc::new(options),
775 })
776 }
777
778 pub fn options(&self) -> &ClientOptions {
780 &self.options
781 }
782
783 pub fn options_mut(&mut self) -> &mut ClientOptions {
788 Arc::make_mut(&mut self.options)
789 }
790
791 pub fn connection(&self) -> &Connection {
793 &self.connection
794 }
795
796 pub fn connection_mut(&mut self) -> &mut Connection {
798 &mut self.connection
799 }
800}
801
802impl Client {
806 pub async fn start_workflow<W>(
811 &self,
812 workflow: W,
813 input: W::Input,
814 options: WorkflowStartOptions,
815 ) -> Result<WorkflowHandle<Self, W>, WorkflowStartError>
816 where
817 W: HasWorkflowDefinition,
818 W::Input: Send,
819 {
820 WorkflowClientTrait::start_workflow(self, workflow, input, options).await
821 }
822
823 pub fn get_workflow_handle<W: HasWorkflowDefinition>(
827 &self,
828 workflow_id: impl Into<String>,
829 ) -> WorkflowHandle<Self, W> {
830 WorkflowClientTrait::get_workflow_handle(self, workflow_id)
831 }
832
833 pub fn list_workflows(
838 &self,
839 query: impl Into<String>,
840 opts: WorkflowListOptions,
841 ) -> ListWorkflowsStream {
842 WorkflowClientTrait::list_workflows(self, query, opts)
843 }
844
845 pub async fn count_workflows(
847 &self,
848 query: impl Into<String>,
849 opts: WorkflowCountOptions,
850 ) -> Result<WorkflowExecutionCount, ClientError> {
851 WorkflowClientTrait::count_workflows(self, query, opts).await
852 }
853
854 pub fn get_async_activity_handle(
858 &self,
859 identifier: ActivityIdentifier,
860 ) -> AsyncActivityHandle<Self> {
861 WorkflowClientTrait::get_async_activity_handle(self, identifier)
862 }
863}
864
865impl NamespacedClient for Client {
866 fn namespace(&self) -> String {
867 self.options.namespace.clone()
868 }
869
870 fn identity(&self) -> String {
871 self.connection.identity().to_owned()
872 }
873
874 fn data_converter(&self) -> &DataConverter {
875 &self.options.data_converter
876 }
877
878 fn client_interceptors(&self) -> &[Arc<dyn ClientInterceptor>] {
879 &self.options.client_interceptors
880 }
881}
882
883#[derive(Clone)]
885pub enum Namespace {
886 Name(String),
888 Id(String),
890}
891
892pub(crate) trait WorkflowClientTrait: NamespacedClient {
895 fn start_workflow<W>(
897 &self,
898 workflow: W,
899 input: W::Input,
900 options: WorkflowStartOptions,
901 ) -> impl Future<Output = Result<WorkflowHandle<Self, W>, WorkflowStartError>>
902 where
903 Self: Sized,
904 W: HasWorkflowDefinition,
905 W::Input: Send;
906
907 fn get_workflow_handle<W: HasWorkflowDefinition>(
914 &self,
915 workflow_id: impl Into<String>,
916 ) -> WorkflowHandle<Self, W>
917 where
918 Self: Sized;
919
920 fn list_workflows(
924 &self,
925 query: impl Into<String>,
926 opts: WorkflowListOptions,
927 ) -> ListWorkflowsStream;
928
929 fn count_workflows(
931 &self,
932 query: impl Into<String>,
933 opts: WorkflowCountOptions,
934 ) -> impl Future<Output = Result<WorkflowExecutionCount, ClientError>>;
935
936 fn get_async_activity_handle(
940 &self,
941 identifier: ActivityIdentifier,
942 ) -> AsyncActivityHandle<Self>
943 where
944 Self: Sized;
945}
946
947pub trait NamespacedClient {
949 fn namespace(&self) -> String;
951 fn identity(&self) -> String;
953 fn data_converter(&self) -> &DataConverter {
956 static DEFAULT: OnceLock<DataConverter> = OnceLock::new();
957 DEFAULT.get_or_init(DataConverter::default)
958 }
959 fn client_interceptors(&self) -> &[Arc<dyn ClientInterceptor>] {
967 &[]
968 }
969}
970
971#[derive(Debug, Clone)]
974pub struct WorkflowExecution {
975 raw: workflow::WorkflowExecutionInfo,
976 data_converter: DataConverter,
977}
978
979impl WorkflowExecution {
980 fn new_with_data_converter(
981 raw: workflow::WorkflowExecutionInfo,
982 data_converter: DataConverter,
983 ) -> Self {
984 Self {
985 raw,
986 data_converter,
987 }
988 }
989
990 pub fn id(&self) -> &str {
992 self.raw
993 .execution
994 .as_ref()
995 .map(|e| e.workflow_id.as_str())
996 .unwrap_or("")
997 }
998
999 pub fn run_id(&self) -> &str {
1001 self.raw
1002 .execution
1003 .as_ref()
1004 .map(|e| e.run_id.as_str())
1005 .unwrap_or("")
1006 }
1007
1008 pub fn workflow_type(&self) -> &str {
1010 self.raw
1011 .r#type
1012 .as_ref()
1013 .map(|t| t.name.as_str())
1014 .unwrap_or("")
1015 }
1016
1017 pub fn status(&self) -> WorkflowExecutionStatus {
1019 WorkflowExecutionStatus::from_raw(self.raw.status)
1020 }
1021
1022 pub fn start_time(&self) -> Option<SystemTime> {
1024 self.raw
1025 .start_time
1026 .as_ref()
1027 .and_then(proto_ts_to_system_time)
1028 }
1029
1030 pub fn execution_time(&self) -> Option<SystemTime> {
1032 self.raw
1033 .execution_time
1034 .as_ref()
1035 .and_then(proto_ts_to_system_time)
1036 }
1037
1038 pub fn close_time(&self) -> Option<SystemTime> {
1040 self.raw
1041 .close_time
1042 .as_ref()
1043 .and_then(proto_ts_to_system_time)
1044 }
1045
1046 pub fn task_queue(&self) -> &str {
1048 &self.raw.task_queue
1049 }
1050
1051 pub fn history_length(&self) -> i64 {
1053 self.raw.history_length
1054 }
1055
1056 pub fn memo(&self) -> Memo {
1058 Memo::from_raw(
1059 self.raw.memo.clone(),
1060 self.data_converter.payload_converter().clone(),
1061 SerializationContextData::Workflow,
1062 )
1063 }
1064
1065 pub fn parent_id(&self) -> Option<&str> {
1067 self.raw
1068 .parent_execution
1069 .as_ref()
1070 .map(|e| e.workflow_id.as_str())
1071 }
1072
1073 pub fn parent_run_id(&self) -> Option<&str> {
1075 self.raw
1076 .parent_execution
1077 .as_ref()
1078 .map(|e| e.run_id.as_str())
1079 }
1080
1081 pub fn search_attributes(&self) -> SearchAttributes {
1083 self.raw
1084 .search_attributes
1085 .as_ref()
1086 .map(SearchAttributes::from_proto)
1087 .unwrap_or_default()
1088 }
1089
1090 pub fn raw(&self) -> &workflow::WorkflowExecutionInfo {
1092 &self.raw
1093 }
1094
1095 pub fn into_raw(self) -> workflow::WorkflowExecutionInfo {
1097 self.raw
1098 }
1099}
1100
1101pub struct ListWorkflowsStream {
1104 inner: Pin<Box<dyn Stream<Item = Result<WorkflowExecution, ClientError>> + Send>>,
1105}
1106
1107impl ListWorkflowsStream {
1108 fn new(
1109 inner: Pin<Box<dyn Stream<Item = Result<WorkflowExecution, ClientError>> + Send>>,
1110 ) -> Self {
1111 Self { inner }
1112 }
1113}
1114
1115impl Stream for ListWorkflowsStream {
1116 type Item = Result<WorkflowExecution, ClientError>;
1117
1118 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
1119 self.inner.as_mut().poll_next(cx)
1120 }
1121}
1122
1123#[derive(Debug, Clone)]
1128pub struct WorkflowExecutionCount {
1129 count: usize,
1130 groups: Vec<WorkflowCountAggregationGroup>,
1131}
1132
1133impl WorkflowExecutionCount {
1134 pub(crate) fn from_response(resp: CountWorkflowExecutionsResponse) -> Self {
1135 Self {
1136 count: resp.count as usize,
1137 groups: resp
1138 .groups
1139 .into_iter()
1140 .map(WorkflowCountAggregationGroup::from_proto)
1141 .collect(),
1142 }
1143 }
1144
1145 pub fn count(&self) -> usize {
1148 self.count
1149 }
1150
1151 pub fn groups(&self) -> &[WorkflowCountAggregationGroup] {
1153 &self.groups
1154 }
1155}
1156
1157#[derive(Debug, Clone)]
1159pub struct WorkflowCountAggregationGroup {
1160 raw: count_workflow_executions_response::AggregationGroup,
1161}
1162
1163impl WorkflowCountAggregationGroup {
1164 fn from_proto(proto: count_workflow_executions_response::AggregationGroup) -> Self {
1165 Self { raw: proto }
1166 }
1167
1168 pub fn get<T: SearchAttributeValue>(&self, index: usize) -> Option<T> {
1173 self.try_get(index).ok().flatten()
1174 }
1175
1176 pub fn try_get<T: SearchAttributeValue>(
1182 &self,
1183 index: usize,
1184 ) -> Result<Option<T>, SearchAttributeError> {
1185 match self.raw.group_values.get(index) {
1186 Some(payload) => T::from_search_attribute_payload(payload).map(Some),
1187 None => Ok(None),
1188 }
1189 }
1190
1191 pub fn count(&self) -> usize {
1193 self.raw.count as usize
1194 }
1195}
1196
1197impl<T> WorkflowClientTrait for T
1198where
1199 T: WorkflowService + NamespacedClient + Clone + Send + Sync + 'static,
1200{
1201 async fn start_workflow<W>(
1202 &self,
1203 workflow: W,
1204 input: W::Input,
1205 options: WorkflowStartOptions,
1206 ) -> Result<WorkflowHandle<Self, W>, WorkflowStartError>
1207 where
1208 W: HasWorkflowDefinition,
1209 W::Input: Send,
1210 {
1211 let namespace = self.namespace();
1212 let interceptor_output = interceptors::call_start_workflow(
1213 self.client_interceptors(),
1214 StartWorkflowInput::new(workflow.name().to_owned(), input, options),
1215 Next::new({
1216 let client = (*self).clone();
1217 move |input: StartWorkflowInput| -> BoxFuture<
1218 '_,
1219 Result<StartWorkflowOutput, WorkflowStartError>,
1220 > {
1221 let mut client = client;
1222 Box::pin(async move {
1223 let (workflow_type, args, options, rpc_options) = input.into_parts();
1224 let data_converter = client.data_converter().clone();
1225 let unencoded_payloads = {
1226 let payload_converter = data_converter.payload_converter();
1227 let context = SerializationContext {
1228 data: &SerializationContextData::Workflow,
1229 converter: payload_converter,
1230 };
1231 args.serialize_payloads(&context)
1232 };
1233 drop(args);
1234
1235 let payloads = data_converter
1236 .codec()
1237 .encode(&SerializationContextData::Workflow, unencoded_payloads?)
1238 .await?;
1239 let namespace = client.namespace();
1240 let workflow_id = options.workflow_id.clone();
1241 let task_queue_name = options.task_queue.clone();
1242
1243 let user_metadata = if options.static_summary.is_some()
1244 || options.static_details.is_some()
1245 {
1246 let payload_converter = PayloadConverter::default();
1247 let context = SerializationContext {
1248 data: &SerializationContextData::Workflow,
1249 converter: &payload_converter,
1250 };
1251 Some(UserMetadata {
1252 summary: options.static_summary.map(|summary| {
1253 payload_converter.to_payload(&context, &summary).expect(
1254 "String-to-JSON payload serialization is infallible",
1255 )
1256 }),
1257 details: options.static_details.map(|details| {
1258 payload_converter.to_payload(&context, &details).expect(
1259 "String-to-JSON payload serialization is infallible",
1260 )
1261 }),
1262 })
1263 } else {
1264 None
1265 };
1266
1267 let run_id = if let Some(start_signal) = options.start_signal {
1268 let mut request = SignalWithStartWorkflowExecutionRequest {
1269 namespace,
1270 workflow_id: workflow_id.clone(),
1271 workflow_type: Some(WorkflowType {
1272 name: workflow_type,
1273 }),
1274 task_queue: Some(TaskQueue {
1275 name: task_queue_name,
1276 kind: TaskQueueKind::Normal as i32,
1277 normal_name: String::new(),
1278 }),
1279 input: payloads.into_payloads(),
1280 signal_name: start_signal.signal_name,
1281 signal_input: start_signal.input,
1282 identity: client.identity(),
1283 request_id: Uuid::new_v4().to_string(),
1284 workflow_id_reuse_policy: options.id_reuse_policy as i32,
1285 workflow_id_conflict_policy: options.id_conflict_policy as i32,
1286 workflow_execution_timeout: options
1287 .execution_timeout
1288 .and_then(|duration| duration.try_into().ok()),
1289 workflow_run_timeout: options
1290 .run_timeout
1291 .and_then(|duration| duration.try_into().ok()),
1292 workflow_task_timeout: options
1293 .task_timeout
1294 .and_then(|duration| duration.try_into().ok()),
1295 search_attributes: options
1296 .search_attributes
1297 .map(|attributes| attributes.into_proto()),
1298 cron_schedule: options.cron_schedule.unwrap_or_default(),
1299 retry_policy: options.retry_policy.map(Into::into),
1300 header: options.header.or(start_signal.header),
1301 user_metadata,
1302 ..Default::default()
1303 }
1304 .into_request();
1305 rpc_options.apply_to(&mut request);
1306 WorkflowService::signal_with_start_workflow_execution(
1307 &mut client,
1308 request,
1309 )
1310 .await?
1311 .into_inner()
1312 .run_id
1313 } else {
1314 let mut request = StartWorkflowExecutionRequest {
1315 namespace,
1316 input: payloads.into_payloads(),
1317 workflow_id: workflow_id.clone(),
1318 workflow_type: Some(WorkflowType {
1319 name: workflow_type,
1320 }),
1321 task_queue: Some(TaskQueue {
1322 name: task_queue_name,
1323 kind: TaskQueueKind::Unspecified as i32,
1324 normal_name: String::new(),
1325 }),
1326 request_id: Uuid::new_v4().to_string(),
1327 workflow_id_reuse_policy: options.id_reuse_policy as i32,
1328 workflow_id_conflict_policy: options.id_conflict_policy as i32,
1329 workflow_execution_timeout: options
1330 .execution_timeout
1331 .and_then(|duration| duration.try_into().ok()),
1332 workflow_run_timeout: options
1333 .run_timeout
1334 .and_then(|duration| duration.try_into().ok()),
1335 workflow_task_timeout: options
1336 .task_timeout
1337 .and_then(|duration| duration.try_into().ok()),
1338 search_attributes: options
1339 .search_attributes
1340 .map(|attributes| attributes.into_proto()),
1341 cron_schedule: options.cron_schedule.unwrap_or_default(),
1342 request_eager_execution: options.enable_eager_workflow_start,
1343 retry_policy: options.retry_policy.map(Into::into),
1344 links: options.links,
1345 completion_callbacks: options.completion_callbacks,
1346 priority: Some(options.priority.into()),
1347 header: options.header,
1348 user_metadata,
1349 ..Default::default()
1350 }
1351 .into_request();
1352 rpc_options.apply_to(&mut request);
1353 client
1354 .start_workflow_execution(request)
1355 .await
1356 .map_err(|status| {
1357 if status.code() == Code::AlreadyExists {
1358 let run_id = decode_status_detail::<
1359 WorkflowExecutionAlreadyStartedFailure,
1360 >(
1361 status.details()
1362 )
1363 .map(|failure| failure.run_id);
1364 WorkflowStartError::AlreadyStarted {
1365 run_id,
1366 source: status,
1367 }
1368 } else {
1369 WorkflowStartError::Rpc(status)
1370 }
1371 })?
1372 .into_inner()
1373 .run_id
1374 };
1375
1376 Ok(StartWorkflowOutput::new(workflow_id, run_id))
1377 })
1378 }
1379 }),
1380 )
1381 .await?;
1382 let StartWorkflowOutput {
1383 workflow_id,
1384 run_id,
1385 } = interceptor_output;
1386
1387 Ok(WorkflowHandle::new(
1388 self.clone(),
1389 WorkflowExecutionInfo {
1390 namespace,
1391 workflow_id,
1392 run_id: Some(run_id.clone()),
1393 first_execution_run_id: Some(run_id),
1394 },
1395 ))
1396 }
1397
1398 fn get_workflow_handle<W: HasWorkflowDefinition>(
1399 &self,
1400 workflow_id: impl Into<String>,
1401 ) -> WorkflowHandle<Self, W>
1402 where
1403 Self: Sized,
1404 {
1405 WorkflowHandle::new(
1406 self.clone(),
1407 WorkflowExecutionInfo {
1408 namespace: self.namespace(),
1409 workflow_id: workflow_id.into(),
1410 run_id: None,
1411 first_execution_run_id: None,
1412 },
1413 )
1414 }
1415
1416 fn list_workflows(
1417 &self,
1418 query: impl Into<String>,
1419 opts: WorkflowListOptions,
1420 ) -> ListWorkflowsStream {
1421 let client = self.clone();
1422 let namespace = self.namespace();
1423 let query = query.into();
1424 let limit = opts.limit;
1425 let rpc_options = opts.rpc_options;
1426
1427 let initial_state = (Vec::new(), VecDeque::new(), 0, false);
1429
1430 let stream = stream::unfold(
1431 initial_state,
1432 move |(next_page_token, mut buffer, mut yielded, exhausted)| {
1433 let client = client.clone();
1434 let namespace = namespace.clone();
1435 let query = query.clone();
1436 let rpc_options = rpc_options.clone();
1437
1438 async move {
1439 if let Some(l) = limit
1440 && yielded >= l
1441 {
1442 return None;
1443 }
1444
1445 if let Some(exec) = buffer.pop_front() {
1446 yielded += 1;
1447 return Some((Ok(exec), (next_page_token, buffer, yielded, exhausted)));
1448 }
1449
1450 if exhausted {
1451 return None;
1452 }
1453
1454 let response = interceptors::call_list_workflows_page(
1455 client.client_interceptors(),
1456 ListWorkflowsPageInput {
1457 query,
1458 next_page_token: next_page_token.clone(),
1459 rpc_options,
1460 },
1461 Next::new({
1462 let mut rpc_client = client.clone();
1463 move |input: ListWorkflowsPageInput| -> BoxFuture<
1464 '_,
1465 Result<ListWorkflowsPageOutput, ClientError>,
1466 > {
1467 Box::pin(async move {
1468 let mut request = ListWorkflowExecutionsRequest {
1469 namespace,
1470 page_size: 0,
1471 next_page_token: input.next_page_token,
1472 query: input.query,
1473 }
1474 .into_request();
1475 input.rpc_options.apply_to(&mut request);
1476 let response = WorkflowService::list_workflow_executions(
1477 &mut rpc_client,
1478 request,
1479 )
1480 .await?
1481 .into_inner();
1482 Ok(ListWorkflowsPageOutput::new(
1483 response.executions,
1484 response.next_page_token,
1485 ))
1486 })
1487 }
1488 }),
1489 )
1490 .await;
1491
1492 match response {
1493 Ok(mut output) => {
1494 let new_exhausted = output.next_page_token.is_empty();
1495 let new_token = output.next_page_token;
1496
1497 let data_converter = client.data_converter().clone();
1498 for execution in &mut output.executions {
1499 if let Some(memo) = execution.memo.as_mut()
1500 && let Err(err) = decode_payloads(
1501 memo,
1502 data_converter.codec(),
1503 &SerializationContextData::Workflow,
1504 )
1505 .await
1506 {
1507 return Some((
1508 Err(ClientError::from(err)),
1509 (new_token, buffer, yielded, true),
1510 ));
1511 }
1512 }
1513 buffer = output
1514 .executions
1515 .into_iter()
1516 .map(|raw| {
1517 WorkflowExecution::new_with_data_converter(
1518 raw,
1519 data_converter.clone(),
1520 )
1521 })
1522 .collect();
1523
1524 if let Some(exec) = buffer.pop_front() {
1525 yielded += 1;
1526 Some((Ok(exec), (new_token, buffer, yielded, new_exhausted)))
1527 } else {
1528 None
1529 }
1530 }
1531 Err(e) => Some((Err(e), (next_page_token, buffer, yielded, true))),
1532 }
1533 }
1534 },
1535 );
1536
1537 ListWorkflowsStream::new(Box::pin(stream))
1538 }
1539
1540 async fn count_workflows(
1541 &self,
1542 query: impl Into<String>,
1543 opts: WorkflowCountOptions,
1544 ) -> Result<WorkflowExecutionCount, ClientError> {
1545 let output = interceptors::call_count_workflows(
1546 self.client_interceptors(),
1547 CountWorkflowsInput {
1548 query: query.into(),
1549 options: opts,
1550 },
1551 Next::new({
1552 let mut client = (*self).clone();
1553 move |input: CountWorkflowsInput| -> BoxFuture<
1554 '_,
1555 Result<CountWorkflowsOutput, ClientError>,
1556 > {
1557 Box::pin(async move {
1558 let mut request = CountWorkflowExecutionsRequest {
1559 namespace: client.namespace(),
1560 query: input.query,
1561 }
1562 .into_request();
1563 input.options.rpc_options.apply_to(&mut request);
1564 let response = WorkflowService::count_workflow_executions(
1565 &mut client,
1566 request,
1567 )
1568 .await?
1569 .into_inner();
1570 Ok(CountWorkflowsOutput::new(response))
1571 })
1572 }
1573 }),
1574 )
1575 .await?;
1576
1577 Ok(WorkflowExecutionCount::from_response(output.response))
1578 }
1579
1580 fn get_async_activity_handle(&self, identifier: ActivityIdentifier) -> AsyncActivityHandle<Self>
1581 where
1582 Self: Sized,
1583 {
1584 AsyncActivityHandle::new(self.clone(), identifier)
1585 }
1586}
1587
1588macro_rules! dbg_panic {
1589 ($($arg:tt)*) => {
1590 use tracing::error;
1591 error!($($arg)*);
1592 debug_assert!(false, $($arg)*);
1593 };
1594}
1595pub(crate) use dbg_panic;
1596
1597#[cfg(test)]
1598mod tests {
1599 use super::*;
1600 use crate::callback_based::CallbackBasedGrpcService;
1601 use std::sync::atomic::{AtomicUsize, Ordering};
1602 use std::time::Instant;
1603 use temporalio_common::search_attributes::SearchAttributeKey;
1604 use tonic::{Status, metadata::Ascii};
1605 use url::Url;
1606
1607 #[test]
1608 fn count_aggregation_group_gets_typed_value() {
1609 let attrs = SearchAttributes::new([SearchAttributeKey::int("group").value_set(42)]);
1610 let group = WorkflowCountAggregationGroup {
1611 raw: count_workflow_executions_response::AggregationGroup {
1612 group_values: vec![attrs.raw_payload("group").unwrap().clone()],
1613 count: 1,
1614 },
1615 };
1616
1617 assert_eq!(group.get::<i64>(0), Some(42));
1618 assert_eq!(group.get::<i64>(1), None);
1619 assert!(group.try_get::<String>(0).is_err());
1620 assert_eq!(group.try_get::<i64>(1).unwrap(), None);
1621 }
1622
1623 fn connection_options_for_system_info_test(
1624 service_override: CallbackBasedGrpcService,
1625 ) -> ConnectionOptions {
1626 ConnectionOptions::new(Url::parse("http://localhost:7233").unwrap())
1627 .service_override(service_override)
1628 .dns_load_balancing(None)
1629 .build()
1630 }
1631
1632 #[test]
1633 fn applies_headers() {
1634 let headers = Arc::new(RwLock::new(ClientHeaders {
1636 user_headers: HashMap::new(),
1637 user_binary_headers: HashMap::new(),
1638 api_key: Some("my-api-key".to_owned()),
1639 }));
1640 headers.clone().write().user_headers.insert(
1641 "my-meta-key".parse().unwrap(),
1642 "my-meta-val".parse().unwrap(),
1643 );
1644 headers.clone().write().user_binary_headers.insert(
1645 "my-bin-meta-key-bin".parse().unwrap(),
1646 vec![1, 2, 3].try_into().unwrap(),
1647 );
1648 let mut interceptor = ServiceCallInterceptor {
1649 client_name: "cute-kitty".to_string(),
1650 client_version: "0.1.0".to_string(),
1651 headers: headers.clone(),
1652 };
1653
1654 let req = interceptor.call(tonic::Request::new(())).unwrap();
1656 assert_eq!(req.metadata().get("my-meta-key").unwrap(), "my-meta-val");
1657 assert_eq!(
1658 req.metadata().get("authorization").unwrap(),
1659 "Bearer my-api-key"
1660 );
1661 assert_eq!(
1662 req.metadata().get_bin("my-bin-meta-key-bin").unwrap(),
1663 vec![1, 2, 3].as_slice()
1664 );
1665
1666 let mut req = tonic::Request::new(());
1668 req.metadata_mut()
1669 .insert("my-meta-key", "my-meta-val2".parse().unwrap());
1670 req.metadata_mut()
1671 .insert("authorization", "my-api-key2".parse().unwrap());
1672 req.metadata_mut()
1673 .insert_bin("my-bin-meta-key-bin", vec![4, 5, 6].try_into().unwrap());
1674 let req = interceptor.call(req).unwrap();
1675 assert_eq!(req.metadata().get("my-meta-key").unwrap(), "my-meta-val2");
1676 assert_eq!(req.metadata().get("authorization").unwrap(), "my-api-key2");
1677 assert_eq!(
1678 req.metadata().get_bin("my-bin-meta-key-bin").unwrap(),
1679 vec![4, 5, 6].as_slice()
1680 );
1681
1682 headers.clone().write().user_headers.insert(
1684 "authorization".parse().unwrap(),
1685 "my-api-key3".parse().unwrap(),
1686 );
1687 let req = interceptor.call(tonic::Request::new(())).unwrap();
1688 assert_eq!(req.metadata().get("my-meta-key").unwrap(), "my-meta-val");
1689 assert_eq!(req.metadata().get("authorization").unwrap(), "my-api-key3");
1690
1691 headers.clone().write().user_headers.clear();
1693 headers.clone().write().user_binary_headers.clear();
1694 headers.clone().write().api_key.take();
1695 let req = interceptor.call(tonic::Request::new(())).unwrap();
1696 assert!(!req.metadata().contains_key("my-meta-key"));
1697 assert!(!req.metadata().contains_key("authorization"));
1698 assert!(!req.metadata().contains_key("my-bin-meta-key-bin"));
1699
1700 let mut req = tonic::Request::new(());
1702 req.metadata_mut()
1703 .insert("grpc-timeout", "1S".parse().unwrap());
1704 let req = interceptor.call(req).unwrap();
1705 assert_eq!(
1706 req.metadata().get("grpc-timeout").unwrap(),
1707 "1S".parse::<MetadataValue<Ascii>>().unwrap()
1708 );
1709 }
1710
1711 #[test]
1712 fn invalid_ascii_header_key() {
1713 let invalid_headers = {
1714 let mut h = HashMap::new();
1715 h.insert("x-binary-key-bin".to_owned(), "value".to_owned());
1716 h
1717 };
1718
1719 let result = parse_ascii_headers(invalid_headers);
1720 assert!(result.is_err());
1721 assert_eq!(
1722 result.err().unwrap().to_string(),
1723 "Invalid ASCII header key 'x-binary-key-bin': invalid gRPC metadata key name"
1724 );
1725 }
1726
1727 #[test]
1728 fn invalid_ascii_header_value() {
1729 let invalid_headers = {
1730 let mut h = HashMap::new();
1731 h.insert("x-ascii-key".to_owned(), "\x00value".to_owned());
1733 h
1734 };
1735
1736 let result = parse_ascii_headers(invalid_headers);
1737 assert!(result.is_err());
1738 assert_eq!(
1739 result.err().unwrap().to_string(),
1740 "Invalid ASCII header value for key 'x-ascii-key': failed to parse metadata value"
1741 );
1742 }
1743
1744 #[test]
1745 fn invalid_binary_header_key() {
1746 let invalid_headers = {
1747 let mut h = HashMap::new();
1748 h.insert("x-ascii-key".to_owned(), vec![1, 2, 3]);
1749 h
1750 };
1751
1752 let result = parse_binary_headers(invalid_headers);
1753 assert!(result.is_err());
1754 assert_eq!(
1755 result.err().unwrap().to_string(),
1756 "Invalid binary header key 'x-ascii-key': invalid gRPC metadata key name"
1757 );
1758 }
1759
1760 #[test]
1761 fn keep_alive_defaults() {
1762 let opts = ConnectionOptions::new(Url::parse("https://smolkitty").unwrap())
1763 .identity("enchicat".to_string())
1764 .client_name("cute-kitty".to_string())
1765 .client_version("0.1.0".to_string())
1766 .build();
1767 assert_eq!(
1768 opts.keep_alive.clone().unwrap().interval,
1769 ClientKeepAliveOptions::default().interval
1770 );
1771 assert_eq!(
1772 opts.keep_alive.clone().unwrap().timeout,
1773 ClientKeepAliveOptions::default().timeout
1774 );
1775
1776 let opts = ConnectionOptions::new(Url::parse("https://smolkitty").unwrap())
1778 .identity("enchicat".to_string())
1779 .client_name("cute-kitty".to_string())
1780 .client_version("0.1.0".to_string())
1781 .keep_alive(None)
1782 .build();
1783 dbg!(&opts.keep_alive);
1784 assert!(opts.keep_alive.is_none());
1785 }
1786
1787 #[rstest::rstest]
1788 #[case(
1789 "unknown method GetSystemInfo for service temporal.api.workflowservice.v1.WorkflowService"
1790 )]
1791 #[case("Method temporal.api.workflowservice.v1.WorkflowService/GetSystemInfo is unimplemented")]
1792 #[case(
1793 "The server does not implement the method /temporal.api.workflowservice.v1.WorkflowService/GetSystemInfo"
1794 )]
1795 #[tokio::test]
1796 async fn get_system_info_missing_method_falls_back_to_empty_capabilities(
1797 #[case] message: &'static str,
1798 ) {
1799 let attempts = Arc::new(AtomicUsize::new(0));
1800 let attempts_clone = attempts.clone();
1801 let service_override = CallbackBasedGrpcService {
1802 callback: Arc::new(move |req| {
1803 let attempts = attempts_clone.clone();
1804 Box::pin(async move {
1805 assert_eq!(req.rpc, "GetSystemInfo");
1806 attempts.fetch_add(1, Ordering::SeqCst);
1807 Err(Status::unimplemented(message))
1808 })
1809 }),
1810 };
1811
1812 let connection =
1813 Connection::connect(connection_options_for_system_info_test(service_override))
1814 .await
1815 .unwrap();
1816
1817 assert!(connection.capabilities().is_none());
1818 assert_eq!(attempts.load(Ordering::SeqCst), 1);
1819 }
1820
1821 #[tokio::test]
1822 async fn get_system_info_non_missing_unimplemented_fails_connect() {
1823 let attempts = Arc::new(AtomicUsize::new(0));
1824 let attempts_clone = attempts.clone();
1825 let service_override = CallbackBasedGrpcService {
1826 callback: Arc::new(move |req| {
1827 let attempts = attempts_clone.clone();
1828 Box::pin(async move {
1829 assert_eq!(req.rpc, "GetSystemInfo");
1830 attempts.fetch_add(1, Ordering::SeqCst);
1831 Err(Status::unimplemented("backend temporarily unimplemented"))
1832 })
1833 }),
1834 };
1835
1836 let err =
1837 match Connection::connect(connection_options_for_system_info_test(service_override))
1838 .await
1839 {
1840 Ok(_) => panic!("connection should fail"),
1841 Err(err) => err,
1842 };
1843
1844 assert!(matches!(
1845 err,
1846 ClientConnectError::SystemInfoCallError(status)
1847 if status.code() == Code::Unimplemented
1848 && status.message() == "backend temporarily unimplemented"
1849 ));
1850 assert_eq!(attempts.load(Ordering::SeqCst), 1);
1851 }
1852
1853 #[tokio::test]
1854 async fn connect_timeout_bounds_connection_attempt() {
1855 let url = Url::parse("http://10.255.255.1:7233").unwrap();
1856 let opts = ConnectionOptions::new(url)
1857 .connect_timeout(Duration::from_millis(500))
1858 .build();
1859 let start = Instant::now();
1860 let result = Connection::connect(opts).await;
1861 assert!(result.is_err(), "connection should fail");
1862 assert!(start.elapsed() < Duration::from_secs(2));
1863 }
1864
1865 mod tls_custom_verifier_tests {
1866 use super::*;
1867 use tokio_rustls::rustls::{
1868 DigitallySignedStruct, Error as RustlsError, SignatureScheme,
1869 client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
1870 pki_types::{CertificateDer, ServerName, UnixTime},
1871 };
1872
1873 #[derive(Debug)]
1876 struct MockVerifier;
1877
1878 impl ServerCertVerifier for MockVerifier {
1879 fn verify_server_cert(
1880 &self,
1881 _end_entity: &CertificateDer<'_>,
1882 _intermediates: &[CertificateDer<'_>],
1883 _server_name: &ServerName<'_>,
1884 _ocsp_response: &[u8],
1885 _now: UnixTime,
1886 ) -> Result<ServerCertVerified, RustlsError> {
1887 Ok(ServerCertVerified::assertion())
1888 }
1889
1890 fn verify_tls12_signature(
1891 &self,
1892 _message: &[u8],
1893 _cert: &CertificateDer<'_>,
1894 _dss: &DigitallySignedStruct,
1895 ) -> Result<HandshakeSignatureValid, RustlsError> {
1896 Ok(HandshakeSignatureValid::assertion())
1897 }
1898
1899 fn verify_tls13_signature(
1900 &self,
1901 _message: &[u8],
1902 _cert: &CertificateDer<'_>,
1903 _dss: &DigitallySignedStruct,
1904 ) -> Result<HandshakeSignatureValid, RustlsError> {
1905 Ok(HandshakeSignatureValid::assertion())
1906 }
1907
1908 fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
1909 vec![
1910 SignatureScheme::ECDSA_NISTP256_SHA256,
1911 SignatureScheme::RSA_PSS_SHA256,
1912 ]
1913 }
1914 }
1915
1916 #[tokio::test]
1917 async fn add_tls_to_channel_with_custom_verifier() {
1918 let tls_opts = TlsOptions {
1919 server_cert_verifier: Some(Arc::new(MockVerifier)),
1920 domain: Some("test.temporal.io".to_string()),
1921 ..Default::default()
1922 };
1923 let endpoint = tonic::transport::Channel::from_static("https://test.temporal.io:7233");
1924 let result = add_tls_to_channel(Some(&tls_opts), endpoint).await;
1925 assert!(
1926 result.is_ok(),
1927 "add_tls_to_channel should succeed with a custom verifier: {:?}",
1928 result.err()
1929 );
1930 }
1931
1932 #[tokio::test]
1933 async fn add_tls_to_channel_with_verifier_and_ca_cert_fails() {
1934 let tls_opts = TlsOptions {
1937 server_root_ca_cert: Some(b"some-ca-cert-bytes".to_vec()),
1938 server_cert_verifier: Some(Arc::new(MockVerifier)),
1939 domain: Some("test.temporal.io".to_string()),
1940 ..Default::default()
1941 };
1942 let endpoint = tonic::transport::Channel::from_static("https://test.temporal.io:7233");
1943 let result = add_tls_to_channel(Some(&tls_opts), endpoint).await;
1944 assert!(
1945 matches!(result, Err(ClientConnectError::InvalidConfig(_))),
1946 "add_tls_to_channel should fail with InvalidConfig when both CA cert and verifier are set: {:?}",
1947 result
1948 );
1949 }
1950
1951 #[tokio::test]
1952 async fn add_tls_to_channel_without_verifier_still_works() {
1953 let tls_opts = TlsOptions {
1955 domain: Some("test.temporal.io".to_string()),
1956 ..Default::default()
1957 };
1958 let endpoint = tonic::transport::Channel::from_static("https://test.temporal.io:7233");
1959 let result = add_tls_to_channel(Some(&tls_opts), endpoint).await;
1960 assert!(
1961 result.is_ok(),
1962 "add_tls_to_channel should succeed without a verifier (native roots): {:?}",
1963 result.err()
1964 );
1965 }
1966 }
1967
1968 mod start_workflow_interceptor_tests {
1969 use super::*;
1970 use crate::request_extensions::RetryConfigForCall;
1971 use parking_lot::Mutex;
1972 use std::sync::atomic::{AtomicUsize, Ordering};
1973 use temporalio_common::{
1974 HasWorkflowDefinition, WorkflowDefinition,
1975 data_converters::{
1976 DefaultFailureConverter, PayloadCodec, PayloadConversionError,
1977 SerializationContext, SerializationContextData, TemporalSerializable,
1978 },
1979 protos::temporal::api::common::v1::Payload,
1980 };
1981 use tonic::{Request, Response};
1982
1983 struct TestWorkflow;
1984
1985 impl WorkflowDefinition for TestWorkflow {
1986 type Input = Vec<String>;
1987 type Output = ();
1988
1989 fn name(&self) -> &str {
1990 "test-workflow"
1991 }
1992 }
1993
1994 impl HasWorkflowDefinition for TestWorkflow {
1995 type Run = Self;
1996 }
1997
1998 #[derive(Default)]
1999 struct RecordedStart {
2000 calls: usize,
2001 workflow_type: String,
2002 payloads: Vec<Payload>,
2003 ascii_metadata: Option<String>,
2004 binary_metadata: Option<Vec<u8>>,
2005 grpc_timeout: Option<String>,
2006 retry_options: Option<RetryOptions>,
2007 }
2008
2009 struct CountingCodec {
2010 encode_calls: Arc<AtomicUsize>,
2011 }
2012
2013 impl PayloadCodec for CountingCodec {
2014 fn encode(
2015 &self,
2016 _context: &SerializationContextData,
2017 payloads: Vec<Payload>,
2018 ) -> futures_util::future::BoxFuture<
2019 'static,
2020 Result<Vec<Payload>, PayloadConversionError>,
2021 > {
2022 self.encode_calls.fetch_add(1, Ordering::SeqCst);
2023 Box::pin(async move { Ok(payloads) })
2024 }
2025
2026 fn decode(
2027 &self,
2028 _context: &SerializationContextData,
2029 payloads: Vec<Payload>,
2030 ) -> futures_util::future::BoxFuture<
2031 'static,
2032 Result<Vec<Payload>, PayloadConversionError>,
2033 > {
2034 Box::pin(async move { Ok(payloads) })
2035 }
2036 }
2037
2038 #[derive(Clone)]
2039 struct MockStartWorkflowClient {
2040 recorded: Arc<Mutex<RecordedStart>>,
2041 data_converter: DataConverter,
2042 }
2043
2044 impl NamespacedClient for MockStartWorkflowClient {
2045 fn namespace(&self) -> String {
2046 "test-namespace".to_owned()
2047 }
2048
2049 fn identity(&self) -> String {
2050 "test-identity".to_owned()
2051 }
2052
2053 fn data_converter(&self) -> &DataConverter {
2054 &self.data_converter
2055 }
2056 }
2057
2058 impl WorkflowService for MockStartWorkflowClient {
2059 fn start_workflow_execution(
2060 &mut self,
2061 request: Request<StartWorkflowExecutionRequest>,
2062 ) -> futures_util::future::BoxFuture<
2063 '_,
2064 Result<Response<StartWorkflowExecutionResponse>, tonic::Status>,
2065 > {
2066 let ascii_metadata = request
2067 .metadata()
2068 .get("call-meta")
2069 .map(|value| value.to_str().unwrap().to_owned());
2070 let binary_metadata = request
2071 .metadata()
2072 .get_bin("call-meta-bin")
2073 .map(|value| value.to_bytes().unwrap().to_vec());
2074 let grpc_timeout = request
2075 .metadata()
2076 .get("grpc-timeout")
2077 .map(|value| value.to_str().unwrap().to_owned());
2078 let retry_options = request
2079 .extensions()
2080 .get::<RetryConfigForCall>()
2081 .map(|config| config.0.clone());
2082 let request = request.into_inner();
2083 let mut recorded = self.recorded.lock();
2084 recorded.calls += 1;
2085 recorded.workflow_type = request.workflow_type.unwrap().name;
2086 recorded.payloads = request.input.unwrap_or_default().payloads;
2087 recorded.ascii_metadata = ascii_metadata;
2088 recorded.binary_metadata = binary_metadata;
2089 recorded.grpc_timeout = grpc_timeout;
2090 recorded.retry_options = retry_options;
2091
2092 Box::pin(async {
2093 Ok(Response::new(StartWorkflowExecutionResponse {
2094 run_id: "server-run-id".to_owned(),
2095 ..Default::default()
2096 }))
2097 })
2098 }
2099
2100 fn signal_with_start_workflow_execution(
2101 &mut self,
2102 request: Request<SignalWithStartWorkflowExecutionRequest>,
2103 ) -> futures_util::future::BoxFuture<
2104 '_,
2105 Result<Response<SignalWithStartWorkflowExecutionResponse>, tonic::Status>,
2106 > {
2107 let ascii_metadata = request
2108 .metadata()
2109 .get("call-meta")
2110 .map(|value| value.to_str().unwrap().to_owned());
2111 let binary_metadata = request
2112 .metadata()
2113 .get_bin("call-meta-bin")
2114 .map(|value| value.to_bytes().unwrap().to_vec());
2115 let grpc_timeout = request
2116 .metadata()
2117 .get("grpc-timeout")
2118 .map(|value| value.to_str().unwrap().to_owned());
2119 let retry_options = request
2120 .extensions()
2121 .get::<RetryConfigForCall>()
2122 .map(|config| config.0.clone());
2123 let request = request.into_inner();
2124 let mut recorded = self.recorded.lock();
2125 recorded.calls += 1;
2126 recorded.workflow_type = request.workflow_type.unwrap().name;
2127 recorded.payloads = request.input.unwrap_or_default().payloads;
2128 recorded.ascii_metadata = ascii_metadata;
2129 recorded.binary_metadata = binary_metadata;
2130 recorded.grpc_timeout = grpc_timeout;
2131 recorded.retry_options = retry_options;
2132
2133 Box::pin(async {
2134 Ok(Response::new(SignalWithStartWorkflowExecutionResponse {
2135 run_id: "signal-server-run-id".to_owned(),
2136 ..Default::default()
2137 }))
2138 })
2139 }
2140 }
2141
2142 #[derive(Clone)]
2143 struct InterceptedClient {
2144 inner: MockStartWorkflowClient,
2145 interceptors: Vec<Arc<dyn ClientInterceptor>>,
2146 }
2147
2148 impl NamespacedClient for InterceptedClient {
2149 fn namespace(&self) -> String {
2150 self.inner.namespace()
2151 }
2152
2153 fn identity(&self) -> String {
2154 self.inner.identity()
2155 }
2156
2157 fn data_converter(&self) -> &DataConverter {
2158 self.inner.data_converter()
2159 }
2160
2161 fn client_interceptors(&self) -> &[Arc<dyn ClientInterceptor>] {
2162 &self.interceptors
2163 }
2164 }
2165
2166 impl WorkflowService for InterceptedClient {
2167 fn start_workflow_execution(
2168 &mut self,
2169 request: Request<StartWorkflowExecutionRequest>,
2170 ) -> futures_util::future::BoxFuture<
2171 '_,
2172 Result<Response<StartWorkflowExecutionResponse>, tonic::Status>,
2173 > {
2174 self.inner.start_workflow_execution(request)
2175 }
2176
2177 fn signal_with_start_workflow_execution(
2178 &mut self,
2179 request: Request<SignalWithStartWorkflowExecutionRequest>,
2180 ) -> futures_util::future::BoxFuture<
2181 '_,
2182 Result<Response<SignalWithStartWorkflowExecutionResponse>, tonic::Status>,
2183 > {
2184 self.inner.signal_with_start_workflow_execution(request)
2185 }
2186 }
2187
2188 struct OrderedInterceptor {
2189 name: &'static str,
2190 events: Arc<Mutex<Vec<String>>>,
2191 encode_calls: Arc<AtomicUsize>,
2192 }
2193
2194 impl ClientInterceptor for OrderedInterceptor {
2195 fn start_workflow<'a>(
2196 &'a self,
2197 mut input: StartWorkflowInput,
2198 next: Next<
2199 'a,
2200 StartWorkflowInput,
2201 BoxFuture<'a, Result<StartWorkflowOutput, WorkflowStartError>>,
2202 >,
2203 ) -> BoxFuture<'a, Result<StartWorkflowOutput, WorkflowStartError>> {
2204 Box::pin(async move {
2205 assert_eq!(self.encode_calls.load(Ordering::SeqCst), 0);
2206 self.events.lock().push(format!("{}-pre", self.name));
2207 tokio::task::yield_now().await;
2208 if self.name == "outer" {
2209 input
2210 .args_mut::<Vec<String>>()
2211 .unwrap()
2212 .push("mutated".to_owned());
2213 } else {
2214 assert_eq!(
2215 input.args_ref::<Vec<String>>().unwrap(),
2216 &["initial".to_owned(), "mutated".to_owned()]
2217 );
2218 input.replace_args("replacement".to_owned());
2219 input.workflow_type = "replacement-workflow".to_owned();
2220 }
2221 let result = next.run(input).await;
2222 tokio::task::yield_now().await;
2223 self.events.lock().push(format!("{}-post", self.name));
2224 result
2225 })
2226 }
2227 }
2228
2229 struct ShortCircuitInterceptor;
2230
2231 impl ClientInterceptor for ShortCircuitInterceptor {
2232 fn start_workflow<'a>(
2233 &'a self,
2234 input: StartWorkflowInput,
2235 _next: Next<
2236 'a,
2237 StartWorkflowInput,
2238 BoxFuture<'a, Result<StartWorkflowOutput, WorkflowStartError>>,
2239 >,
2240 ) -> BoxFuture<'a, Result<StartWorkflowOutput, WorkflowStartError>> {
2241 assert_eq!(
2242 input.args_ref::<Vec<String>>().unwrap(),
2243 &["initial".to_owned()]
2244 );
2245 Box::pin(async {
2246 Ok(StartWorkflowOutput::new(
2247 "short-circuit-workflow-id",
2248 "short-circuit-run-id",
2249 ))
2250 })
2251 }
2252 }
2253
2254 struct CountingInput {
2255 conversion_calls: Arc<AtomicUsize>,
2256 }
2257
2258 impl TemporalSerializable for CountingInput {
2259 fn to_payloads(
2260 &self,
2261 _context: &SerializationContext<'_>,
2262 ) -> Result<Vec<Payload>, PayloadConversionError> {
2263 self.conversion_calls.fetch_add(1, Ordering::SeqCst);
2264 Ok(vec![Payload::default()])
2265 }
2266 }
2267
2268 struct ConversionTimingInterceptor {
2269 conversion_calls: Arc<AtomicUsize>,
2270 }
2271
2272 impl ClientInterceptor for ConversionTimingInterceptor {
2273 fn start_workflow<'a>(
2274 &'a self,
2275 mut input: StartWorkflowInput,
2276 next: Next<
2277 'a,
2278 StartWorkflowInput,
2279 BoxFuture<'a, Result<StartWorkflowOutput, WorkflowStartError>>,
2280 >,
2281 ) -> BoxFuture<'a, Result<StartWorkflowOutput, WorkflowStartError>> {
2282 input.replace_args(CountingInput {
2283 conversion_calls: self.conversion_calls.clone(),
2284 });
2285 let future = next.run(input);
2286 assert_eq!(self.conversion_calls.load(Ordering::SeqCst), 0);
2287 future
2288 }
2289 }
2290
2291 fn mock_client(
2292 interceptors: Vec<Arc<dyn ClientInterceptor>>,
2293 encode_calls: Arc<AtomicUsize>,
2294 ) -> (InterceptedClient, Arc<Mutex<RecordedStart>>) {
2295 let recorded = Arc::new(Mutex::new(RecordedStart::default()));
2296 let data_converter = DataConverter::new(
2297 PayloadConverter::default(),
2298 DefaultFailureConverter,
2299 CountingCodec {
2300 encode_calls: encode_calls.clone(),
2301 },
2302 );
2303 (
2304 InterceptedClient {
2305 inner: MockStartWorkflowClient {
2306 recorded: recorded.clone(),
2307 data_converter,
2308 },
2309 interceptors,
2310 },
2311 recorded,
2312 )
2313 }
2314
2315 #[tokio::test]
2316 async fn interceptors_order_mutate_replace_and_defer_conversion() {
2317 let events = Arc::new(Mutex::new(Vec::new()));
2318 let encode_calls = Arc::new(AtomicUsize::new(0));
2319 let interceptors: Vec<Arc<dyn ClientInterceptor>> = vec![
2320 Arc::new(OrderedInterceptor {
2321 name: "outer",
2322 events: events.clone(),
2323 encode_calls: encode_calls.clone(),
2324 }),
2325 Arc::new(OrderedInterceptor {
2326 name: "inner",
2327 events: events.clone(),
2328 encode_calls: encode_calls.clone(),
2329 }),
2330 ];
2331 let (client, recorded) = mock_client(interceptors, encode_calls.clone());
2332
2333 let handle = client
2334 .start_workflow(
2335 TestWorkflow,
2336 vec!["initial".to_owned()],
2337 WorkflowStartOptions::new("task-queue", "workflow-id").build(),
2338 )
2339 .await
2340 .unwrap();
2341
2342 assert_eq!(
2343 events.lock().as_slice(),
2344 ["outer-pre", "inner-pre", "inner-post", "outer-post"]
2345 );
2346 assert_eq!(encode_calls.load(Ordering::SeqCst), 1);
2347 assert_eq!(handle.run_id(), Some("server-run-id"));
2348 let payloads = {
2349 let recorded = recorded.lock();
2350 assert_eq!(recorded.calls, 1);
2351 assert_eq!(recorded.workflow_type, "replacement-workflow");
2352 recorded.payloads.clone()
2353 };
2354 let replacement: String = client
2355 .data_converter()
2356 .from_payloads(&SerializationContextData::Workflow, payloads)
2357 .await
2358 .unwrap();
2359 assert_eq!(replacement, "replacement");
2360 }
2361
2362 #[tokio::test]
2363 async fn interceptor_can_short_circuit() {
2364 let encode_calls = Arc::new(AtomicUsize::new(0));
2365 let (client, recorded) = mock_client(
2366 vec![Arc::new(ShortCircuitInterceptor)],
2367 encode_calls.clone(),
2368 );
2369 let handle = client
2370 .start_workflow(
2371 TestWorkflow,
2372 vec!["initial".to_owned()],
2373 WorkflowStartOptions::new("task-queue", "ignored-workflow-id").build(),
2374 )
2375 .await
2376 .unwrap();
2377
2378 assert_eq!(handle.info().workflow_id, "short-circuit-workflow-id");
2379 assert_eq!(handle.run_id(), Some("short-circuit-run-id"));
2380 assert_eq!(recorded.lock().calls, 0);
2381 assert_eq!(encode_calls.load(Ordering::SeqCst), 0);
2382 }
2383
2384 #[tokio::test]
2385 async fn payload_conversion_waits_for_next_future_poll() {
2386 let conversion_calls = Arc::new(AtomicUsize::new(0));
2387 let encode_calls = Arc::new(AtomicUsize::new(0));
2388 let recorded = Arc::new(Mutex::new(RecordedStart::default()));
2389 let data_converter = DataConverter::new(
2390 PayloadConverter::UseWrappers,
2391 DefaultFailureConverter,
2392 CountingCodec {
2393 encode_calls: encode_calls.clone(),
2394 },
2395 );
2396 let client = InterceptedClient {
2397 inner: MockStartWorkflowClient {
2398 recorded: recorded.clone(),
2399 data_converter,
2400 },
2401 interceptors: vec![Arc::new(ConversionTimingInterceptor {
2402 conversion_calls: conversion_calls.clone(),
2403 })],
2404 };
2405
2406 client
2407 .start_workflow(
2408 TestWorkflow,
2409 vec!["initial".to_owned()],
2410 WorkflowStartOptions::new("task-queue", "workflow-id").build(),
2411 )
2412 .await
2413 .unwrap();
2414
2415 assert_eq!(conversion_calls.load(Ordering::SeqCst), 1);
2416 assert_eq!(encode_calls.load(Ordering::SeqCst), 1);
2417 assert_eq!(recorded.lock().calls, 1);
2418 }
2419
2420 #[tokio::test]
2421 async fn custom_client_defaults_to_empty_chain() {
2422 let recorded = Arc::new(Mutex::new(RecordedStart::default()));
2423 let client = MockStartWorkflowClient {
2424 recorded: recorded.clone(),
2425 data_converter: DataConverter::default(),
2426 };
2427 assert!(client.client_interceptors().is_empty());
2428
2429 client
2430 .start_workflow(
2431 TestWorkflow,
2432 vec!["initial".to_owned()],
2433 WorkflowStartOptions::new("task-queue", "workflow-id").build(),
2434 )
2435 .await
2436 .unwrap();
2437 assert_eq!(recorded.lock().calls, 1);
2438 }
2439
2440 #[tokio::test]
2441 async fn rpc_options_reach_the_request() {
2442 let (client, recorded) = mock_client(Vec::new(), Arc::new(AtomicUsize::new(0)));
2443 let mut rpc_options = RpcOptions {
2444 timeout: Some(Duration::from_millis(250)),
2445 retry_options: Some(RetryOptions::no_retries()),
2446 ..Default::default()
2447 };
2448 rpc_options
2449 .metadata
2450 .insert("call-meta", "call-value")
2451 .unwrap();
2452 rpc_options
2453 .metadata
2454 .insert_binary("call-meta-bin", vec![0, 255])
2455 .unwrap();
2456 let mut options = WorkflowStartOptions::new("task-queue", "workflow-id").build();
2457 options.rpc_options = rpc_options.clone();
2458
2459 client
2460 .start_workflow(TestWorkflow, vec!["initial".to_owned()], options)
2461 .await
2462 .unwrap();
2463
2464 {
2465 let recorded = recorded.lock();
2466 assert_eq!(recorded.ascii_metadata.as_deref(), Some("call-value"));
2467 assert_eq!(recorded.binary_metadata.as_deref(), Some(&[0, 255][..]));
2468 assert_eq!(recorded.grpc_timeout.as_deref(), Some("250000u"));
2469 assert_eq!(recorded.retry_options, Some(RetryOptions::no_retries()));
2470 }
2471
2472 let mut options = WorkflowStartOptions::new("task-queue", "signal-workflow-id").build();
2473 options.start_signal = Some(WorkflowStartSignal::new("signal-name").build());
2474 options.rpc_options = rpc_options;
2475 let handle = client
2476 .start_workflow(TestWorkflow, vec!["initial".to_owned()], options)
2477 .await
2478 .unwrap();
2479
2480 let recorded = recorded.lock();
2481 assert_eq!(recorded.calls, 2);
2482 assert_eq!(recorded.ascii_metadata.as_deref(), Some("call-value"));
2483 assert_eq!(recorded.binary_metadata.as_deref(), Some(&[0, 255][..]));
2484 assert_eq!(recorded.grpc_timeout.as_deref(), Some("250000u"));
2485 assert_eq!(recorded.retry_options, Some(RetryOptions::no_retries()));
2486 assert_eq!(handle.run_id(), Some("signal-server-run-id"));
2487 }
2488
2489 #[test]
2490 fn rpc_metadata_combines_with_and_overrides_connection_defaults() {
2491 let headers = Arc::new(RwLock::new(ClientHeaders {
2492 user_headers: HashMap::from([
2493 (
2494 "shared-meta".parse().unwrap(),
2495 "connection-value".parse().unwrap(),
2496 ),
2497 (
2498 "connection-meta".parse().unwrap(),
2499 "connection-only".parse().unwrap(),
2500 ),
2501 ]),
2502 user_binary_headers: HashMap::from([
2503 (
2504 "shared-meta-bin".parse().unwrap(),
2505 BinaryMetadataValue::from_bytes(&[1]),
2506 ),
2507 (
2508 "connection-meta-bin".parse().unwrap(),
2509 BinaryMetadataValue::from_bytes(&[2]),
2510 ),
2511 ]),
2512 api_key: None,
2513 }));
2514 let mut service_interceptor = ServiceCallInterceptor {
2515 client_name: "test-client".to_owned(),
2516 client_version: "test-version".to_owned(),
2517 headers,
2518 };
2519 let mut rpc_options = RpcOptions::default();
2520 rpc_options
2521 .metadata
2522 .insert("shared-meta", "call-value")
2523 .unwrap();
2524 rpc_options
2525 .metadata
2526 .insert("call-meta", "call-only")
2527 .unwrap();
2528 rpc_options
2529 .metadata
2530 .insert_binary("shared-meta-bin", vec![3])
2531 .unwrap();
2532 rpc_options
2533 .metadata
2534 .insert_binary("call-meta-bin", vec![4])
2535 .unwrap();
2536 let mut request = Request::new(());
2537 rpc_options.apply_to(&mut request);
2538
2539 let request = service_interceptor.call(request).unwrap();
2540 assert_eq!(request.metadata().get("shared-meta").unwrap(), "call-value");
2541 assert_eq!(request.metadata().get("call-meta").unwrap(), "call-only");
2542 assert_eq!(
2543 request.metadata().get("connection-meta").unwrap(),
2544 "connection-only"
2545 );
2546 assert_eq!(
2547 request.metadata().get_bin("shared-meta-bin").unwrap(),
2548 &[3][..]
2549 );
2550 assert_eq!(
2551 request.metadata().get_bin("call-meta-bin").unwrap(),
2552 &[4][..]
2553 );
2554 assert_eq!(
2555 request.metadata().get_bin("connection-meta-bin").unwrap(),
2556 &[2][..]
2557 );
2558 }
2559 }
2560
2561 mod list_workflows_tests {
2562 use super::*;
2563 use crate::test_helpers::{FailingCodec, XorCodec};
2564 use futures_util::{FutureExt, StreamExt};
2565 use std::sync::atomic::{AtomicUsize, Ordering};
2566 use temporalio_common::{
2567 data_converters::DefaultFailureConverter,
2568 protos::temporal::api::common::v1::{
2569 Memo as ProtoMemo, Payload, WorkflowExecution as ProtoWorkflowExecution,
2570 },
2571 };
2572 use tonic::{Request, Response};
2573
2574 #[derive(Clone)]
2575 struct MockListWorkflowsClient {
2576 call_count: Arc<AtomicUsize>,
2577 page_size: usize,
2579 total_workflows: usize,
2581 data_converter: DataConverter,
2582 memo_payload: Option<Payload>,
2583 interceptors: Vec<Arc<dyn ClientInterceptor>>,
2584 }
2585
2586 impl NamespacedClient for MockListWorkflowsClient {
2587 fn namespace(&self) -> String {
2588 "test-namespace".to_string()
2589 }
2590 fn identity(&self) -> String {
2591 "test-identity".to_string()
2592 }
2593 fn data_converter(&self) -> &DataConverter {
2594 &self.data_converter
2595 }
2596 fn client_interceptors(&self) -> &[Arc<dyn ClientInterceptor>] {
2597 &self.interceptors
2598 }
2599 }
2600
2601 struct CountingListInterceptor {
2602 calls: Arc<AtomicUsize>,
2603 }
2604
2605 impl ClientInterceptor for CountingListInterceptor {
2606 fn list_workflows_page<'a>(
2607 &'a self,
2608 input: ListWorkflowsPageInput,
2609 next: Next<
2610 'a,
2611 ListWorkflowsPageInput,
2612 BoxFuture<'a, Result<ListWorkflowsPageOutput, ClientError>>,
2613 >,
2614 ) -> BoxFuture<'a, Result<ListWorkflowsPageOutput, ClientError>> {
2615 self.calls.fetch_add(1, Ordering::SeqCst);
2616 next.run(input)
2617 }
2618 }
2619
2620 impl WorkflowService for MockListWorkflowsClient {
2621 fn list_workflow_executions(
2622 &mut self,
2623 request: Request<ListWorkflowExecutionsRequest>,
2624 ) -> futures_util::future::BoxFuture<
2625 '_,
2626 Result<Response<ListWorkflowExecutionsResponse>, tonic::Status>,
2627 > {
2628 self.call_count.fetch_add(1, Ordering::SeqCst);
2629 let req = request.into_inner();
2630
2631 let offset: usize = if req.next_page_token.is_empty() {
2633 0
2634 } else {
2635 String::from_utf8(req.next_page_token)
2636 .unwrap()
2637 .parse()
2638 .unwrap()
2639 };
2640
2641 let remaining = self.total_workflows.saturating_sub(offset);
2642 let count = remaining.min(self.page_size);
2643 let new_offset = offset + count;
2644
2645 let executions: Vec<_> = (offset..offset + count)
2646 .map(|i| workflow::WorkflowExecutionInfo {
2647 execution: Some(ProtoWorkflowExecution {
2648 workflow_id: format!("wf-{i}"),
2649 run_id: format!("run-{i}"),
2650 }),
2651 r#type: Some(WorkflowType {
2652 name: "TestWorkflow".to_string(),
2653 }),
2654 task_queue: "test-queue".to_string(),
2655 memo: self.memo_payload.clone().map(|payload| ProtoMemo {
2656 fields: HashMap::from([("memo-key".to_owned(), payload)]),
2657 }),
2658 ..Default::default()
2659 })
2660 .collect();
2661
2662 let next_page_token = if new_offset < self.total_workflows {
2663 new_offset.to_string().into_bytes()
2664 } else {
2665 vec![]
2666 };
2667
2668 async move {
2669 Ok(Response::new(ListWorkflowExecutionsResponse {
2670 executions,
2671 next_page_token,
2672 }))
2673 }
2674 .boxed()
2675 }
2676 }
2677
2678 #[tokio::test]
2679 async fn list_workflows_paginates_through_all_results() {
2680 let call_count = Arc::new(AtomicUsize::new(0));
2681 let interceptor_calls = Arc::new(AtomicUsize::new(0));
2682 let client = MockListWorkflowsClient {
2683 call_count: call_count.clone(),
2684 page_size: 3,
2685 total_workflows: 10,
2686 data_converter: DataConverter::default(),
2687 memo_payload: None,
2688 interceptors: vec![Arc::new(CountingListInterceptor {
2689 calls: interceptor_calls.clone(),
2690 })],
2691 };
2692
2693 let stream = client.list_workflows("", WorkflowListOptions::default());
2694 let results: Vec<_> = stream.collect().await;
2695
2696 assert_eq!(results.len(), 10);
2697 for (i, result) in results.iter().enumerate() {
2698 let wf = result.as_ref().unwrap();
2699 assert_eq!(wf.id(), format!("wf-{i}"));
2700 assert_eq!(wf.run_id(), format!("run-{i}"));
2701 }
2702 assert_eq!(call_count.load(Ordering::SeqCst), 4);
2704 assert_eq!(interceptor_calls.load(Ordering::SeqCst), 4);
2705 }
2706
2707 #[tokio::test]
2708 async fn list_workflows_respects_limit() {
2709 let call_count = Arc::new(AtomicUsize::new(0));
2710 let client = MockListWorkflowsClient {
2711 call_count: call_count.clone(),
2712 page_size: 3,
2713 total_workflows: 10,
2714 data_converter: DataConverter::default(),
2715 memo_payload: None,
2716 interceptors: Vec::new(),
2717 };
2718
2719 let opts = WorkflowListOptions::builder().limit(5).build();
2720 let stream = client.list_workflows("", opts);
2721 let results: Vec<_> = stream.collect().await;
2722
2723 assert_eq!(results.len(), 5);
2724 for (i, result) in results.iter().enumerate() {
2725 let wf = result.as_ref().unwrap();
2726 assert_eq!(wf.id(), format!("wf-{i}"));
2727 }
2728 assert_eq!(call_count.load(Ordering::SeqCst), 2);
2730 }
2731
2732 #[tokio::test]
2733 async fn list_workflows_limit_less_than_page_size() {
2734 let call_count = Arc::new(AtomicUsize::new(0));
2735 let client = MockListWorkflowsClient {
2736 call_count: call_count.clone(),
2737 page_size: 10,
2738 total_workflows: 100,
2739 data_converter: DataConverter::default(),
2740 memo_payload: None,
2741 interceptors: Vec::new(),
2742 };
2743
2744 let opts = WorkflowListOptions::builder().limit(3).build();
2745 let stream = client.list_workflows("", opts);
2746 let results: Vec<_> = stream.collect().await;
2747
2748 assert_eq!(results.len(), 3);
2749 assert_eq!(call_count.load(Ordering::SeqCst), 1);
2751 }
2752
2753 #[tokio::test]
2754 async fn list_workflows_empty_results() {
2755 let call_count = Arc::new(AtomicUsize::new(0));
2756 let client = MockListWorkflowsClient {
2757 call_count: call_count.clone(),
2758 page_size: 10,
2759 total_workflows: 0,
2760 data_converter: DataConverter::default(),
2761 memo_payload: None,
2762 interceptors: Vec::new(),
2763 };
2764
2765 let stream = client.list_workflows("", WorkflowListOptions::default());
2766 let results: Vec<_> = stream.collect().await;
2767
2768 assert_eq!(results.len(), 0);
2769 assert_eq!(call_count.load(Ordering::SeqCst), 1);
2770 }
2771
2772 #[tokio::test]
2773 async fn list_workflows_exposes_typed_memo() {
2774 let data_converter = DataConverter::new(
2775 PayloadConverter::default(),
2776 DefaultFailureConverter,
2777 XorCodec,
2778 );
2779 let memo_payload = data_converter
2780 .to_payload(
2781 &SerializationContextData::Workflow,
2782 &"memo-value".to_owned(),
2783 )
2784 .await
2785 .unwrap();
2786 let client = MockListWorkflowsClient {
2787 call_count: Arc::new(AtomicUsize::new(0)),
2788 page_size: 1,
2789 total_workflows: 1,
2790 data_converter,
2791 memo_payload: Some(memo_payload),
2792 interceptors: Vec::new(),
2793 };
2794
2795 let workflow = client
2796 .list_workflows("", WorkflowListOptions::default())
2797 .next()
2798 .await
2799 .unwrap()
2800 .unwrap();
2801
2802 assert_eq!(
2803 workflow.memo().get::<String>("memo-key").unwrap(),
2804 Some("memo-value".to_owned())
2805 );
2806 }
2807
2808 #[tokio::test]
2809 async fn list_workflows_yields_codec_error_then_ends() {
2810 let client = MockListWorkflowsClient {
2811 call_count: Arc::new(AtomicUsize::new(0)),
2812 page_size: 1,
2813 total_workflows: 1,
2814 data_converter: DataConverter::new(
2815 PayloadConverter::default(),
2816 DefaultFailureConverter,
2817 FailingCodec,
2818 ),
2819 memo_payload: Some(Payload::default()),
2820 interceptors: Vec::new(),
2821 };
2822 let mut stream = client.list_workflows("", WorkflowListOptions::default());
2823
2824 let err = stream.next().await.unwrap().unwrap_err();
2825
2826 assert!(matches!(err, ClientError::PayloadConversion(_)));
2827 assert!(stream.next().await.is_none());
2828 }
2829 }
2830}