1use std::any::TypeId;
10use std::collections::{BTreeMap, BTreeSet};
11use std::sync::Arc;
12use std::time::{Duration, SystemTime};
13
14use dex_protocol::dex::flow_service_client::FlowServiceClient;
15use dex_protocol::dex::{
16 ActiveStepSearchMode as ProtoSearchMode, AttributeStoreNames, FlowAlreadyStartedOptions,
17 FlowConfig as ProtoFlowConfig, FlowResetStepMethod, FlowResetType, FlowRetryPolicy,
18 FlowStartOptions, FlowStatus as ProtoFlowStatus, FlowTimeoutPolicy as ProtoFlowTimeoutPolicy,
19 GetFlowSummaryRequest, IdReusePolicy as ProtoIdReusePolicy, InvokeRpcRequest,
20 ListStreamMessagesRequest, ReadStreamRequest, ResetFlowRequest, SearchFlowsRequest,
21 SkipTimerRequest, StartFlowRequest, StepDurability as ProtoStepDurability, StopFlowRequest,
22 StopType as ProtoStopType, TriggerContinueAsNewRequest, UpdateFlowConfigRequest,
23 WaitForAttributeRequest, WaitForFlowRequest, WaitForStepCompletionRequest,
24 WorkerTarget as ProtoWorkerTarget, WriteStreamRequest,
25};
26use tokio::runtime::Runtime;
27use tonic::{Code, Request, transport::Endpoint};
28use uuid::Uuid;
29
30mod attribute_reads;
31
32pub use attribute_reads::{AttributeRead, AttributeReads};
33
34use crate::credential::{CredentialChannel, CredentialInterceptor};
35use crate::sdk_error::{FlowTargetRequirement, ServiceError};
36use crate::server_protocol::{
37 NegotiatedServerProtocolVersion, negotiate_server_protocol, server_info_request_error,
38};
39use crate::stop_flow_options::StopType;
40use crate::time_travel_options::{TimeTravelPoint, TimeTravelStepMethod};
41use crate::value_hydrator::ValueHydrator;
42use crate::value_mapper;
43use crate::wait_options::ClientRequestBudget;
44use crate::worker_dispatcher::{map_flow_timeout_handler_options, map_step_options};
45use crate::{
46 ActiveStepSearchMode, Attribute, AttributeMap, AttributeMatch, BlobCache, ClientOptions, Flow,
47 FlowConfig, FlowErrorType, FlowInfo, FlowResult, FlowStatus, FlowTimeoutPolicy, IdReusePolicy,
48 Registry, RetryPolicy, Rpc, RpcInvokeOptions, SdkError, SdkResult, SearchFlowEntry,
49 SearchFlowsOptions, SearchFlowsPage, StartFlowOptions, StepCompletion, StepDurability,
50 StepExecutionId, StopFlowOptions, Stream, StreamMessage, StreamMessagesPage, TimeTravelOptions,
51 TimerId, Value, WaitForAttributeOptions, WaitForStepCompletionOptions,
52};
53
54pub struct Client {
74 runtime: Runtime,
75 registry: Registry,
76 service: FlowServiceClient<CredentialChannel>,
77 hydrator: ValueHydrator,
78 negotiated_protocol_version: NegotiatedServerProtocolVersion,
79}
80
81impl Client {
82 pub fn new(registry: Registry, blob_cache: Arc<BlobCache>, options: ClientOptions) -> Self {
90 Self::try_new(registry, blob_cache, options)
91 .unwrap_or_else(|error| panic!("cannot create Dex Client: {error}"))
92 }
93
94 pub fn try_new(
101 registry: Registry,
102 blob_cache: Arc<BlobCache>,
103 options: ClientOptions,
104 ) -> SdkResult<Self> {
105 let runtime = Runtime::new().map_err(service_error)?;
106 let endpoint = Endpoint::from_shared(endpoint_address(options.server_address_value()))
107 .map_err(service_error)?;
108 let service = {
109 let _runtime_guard = runtime.enter();
110 FlowServiceClient::new(CredentialChannel::new(
111 endpoint.connect_lazy(),
112 CredentialInterceptor::new(options.credential_value()),
113 ))
114 };
115 let hydrator = ValueHydrator::new(service.clone(), blob_cache);
116 Ok(Self {
117 runtime,
118 registry,
119 service,
120 hydrator,
121 negotiated_protocol_version: NegotiatedServerProtocolVersion::default(),
122 })
123 }
124
125 pub fn start_flow<SomeFlow: Flow>(
134 &self,
135 flow: &SomeFlow,
136 flow_id: &str,
137 input: SomeFlow::StartInput,
138 ) -> SdkResult<String> {
139 self.start_flow_with_options(flow, flow_id, input, StartFlowOptions::new())
140 }
141
142 pub fn start_flow_with_options<SomeFlow: Flow>(
157 &self,
158 flow: &SomeFlow,
159 flow_id: &str,
160 input: SomeFlow::StartInput,
161 options: StartFlowOptions,
162 ) -> SdkResult<String> {
163 require_name(flow_id, "Flow ID")?;
164 let registered = self.registry.flow(flow.flow_type())?;
165 if registered.type_id != TypeId::of::<SomeFlow>() {
166 return Err(SdkError::FlowDefinition {
167 message: format!(
168 "Flow {} does not match the registered Rust type",
169 flow.flow_type()
170 ),
171 });
172 }
173 let (start_step_type, step_input, step_options) = match ®istered.start_step {
174 Some(step) => (
175 step.name.to_string(),
176 Some(value_mapper::encode(&input)?),
177 Some(
178 map_step_options(
179 registered,
180 registered
181 .handler
182 .step_options(step.name)
183 .map_err(sdk_handler_error)?,
184 )
185 .map_err(sdk_handler_error)?,
186 ),
187 ),
188 None => (String::new(), None, None),
189 };
190 let flow_timeout_seconds = optional_seconds(options.timeout)?;
191 let flow_timeout_policy =
192 map_flow_timeout_policy(registered, flow_timeout_seconds, options.timeout_policy)?;
193 let request = StartFlowRequest {
194 flow_id: flow_id.to_string(),
195 flow_type: registered.name.to_string(),
196 flow_timeout_seconds,
197 flow_timeout_policy,
198 start_step_type,
199 step_input,
200 step_options,
201 flow_start_options: Some(self.map_start_options(
202 registered,
203 &options,
204 flow_timeout_seconds,
205 flow_timeout_policy,
206 )?),
207 request_id: options
208 .request_id
209 .clone()
210 .unwrap_or_else(|| Uuid::new_v4().to_string()),
211 };
212 let mut service = self.service.clone();
213 self.runtime.block_on(async {
214 service
215 .start_flow(request)
216 .await
217 .map(|response| response.into_inner().run_id)
218 .map_err(|status| {
219 SdkError::from_status(
220 status,
221 "start_flow",
222 Some(flow_id),
223 FlowTargetRequirement::None,
224 )
225 })
226 })
227 }
228
229 pub fn invoke_rpc<Input: Value, Output: Value>(
243 &self,
244 flow_id: &str,
245 rpc: Rpc<Input, Output>,
246 input: Input,
247 ) -> SdkResult<Output> {
248 self.do_invoke_rpc(flow_id, rpc.name(), &input, &RpcInvokeOptions::new())
249 }
250
251 pub fn invoke_rpc_with_options<Input: Value, Output: Value>(
258 &self,
259 flow_id: &str,
260 rpc: Rpc<Input, Output>,
261 input: Input,
262 options: RpcInvokeOptions,
263 ) -> SdkResult<Output> {
264 self.do_invoke_rpc(flow_id, rpc.name(), &input, &options)
265 }
266
267 pub fn invoke_rpc_without_input<Output: Value>(
271 &self,
272 flow_id: &str,
273 rpc: Rpc<(), Output>,
274 ) -> SdkResult<Output> {
275 self.do_invoke_rpc(flow_id, rpc.name(), &(), &RpcInvokeOptions::new())
276 }
277
278 pub fn invoke_rpc_without_input_with_options<Output: Value>(
284 &self,
285 flow_id: &str,
286 rpc: Rpc<(), Output>,
287 options: RpcInvokeOptions,
288 ) -> SdkResult<Output> {
289 self.do_invoke_rpc(flow_id, rpc.name(), &(), &options)
290 }
291
292 pub fn write_stream<T: Value>(
302 &self,
303 flow_id: &str,
304 stream: &Stream<T>,
305 source: &str,
306 value: T,
307 ) -> SdkResult<()> {
308 require_name(flow_id, "Flow ID")?;
309 require_name(source, "Stream source")?;
310 let flow_type = self.registry.flow_for_stream(stream)?.name.to_string();
311 let request = WriteStreamRequest {
312 flow_id: flow_id.to_string(),
313 flow_type,
314 stream_name: stream.name().to_string(),
315 stream_capacity_bytes: stream.stream_capacity_bytes(),
316 value: Some(value_mapper::encode(&value)?),
317 source: source.to_string(),
318 };
319 self.call_empty(
320 "write_stream",
321 Some(flow_id),
322 FlowTargetRequirement::None,
323 |mut service| async move { service.write_stream(request).await },
324 )
325 }
326
327 pub fn read_stream<T: Value>(
331 &self,
332 flow_id: &str,
333 stream: &Stream<T>,
334 resume_token: &str,
335 ) -> SdkResult<StreamMessage<T>> {
336 self.read_stream_result(flow_id, stream, resume_token, None)
337 }
338
339 pub fn read_stream_with_timeout<T: Value>(
346 &self,
347 flow_id: &str,
348 stream: &Stream<T>,
349 resume_token: &str,
350 timeout: Duration,
351 ) -> SdkResult<StreamMessage<T>> {
352 self.read_stream_result(flow_id, stream, resume_token, Some(timeout))
353 }
354
355 pub fn list_stream_messages<T: Value>(
367 &self,
368 flow_id: &str,
369 stream: &Stream<T>,
370 page_size: i32,
371 before_page_token: &str,
372 ) -> SdkResult<StreamMessagesPage<T>> {
373 require_name(flow_id, "Flow ID")?;
374 if page_size < 1 {
375 return Err(invalid("Stream page size must be positive"));
376 }
377 let flow_type = self.registry.flow_for_stream(stream)?.name.to_string();
378 let request = ListStreamMessagesRequest {
379 flow_id: flow_id.to_string(),
380 flow_type,
381 stream_name: stream.name().to_string(),
382 page_size,
383 before_page_token: before_page_token.to_string(),
384 };
385 let mut service = self.service.clone();
386 let response = self.runtime.block_on(async {
387 service
388 .list_stream_messages(request)
389 .await
390 .map(|response| response.into_inner())
391 .map_err(|status| {
392 SdkError::from_status(
393 status,
394 "list_stream_messages",
395 Some(flow_id),
396 FlowTargetRequirement::None,
397 )
398 })
399 })?;
400 let messages = response
401 .messages
402 .into_iter()
403 .map(|message| {
404 if message.resume_token.is_empty() {
405 return Err(invalid("ListStreamMessages returned an empty resume token"));
406 }
407 let value = message
408 .value
409 .ok_or_else(|| invalid("ListStreamMessages omitted a Stream message Value"))?;
410 let created_time = message.created_time.map(timestamp).ok_or_else(|| {
411 invalid("ListStreamMessages omitted a Stream message creation time")
412 })?;
413 Ok(StreamMessage {
414 value: value_mapper::decode(&value)?,
415 resume_token: message.resume_token,
416 created_time,
417 source: message.source,
418 })
419 })
420 .collect::<SdkResult<Vec<_>>>()?;
421 Ok(StreamMessagesPage {
422 messages,
423 next_page_token: response.next_page_token,
424 })
425 }
426
427 pub fn wait_for_flow(&self, flow_id: &str) -> SdkResult<FlowResult> {
434 self.wait_for_flow_result(flow_id, None)
435 }
436
437 pub fn wait_for_flow_with_timeout(
444 &self,
445 flow_id: &str,
446 timeout: Duration,
447 ) -> SdkResult<FlowResult> {
448 self.wait_for_flow_result(flow_id, Some(timeout))
449 }
450
451 pub fn describe_flow(&self, flow_id: &str) -> SdkResult<FlowInfo> {
457 let mut service = self.service.clone();
458 let response = self.runtime.block_on(async {
459 service
460 .get_flow_summary(GetFlowSummaryRequest {
461 flow_id: flow_id.to_string(),
462 run_id: String::new(),
463 })
464 .await
465 .map(|response| response.into_inner())
466 .map_err(|status| {
467 SdkError::from_status(
468 status,
469 "describe_flow",
470 Some(flow_id),
471 FlowTargetRequirement::Existing,
472 )
473 })
474 })?;
475 let execution = response
476 .flow_execution_id
477 .ok_or_else(|| service_error("GetFlowSummary omitted FlowExecutionID"))?;
478 Ok(FlowInfo {
479 flow_id: execution.flow_id,
480 run_id: execution.run_id,
481 flow_type: response.flow_type,
482 status: map_flow_status(response.flow_status)?,
483 started_at: response
484 .start_time
485 .map_or(SystemTime::UNIX_EPOCH, timestamp),
486 })
487 }
488
489 pub fn search_flows(&self, query: &str, page_size: i32) -> SdkResult<SearchFlowsPage> {
494 self.search_flows_page(query, page_size, "")
495 }
496
497 pub fn search_flows_page(
502 &self,
503 query: &str,
504 page_size: i32,
505 next_page_token: &str,
506 ) -> SdkResult<SearchFlowsPage> {
507 self.search_flows_with_options(query, page_size, next_page_token, SearchFlowsOptions::new())
508 }
509
510 pub fn search_flows_with_options(
512 &self,
513 query: &str,
514 page_size: i32,
515 next_page_token: &str,
516 options: SearchFlowsOptions,
517 ) -> SdkResult<SearchFlowsPage> {
518 if page_size < 0 {
519 return Err(invalid("search page size must not be negative"));
520 }
521 let mut service = self.service.clone();
522 self.runtime.block_on(async {
523 let response = service
524 .search_flows(SearchFlowsRequest {
525 query: query.to_string(),
526 page_size,
527 next_page_token: next_page_token.to_string(),
528 include_continued_as_new: options.include_continued_as_new,
529 })
530 .await
531 .map(|response| response.into_inner())
532 .map_err(|status| {
533 SdkError::from_status(status, "search_flows", None, FlowTargetRequirement::None)
534 })?;
535 let values = response
536 .flow_runs
537 .iter()
538 .flat_map(|entry| {
539 entry.indexed_attributes.iter().map(|attribute| {
540 attribute
541 .value
542 .clone()
543 .map(|value| (entry.flow_id.clone(), value))
544 .ok_or_else(|| invalid("Indexed Attribute has no Value"))
545 })
546 })
547 .collect::<SdkResult<Vec<_>>>()?;
548 let mut values = self.hydrator.hydrate_for_flows(values).await?.into_iter();
549 let mut flows = Vec::with_capacity(response.flow_runs.len());
550 for entry in response.flow_runs {
551 let mut indexed_attributes = BTreeMap::new();
552 for attribute in entry.indexed_attributes {
553 let value = values
554 .next()
555 .ok_or_else(|| invalid("Indexed Attribute hydration count mismatch"))?;
556 indexed_attributes.insert(attribute.key, value_mapper::decode_untyped(&value)?);
557 }
558 flows.push(SearchFlowEntry {
559 flow_id: entry.flow_id,
560 run_id: entry.run_id,
561 flow_type: entry.flow_type,
562 status: map_flow_status(entry.flow_status)?,
563 started_at: entry.start_time.map(timestamp),
564 closed_at: entry.close_time.map(timestamp),
565 indexed_attributes,
566 });
567 }
568 Ok(SearchFlowsPage {
569 flows,
570 next_page_token: response.next_page_token,
571 })
572 })
573 }
574
575 pub fn stop_flow(&self, flow_id: &str, options: StopFlowOptions) -> SdkResult<()> {
581 let stop_type = match options.stop_type {
582 StopType::Cancel => ProtoStopType::Cancel,
583 StopType::Terminate => ProtoStopType::Terminate,
584 StopType::Fail => ProtoStopType::Fail,
585 };
586 self.call_empty(
587 "stop_flow",
588 Some(flow_id),
589 FlowTargetRequirement::Active,
590 |mut service| async move {
591 service
592 .stop_flow(StopFlowRequest {
593 flow_id: flow_id.to_string(),
594 run_id: String::new(),
595 reason: options.reason.unwrap_or_default(),
596 stop_type: stop_type as i32,
597 })
598 .await
599 },
600 )
601 }
602
603 pub fn time_travel(&self, flow_id: &str, options: TimeTravelOptions) -> SdkResult<String> {
612 let mut request = ResetFlowRequest {
613 flow_id: flow_id.to_string(),
614 reset_type: FlowResetType::Unspecified as i32,
615 reason: options.reason.unwrap_or_default(),
616 skip_writes_reapply: options.skip_writes_reapply,
617 ..Default::default()
618 };
619 match options.point {
620 TimeTravelPoint::Beginning => request.reset_type = FlowResetType::Beginning as i32,
621 TimeTravelPoint::HistoryEventTime(time) => {
622 request.reset_type = FlowResetType::HistoryEventTime as i32;
623 request.history_event_time = rfc3339(time)?;
624 }
625 TimeTravelPoint::StepType(step_type) => {
626 request.reset_type = FlowResetType::StepType as i32;
627 request.step_type = step_type.to_string();
628 }
629 TimeTravelPoint::StepExecution(execution, method) => {
630 request.reset_type = FlowResetType::StepExecutionId as i32;
631 request.step_execution_id =
632 format!("{}-{}", execution.step_type, execution.execution_number);
633 request.step_method = match method {
634 TimeTravelStepMethod::WaitFor => FlowResetStepMethod::WaitFor as i32,
635 TimeTravelStepMethod::Execute => FlowResetStepMethod::Execute as i32,
636 };
637 }
638 }
639 let mut service = self.service.clone();
640 self.runtime.block_on(async {
641 service
642 .reset_flow(request)
643 .await
644 .map(|response| response.into_inner().run_id)
645 .map_err(|status| {
646 SdkError::from_status(
647 status,
648 "time_travel",
649 Some(flow_id),
650 FlowTargetRequirement::Existing,
651 )
652 })
653 })
654 }
655
656 pub fn skip_timer(
663 &self,
664 flow_id: &str,
665 step_execution: StepExecutionId,
666 timer: TimerId,
667 ) -> SdkResult<()> {
668 let (timer_condition_id, timer_condition_index) = match timer {
669 TimerId::ConditionId(id) => (id, None),
670 TimerId::ConditionIndex(index) => (
671 String::new(),
672 Some(i32::try_from(index).map_err(|_| invalid("timer index exceeds int32"))?),
673 ),
674 };
675 self.call_empty(
676 "skip_timer",
677 Some(flow_id),
678 FlowTargetRequirement::Active,
679 |mut service| async move {
680 service
681 .skip_timer(SkipTimerRequest {
682 flow_id: flow_id.to_string(),
683 run_id: String::new(),
684 step_execution_id: format!(
685 "{}-{}",
686 step_execution.step_type, step_execution.execution_number
687 ),
688 timer_condition_id,
689 timer_condition_index,
690 })
691 .await
692 },
693 )
694 }
695
696 pub fn wait_for_step_completion(
706 &self,
707 flow_id: &str,
708 step_execution: StepExecutionId,
709 options: WaitForStepCompletionOptions,
710 ) -> SdkResult<()> {
711 let request_budget = ClientRequestBudget::new(options.request_timeout)?;
712 let internal_handler_timeout_seconds = seconds32(options.internal_handler_timeout)?;
713 loop {
714 let request_attempt =
715 request_budget.next_attempt("wait_for_step_completion", flow_id)?;
716 let request_flow_id = flow_id.to_string();
717 let request_step_type = step_execution.step_type.to_string();
718 let request_step_execution_number = step_execution.execution_number.to_string();
719 let request_id = options.request_id.clone();
720 let result = self.call_empty(
721 "wait_for_step_completion",
722 Some(flow_id),
723 FlowTargetRequirement::Active,
724 |mut service| async move {
725 let mut request = Request::new(WaitForStepCompletionRequest {
726 flow_id: request_flow_id,
727 step_type: request_step_type,
728 step_execution_number: request_step_execution_number,
729 request_timeout_seconds: request_attempt.request_timeout_seconds,
730 internal_handler_timeout_seconds,
731 request_id,
732 });
733 if let Some(transport_timeout) = request_attempt.transport_timeout {
734 request.set_timeout(transport_timeout);
735 }
736 service.wait_for_step_completion(request).await
737 },
738 );
739 match result {
740 Err(SdkError::LongPollTimeout { .. }) => continue,
741 Err(error)
742 if error.service_error().is_some_and(|service| {
743 matches!(service.code(), Code::DeadlineExceeded | Code::Cancelled)
744 }) && request_budget.has_deadline() =>
745 {
746 return Err(SdkError::request_timeout(
747 "wait_for_step_completion",
748 flow_id,
749 ));
750 }
751 result => return result,
752 }
753 }
754 }
755
756 pub fn wait_for_attribute_match<T: Value>(
765 &self,
766 flow_id: &str,
767 attribute: &Attribute<T>,
768 attribute_match: AttributeMatch<T>,
769 options: WaitForAttributeOptions,
770 ) -> SdkResult<T> {
771 require_waitable_attribute(attribute.name(), attribute.is_hard_delete_on_null())?;
772 self.wait_for_attribute_value(flow_id, attribute.name(), &attribute_match, options)
773 }
774
775 pub fn wait_for_attribute_map_instance_match<T: Value>(
782 &self,
783 flow_id: &str,
784 attribute: &AttributeMap<T>,
785 instance: &str,
786 attribute_match: AttributeMatch<T>,
787 options: WaitForAttributeOptions,
788 ) -> SdkResult<T> {
789 require_waitable_attribute(attribute.name(), attribute.is_hard_delete_on_null())?;
790 self.wait_for_attribute_value(
791 flow_id,
792 &map_physical_name(attribute.name(), instance)?,
793 &attribute_match,
794 options,
795 )
796 }
797
798 fn wait_for_attribute_value<T: Value>(
799 &self,
800 flow_id: &str,
801 key: &str,
802 attribute_match: &AttributeMatch<T>,
803 options: WaitForAttributeOptions,
804 ) -> SdkResult<T> {
805 let mut encoded_match = attribute_match.encode()?;
806 encoded_match.key = key.to_string();
807 let request_budget = ClientRequestBudget::new(options.request_timeout)?;
808 let internal_handler_timeout_seconds = seconds32(options.internal_handler_timeout)?;
809 let response = loop {
810 let request_attempt =
811 request_budget.next_attempt("wait_for_attribute_match", flow_id)?;
812 let mut service = self.service.clone();
813 let mut request = Request::new(WaitForAttributeRequest {
814 flow_id: flow_id.to_string(),
815 r#match: Some(encoded_match.clone()),
816 request_timeout_seconds: request_attempt.request_timeout_seconds,
817 internal_handler_timeout_seconds,
818 request_id: options.request_id.clone(),
819 });
820 if let Some(transport_timeout) = request_attempt.transport_timeout {
821 request.set_timeout(transport_timeout);
822 }
823 let result = self
824 .runtime
825 .block_on(service.wait_for_attribute(request))
826 .map_err(|status| {
827 SdkError::from_status(
828 status,
829 "wait_for_attribute_match",
830 Some(flow_id),
831 FlowTargetRequirement::Active,
832 )
833 });
834 match result {
835 Ok(response) => break response.into_inner(),
836 Err(SdkError::LongPollTimeout { .. }) => continue,
837 Err(error)
838 if error.service_error().is_some_and(|service| {
839 matches!(service.code(), Code::DeadlineExceeded | Code::Cancelled)
840 }) && request_budget.has_deadline() =>
841 {
842 return Err(SdkError::request_timeout(
843 "wait_for_attribute_match",
844 flow_id,
845 ));
846 }
847 Err(error) => return Err(error),
848 }
849 };
850 let matched_value = response.matched_value.ok_or_else(|| SdkError::Service {
851 service: ServiceError::local(
852 "wait_for_attribute_match",
853 "WaitForAttribute response is incomplete",
854 ),
855 })?;
856 value_mapper::decode(&matched_value)
857 }
858
859 pub fn update_flow_config(&self, flow_id: &str, config: FlowConfig) -> SdkResult<()> {
864 let flow_config = self.map_flow_config(Some(&config))?;
865 self.call_empty(
866 "update_flow_config",
867 Some(flow_id),
868 FlowTargetRequirement::Active,
869 |mut service| async move {
870 service
871 .update_flow_config(UpdateFlowConfigRequest {
872 flow_id: flow_id.to_string(),
873 run_id: String::new(),
874 flow_config: Some(flow_config),
875 })
876 .await
877 },
878 )
879 }
880
881 pub fn trigger_continue_as_new(&self, flow_id: &str) -> SdkResult<()> {
883 self.call_empty(
884 "trigger_continue_as_new",
885 Some(flow_id),
886 FlowTargetRequirement::Active,
887 |mut service| async move {
888 service
889 .trigger_continue_as_new(TriggerContinueAsNewRequest {
890 flow_id: flow_id.to_string(),
891 run_id: String::new(),
892 })
893 .await
894 },
895 )
896 }
897
898 pub fn health_check(&self) -> SdkResult<()> {
902 self.call_empty(
903 "health_check",
904 None,
905 FlowTargetRequirement::None,
906 |mut service| async move { service.health_check(()).await },
907 )
908 }
909
910 fn do_invoke_rpc<Input: Value, Output: Value>(
911 &self,
912 flow_id: &str,
913 rpc_name: &str,
914 input: &Input,
915 invoke_options: &RpcInvokeOptions,
916 ) -> SdkResult<Output> {
917 let (flow, rpc) = self.registry.rpc_with_flow(rpc_name)?;
918 let mut lock_attribute_keys = rpc
919 .locks
920 .iter()
921 .map(|lock| lock.physical_name())
922 .collect::<BTreeSet<_>>();
923 for lock in &invoke_options.lock_attribute_map_instances {
924 let Some(instance) = lock.instance() else {
925 return Err(SdkError::FlowDefinition {
926 message: "RpcInvokeOptions lock must target an AttributeMap instance".into(),
927 });
928 };
929 require_rpc_invoke_definition(
930 flow,
931 lock.attribute_name(),
932 crate::persistence::PersistenceKind::AttributeMap,
933 )?;
934 crate::registry::validate_map_instance(instance).map_err(invalid)?;
935 lock_attribute_keys.insert(lock.physical_name());
936 }
937 let mut load_attribute_map_instances = rpc
938 .load_attribute_maps
939 .iter()
940 .map(|load| map_load_name(&load.name, load.instance.as_deref()))
941 .collect::<BTreeSet<_>>();
942 for load in &invoke_options.load_attribute_map_instances {
943 let instance = require_rpc_invoke_map_load(
944 flow,
945 &load.name,
946 load.instance.as_deref(),
947 crate::persistence::PersistenceKind::AttributeMap,
948 )?;
949 load_attribute_map_instances
950 .insert(crate::registry::physical_name(&load.name, instance));
951 }
952 let mut load_channel_names = rpc
953 .load_channels
954 .iter()
955 .map(|load| load.name.clone())
956 .collect::<Vec<_>>();
957 load_channel_names.sort();
958 let mut load_channel_map_instances = rpc
959 .load_channel_maps
960 .iter()
961 .map(|load| map_load_name(&load.name, load.instance.as_deref()))
962 .collect::<BTreeSet<_>>();
963 for load in &invoke_options.load_channel_map_instances {
964 let instance = require_rpc_invoke_map_load(
965 flow,
966 &load.name,
967 load.instance.as_deref(),
968 crate::persistence::PersistenceKind::ChannelMap,
969 )?;
970 load_channel_map_instances.insert(crate::registry::physical_name(&load.name, instance));
971 }
972 let request = InvokeRpcRequest {
973 flow_id: flow_id.to_string(),
974 run_id: String::new(),
975 rpc_name: rpc_name.to_string(),
976 input: Some(value_mapper::encode(input)?),
977 timeout_seconds: optional_seconds(rpc.timeout)?,
978 lock_attribute_keys: lock_attribute_keys.into_iter().collect(),
979 request_id: Uuid::new_v4().to_string(),
980 is_transactional: rpc.is_transactional,
981 load_attribute_map_instances: load_attribute_map_instances.into_iter().collect(),
982 load_channel_names,
983 load_channel_map_instances: load_channel_map_instances.into_iter().collect(),
984 };
985 let mut service = self.service.clone();
986 let output = self.runtime.block_on(async {
987 service
988 .invoke_rpc(request)
989 .await
990 .map(|response| response.into_inner().output)
991 .map_err(|status| {
992 SdkError::from_status(
993 status,
994 "invoke_rpc",
995 Some(flow_id),
996 FlowTargetRequirement::Active,
997 )
998 })
999 })?;
1000 let output = output.ok_or_else(|| invalid("InvokeRPC omitted output"))?;
1001 let output = self
1002 .runtime
1003 .block_on(self.hydrator.hydrate(flow_id, output))?;
1004 value_mapper::decode(&output)
1005 }
1006
1007 fn read_stream_result<T: Value>(
1008 &self,
1009 flow_id: &str,
1010 stream: &Stream<T>,
1011 resume_token: &str,
1012 timeout: Option<Duration>,
1013 ) -> SdkResult<StreamMessage<T>> {
1014 require_name(flow_id, "Flow ID")?;
1015 let flow_type = self.registry.flow_for_stream(stream)?.name.to_string();
1016 let wait_time_seconds = optional_seconds(timeout)?;
1017 let request = ReadStreamRequest {
1018 flow_id: flow_id.to_string(),
1019 flow_type,
1020 stream_name: stream.name().to_string(),
1021 resume_token: resume_token.to_string(),
1022 wait_time_seconds,
1023 };
1024 let mut service = self.service.clone();
1025 let response = self.runtime.block_on(async {
1026 service
1027 .read_stream(request)
1028 .await
1029 .map(|response| response.into_inner())
1030 .map_err(|status| {
1031 SdkError::from_status(
1032 status,
1033 "read_stream",
1034 Some(flow_id),
1035 FlowTargetRequirement::None,
1036 )
1037 })
1038 })?;
1039 let message = response
1040 .message
1041 .ok_or_else(|| invalid("ReadStream omitted its message"))?;
1042 if message.resume_token.is_empty() {
1043 return Err(invalid("ReadStream returned an empty resume token"));
1044 }
1045 let value = message
1046 .value
1047 .ok_or_else(|| invalid("ReadStream omitted its Value"))?;
1048 let created_time = message
1049 .created_time
1050 .map(timestamp)
1051 .ok_or_else(|| invalid("ReadStream omitted its creation time"))?;
1052 Ok(StreamMessage {
1053 value: value_mapper::decode(&value)?,
1054 resume_token: message.resume_token,
1055 created_time,
1056 source: message.source,
1057 })
1058 }
1059
1060 fn wait_for_flow_result(
1061 &self,
1062 flow_id: &str,
1063 timeout: Option<Duration>,
1064 ) -> SdkResult<FlowResult> {
1065 let mut service = self.service.clone();
1066 let response = self.runtime.block_on(async {
1067 service
1068 .wait_for_flow(WaitForFlowRequest {
1069 flow_id: flow_id.to_string(),
1070 run_id: String::new(),
1071 needs_results: true,
1072 wait_time_seconds: optional_seconds(timeout)?,
1073 })
1074 .await
1075 .map(|response| response.into_inner())
1076 .map_err(|status| {
1077 SdkError::from_status(
1078 status,
1079 "wait_for_flow",
1080 Some(flow_id),
1081 FlowTargetRequirement::Existing,
1082 )
1083 })
1084 });
1085 let response = response?;
1086 let mut outputs = Vec::with_capacity(response.results.len());
1087 for result in &response.results {
1088 outputs.push(result.completed_step_output.clone().ok_or_else(|| {
1089 SdkError::ValueMapping {
1090 message: "Step completion output is required".to_string(),
1091 }
1092 })?);
1093 }
1094 let outputs = self
1095 .runtime
1096 .block_on(self.hydrator.hydrate_all(flow_id, outputs))?;
1097 let completions = response
1098 .results
1099 .into_iter()
1100 .zip(outputs)
1101 .map(|(result, output)| {
1102 StepCompletion::new(
1103 result.completed_step_type,
1104 result.completed_step_execution_id,
1105 output,
1106 )
1107 })
1108 .collect();
1109 Ok(FlowResult::new(
1110 map_flow_status(response.flow_status)?,
1111 map_flow_error_type(response.error_type),
1112 (!response.error_message.is_empty()).then_some(response.error_message),
1113 completions,
1114 ))
1115 }
1116
1117 fn map_start_options(
1118 &self,
1119 flow: &crate::registry::RegisteredFlow,
1120 options: &StartFlowOptions,
1121 flow_timeout_seconds: i32,
1122 flow_timeout_policy: i32,
1123 ) -> SdkResult<FlowStartOptions> {
1124 let attributes = options
1125 .attributes
1126 .iter()
1127 .map(|attribute| {
1128 let definition =
1129 flow.persistence
1130 .get(&attribute.attribute_name)
1131 .ok_or_else(|| {
1132 invalid(format!(
1133 "initial Attribute is not registered: {}",
1134 attribute.key
1135 ))
1136 })?;
1137 attribute.encode_attribute_write(definition)
1138 })
1139 .collect::<SdkResult<Vec<_>>>()?;
1140 if let Some(write) = attributes.iter().find(|write| write.is_hard_delete_on_null) {
1141 self.require_hard_delete_attribute_support("start_flow", &write.key)?;
1142 }
1143 let config = options.config_override.as_ref();
1144 let flow_config_override = Some(map_pull_flow_config(config)?);
1145 #[expect(
1146 clippy::needless_update,
1147 reason = "The test preserves forward-compatible construction of SDK options."
1148 )]
1149 let flow_start_options = FlowStartOptions {
1150 id_reuse_policy: match options.id_reuse_policy {
1151 IdReusePolicy::Default => ProtoIdReusePolicy::Unspecified,
1152 IdReusePolicy::AllowIfPreviousFailed => {
1153 ProtoIdReusePolicy::AllowIfPreviousExistsAbnormally
1154 }
1155 IdReusePolicy::AllowIfNotRunning => ProtoIdReusePolicy::AllowIfNoRunning,
1156 IdReusePolicy::Disallow => ProtoIdReusePolicy::DisallowReuse,
1157 IdReusePolicy::TerminateIfRunning => ProtoIdReusePolicy::AllowTerminateIfRunning,
1158 } as i32,
1159 flow_start_delay_seconds: optional_seconds(options.start_delay)?,
1160 retry_policy: options
1161 .retry_policy
1162 .clone()
1163 .map(map_flow_retry)
1164 .transpose()?,
1165 attributes,
1166 flow_config_override,
1167 flow_already_started_options: Some(FlowAlreadyStartedOptions {
1168 ignore_already_started_error: options.ignore_already_started,
1169 }),
1170 timeout_handler_options: map_flow_timeout_handler_options(
1171 flow,
1172 flow_timeout_seconds,
1173 flow_timeout_policy,
1174 options.timeout_handler_options.as_ref(),
1175 )
1176 .map_err(sdk_handler_error)?,
1177 ..Default::default()
1178 };
1179 Ok(flow_start_options)
1180 }
1181
1182 fn require_hard_delete_attribute_support(
1183 &self,
1184 operation: &'static str,
1185 attribute_key: &str,
1186 ) -> SdkResult<()> {
1187 if !self.negotiated_protocol_version.has_negotiated() {
1188 let mut service = self.service.clone();
1189 let server_info = self
1190 .runtime
1191 .block_on(service.get_server_info(()))
1192 .map_err(|status| SdkError::Service {
1193 service: ServiceError::local(operation, server_info_request_error(status)),
1194 })?
1195 .into_inner();
1196 let negotiated_protocol_version =
1197 negotiate_server_protocol(&server_info).map_err(|error| SdkError::Service {
1198 service: ServiceError::local(operation, error),
1199 })?;
1200 self.negotiated_protocol_version
1201 .store(negotiated_protocol_version);
1202 }
1203 self.negotiated_protocol_version
1204 .require_hard_delete_attribute_support(attribute_key)
1205 .map_err(|error| SdkError::Service {
1206 service: ServiceError::local(operation, error),
1207 })
1208 }
1209
1210 fn map_flow_config(&self, config: Option<&FlowConfig>) -> SdkResult<ProtoFlowConfig> {
1211 map_flow_config(config)
1212 }
1213
1214 fn call_empty<Response, Future, Call>(
1215 &self,
1216 operation: &'static str,
1217 flow_id: Option<&str>,
1218 requirement: FlowTargetRequirement,
1219 call: Call,
1220 ) -> SdkResult<()>
1221 where
1222 Call: FnOnce(FlowServiceClient<CredentialChannel>) -> Future,
1223 Future: std::future::Future<Output = Result<tonic::Response<Response>, tonic::Status>>,
1224 {
1225 let service = self.service.clone();
1226 self.runtime
1227 .block_on(call(service))
1228 .map(|_| ())
1229 .map_err(|status| SdkError::from_status(status, operation, flow_id, requirement))
1230 }
1231}
1232
1233pub(crate) fn map_pull_flow_config(config: Option<&FlowConfig>) -> SdkResult<ProtoFlowConfig> {
1234 Ok(ProtoFlowConfig {
1235 worker_target: Some(ProtoWorkerTarget { is_pull_mode: true }),
1236 ..map_flow_config(config)?
1237 })
1238}
1239
1240pub(crate) fn map_flow_config(config: Option<&FlowConfig>) -> SdkResult<ProtoFlowConfig> {
1241 Ok(ProtoFlowConfig {
1242 active_step_search_mode: config
1243 .and_then(|config| config.active_step_search_mode)
1244 .map(|mode| match mode {
1245 ActiveStepSearchMode::All => ProtoSearchMode::EnabledForAll as i32,
1246 ActiveStepSearchMode::WithWaitFor => {
1247 ProtoSearchMode::EnabledForStepsWithWaitFor as i32
1248 }
1249 ActiveStepSearchMode::Disabled => ProtoSearchMode::Disabled as i32,
1250 }),
1251 continue_as_new_threshold: config
1252 .and_then(|config| config.continue_as_new_threshold)
1253 .map(i32::try_from)
1254 .transpose()
1255 .map_err(|_| invalid("continue-as-new threshold exceeds int32"))?,
1256 continue_as_new_page_size_in_bytes: config
1257 .and_then(|config| config.continue_as_new_page_size_bytes)
1258 .map(i32::try_from)
1259 .transpose()
1260 .map_err(|_| invalid("continue-as-new page size exceeds int32"))?,
1261 step_durability: config
1262 .and_then(|config| config.step_durability)
1263 .map(|durability| {
1264 (match durability {
1265 StepDurability::Default => ProtoStepDurability::Unspecified,
1266 StepDurability::Sync => ProtoStepDurability::Sync,
1267 StepDurability::Async => ProtoStepDurability::Async,
1268 }) as i32
1269 }),
1270 worker_target: None,
1271 attribute_store_names: config.and_then(|config| {
1272 config
1273 .attribute_store_names
1274 .as_ref()
1275 .map(|names| AttributeStoreNames {
1276 names: names.clone(),
1277 })
1278 }),
1279 })
1280}
1281
1282pub(crate) fn map_flow_retry(retry: RetryPolicy) -> SdkResult<FlowRetryPolicy> {
1283 Ok(FlowRetryPolicy {
1284 initial_interval_seconds: optional_seconds(retry.initial_interval)?,
1285 backoff_coefficient: retry.backoff_coefficient.unwrap_or_default() as f32,
1286 maximum_interval_seconds: optional_seconds(retry.maximum_interval)?,
1287 maximum_attempts: i32::try_from(retry.maximum_attempts.unwrap_or_default())
1288 .map_err(|_| invalid("maximum attempts exceed int32"))?,
1289 })
1290}
1291
1292pub(crate) fn map_flow_status(status: i32) -> SdkResult<FlowStatus> {
1293 match ProtoFlowStatus::try_from(status).ok() {
1294 Some(ProtoFlowStatus::Running) => Ok(FlowStatus::Running),
1295 Some(ProtoFlowStatus::Completed) => Ok(FlowStatus::Completed),
1296 Some(ProtoFlowStatus::Failed) => Ok(FlowStatus::Failed),
1297 Some(ProtoFlowStatus::ServerSideTimeoutInternalOnly) => {
1298 Ok(FlowStatus::ServerSideTimeoutInternalOnly)
1299 }
1300 Some(ProtoFlowStatus::Terminated) => Ok(FlowStatus::Terminated),
1301 Some(ProtoFlowStatus::Canceled) => Ok(FlowStatus::Canceled),
1302 Some(ProtoFlowStatus::ContinuedAsNew) => Ok(FlowStatus::ContinuedAsNew),
1303 _ => Err(invalid(format!("unknown Flow status {status}"))),
1304 }
1305}
1306
1307pub(crate) fn map_flow_error_type(error_type: i32) -> Option<FlowErrorType> {
1308 use dex_protocol::dex::FlowErrorType as ProtoFlowErrorType;
1309
1310 match ProtoFlowErrorType::try_from(error_type).ok() {
1311 Some(ProtoFlowErrorType::StepDecisionFailingFlow) => {
1312 Some(FlowErrorType::StepDecisionFailed)
1313 }
1314 Some(ProtoFlowErrorType::ClientApiFailingFlow) => Some(FlowErrorType::ClientApiFailed),
1315 Some(ProtoFlowErrorType::WorkerApiFail) => Some(FlowErrorType::WorkerApiFailed),
1316 Some(ProtoFlowErrorType::InvalidUserFlowCode) => Some(FlowErrorType::InvalidUserFlowCode),
1317 Some(ProtoFlowErrorType::Internal) => Some(FlowErrorType::Internal),
1318 Some(ProtoFlowErrorType::FlowTimeout) => Some(FlowErrorType::FlowTimeout),
1319 _ => None,
1320 }
1321}
1322
1323pub(crate) fn map_flow_timeout_policy(
1324 flow: &crate::registry::RegisteredFlow,
1325 timeout_seconds: i32,
1326 policy: FlowTimeoutPolicy,
1327) -> SdkResult<i32> {
1328 if timeout_seconds == 0 {
1329 if policy != FlowTimeoutPolicy::Default {
1330 return Err(invalid("Flow timeout policy requires a positive timeout"));
1331 }
1332 return Ok(ProtoFlowTimeoutPolicy::Unspecified as i32);
1333 }
1334 let resolved = match policy {
1335 FlowTimeoutPolicy::Default if flow.handler.has_timeout_handler() => {
1336 FlowTimeoutPolicy::Handler
1337 }
1338 FlowTimeoutPolicy::Default => FlowTimeoutPolicy::Fail,
1339 configured => configured,
1340 };
1341 if resolved == FlowTimeoutPolicy::Handler && !flow.handler.has_timeout_handler() {
1342 return Err(invalid(format!(
1343 "Flow {} has no timeout handler",
1344 flow.name
1345 )));
1346 }
1347 Ok(match resolved {
1348 FlowTimeoutPolicy::Default => ProtoFlowTimeoutPolicy::Unspecified,
1349 FlowTimeoutPolicy::Fail => ProtoFlowTimeoutPolicy::Fail,
1350 FlowTimeoutPolicy::Cancel => ProtoFlowTimeoutPolicy::Cancel,
1351 FlowTimeoutPolicy::Handler => ProtoFlowTimeoutPolicy::Handler,
1352 } as i32)
1353}
1354
1355fn timestamp(timestamp: prost_types::Timestamp) -> SystemTime {
1356 if timestamp.seconds >= 0 {
1357 SystemTime::UNIX_EPOCH
1358 + Duration::from_secs(timestamp.seconds as u64)
1359 + Duration::from_nanos(timestamp.nanos.max(0) as u64)
1360 } else {
1361 SystemTime::UNIX_EPOCH - Duration::from_secs(timestamp.seconds.unsigned_abs())
1362 }
1363}
1364
1365fn rfc3339(time: SystemTime) -> SdkResult<String> {
1366 let duration = time
1367 .duration_since(SystemTime::UNIX_EPOCH)
1368 .map_err(service_error)?;
1369 let days = (duration.as_secs() / 86_400) as i64;
1370 let seconds = duration.as_secs() % 86_400;
1371 let (year, month, day) = civil_from_days(days);
1372 let hour = seconds / 3_600;
1373 let minute = seconds % 3_600 / 60;
1374 let second = seconds % 60;
1375 Ok(format!(
1376 "{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z"
1377 ))
1378}
1379
1380fn civil_from_days(days: i64) -> (i64, i64, i64) {
1381 let days = days + 719_468;
1382 let era = if days >= 0 { days } else { days - 146_096 } / 146_097;
1383 let day_of_era = days - era * 146_097;
1384 let year_of_era =
1385 (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
1386 let mut year = year_of_era + era * 400;
1387 let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
1388 let month_prime = (5 * day_of_year + 2) / 153;
1389 let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
1390 let month = month_prime + if month_prime < 10 { 3 } else { -9 };
1391 year += i64::from(month <= 2);
1392 (year, month, day)
1393}
1394
1395fn map_load_name(name: &str, instance: Option<&str>) -> String {
1396 match instance {
1397 Some(instance) => crate::registry::physical_name(name, instance),
1398 None => format!("{name}/"),
1399 }
1400}
1401
1402fn require_rpc_invoke_map_load<'a>(
1403 flow: &crate::registry::RegisteredFlow,
1404 name: &str,
1405 instance: Option<&'a str>,
1406 expected_kind: crate::persistence::PersistenceKind,
1407) -> SdkResult<&'a str> {
1408 require_rpc_invoke_definition(flow, name, expected_kind)?;
1409 let instance = instance.ok_or_else(|| SdkError::FlowDefinition {
1410 message: "RpcInvokeOptions requires an exact map instance".to_string(),
1411 })?;
1412 crate::registry::validate_map_instance(instance).map_err(invalid)?;
1413 Ok(instance)
1414}
1415
1416fn require_rpc_invoke_definition(
1417 flow: &crate::registry::RegisteredFlow,
1418 name: &str,
1419 expected_kind: crate::persistence::PersistenceKind,
1420) -> SdkResult<()> {
1421 flow.persistence
1422 .get(name)
1423 .filter(|definition| definition.kind == expected_kind)
1424 .ok_or_else(|| SdkError::FlowDefinition {
1425 message: format!(
1426 "Flow {} does not register the selected map {name}",
1427 flow.name
1428 ),
1429 })?;
1430 Ok(())
1431}
1432
1433fn optional_seconds(duration: Option<Duration>) -> SdkResult<i32> {
1434 duration
1435 .map(seconds32)
1436 .transpose()
1437 .map(Option::unwrap_or_default)
1438}
1439
1440pub(crate) fn seconds32(duration: Duration) -> SdkResult<i32> {
1441 if duration.subsec_nanos() != 0 {
1442 return Err(invalid("Duration must use whole seconds"));
1443 }
1444 i32::try_from(duration.as_secs()).map_err(|_| invalid("Duration exceeds int32"))
1445}
1446
1447fn endpoint_address(address: &str) -> String {
1448 if address.contains("://") {
1449 address.to_string()
1450 } else {
1451 format!("http://{address}")
1452 }
1453}
1454
1455fn require_name(value: &str, kind: &str) -> SdkResult<()> {
1456 if value.is_empty() {
1457 Err(invalid(format!("{kind} is required")))
1458 } else {
1459 Ok(())
1460 }
1461}
1462
1463fn map_physical_name(name: &str, instance: &str) -> SdkResult<String> {
1464 crate::registry::validate_map_instance(instance).map_err(invalid)?;
1465 Ok(crate::registry::physical_name(name, instance))
1466}
1467
1468fn require_waitable_attribute(attribute_name: &str, is_hard_delete_on_null: bool) -> SdkResult<()> {
1469 if is_hard_delete_on_null {
1470 return Err(invalid(format!(
1471 "wait_for_attribute_match does not support hard-delete Attribute {attribute_name}"
1472 )));
1473 }
1474 Ok(())
1475}
1476
1477fn sdk_handler_error(error: impl std::fmt::Display) -> SdkError {
1478 invalid(error.to_string())
1479}
1480
1481pub(crate) fn invalid(message: impl Into<String>) -> SdkError {
1482 SdkError::InvalidArgument {
1483 message: message.into(),
1484 }
1485}
1486
1487fn service_error(error: impl std::fmt::Display) -> SdkError {
1488 SdkError::Service {
1489 service: ServiceError::local("client", error.to_string()),
1490 }
1491}
1492
1493#[cfg(test)]
1494mod tests {
1495 use super::map_flow_config;
1496 use crate::FlowConfig;
1497
1498 #[test]
1499 fn attribute_store_names_preserve_protocol_presence() {
1500 let absent = map_flow_config(Some(&FlowConfig::new())).expect("map absent config");
1501 let named = map_flow_config(Some(
1502 &FlowConfig::new()
1503 .attribute_store_names(vec!["profiles".to_owned(), "audit".to_owned()]),
1504 ))
1505 .expect("map named config");
1506 let disabled = map_flow_config(Some(&FlowConfig::new().attribute_store_names(vec![])))
1507 .expect("map disabled config");
1508
1509 assert_eq!(absent.attribute_store_names, None);
1510 assert_eq!(
1511 named.attribute_store_names.expect("named stores").names,
1512 vec!["profiles", "audit"]
1513 );
1514 assert_eq!(
1515 disabled
1516 .attribute_store_names
1517 .expect("disabled stores")
1518 .names,
1519 Vec::<String>::new()
1520 );
1521 }
1522
1523 mod hard_delete {
1524 use std::convert::Infallible;
1525 use std::sync::atomic::{AtomicU32, Ordering};
1526 use std::sync::{Arc, LazyLock, Mutex};
1527 use std::task::{Context, Poll};
1528
1529 use dex_protocol::dex::ServerInfo;
1530 use tokio::net::TcpListener;
1531 use tokio::sync::oneshot;
1532 use tokio_stream::wrappers::TcpListenerStream;
1533 use tonic::codegen::{BoxFuture, Service, http};
1534 use tonic::server::{NamedService, UnaryService};
1535 use tonic::transport::Server;
1536 use tonic::{Request, Response, Status};
1537
1538 use crate::{
1539 Attribute, AttributeMap, AttributeMatch, BlobCache, BlobCacheConfig, Client,
1540 ClientOptions, Flow, PersistenceSchema, Registry, SdkError, StartFlowOptions,
1541 WaitForAttributeOptions,
1542 };
1543
1544 static API_KEY: LazyLock<Attribute<String>> =
1545 LazyLock::new(|| Attribute::new("api-key").hard_delete_on_null());
1546 static TENANT_KEYS: LazyLock<AttributeMap<String>> =
1547 LazyLock::new(|| AttributeMap::new("tenant-keys").hard_delete_on_null());
1548 static PLAIN_NOTE: LazyLock<Attribute<String>> =
1549 LazyLock::new(|| Attribute::new("plain-note"));
1550
1551 struct HardDeleteClientFlow;
1552
1553 impl Flow for HardDeleteClientFlow {
1554 type StartInput = ();
1555
1556 fn persistence(&self) -> PersistenceSchema {
1557 PersistenceSchema::new()
1558 .attribute(&API_KEY)
1559 .attribute_map(&TENANT_KEYS)
1560 .attribute(&PLAIN_NOTE)
1561 }
1562 }
1563
1564 #[derive(Clone)]
1565 struct OldFlowService {
1566 paths: Arc<Mutex<Vec<String>>>,
1567 current_protocol_version: Arc<AtomicU32>,
1568 }
1569
1570 impl NamedService for OldFlowService {
1571 const NAME: &'static str = "dex.FlowService";
1572 }
1573
1574 impl<B> Service<http::Request<B>> for OldFlowService
1575 where
1576 B: tonic::codegen::Body + Send + 'static,
1577 B::Error: Into<tonic::codegen::StdError> + Send + 'static,
1578 {
1579 type Response = http::Response<tonic::body::Body>;
1580 type Error = Infallible;
1581 type Future = BoxFuture<Self::Response, Self::Error>;
1582
1583 fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
1584 Poll::Ready(Ok(()))
1585 }
1586
1587 fn call(&mut self, request: http::Request<B>) -> Self::Future {
1588 let path = request.uri().path().to_string();
1589 self.paths.lock().expect("paths lock").push(path.clone());
1590 let server_info = GetServerInfo {
1591 current_protocol_version: self.current_protocol_version.load(Ordering::SeqCst),
1592 };
1593 Box::pin(async move {
1594 if path == "/dex.FlowService/GetServerInfo" {
1595 let mut grpc = tonic::server::Grpc::new(tonic_prost::ProstCodec::default());
1596 Ok(grpc.unary(server_info, request).await)
1597 } else {
1598 Ok(Status::unimplemented("fake FlowService").into_http())
1599 }
1600 })
1601 }
1602 }
1603
1604 struct GetServerInfo {
1605 current_protocol_version: u32,
1606 }
1607
1608 impl UnaryService<()> for GetServerInfo {
1609 type Response = ServerInfo;
1610 type Future = BoxFuture<Response<Self::Response>, Status>;
1611
1612 fn call(&mut self, _: Request<()>) -> Self::Future {
1613 let current_protocol_version = self.current_protocol_version;
1614 Box::pin(async move {
1615 Ok(Response::new(ServerInfo {
1616 server_version: "fake".to_string(),
1617 minimum_supported_protocol_version: 1,
1618 current_protocol_version,
1619 server_engine: 0,
1620 }))
1621 })
1622 }
1623 }
1624
1625 fn client_for(address: &str, cache_directory: &tempfile::TempDir) -> Client {
1626 let cache = Arc::new(
1627 BlobCache::open(
1628 BlobCacheConfig::new(cache_directory.path(), 1024 * 1024, 0)
1629 .expect("blob cache config"),
1630 )
1631 .expect("blob cache"),
1632 );
1633 Client::try_new(
1634 Registry::new()
1635 .register(HardDeleteClientFlow)
1636 .expect("register hard-delete Flow"),
1637 cache,
1638 ClientOptions::new().server_address(address),
1639 )
1640 .expect("Client")
1641 }
1642
1643 #[test]
1644 fn wait_for_attribute_rejects_hard_delete_attributes_before_contacting_dex() {
1645 let cache_directory = tempfile::tempdir().expect("blob cache directory");
1646 let client = client_for("127.0.0.1:1", &cache_directory);
1647 let single = client.wait_for_attribute_match(
1648 "flow-1",
1649 &API_KEY,
1650 AttributeMatch::equal_to("key-1".to_string()),
1651 WaitForAttributeOptions::new(),
1652 );
1653 let map = client.wait_for_attribute_map_instance_match(
1654 "flow-1",
1655 &TENANT_KEYS,
1656 "tenant-a",
1657 AttributeMatch::equal_to("key-a".to_string()),
1658 WaitForAttributeOptions::new(),
1659 );
1660 for result in [single, map] {
1661 let Err(SdkError::InvalidArgument { message }) = result else {
1662 panic!("expected a local InvalidArgument");
1663 };
1664 assert!(message.contains("does not support hard-delete Attribute"));
1665 }
1666 }
1667
1668 #[test]
1669 fn start_flow_negotiates_once_and_rejects_hard_delete_writes_to_old_servers() {
1670 let runtime = tokio::runtime::Runtime::new().expect("server runtime");
1671 let listener = runtime
1672 .block_on(TcpListener::bind("127.0.0.1:0"))
1673 .expect("bind fake FlowService");
1674 let address = listener.local_addr().expect("fake server address");
1675 let paths = Arc::new(Mutex::new(Vec::new()));
1676 let current_protocol_version = Arc::new(AtomicU32::new(2));
1677 let (shutdown, stopped) = oneshot::channel::<()>();
1678 runtime.spawn(
1679 Server::builder()
1680 .add_service(OldFlowService {
1681 paths: Arc::clone(&paths),
1682 current_protocol_version: Arc::clone(¤t_protocol_version),
1683 })
1684 .serve_with_incoming_shutdown(TcpListenerStream::new(listener), async {
1685 stopped.await.expect("FlowService shutdown signal");
1686 }),
1687 );
1688 let cache_directory = tempfile::tempdir().expect("blob cache directory");
1689 let client = client_for(&address.to_string(), &cache_directory);
1690
1691 for options in [
1692 StartFlowOptions::new().initial_attribute(&API_KEY, "key-1".to_string()),
1693 StartFlowOptions::new().initial_attribute_map(
1694 &TENANT_KEYS,
1695 "tenant-a",
1696 "key-a".to_string(),
1697 ),
1698 ] {
1699 let error = client
1700 .start_flow_with_options(&HardDeleteClientFlow, "flow-1", (), options)
1701 .expect_err("old Server rejects hard-delete writes");
1702 let SdkError::Service { service } = error else {
1703 panic!("expected a Service error, got {error}");
1704 };
1705 assert!(service.detail().contains("protocol version 3 or later"));
1706 assert!(
1707 service
1708 .detail()
1709 .contains("negotiated protocol version is 2")
1710 );
1711 }
1712 assert_eq!(
1713 vec!["/dex.FlowService/GetServerInfo".to_string()],
1714 *paths.lock().expect("paths lock")
1715 );
1716
1717 current_protocol_version.store(3, Ordering::SeqCst);
1718 let error = client
1719 .start_flow_with_options(
1720 &HardDeleteClientFlow,
1721 "flow-1",
1722 (),
1723 StartFlowOptions::new().initial_attribute(&API_KEY, "key-1".to_string()),
1724 )
1725 .expect_err("cached negotiation still rejects the write");
1726 assert!(
1727 error
1728 .to_string()
1729 .contains("negotiated protocol version is 2")
1730 );
1731 client
1732 .start_flow_with_options(
1733 &HardDeleteClientFlow,
1734 "flow-1",
1735 (),
1736 StartFlowOptions::new().initial_attribute(&PLAIN_NOTE, "note".to_string()),
1737 )
1738 .expect_err("fake StartFlow is unimplemented");
1739 assert_eq!(
1740 vec![
1741 "/dex.FlowService/GetServerInfo".to_string(),
1742 "/dex.FlowService/StartFlow".to_string(),
1743 ],
1744 *paths.lock().expect("paths lock")
1745 );
1746
1747 let fresh_client = client_for(&address.to_string(), &cache_directory);
1748 fresh_client
1749 .start_flow_with_options(
1750 &HardDeleteClientFlow,
1751 "flow-1",
1752 (),
1753 StartFlowOptions::new().initial_attribute(&API_KEY, "key-1".to_string()),
1754 )
1755 .expect_err("fake StartFlow is unimplemented");
1756 assert_eq!(
1757 Some(&"/dex.FlowService/StartFlow".to_string()),
1758 paths.lock().expect("paths lock").last()
1759 );
1760 shutdown.send(()).expect("stop fake FlowService");
1761 }
1762 }
1763}
1764
1765#[cfg(test)]
1766#[path = "client_pull_routing_tests.rs"]
1767mod pull_routing_tests;