Skip to main content

temporalio_client/activity/
activity_handle.rs

1use crate::{
2    ActivityCancelOptions, ActivityDescribeOptions, ActivityExecutionDescription,
3    ActivityTerminateOptions, NamespacedClient,
4    errors::{ActivityInteractionError, ActivityResultError},
5    grpc::WorkflowService,
6};
7use std::marker::PhantomData;
8use temporalio_common::{
9    ActivityDefinition,
10    data_converters::{
11        ActivitySerializationContext, DecodablePayloads, NoopDecodeHint, SerializationContextData,
12    },
13    payload_visitor::decode_payloads,
14    protos::temporal::api::{
15        activity::v1::{ActivityExecutionOutcome, activity_execution_outcome},
16        failure::v1::failure::FailureInfo,
17        workflowservice::v1::{
18            DescribeActivityExecutionRequest, PollActivityExecutionRequest,
19            RequestCancelActivityExecutionRequest, TerminateActivityExecutionRequest,
20        },
21    },
22};
23use tonic::IntoRequest;
24use uuid::Uuid;
25
26/// Handle associated with a standalone activity execution that can be used to wait for the result
27/// or to manage execution of the activity. Obtained from
28/// [`Client::start_activity`](crate::Client::start_activity) or
29/// [`Client::get_activity_handle`](crate::Client::get_activity_handle).
30///
31/// If [`run_id`](Self::run_id) is set, the handle always targets that specific execution.
32/// If [`run_id`](Self::run_id) is `None`, each method call targets the latest run of the specified
33/// [`activity_id`](Self::activity_id) at the time the method is called - this means consecutive
34/// method calls may target different executions if an activity was started again with the same ID.
35pub struct ActivityHandle<ClientT, ActivityT>
36where
37    ActivityT: ActivityDefinition,
38{
39    client: ClientT,
40    activity_id: String,
41    run_id: Option<String>,
42    _phantom: PhantomData<ActivityT>,
43}
44
45impl<ClientT, ActivityT> ActivityHandle<ClientT, ActivityT>
46where
47    ActivityT: ActivityDefinition,
48{
49    pub(crate) fn new(client: ClientT, activity_id: String, run_id: Option<String>) -> Self {
50        Self {
51            client,
52            activity_id,
53            run_id,
54            _phantom: PhantomData,
55        }
56    }
57
58    /// Activity ID this handle is associated with.
59    pub fn activity_id(&self) -> &str {
60        &self.activity_id
61    }
62
63    /// Run ID of the activity execution this handle is associated with. If `None`, each method call
64    /// targets the latest run of the specified [`activity_id`](Self::activity_id) at the time the
65    /// method is called - this means consecutive method calls may target different executions if
66    /// an activity was started again with the same ID.
67    pub fn run_id(&self) -> Option<&str> {
68        self.run_id.as_deref()
69    }
70}
71
72impl<ClientT, ActivityT> ActivityHandle<ClientT, ActivityT>
73where
74    ClientT: WorkflowService + NamespacedClient + Clone,
75    ActivityT: ActivityDefinition,
76{
77    /// Wait for the activity to complete and fetch its result. If the activity was not successful
78    /// (e.g. failed, canceled, timed out), this method returns [`ActivityResultError::ActivityFailed`].
79    pub async fn result(&self) -> Result<ActivityT::Output, ActivityResultError> {
80        let mut client = self.client.clone();
81        loop {
82            let resp = client
83                .poll_activity_execution(
84                    PollActivityExecutionRequest {
85                        namespace: client.namespace(),
86                        activity_id: self.activity_id.clone(),
87                        run_id: self.run_id.clone().unwrap_or_default(),
88                    }
89                    .into_request(),
90                )
91                .await?
92                .into_inner();
93
94            // If resp.outcome.value is None, poll again
95            let Some(ActivityExecutionOutcome {
96                value: Some(outcome),
97                ..
98            }) = resp.outcome
99            else {
100                continue;
101            };
102
103            let dc = client.data_converter();
104            let ctx = SerializationContextData::Activity(ActivitySerializationContext::new());
105
106            return match outcome {
107                activity_execution_outcome::Value::Result(payloads) => {
108                    Ok(dc.from_payloads(&ctx, payloads.payloads).await?)
109                }
110                activity_execution_outcome::Value::Failure(mut failure) => {
111                    decode_payloads(&mut failure, dc.codec(), &ctx).await?;
112                    Err(match failure.failure_info {
113                        Some(FailureInfo::CanceledFailureInfo(info)) => {
114                            let payloads = info.details.unwrap_or_default().payloads;
115                            let details = DecodablePayloads::new(
116                                payloads,
117                                dc.payload_converter().clone(),
118                                ctx,
119                            );
120                            ActivityResultError::Cancelled { details }
121                        }
122                        Some(FailureInfo::TerminatedFailureInfo(_)) => {
123                            ActivityResultError::Terminated
124                        }
125                        _ => ActivityResultError::ActivityFailed(dc.to_error(
126                            &ctx,
127                            failure,
128                            NoopDecodeHint,
129                        )?),
130                    })
131                }
132            };
133        }
134    }
135
136    /// Describes the current state of the activity execution.
137    pub async fn describe(
138        &self,
139        options: ActivityDescribeOptions,
140    ) -> Result<ActivityExecutionDescription<ActivityT>, ActivityInteractionError> {
141        let mut client = self.client.clone();
142        let resp = client
143            .describe_activity_execution(
144                DescribeActivityExecutionRequest {
145                    namespace: client.namespace(),
146                    activity_id: self.activity_id.clone(),
147                    run_id: self.run_id.clone().unwrap_or_default(),
148                    include_input: options.include_input,
149                    include_outcome: options.include_outcome,
150                    include_heartbeat_details: options.include_heartbeat_details,
151                    include_last_failure: options.include_last_failure,
152                    ..Default::default()
153                }
154                .into_request(),
155            )
156            .await?
157            .into_inner();
158
159        Ok(ActivityExecutionDescription::new(
160            client.data_converter().clone(),
161            SerializationContextData::Activity(ActivitySerializationContext::new()),
162            resp,
163        )
164        .await?)
165    }
166
167    /// Requests cancellation of the activity. Does not wait for the cancellation to complete.
168    pub async fn cancel(
169        &self,
170        options: ActivityCancelOptions,
171    ) -> Result<(), ActivityInteractionError> {
172        let mut client = self.client.clone();
173        client
174            .request_cancel_activity_execution(
175                RequestCancelActivityExecutionRequest {
176                    namespace: client.namespace(),
177                    activity_id: self.activity_id.clone(),
178                    run_id: self.run_id.clone().unwrap_or_default(),
179                    identity: client.identity(),
180                    request_id: Uuid::new_v4().to_string(),
181                    reason: options.reason,
182                }
183                .into_request(),
184            )
185            .await?;
186
187        Ok(())
188    }
189
190    /// Terminates activity execution.
191    pub async fn terminate(
192        &self,
193        options: ActivityTerminateOptions,
194    ) -> Result<(), ActivityInteractionError> {
195        let mut client = self.client.clone();
196        client
197            .terminate_activity_execution(
198                TerminateActivityExecutionRequest {
199                    namespace: client.namespace(),
200                    activity_id: self.activity_id.clone(),
201                    run_id: self.run_id.clone().unwrap_or_default(),
202                    identity: client.identity(),
203                    request_id: Uuid::new_v4().to_string(),
204                    reason: options.reason,
205                }
206                .into_request(),
207            )
208            .await?;
209
210        Ok(())
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use crate::test_helpers::XorCodec;
218    use futures_util::future::BoxFuture;
219    use temporalio_common::{
220        UntypedActivity,
221        data_converters::{DataConverter, DefaultFailureConverter, PayloadConverter},
222        error::{ApplicationFailure, OutgoingActivityError, OutgoingError},
223        payload_visitor::encode_payloads,
224        protos::temporal::api::{
225            activity::v1::ActivityExecutionInfo,
226            failure::v1::Failure,
227            workflowservice::v1::{
228                DescribeActivityExecutionResponse, PollActivityExecutionResponse,
229            },
230        },
231    };
232    use tonic::{Request, Response, Status};
233
234    #[derive(Clone)]
235    struct MockActivityClient {
236        data_converter: DataConverter,
237        failure: Failure,
238    }
239
240    impl NamespacedClient for MockActivityClient {
241        fn namespace(&self) -> String {
242            "test-namespace".to_owned()
243        }
244
245        fn identity(&self) -> String {
246            "test-identity".to_owned()
247        }
248
249        fn data_converter(&self) -> &DataConverter {
250            &self.data_converter
251        }
252    }
253
254    impl WorkflowService for MockActivityClient {
255        fn poll_activity_execution(
256            &mut self,
257            _request: Request<PollActivityExecutionRequest>,
258        ) -> BoxFuture<'_, Result<Response<PollActivityExecutionResponse>, Status>> {
259            let failure = self.failure.clone();
260            Box::pin(async move {
261                Ok(Response::new(PollActivityExecutionResponse {
262                    outcome: Some(ActivityExecutionOutcome {
263                        value: Some(activity_execution_outcome::Value::Failure(failure)),
264                        ..Default::default()
265                    }),
266                    ..Default::default()
267                }))
268            })
269        }
270
271        fn describe_activity_execution(
272            &mut self,
273            _request: Request<DescribeActivityExecutionRequest>,
274        ) -> BoxFuture<'_, Result<Response<DescribeActivityExecutionResponse>, Status>> {
275            let failure = self.failure.clone();
276            Box::pin(async move {
277                Ok(Response::new(DescribeActivityExecutionResponse {
278                    info: Some(ActivityExecutionInfo {
279                        last_failure: Some(failure.clone()),
280                        ..Default::default()
281                    }),
282                    outcome: Some(ActivityExecutionOutcome {
283                        value: Some(activity_execution_outcome::Value::Failure(failure)),
284                        ..Default::default()
285                    }),
286                    ..Default::default()
287                }))
288            })
289        }
290    }
291
292    async fn activity_client_with_encoded_failure() -> MockActivityClient {
293        let data_converter = DataConverter::new(
294            PayloadConverter::default(),
295            DefaultFailureConverter::new(true),
296            XorCodec,
297        );
298        let context = SerializationContextData::Activity(ActivitySerializationContext::new());
299        let mut failure = data_converter.to_failure(
300            &context,
301            OutgoingError::Activity(OutgoingActivityError::Application(Box::new(
302                ApplicationFailure::new(anyhow::anyhow!("private message")),
303            ))),
304        );
305        encode_payloads(&mut failure, data_converter.codec(), &context)
306            .await
307            .unwrap();
308        MockActivityClient {
309            data_converter,
310            failure,
311        }
312    }
313
314    #[tokio::test]
315    async fn result_decodes_failure_attributes_with_codec() {
316        let handle = ActivityHandle::<_, UntypedActivity>::new(
317            activity_client_with_encoded_failure().await,
318            "activity-id".to_owned(),
319            None,
320        );
321
322        let ActivityResultError::ActivityFailed(error) = handle.result().await.unwrap_err() else {
323            panic!("expected failed activity");
324        };
325        assert_eq!(error.failure().message, "private message");
326    }
327
328    #[tokio::test]
329    async fn describe_decodes_failure_attributes_with_codec() {
330        let handle = ActivityHandle::<_, UntypedActivity>::new(
331            activity_client_with_encoded_failure().await,
332            "activity-id".to_owned(),
333            None,
334        );
335
336        let description = handle
337            .describe(
338                ActivityDescribeOptions::builder()
339                    .include_outcome(true)
340                    .include_last_failure(true)
341                    .build(),
342            )
343            .await
344            .unwrap();
345        let outcome = description.outcome().await.unwrap().unwrap().unwrap_err();
346        let last_failure = description.last_failure().unwrap().unwrap();
347        assert_eq!(outcome.failure().message, "private message");
348        assert_eq!(last_failure.failure().message, "private message");
349    }
350}