1use crate::{
2 CancelWorkflowInput, DescribeWorkflowInput, DescribeWorkflowOutput,
3 FetchWorkflowHistoryPageInput, FetchWorkflowHistoryPageOutput, NamespacedClient, Next,
4 PollWorkflowUpdateInput, PollWorkflowUpdateOutput, QueryWorkflowInput, QueryWorkflowOutput,
5 RpcOptions, SignalWorkflowInput, StartWorkflowUpdateInput, StartWorkflowUpdateOutput,
6 TerminateWorkflowInput, WorkflowCancelOptions, WorkflowDescribeOptions,
7 WorkflowExecuteUpdateOptions, WorkflowExecutionStatus, WorkflowFetchHistoryOptions,
8 WorkflowGetResultOptions, WorkflowQueryOptions, WorkflowSignalOptions,
9 WorkflowStartUpdateOptions, WorkflowTerminateOptions,
10 errors::{
11 WorkflowGetResultError, WorkflowInteractionError, WorkflowQueryError, WorkflowUpdateError,
12 },
13 grpc::WorkflowService,
14 interceptors,
15};
16use futures_util::{TryStreamExt, future::BoxFuture, stream, stream::Stream};
17use std::{
18 collections::VecDeque,
19 fmt::Debug,
20 marker::PhantomData,
21 pin::Pin,
22 task::{Context, Poll},
23};
24pub use temporalio_common::UntypedWorkflow;
25use temporalio_common::{
26 HasWorkflowDefinition, QueryDefinition, SignalDefinition, UpdateDefinition, WorkflowDefinition,
27 data_converters::{
28 DataConverter, DecodablePayloads, GenericPayloadConverter, PayloadConversionError,
29 PayloadConverter, RawValue, SerializationContext, SerializationContextData,
30 WorkflowSerializationContext,
31 },
32 error::IncomingError,
33 payload_visitor::decode_payloads,
34 protos::{
35 coresdk::FromPayloadsExt,
36 proto_ts_to_system_time,
37 temporal::api::{
38 common::v1::{Header, Payload, Payloads, WorkflowExecution as ProtoWorkflowExecution},
39 enums::v1::{HistoryEventFilterType, UpdateWorkflowExecutionLifecycleStage},
40 history::{
41 self,
42 v1::{History, HistoryEvent, history_event::Attributes},
43 },
44 query::v1::WorkflowQuery,
45 sdk::v1::UserMetadata,
46 update::{self, v1::WaitPolicy},
47 workflow::v1 as workflow,
48 workflowservice::v1::{
49 DescribeWorkflowExecutionRequest, DescribeWorkflowExecutionResponse,
50 GetWorkflowExecutionHistoryRequest, PollWorkflowExecutionUpdateRequest,
51 QueryWorkflowRequest, RequestCancelWorkflowExecutionRequest,
52 SignalWorkflowExecutionRequest, TerminateWorkflowExecutionRequest,
53 UpdateWorkflowExecutionRequest,
54 },
55 },
56 },
57 search_attributes::SearchAttributes,
58};
59use tonic::IntoRequest;
60use uuid::Uuid;
61
62#[derive(Debug, Clone, Default, PartialEq, Eq)]
63struct DecodedUserMetadata {
64 summary: Option<String>,
65 details: Option<String>,
66}
67
68fn decode_user_metadata(
69 context: &SerializationContextData,
70 user_metadata: Option<UserMetadata>,
71) -> Result<DecodedUserMetadata, PayloadConversionError> {
72 let payload_converter = PayloadConverter::default();
73 let context = SerializationContext::new(context, &payload_converter);
74 let (summary, details) = user_metadata
75 .map(|metadata| (metadata.summary, metadata.details))
76 .unwrap_or_default();
77 Ok(DecodedUserMetadata {
78 summary: match summary {
79 Some(payload) => Some(payload_converter.from_payload(&context, payload)?),
80 None => None,
81 },
82 details: match details {
83 Some(payload) => Some(payload_converter.from_payload(&context, payload)?),
84 None => None,
85 },
86 })
87}
88
89#[derive(Clone, Debug)]
91#[non_exhaustive]
92pub struct WorkflowResultDetails {
93 payloads: DecodablePayloads,
94}
95
96impl WorkflowResultDetails {
97 async fn new(
98 payloads: Vec<Payload>,
99 data_converter: &DataConverter,
100 ) -> Result<Self, PayloadConversionError> {
101 let payloads = data_converter
102 .codec()
103 .decode(
104 &SerializationContextData::Workflow(WorkflowSerializationContext::new()),
105 payloads,
106 )
107 .await?;
108 Ok(Self {
109 payloads: DecodablePayloads::new(
110 payloads,
111 data_converter.payload_converter().clone(),
112 SerializationContextData::Workflow(WorkflowSerializationContext::new()),
113 ),
114 })
115 }
116
117 pub fn deserialize<T: temporalio_common::data_converters::TemporalDeserializable + 'static>(
119 &self,
120 ) -> Result<T, PayloadConversionError> {
121 self.payloads.deserialize()
122 }
123
124 pub fn raw(&self) -> &[Payload] {
126 self.payloads.raw()
127 }
128
129 pub fn into_raw(self) -> RawValue {
131 self.payloads.into_raw()
132 }
133}
134
135#[derive(Debug)]
137#[allow(clippy::large_enum_variant)]
138pub enum WorkflowExecutionResult<T> {
139 Succeeded(T),
141 Failed(IncomingError),
143 Cancelled {
145 details: WorkflowResultDetails,
147 },
148 Terminated {
150 details: WorkflowResultDetails,
152 },
153 TimedOut,
155 ContinuedAsNew,
157}
158
159#[derive(Debug, Clone)]
163pub struct WorkflowExecutionDescription {
164 pub raw_description: DescribeWorkflowExecutionResponse,
166 history_length: usize,
167 static_summary: Option<String>,
168 static_details: Option<String>,
169 data_converter: DataConverter,
170}
171
172impl WorkflowExecutionDescription {
173 async fn new(
174 mut raw_description: DescribeWorkflowExecutionResponse,
175 data_converter: &DataConverter,
176 ) -> Result<Self, PayloadConversionError> {
177 let raw_user_metadata = raw_description
178 .execution_config
179 .as_ref()
180 .and_then(|cfg| cfg.user_metadata.clone());
181 decode_payloads(
182 &mut raw_description,
183 data_converter.codec(),
184 &SerializationContextData::Workflow(WorkflowSerializationContext::new()),
185 )
186 .await?;
187 let decoded_metadata = decode_user_metadata(
188 &SerializationContextData::Workflow(WorkflowSerializationContext::new()),
189 raw_user_metadata,
190 )?;
191 let history_length_raw = raw_description
192 .workflow_execution_info
193 .as_ref()
194 .map(|info| info.history_length)
195 .unwrap_or(0);
196 let history_length = history_length_raw.try_into().map_err(|_| {
197 PayloadConversionError::EncodingError(
198 format!("workflow history_length must be non-negative, got {history_length_raw}")
199 .into(),
200 )
201 })?;
202 Ok(Self {
203 raw_description,
204 history_length,
205 static_summary: decoded_metadata.summary,
206 static_details: decoded_metadata.details,
207 data_converter: data_converter.clone(),
208 })
209 }
210
211 pub fn id(&self) -> &str {
213 self.execution().workflow_id.as_str()
214 }
215
216 pub fn run_id(&self) -> &str {
218 self.execution().run_id.as_str()
219 }
220
221 pub fn workflow_type(&self) -> &str {
223 self.workflow_type_info().name.as_str()
224 }
225
226 pub fn status(&self) -> WorkflowExecutionStatus {
228 WorkflowExecutionStatus::from_raw(self.workflow_info().status)
229 }
230
231 pub fn start_time(&self) -> Option<std::time::SystemTime> {
233 self.workflow_info()
234 .start_time
235 .as_ref()
236 .and_then(proto_ts_to_system_time)
237 }
238
239 pub fn execution_time(&self) -> Option<std::time::SystemTime> {
241 self.workflow_info()
242 .execution_time
243 .as_ref()
244 .and_then(proto_ts_to_system_time)
245 }
246
247 pub fn close_time(&self) -> Option<std::time::SystemTime> {
249 self.workflow_info()
250 .close_time
251 .as_ref()
252 .and_then(proto_ts_to_system_time)
253 }
254
255 pub fn task_queue(&self) -> &str {
257 self.workflow_info().task_queue.as_str()
258 }
259
260 pub fn history_length(&self) -> usize {
262 self.history_length
263 }
264
265 pub fn memo(&self) -> crate::Memo {
267 crate::Memo::from_raw(
268 self.workflow_info().memo.clone(),
269 self.data_converter.payload_converter().clone(),
270 SerializationContextData::Workflow(WorkflowSerializationContext::new()),
271 )
272 }
273
274 pub fn parent_id(&self) -> Option<&str> {
276 self.workflow_info()
277 .parent_execution
278 .as_ref()
279 .map(|e| e.workflow_id.as_str())
280 }
281
282 pub fn parent_run_id(&self) -> Option<&str> {
284 self.workflow_info()
285 .parent_execution
286 .as_ref()
287 .map(|e| e.run_id.as_str())
288 }
289
290 pub fn search_attributes(&self) -> SearchAttributes {
292 self.workflow_info()
293 .search_attributes
294 .as_ref()
295 .map(SearchAttributes::from_proto)
296 .unwrap_or_default()
297 }
298
299 pub fn static_summary(&self) -> Option<&str> {
301 self.static_summary.as_deref()
302 }
303
304 pub fn static_details(&self) -> Option<&str> {
306 self.static_details.as_deref()
307 }
308
309 pub fn raw(&self) -> &DescribeWorkflowExecutionResponse {
311 &self.raw_description
312 }
313
314 pub fn into_raw(self) -> DescribeWorkflowExecutionResponse {
316 self.raw_description
317 }
318
319 fn workflow_info(&self) -> &workflow::WorkflowExecutionInfo {
320 self.raw_description
321 .workflow_execution_info
322 .as_ref()
323 .expect("describe response missing workflow_execution_info")
324 }
325
326 fn execution(&self) -> &ProtoWorkflowExecution {
327 self.workflow_info()
328 .execution
329 .as_ref()
330 .expect("describe response missing workflow_execution_info.execution")
331 }
332
333 fn workflow_type_info(
334 &self,
335 ) -> &temporalio_common::protos::temporal::api::common::v1::WorkflowType {
336 self.workflow_info()
337 .r#type
338 .as_ref()
339 .expect("describe response missing workflow_execution_info.type")
340 }
341}
342
343#[derive(derive_more::Debug)]
348pub struct WorkflowHistory {
349 #[debug(skip)]
350 inner: Pin<Box<dyn Stream<Item = Result<HistoryEvent, WorkflowInteractionError>> + Send>>,
351 workflow_id: Option<String>,
352}
353
354impl From<history::v1::History> for WorkflowHistory {
355 fn from(history: history::v1::History) -> Self {
356 let workflow_id =
357 history
358 .events
359 .first()
360 .and_then(|event| match event.attributes.as_ref() {
361 Some(Attributes::WorkflowExecutionStartedEventAttributes(attributes))
362 if !attributes.workflow_id.is_empty() =>
363 {
364 Some(attributes.workflow_id.clone())
365 }
366 _ => None,
367 });
368 Self {
369 inner: Box::pin(stream::iter(history.events.into_iter().map(Ok))),
370 workflow_id,
371 }
372 }
373}
374
375impl Stream for WorkflowHistory {
376 type Item = Result<HistoryEvent, WorkflowInteractionError>;
377
378 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
379 self.inner.as_mut().poll_next(cx)
380 }
381}
382
383#[derive(Debug, thiserror::Error)]
385#[non_exhaustive]
386pub enum WorkflowHistoryError {
387 #[error("failed to fetch workflow history: {0}")]
389 Fetch(#[from] WorkflowInteractionError),
390 #[error("failed to convert workflow history JSON: {0}")]
392 Json(#[from] serde_json::Error),
393}
394
395impl WorkflowHistory {
396 pub fn from_json(bytes: &[u8]) -> Result<Self, WorkflowHistoryError> {
398 let history: History = serde_json::from_slice(bytes)?;
399 Ok(history.into())
400 }
401
402 pub async fn to_json(self) -> Result<Vec<u8>, WorkflowHistoryError> {
404 Ok(serde_json::to_vec(&History {
405 events: self.into_events().await?,
406 })?)
407 }
408
409 pub fn workflow_id(&self) -> Option<&str> {
411 self.workflow_id.as_deref()
412 }
413
414 pub async fn into_events(self) -> Result<Vec<HistoryEvent>, WorkflowInteractionError> {
416 self.inner.try_collect().await
417 }
418}
419
420#[derive(Clone)]
423pub struct WorkflowHandle<ClientT, W> {
424 client: ClientT,
425 info: WorkflowExecutionInfo,
426
427 _wf_type: PhantomData<W>,
428}
429
430impl<CT, W> WorkflowHandle<CT, W> {
431 pub fn run_id(&self) -> Option<&str> {
433 self.info.run_id.as_deref()
434 }
435}
436
437#[derive(Debug, Clone, bon::Builder)]
439#[builder(on(String, into), state_mod(vis = "pub"))]
440#[non_exhaustive]
441pub struct WorkflowExecutionInfo {
442 pub namespace: String,
444 pub workflow_id: String,
446 pub run_id: Option<String>,
448 pub first_execution_run_id: Option<String>,
452}
453
454impl WorkflowExecutionInfo {
455 pub fn bind_untyped<CT>(self, client: CT) -> UntypedWorkflowHandle<CT>
457 where
458 CT: WorkflowService + Clone,
459 {
460 UntypedWorkflowHandle::new(client, self)
461 }
462}
463
464pub type UntypedWorkflowHandle<CT> = WorkflowHandle<CT, UntypedWorkflow>;
467
468pub struct UntypedSignal<W> {
472 name: String,
473 _wf: PhantomData<W>,
474}
475
476impl<W> UntypedSignal<W> {
477 pub fn new(name: impl Into<String>) -> Self {
479 Self {
480 name: name.into(),
481 _wf: PhantomData,
482 }
483 }
484}
485
486impl<W: WorkflowDefinition> SignalDefinition for UntypedSignal<W> {
487 type Workflow = W;
488 type Input = RawValue;
489
490 fn name(&self) -> &str {
491 &self.name
492 }
493}
494
495pub struct UntypedQuery<W> {
499 name: String,
500 _wf: PhantomData<W>,
501}
502
503impl<W> UntypedQuery<W> {
504 pub fn new(name: impl Into<String>) -> Self {
506 Self {
507 name: name.into(),
508 _wf: PhantomData,
509 }
510 }
511}
512
513impl<W: WorkflowDefinition> QueryDefinition for UntypedQuery<W> {
514 type Workflow = W;
515 type Input = RawValue;
516 type Output = RawValue;
517
518 fn name(&self) -> &str {
519 &self.name
520 }
521}
522
523pub struct UntypedUpdate<W> {
527 name: String,
528 _wf: PhantomData<W>,
529}
530
531impl<W> UntypedUpdate<W> {
532 pub fn new(name: impl Into<String>) -> Self {
534 Self {
535 name: name.into(),
536 _wf: PhantomData,
537 }
538 }
539}
540
541impl<W: WorkflowDefinition> UpdateDefinition for UntypedUpdate<W> {
542 type Workflow = W;
543 type Input = RawValue;
544 type Output = RawValue;
545
546 fn name(&self) -> &str {
547 &self.name
548 }
549}
550
551#[allow(clippy::too_many_arguments)]
555pub(crate) fn build_update_workflow_request(
556 namespace: String,
557 identity: String,
558 workflow_id: String,
559 run_id: String,
560 update_id: String,
561 update_name: String,
562 header: Option<Header>,
563 payloads: Vec<Payload>,
564) -> UpdateWorkflowExecutionRequest {
565 UpdateWorkflowExecutionRequest {
566 namespace,
567 workflow_execution: Some(ProtoWorkflowExecution {
568 workflow_id,
569 run_id,
570 }),
571 wait_policy: Some(WaitPolicy {
572 lifecycle_stage: UpdateWorkflowExecutionLifecycleStage::Accepted.into(),
573 }),
574 request: Some(update::v1::Request {
575 meta: Some(update::v1::Meta {
576 update_id,
577 identity,
578 }),
579 input: Some(update::v1::Input {
580 header,
581 name: update_name,
582 args: Some(Payloads { payloads }),
583 }),
584 ..Default::default()
585 }),
586 ..Default::default()
587 }
588}
589
590impl<CT, W> WorkflowHandle<CT, W>
591where
592 CT: WorkflowService + Clone,
593 W: HasWorkflowDefinition,
594{
595 pub fn new(client: CT, info: WorkflowExecutionInfo) -> Self {
597 Self {
598 client,
599 info,
600 _wf_type: PhantomData::<W>,
601 }
602 }
603
604 pub fn info(&self) -> &WorkflowExecutionInfo {
606 &self.info
607 }
608
609 pub fn client(&self) -> &CT {
611 &self.client
612 }
613
614 pub async fn get_result(
616 &self,
617 opts: WorkflowGetResultOptions,
618 ) -> Result<W::Output, WorkflowGetResultError>
619 where
620 CT: WorkflowService + NamespacedClient + Clone + 'static,
621 {
622 let raw = self.get_result_raw(opts).await?;
623 match raw {
624 WorkflowExecutionResult::Succeeded(v) => Ok(v),
625 WorkflowExecutionResult::Failed(f) => Err(WorkflowGetResultError::Failed(Box::new(f))),
626 WorkflowExecutionResult::Cancelled { details } => {
627 Err(WorkflowGetResultError::Cancelled { details })
628 }
629 WorkflowExecutionResult::Terminated { details } => {
630 Err(WorkflowGetResultError::Terminated { details })
631 }
632 WorkflowExecutionResult::TimedOut => Err(WorkflowGetResultError::TimedOut),
633 WorkflowExecutionResult::ContinuedAsNew => Err(WorkflowGetResultError::ContinuedAsNew),
634 }
635 }
636
637 async fn get_result_raw(
641 &self,
642 opts: WorkflowGetResultOptions,
643 ) -> Result<WorkflowExecutionResult<W::Output>, WorkflowInteractionError>
644 where
645 CT: WorkflowService + NamespacedClient + Clone + 'static,
646 {
647 let mut run_id = self.info.run_id.clone().unwrap_or_default();
648 let fetch_opts = WorkflowFetchHistoryOptions::builder()
649 .skip_archival(true)
650 .wait_new_event(true)
651 .event_filter_type(HistoryEventFilterType::CloseEvent)
652 .rpc_options(opts.rpc_options.clone())
653 .build();
654
655 loop {
656 let history = self.fetch_history_for_run(&run_id, fetch_opts.clone());
657 let mut events = history.into_events().await?;
658
659 if events.is_empty() {
660 continue;
661 }
662
663 let event_attrs = events.pop().and_then(|ev| ev.attributes);
664
665 macro_rules! follow {
666 ($attrs:ident) => {
667 if opts.follow_runs && $attrs.new_execution_run_id != "" {
668 run_id = $attrs.new_execution_run_id;
669 continue;
670 }
671 };
672 }
673
674 let dc = self.client.data_converter();
675
676 break match event_attrs {
677 Some(Attributes::WorkflowExecutionCompletedEventAttributes(attrs)) => {
678 follow!(attrs);
679 let payload = attrs
680 .result
681 .and_then(|p| p.payloads.into_iter().next())
682 .unwrap_or_default();
683 let result: W::Output = dc
684 .from_payload(&SerializationContextData::Workflow(WorkflowSerializationContext::new()), payload)
685 .await?;
686 Ok(WorkflowExecutionResult::Succeeded(result))
687 }
688 Some(Attributes::WorkflowExecutionFailedEventAttributes(attrs)) => {
689 follow!(attrs);
690 let mut failure = attrs.failure.unwrap_or_default();
691 decode_payloads(
692 &mut failure,
693 dc.codec(),
694 &SerializationContextData::Workflow(WorkflowSerializationContext::new()),
695 )
696 .await?;
697 let error = dc.failure_converter().to_error(
698 failure,
699 dc.payload_converter(),
700 &SerializationContextData::Workflow(WorkflowSerializationContext::new()),
701 )?;
702 Ok(WorkflowExecutionResult::Failed(error))
703 }
704 Some(Attributes::WorkflowExecutionCanceledEventAttributes(attrs)) => {
705 Ok(WorkflowExecutionResult::Cancelled {
706 details: WorkflowResultDetails::new(Vec::from_payloads(attrs.details), dc)
707 .await?,
708 })
709 }
710 Some(Attributes::WorkflowExecutionTimedOutEventAttributes(attrs)) => {
711 follow!(attrs);
712 Ok(WorkflowExecutionResult::TimedOut)
713 }
714 Some(Attributes::WorkflowExecutionTerminatedEventAttributes(attrs)) => {
715 Ok(WorkflowExecutionResult::Terminated {
716 details: WorkflowResultDetails::new(Vec::from_payloads(attrs.details), dc)
717 .await?,
718 })
719 }
720 Some(Attributes::WorkflowExecutionContinuedAsNewEventAttributes(attrs)) => {
721 if opts.follow_runs {
722 if !attrs.new_execution_run_id.is_empty() {
723 run_id = attrs.new_execution_run_id;
724 continue;
725 } else {
726 return Err(WorkflowInteractionError::Other(
727 "New execution run id was empty in continue as new event!".into(),
728 ));
729 }
730 } else {
731 Ok(WorkflowExecutionResult::ContinuedAsNew)
732 }
733 }
734 o => Err(WorkflowInteractionError::Other(
735 format!(
736 "Server returned an event that didn't match the CloseEvent filter. \
737 This is either a server bug or a new event the SDK does not understand. \
738 Event details: {o:?}"
739 )
740 .into(),
741 )),
742 };
743 }
744 }
745
746 pub async fn signal<S>(
748 &self,
749 signal: S,
750 input: S::Input,
751 opts: WorkflowSignalOptions,
752 ) -> Result<(), WorkflowInteractionError>
753 where
754 CT: WorkflowService + NamespacedClient + Clone,
755 S: SignalDefinition<Workflow = W::Run>,
756 S::Input: Send,
757 {
758 interceptors::call_signal_workflow(
759 self.client.client_interceptors(),
760 SignalWorkflowInput::new(
761 self.info.workflow_id.clone(),
762 self.info.run_id.clone().unwrap_or_default(),
763 signal.name().to_string(),
764 input,
765 opts,
766 ),
767 Next::new({
768 let mut client = self.client.clone();
769 move |input: SignalWorkflowInput| -> BoxFuture<
770 '_,
771 Result<(), WorkflowInteractionError>,
772 > {
773 Box::pin(async move {
774 let (workflow_id, run_id, signal_name, args, options) =
775 input.into_parts();
776 let data_converter = client.data_converter().clone();
777 let unencoded_payloads = {
778 let payload_converter = data_converter.payload_converter();
779 let context_data = SerializationContextData::Workflow(
780 WorkflowSerializationContext::new(),
781 );
782 let context =
783 SerializationContext::new(&context_data, payload_converter);
784 args.serialize_payloads(&context)
785 };
786 drop(args);
787 let payloads = data_converter
788 .codec()
789 .encode(&SerializationContextData::Workflow(WorkflowSerializationContext::new()), unencoded_payloads?)
790 .await?;
791 let mut request = SignalWorkflowExecutionRequest {
792 namespace: client.namespace(),
793 workflow_execution: Some(ProtoWorkflowExecution {
794 workflow_id,
795 run_id,
796 }),
797 signal_name,
798 input: Some(Payloads { payloads }),
799 identity: client.identity(),
800 request_id: options
801 .request_id
802 .unwrap_or_else(|| Uuid::new_v4().to_string()),
803 header: options.header,
804 ..Default::default()
805 }
806 .into_request();
807 options.rpc_options.apply_to(&mut request);
808 WorkflowService::signal_workflow_execution(&mut client, request)
809 .await
810 .map_err(WorkflowInteractionError::from_status)?;
811 Ok(())
812 })
813 }
814 }),
815 )
816 .await
817 }
818
819 pub async fn query<Q>(
821 &self,
822 query: Q,
823 input: Q::Input,
824 opts: WorkflowQueryOptions,
825 ) -> Result<Q::Output, WorkflowQueryError>
826 where
827 CT: WorkflowService + NamespacedClient + Clone,
828 Q: QueryDefinition<Workflow = W::Run>,
829 Q::Input: Send,
830 {
831 let output = interceptors::call_query_workflow(
832 self.client.client_interceptors(),
833 QueryWorkflowInput::new(
834 self.info.workflow_id.clone(),
835 self.info.run_id.clone().unwrap_or_default(),
836 query.name().to_string(),
837 input,
838 opts,
839 ),
840 Next::new({
841 let mut client = self.client.clone();
842 move |input: QueryWorkflowInput| -> BoxFuture<
843 '_,
844 Result<QueryWorkflowOutput, WorkflowQueryError>,
845 > {
846 Box::pin(async move {
847 let (workflow_id, run_id, query_name, args, options) = input.into_parts();
848 let data_converter = client.data_converter().clone();
849 let unencoded_payloads = {
850 let payload_converter = data_converter.payload_converter();
851 let context_data = SerializationContextData::Workflow(
852 WorkflowSerializationContext::new(),
853 );
854 let context =
855 SerializationContext::new(&context_data, payload_converter);
856 args.serialize_payloads(&context)
857 };
858 drop(args);
859 let payloads = data_converter
860 .codec()
861 .encode(&SerializationContextData::Workflow(WorkflowSerializationContext::new()), unencoded_payloads?)
862 .await?;
863 let mut request = QueryWorkflowRequest {
864 namespace: client.namespace(),
865 execution: Some(ProtoWorkflowExecution {
866 workflow_id,
867 run_id,
868 }),
869 query: Some(WorkflowQuery {
870 query_type: query_name,
871 query_args: Some(Payloads { payloads }),
872 header: options.header,
873 }),
874 query_reject_condition: options
875 .reject_condition
876 .map(|condition| condition as i32)
877 .unwrap_or(1),
878 }
879 .into_request();
880 options.rpc_options.apply_to(&mut request);
881 let response = client
882 .query_workflow(request)
883 .await
884 .map_err(WorkflowQueryError::from_status)?
885 .into_inner();
886 Ok(QueryWorkflowOutput::new(response))
887 })
888 }
889 }),
890 )
891 .await?;
892 let response = output.response;
893
894 if let Some(rejected) = response.query_rejected {
895 return Err(WorkflowQueryError::Rejected {
896 status: (rejected.status != 0)
897 .then(|| WorkflowExecutionStatus::from_raw(rejected.status)),
898 });
899 }
900
901 let result_payloads = response
902 .query_result
903 .map(|p| p.payloads)
904 .unwrap_or_default();
905
906 self.client
907 .data_converter()
908 .from_payloads(
909 &SerializationContextData::Workflow(WorkflowSerializationContext::new()),
910 result_payloads,
911 )
912 .await
913 .map_err(WorkflowQueryError::from)
914 }
915
916 pub async fn execute_update<U>(
918 &self,
919 update: U,
920 input: U::Input,
921 options: WorkflowExecuteUpdateOptions,
922 ) -> Result<U::Output, WorkflowUpdateError>
923 where
924 CT: WorkflowService + NamespacedClient + Clone,
925 U: UpdateDefinition<Workflow = W::Run>,
926 U::Input: Send,
927 U::Output: 'static,
928 {
929 let rpc_options = options.rpc_options.clone();
930 let handle = self.start_update(update, input, options.into()).await?;
931 handle.get_result(rpc_options).await
932 }
933
934 pub async fn start_update<U>(
937 &self,
938 update: U,
939 input: U::Input,
940 options: WorkflowStartUpdateOptions,
941 ) -> Result<WorkflowUpdateHandle<CT, U::Output>, WorkflowUpdateError>
942 where
943 CT: WorkflowService + NamespacedClient + Clone,
944 U: UpdateDefinition<Workflow = W::Run>,
945 U::Input: Send,
946 {
947 let output = interceptors::call_start_workflow_update(
948 self.client.client_interceptors(),
949 StartWorkflowUpdateInput::new(
950 self.info().workflow_id.clone(),
951 self.info().run_id.clone().unwrap_or_default(),
952 update.name().to_string(),
953 input,
954 options,
955 ),
956 Next::new({
957 let mut client = self.client.clone();
958 move |input: StartWorkflowUpdateInput| -> BoxFuture<
959 '_,
960 Result<StartWorkflowUpdateOutput, WorkflowUpdateError>,
961 > {
962 Box::pin(async move {
963 let (workflow_id, run_id, update_name, args, options) =
964 input.into_parts();
965 let data_converter = client.data_converter().clone();
966 let unencoded_payloads = {
967 let payload_converter = data_converter.payload_converter();
968 let context_data = SerializationContextData::Workflow(
969 WorkflowSerializationContext::new(),
970 );
971 let context =
972 SerializationContext::new(&context_data, payload_converter);
973 args.serialize_payloads(&context)
974 };
975 drop(args);
976 let payloads = data_converter
977 .codec()
978 .encode(
979 &SerializationContextData::Workflow(
980 WorkflowSerializationContext::new(),
981 ),
982 unencoded_payloads?,
983 )
984 .await?;
985 let update_id = options
986 .update_id
987 .unwrap_or_else(|| Uuid::new_v4().to_string());
988 let mut request = build_update_workflow_request(
989 client.namespace(),
990 client.identity(),
991 workflow_id.clone(),
992 run_id,
993 update_id.clone(),
994 update_name,
995 options.header,
996 payloads,
997 )
998 .into_request();
999 options.rpc_options.apply_to(&mut request);
1000 let response =
1001 WorkflowService::update_workflow_execution(&mut client, request)
1002 .await
1003 .map_err(WorkflowUpdateError::from_status)?
1004 .into_inner();
1005 let run_id = response
1006 .update_ref
1007 .as_ref()
1008 .and_then(|reference| reference.workflow_execution.as_ref())
1009 .map(|execution| execution.run_id.clone())
1010 .filter(|run_id| !run_id.is_empty());
1011 Ok(StartWorkflowUpdateOutput::new(
1012 update_id,
1013 workflow_id,
1014 run_id,
1015 response.outcome,
1016 ))
1017 })
1018 }
1019 }),
1020 )
1021 .await?;
1022
1023 Ok(WorkflowUpdateHandle::new(
1024 self.client.clone(),
1025 output.update_id,
1026 output.workflow_id,
1027 output.run_id.or_else(|| self.info().run_id.clone()),
1028 output.known_outcome,
1029 ))
1030 }
1031
1032 pub fn get_update_handle<U>(
1038 &self,
1039 update: U,
1040 update_id: impl Into<String>,
1041 ) -> WorkflowUpdateHandle<CT, U::Output>
1042 where
1043 U: UpdateDefinition<Workflow = W::Run>,
1044 {
1045 let _ = update;
1046 WorkflowUpdateHandle::new(
1047 self.client.clone(),
1048 update_id.into(),
1049 self.info.workflow_id.clone(),
1050 self.info.run_id.clone(),
1051 None,
1052 )
1053 }
1054
1055 pub async fn cancel(&self, opts: WorkflowCancelOptions) -> Result<(), WorkflowInteractionError>
1057 where
1058 CT: NamespacedClient,
1059 {
1060 interceptors::call_cancel_workflow(
1061 self.client.client_interceptors(),
1062 CancelWorkflowInput {
1063 workflow_id: self.info.workflow_id.clone(),
1064 run_id: self.info.run_id.clone().unwrap_or_default(),
1065 first_execution_run_id: self
1066 .info
1067 .first_execution_run_id
1068 .clone()
1069 .unwrap_or_default(),
1070 options: opts,
1071 },
1072 Next::new({
1073 let mut client = self.client.clone();
1074 move |input: CancelWorkflowInput| -> BoxFuture<
1075 '_,
1076 Result<(), WorkflowInteractionError>,
1077 > {
1078 Box::pin(async move {
1079 let mut request = RequestCancelWorkflowExecutionRequest {
1080 namespace: client.namespace(),
1081 workflow_execution: Some(ProtoWorkflowExecution {
1082 workflow_id: input.workflow_id,
1083 run_id: input.run_id,
1084 }),
1085 identity: client.identity(),
1086 request_id: input
1087 .options
1088 .request_id
1089 .clone()
1090 .unwrap_or_else(|| Uuid::new_v4().to_string()),
1091 first_execution_run_id: input.first_execution_run_id,
1092 reason: input.options.reason.clone(),
1093 links: vec![],
1094 }
1095 .into_request();
1096 input.options.rpc_options.apply_to(&mut request);
1097 WorkflowService::request_cancel_workflow_execution(&mut client, request)
1098 .await
1099 .map_err(WorkflowInteractionError::from_status)?;
1100 Ok(())
1101 })
1102 }
1103 }),
1104 )
1105 .await
1106 }
1107
1108 pub async fn terminate(
1110 &self,
1111 opts: WorkflowTerminateOptions,
1112 ) -> Result<(), WorkflowInteractionError>
1113 where
1114 CT: NamespacedClient,
1115 {
1116 interceptors::call_terminate_workflow(
1117 self.client.client_interceptors(),
1118 TerminateWorkflowInput {
1119 workflow_id: self.info.workflow_id.clone(),
1120 run_id: self.info.run_id.clone().unwrap_or_default(),
1121 first_execution_run_id: self
1122 .info
1123 .first_execution_run_id
1124 .clone()
1125 .unwrap_or_default(),
1126 options: opts,
1127 },
1128 Next::new({
1129 let mut client = self.client.clone();
1130 move |input: TerminateWorkflowInput| -> BoxFuture<
1131 '_,
1132 Result<(), WorkflowInteractionError>,
1133 > {
1134 Box::pin(async move {
1135 let mut request = TerminateWorkflowExecutionRequest {
1136 namespace: client.namespace(),
1137 workflow_execution: Some(ProtoWorkflowExecution {
1138 workflow_id: input.workflow_id,
1139 run_id: input.run_id,
1140 }),
1141 reason: input.options.reason.clone(),
1142 details: input.options.details.clone(),
1143 identity: client.identity(),
1144 first_execution_run_id: input.first_execution_run_id,
1145 links: vec![],
1146 }
1147 .into_request();
1148 input.options.rpc_options.apply_to(&mut request);
1149 WorkflowService::terminate_workflow_execution(&mut client, request)
1150 .await
1151 .map_err(WorkflowInteractionError::from_status)?;
1152 Ok(())
1153 })
1154 }
1155 }),
1156 )
1157 .await
1158 }
1159
1160 pub async fn describe(
1162 &self,
1163 opts: WorkflowDescribeOptions,
1164 ) -> Result<WorkflowExecutionDescription, WorkflowInteractionError>
1165 where
1166 CT: NamespacedClient,
1167 {
1168 let output = interceptors::call_describe_workflow(
1169 self.client.client_interceptors(),
1170 DescribeWorkflowInput {
1171 workflow_id: self.info.workflow_id.clone(),
1172 run_id: self.info.run_id.clone().unwrap_or_default(),
1173 options: opts,
1174 },
1175 Next::new({
1176 let mut client = self.client.clone();
1177 move |input: DescribeWorkflowInput| -> BoxFuture<
1178 '_,
1179 Result<DescribeWorkflowOutput, WorkflowInteractionError>,
1180 > {
1181 Box::pin(async move {
1182 let mut request = DescribeWorkflowExecutionRequest {
1183 namespace: client.namespace(),
1184 execution: Some(ProtoWorkflowExecution {
1185 workflow_id: input.workflow_id,
1186 run_id: input.run_id,
1187 }),
1188 }
1189 .into_request();
1190 input.options.rpc_options.apply_to(&mut request);
1191 let response =
1192 WorkflowService::describe_workflow_execution(&mut client, request)
1193 .await
1194 .map_err(WorkflowInteractionError::from_status)?
1195 .into_inner();
1196 Ok(DescribeWorkflowOutput::new(response))
1197 })
1198 }
1199 }),
1200 )
1201 .await?;
1202 WorkflowExecutionDescription::new(output.response, self.client.data_converter())
1203 .await
1204 .map_err(WorkflowInteractionError::from)
1205 }
1206 pub fn fetch_history(&self, opts: WorkflowFetchHistoryOptions) -> WorkflowHistory
1210 where
1211 CT: NamespacedClient + 'static,
1212 {
1213 let run_id = self.info.run_id.clone().unwrap_or_default();
1214 self.fetch_history_for_run(&run_id, opts)
1215 }
1216
1217 fn fetch_history_for_run(
1218 &self,
1219 run_id: &str,
1220 opts: WorkflowFetchHistoryOptions,
1221 ) -> WorkflowHistory
1222 where
1223 CT: NamespacedClient + 'static,
1224 {
1225 let client = self.client.clone();
1226 let workflow_id = self.info.workflow_id.clone();
1227 let history_workflow_id = workflow_id.clone();
1228 let run_id = run_id.to_string();
1229
1230 let stream = stream::unfold(
1231 (Vec::new(), VecDeque::new(), false),
1232 move |(mut next_page_token, mut buffer, mut exhausted)| {
1233 let client = client.clone();
1234 let workflow_id = workflow_id.clone();
1235 let run_id = run_id.clone();
1236 let opts = opts.clone();
1237
1238 async move {
1239 loop {
1240 if let Some(event) = buffer.pop_front() {
1241 return Some((Ok(event), (next_page_token, buffer, exhausted)));
1242 }
1243
1244 if exhausted {
1245 return None;
1246 }
1247
1248 let output = interceptors::call_fetch_workflow_history_page(
1249 client.client_interceptors(),
1250 FetchWorkflowHistoryPageInput {
1251 workflow_id: workflow_id.clone(),
1252 run_id: run_id.clone(),
1253 next_page_token: next_page_token.clone(),
1254 options: opts.clone(),
1255 },
1256 Next::new({
1257 let mut rpc_client = client.clone();
1258 move |input: FetchWorkflowHistoryPageInput| -> BoxFuture<
1259 '_,
1260 Result<
1261 FetchWorkflowHistoryPageOutput,
1262 WorkflowInteractionError,
1263 >,
1264 > {
1265 Box::pin(async move {
1266 let mut request = GetWorkflowExecutionHistoryRequest {
1267 namespace: rpc_client.namespace(),
1268 execution: Some(ProtoWorkflowExecution {
1269 workflow_id: input.workflow_id,
1270 run_id: input.run_id,
1271 }),
1272 next_page_token: input.next_page_token,
1273 skip_archival: input.options.skip_archival,
1274 wait_new_event: input.options.wait_new_event,
1275 history_event_filter_type: input
1276 .options
1277 .event_filter_type
1278 as i32,
1279 ..Default::default()
1280 }
1281 .into_request();
1282 input.options.rpc_options.apply_to(&mut request);
1283 let response =
1284 WorkflowService::get_workflow_execution_history(
1285 &mut rpc_client,
1286 request,
1287 )
1288 .await
1289 .map_err(WorkflowInteractionError::from_status)?
1290 .into_inner();
1291 Ok(FetchWorkflowHistoryPageOutput::new(
1292 response
1293 .history
1294 .map(|history| history.events)
1295 .unwrap_or_default(),
1296 response.next_page_token,
1297 ))
1298 })
1299 }
1300 }),
1301 )
1302 .await;
1303
1304 match output {
1305 Ok(output) => {
1306 exhausted = output.next_page_token.is_empty();
1307 next_page_token = output.next_page_token;
1308 buffer = output.events.into();
1309 }
1310 Err(error) => {
1311 return Some((Err(error), (next_page_token, buffer, true)));
1312 }
1313 }
1314 }
1315 }
1316 },
1317 );
1318
1319 WorkflowHistory {
1320 inner: Box::pin(stream),
1321 workflow_id: Some(history_workflow_id),
1322 }
1323 }
1324}
1325
1326pub struct WorkflowUpdateHandle<CT, T> {
1330 client: CT,
1331 update_id: String,
1332 workflow_id: String,
1333 run_id: Option<String>,
1334 known_outcome: Option<update::v1::Outcome>,
1336 _output: PhantomData<T>,
1337}
1338
1339impl<CT, T> WorkflowUpdateHandle<CT, T> {
1340 pub(crate) fn new(
1341 client: CT,
1342 update_id: String,
1343 workflow_id: String,
1344 run_id: Option<String>,
1345 known_outcome: Option<update::v1::Outcome>,
1346 ) -> Self {
1347 Self {
1348 client,
1349 update_id,
1350 workflow_id,
1351 run_id,
1352 known_outcome,
1353 _output: PhantomData,
1354 }
1355 }
1356
1357 pub fn id(&self) -> &str {
1359 &self.update_id
1360 }
1361
1362 pub fn workflow_id(&self) -> &str {
1364 &self.workflow_id
1365 }
1366
1367 pub fn workflow_run_id(&self) -> Option<&str> {
1369 self.run_id.as_deref()
1370 }
1371}
1372
1373impl<CT, T: 'static> WorkflowUpdateHandle<CT, T>
1374where
1375 CT: WorkflowService + NamespacedClient + Clone,
1376{
1377 pub async fn get_result(&self, rpc_options: RpcOptions) -> Result<T, WorkflowUpdateError>
1379 where
1380 T: temporalio_common::data_converters::TemporalDeserializable,
1381 {
1382 let output = interceptors::call_poll_workflow_update(
1383 self.client.client_interceptors(),
1384 PollWorkflowUpdateInput {
1385 update_id: self.update_id.clone(),
1386 workflow_id: self.workflow_id.clone(),
1387 run_id: self.run_id.clone().unwrap_or_default(),
1388 rpc_options,
1389 },
1390 Next::new({
1391 let mut client = self.client.clone();
1392 let known_outcome = self.known_outcome.clone();
1393 move |input: PollWorkflowUpdateInput| -> BoxFuture<
1394 '_,
1395 Result<PollWorkflowUpdateOutput, WorkflowUpdateError>,
1396 > {
1397 Box::pin(async move {
1398 if let Some(outcome) = known_outcome {
1399 return Ok(PollWorkflowUpdateOutput::new(outcome));
1400 }
1401 loop {
1405 let mut request = PollWorkflowExecutionUpdateRequest {
1406 namespace: client.namespace(),
1407 update_ref: Some(update::v1::UpdateRef {
1408 workflow_execution: Some(ProtoWorkflowExecution {
1409 workflow_id: input.workflow_id.clone(),
1410 run_id: input.run_id.clone(),
1411 }),
1412 update_id: input.update_id.clone(),
1413 }),
1414 identity: client.identity(),
1415 wait_policy: Some(WaitPolicy {
1416 lifecycle_stage:
1417 UpdateWorkflowExecutionLifecycleStage::Completed.into(),
1418 }),
1419 }
1420 .into_request();
1421 input.rpc_options.apply_to(&mut request);
1422 let response = WorkflowService::poll_workflow_execution_update(
1423 &mut client,
1424 request,
1425 )
1426 .await
1427 .map_err(WorkflowUpdateError::from_status)?
1428 .into_inner();
1429 if let Some(outcome) = response.outcome {
1430 return Ok(PollWorkflowUpdateOutput::new(outcome));
1431 }
1432 }
1433 })
1434 }
1435 }),
1436 )
1437 .await?;
1438 let outcome = output.outcome;
1439
1440 match outcome.value {
1441 Some(update::v1::outcome::Value::Success(success)) => self
1442 .client
1443 .data_converter()
1444 .from_payloads(
1445 &SerializationContextData::Workflow(WorkflowSerializationContext::new()),
1446 success.payloads,
1447 )
1448 .await
1449 .map_err(WorkflowUpdateError::from),
1450 Some(update::v1::outcome::Value::Failure(failure)) => {
1451 Err(WorkflowUpdateError::Failed(Box::new(failure)))
1452 }
1453 None => Err(WorkflowUpdateError::Other(
1454 "Update returned no outcome value".into(),
1455 )),
1456 }
1457 }
1458}
1459
1460#[cfg(test)]
1461mod tests {
1462 use super::*;
1463 use crate::{ClientInterceptor, test_helpers::XorCodec};
1464 use futures_util::{FutureExt, StreamExt};
1465 use std::{
1466 collections::{HashMap, VecDeque},
1467 sync::{
1468 Arc, Mutex,
1469 atomic::{AtomicUsize, Ordering},
1470 },
1471 };
1472 use temporalio_common::{
1473 data_converters::DefaultFailureConverter,
1474 protos::temporal::api::{
1475 common::v1::{Memo, SearchAttributes},
1476 enums::v1::WorkflowExecutionStatus as ProtoWorkflowExecutionStatus,
1477 history::v1::WorkflowExecutionStartedEventAttributes,
1478 sdk::v1::UserMetadata,
1479 workflow::v1::WorkflowExecutionConfig,
1480 workflowservice::v1::GetWorkflowExecutionHistoryResponse,
1481 },
1482 };
1483 use tonic::{Request, Response};
1484
1485 #[tokio::test]
1486 async fn workflow_history_workflow_id_roundtrips() {
1487 let event = HistoryEvent {
1488 event_id: 1,
1489 attributes: Some(Attributes::WorkflowExecutionStartedEventAttributes(
1490 WorkflowExecutionStartedEventAttributes {
1491 workflow_id: "workflow-id".to_owned(),
1492 original_execution_run_id: "run-id".to_owned(),
1493 ..Default::default()
1494 },
1495 )),
1496 ..Default::default()
1497 };
1498 let history = WorkflowHistory {
1499 inner: Box::pin(stream::iter(std::iter::once(Ok(event)))),
1500 workflow_id: None,
1501 };
1502
1503 let bytes = history.to_json().await.unwrap();
1504
1505 let decoded = WorkflowHistory::from_json(&bytes).unwrap();
1506 assert_eq!(decoded.workflow_id(), Some("workflow-id"));
1507 }
1508
1509 #[derive(Clone)]
1510 struct MockHistoryClient {
1511 responses: Arc<Mutex<VecDeque<Result<GetWorkflowExecutionHistoryResponse, tonic::Status>>>>,
1512 calls: Arc<AtomicUsize>,
1513 interceptors: Vec<Arc<dyn ClientInterceptor>>,
1514 }
1515
1516 impl NamespacedClient for MockHistoryClient {
1517 fn namespace(&self) -> String {
1518 "test-namespace".to_owned()
1519 }
1520
1521 fn identity(&self) -> String {
1522 "test-identity".to_owned()
1523 }
1524
1525 fn client_interceptors(&self) -> &[Arc<dyn ClientInterceptor>] {
1526 &self.interceptors
1527 }
1528 }
1529
1530 impl WorkflowService for MockHistoryClient {
1531 fn get_workflow_execution_history(
1532 &mut self,
1533 _request: Request<GetWorkflowExecutionHistoryRequest>,
1534 ) -> BoxFuture<'_, Result<Response<GetWorkflowExecutionHistoryResponse>, tonic::Status>>
1535 {
1536 self.calls.fetch_add(1, Ordering::SeqCst);
1537 let response = self.responses.lock().unwrap().pop_front().unwrap();
1538 async move { response.map(Response::new) }.boxed()
1539 }
1540 }
1541
1542 struct CountingHistoryInterceptor(Arc<AtomicUsize>);
1543
1544 impl ClientInterceptor for CountingHistoryInterceptor {
1545 fn fetch_workflow_history_page<'a>(
1546 &'a self,
1547 input: FetchWorkflowHistoryPageInput,
1548 next: Next<
1549 'a,
1550 FetchWorkflowHistoryPageInput,
1551 BoxFuture<'a, Result<FetchWorkflowHistoryPageOutput, WorkflowInteractionError>>,
1552 >,
1553 ) -> BoxFuture<'a, Result<FetchWorkflowHistoryPageOutput, WorkflowInteractionError>>
1554 {
1555 self.0.fetch_add(1, Ordering::SeqCst);
1556 next.run(input)
1557 }
1558 }
1559
1560 fn history_response(
1561 event_ids: impl IntoIterator<Item = i64>,
1562 next_page_token: &[u8],
1563 ) -> GetWorkflowExecutionHistoryResponse {
1564 GetWorkflowExecutionHistoryResponse {
1565 history: Some(History {
1566 events: event_ids
1567 .into_iter()
1568 .map(|event_id| HistoryEvent {
1569 event_id,
1570 ..Default::default()
1571 })
1572 .collect(),
1573 }),
1574 next_page_token: next_page_token.to_vec(),
1575 ..Default::default()
1576 }
1577 }
1578
1579 fn history_handle(
1580 responses: impl IntoIterator<Item = Result<GetWorkflowExecutionHistoryResponse, tonic::Status>>,
1581 calls: Arc<AtomicUsize>,
1582 interceptors: Vec<Arc<dyn ClientInterceptor>>,
1583 ) -> WorkflowHandle<MockHistoryClient, UntypedWorkflow> {
1584 WorkflowHandle::new(
1585 MockHistoryClient {
1586 responses: Arc::new(Mutex::new(responses.into_iter().collect())),
1587 calls,
1588 interceptors,
1589 },
1590 WorkflowExecutionInfo {
1591 namespace: "test-namespace".to_owned(),
1592 workflow_id: "workflow-id".to_owned(),
1593 run_id: Some("run-id".to_owned()),
1594 first_execution_run_id: None,
1595 },
1596 )
1597 }
1598
1599 #[tokio::test]
1600 async fn workflow_history_fetches_pages_lazily() {
1601 let calls = Arc::new(AtomicUsize::new(0));
1602 let interceptor_calls = Arc::new(AtomicUsize::new(0));
1603 let handle = history_handle(
1604 [
1605 Ok(history_response([], b"second-page")),
1606 Ok(history_response([1, 2], b"third-page")),
1607 Ok(history_response([3], b"")),
1608 ],
1609 calls.clone(),
1610 vec![Arc::new(CountingHistoryInterceptor(
1611 interceptor_calls.clone(),
1612 ))],
1613 );
1614
1615 let mut history = handle.fetch_history(WorkflowFetchHistoryOptions::default());
1616 assert_eq!(calls.load(Ordering::SeqCst), 0);
1617
1618 assert_eq!(history.next().await.unwrap().unwrap().event_id, 1);
1619 assert_eq!(calls.load(Ordering::SeqCst), 2);
1620 assert_eq!(history.next().await.unwrap().unwrap().event_id, 2);
1621 assert_eq!(calls.load(Ordering::SeqCst), 2);
1622 assert_eq!(history.next().await.unwrap().unwrap().event_id, 3);
1623 assert_eq!(calls.load(Ordering::SeqCst), 3);
1624 assert!(history.next().await.is_none());
1625 assert_eq!(interceptor_calls.load(Ordering::SeqCst), 3);
1626 }
1627
1628 #[tokio::test]
1629 async fn workflow_history_yields_page_error_then_ends() {
1630 let calls = Arc::new(AtomicUsize::new(0));
1631 let handle = history_handle(
1632 [
1633 Ok(history_response([1], b"second-page")),
1634 Err(tonic::Status::unavailable("history unavailable")),
1635 ],
1636 calls.clone(),
1637 Vec::new(),
1638 );
1639 let mut history = handle.fetch_history(WorkflowFetchHistoryOptions::default());
1640
1641 assert_eq!(history.next().await.unwrap().unwrap().event_id, 1);
1642 assert!(matches!(
1643 history.next().await.unwrap(),
1644 Err(WorkflowInteractionError::Rpc(status)) if status.code() == tonic::Code::Unavailable
1645 ));
1646 assert!(history.next().await.is_none());
1647 assert_eq!(calls.load(Ordering::SeqCst), 2);
1648 }
1649
1650 #[tokio::test]
1651 async fn workflow_result_details_support_typed_decoding() {
1652 let converter = DataConverter::new(
1653 PayloadConverter::default(),
1654 DefaultFailureConverter::default(),
1655 XorCodec,
1656 );
1657 let payloads = converter
1658 .to_payloads(
1659 &SerializationContextData::Workflow(WorkflowSerializationContext::new()),
1660 &"workflow-result-details".to_owned(),
1661 )
1662 .await
1663 .unwrap();
1664 let details = WorkflowResultDetails::new(payloads.clone(), &converter)
1665 .await
1666 .unwrap();
1667
1668 assert_ne!(details.raw(), payloads);
1669 let decoded_payloads = details.raw().to_vec();
1670 assert_eq!(
1671 details.deserialize::<String>().unwrap(),
1672 "workflow-result-details"
1673 );
1674 assert_eq!(details.into_raw().payloads, decoded_payloads);
1675 }
1676
1677 #[tokio::test]
1678 async fn workflow_result_detail_conversion_errors_are_reported() {
1679 let details =
1680 WorkflowResultDetails::new(vec![Payload::default()], &DataConverter::default())
1681 .await
1682 .unwrap();
1683
1684 assert_eq!(details.raw(), &[Payload::default()]);
1685 assert!(details.deserialize::<String>().is_err());
1686 }
1687
1688 #[tokio::test]
1689 async fn workflow_description_memo_uses_saved_converter() {
1690 let converter = DataConverter::new(
1691 PayloadConverter::default(),
1692 DefaultFailureConverter::default(),
1693 XorCodec,
1694 );
1695 let encoded = converter
1696 .to_payload(
1697 &SerializationContextData::Workflow(WorkflowSerializationContext::new()),
1698 &"memo-value".to_owned(),
1699 )
1700 .await
1701 .unwrap();
1702 let description = WorkflowExecutionDescription::new(
1703 DescribeWorkflowExecutionResponse {
1704 workflow_execution_info: Some(workflow::WorkflowExecutionInfo {
1705 memo: Some(Memo {
1706 fields: HashMap::from([("memo-key".to_owned(), encoded)]),
1707 }),
1708 ..Default::default()
1709 }),
1710 ..Default::default()
1711 },
1712 &converter,
1713 )
1714 .await
1715 .unwrap();
1716 let memo = description.memo();
1717
1718 assert_eq!(
1719 memo.get::<String>("memo-key").unwrap(),
1720 Some("memo-value".to_owned())
1721 );
1722 }
1723
1724 #[tokio::test]
1725 async fn workflow_description_accessors_expose_decoded_fields() {
1726 let converter = DataConverter::default();
1727 let memo_payload = converter
1728 .to_payload(
1729 &SerializationContextData::Workflow(WorkflowSerializationContext::new()),
1730 &"memo-value",
1731 )
1732 .await
1733 .unwrap();
1734 let search_attr_payload = converter
1735 .to_payload(
1736 &SerializationContextData::Workflow(WorkflowSerializationContext::new()),
1737 &"search-value",
1738 )
1739 .await
1740 .unwrap();
1741 let summary_payload = converter
1742 .to_payload(
1743 &SerializationContextData::Workflow(WorkflowSerializationContext::new()),
1744 &"workflow summary",
1745 )
1746 .await
1747 .unwrap();
1748 let details_payload = converter
1749 .to_payload(
1750 &SerializationContextData::Workflow(WorkflowSerializationContext::new()),
1751 &"workflow details",
1752 )
1753 .await
1754 .unwrap();
1755 let description = WorkflowExecutionDescription::new(
1756 DescribeWorkflowExecutionResponse {
1757 workflow_execution_info: Some(workflow::WorkflowExecutionInfo {
1758 execution: Some(ProtoWorkflowExecution {
1759 workflow_id: "wf-id".to_string(),
1760 run_id: "run-id".to_string(),
1761 }),
1762 r#type: Some(
1763 temporalio_common::protos::temporal::api::common::v1::WorkflowType {
1764 name: "wf-type".to_string(),
1765 },
1766 ),
1767 status: ProtoWorkflowExecutionStatus::Completed as i32,
1768 task_queue: "task-queue".to_string(),
1769 history_length: 42,
1770 memo: Some(Memo {
1771 fields: HashMap::from([("memo-key".to_string(), memo_payload.clone())]),
1772 }),
1773 parent_execution: Some(ProtoWorkflowExecution {
1774 workflow_id: "parent-id".to_string(),
1775 run_id: "parent-run-id".to_string(),
1776 }),
1777 search_attributes: Some(SearchAttributes {
1778 indexed_fields: HashMap::from([(
1779 "CustomKeywordField".to_string(),
1780 search_attr_payload.clone(),
1781 )]),
1782 }),
1783 ..Default::default()
1784 }),
1785 execution_config: Some(WorkflowExecutionConfig {
1786 user_metadata: Some(UserMetadata {
1787 summary: Some(summary_payload),
1788 details: Some(details_payload),
1789 }),
1790 ..Default::default()
1791 }),
1792 ..Default::default()
1793 },
1794 &converter,
1795 )
1796 .await
1797 .unwrap();
1798
1799 assert_eq!(description.id(), "wf-id");
1800 assert_eq!(description.run_id(), "run-id");
1801 assert_eq!(description.workflow_type(), "wf-type");
1802 assert_eq!(description.status(), WorkflowExecutionStatus::Completed);
1803 let mut unknown_status_description = description.clone();
1804 unknown_status_description
1805 .raw_description
1806 .workflow_execution_info
1807 .as_mut()
1808 .unwrap()
1809 .status = 123_456;
1810 assert_eq!(
1811 unknown_status_description.status(),
1812 WorkflowExecutionStatus::Unknown
1813 );
1814 assert_eq!(description.task_queue(), "task-queue");
1815 assert_eq!(description.history_length(), 42);
1816 assert_eq!(description.parent_id(), Some("parent-id"));
1817 assert_eq!(description.parent_run_id(), Some("parent-run-id"));
1818 let memo = description.memo();
1819 assert_eq!(memo.raw_value("memo-key"), Some(&memo_payload));
1820 assert_eq!(
1821 memo.get::<String>("memo-key").unwrap(),
1822 Some("memo-value".to_owned())
1823 );
1824 let search_attributes = description.search_attributes();
1825 assert_eq!(
1826 search_attributes.raw_payload("CustomKeywordField"),
1827 Some(&search_attr_payload)
1828 );
1829 assert_eq!(description.static_summary(), Some("workflow summary"));
1830 assert_eq!(description.static_details(), Some("workflow details"));
1831 }
1832
1833 #[tokio::test]
1834 async fn workflow_description_rejects_negative_history_length() {
1835 let err = WorkflowExecutionDescription::new(
1836 DescribeWorkflowExecutionResponse {
1837 workflow_execution_info: Some(workflow::WorkflowExecutionInfo {
1838 history_length: -1,
1839 ..Default::default()
1840 }),
1841 ..Default::default()
1842 },
1843 &DataConverter::default(),
1844 )
1845 .await
1846 .unwrap_err();
1847
1848 assert_eq!(
1849 err.to_string(),
1850 "Encoding error: workflow history_length must be non-negative, got -1"
1851 );
1852 }
1853}