Skip to main content

temporalio_client/
async_activity_handle.rs

1//! Handle for completing activities asynchronously via a client.
2
3use crate::{
4    CompleteAsyncActivityInput, FailAsyncActivityInput, HeartbeatAsyncActivityInput,
5    NamespacedClient, Next, ReportAsyncActivityCancellationInput, RpcOptions, TemporalClientValue,
6    errors::AsyncActivityError, grpc::WorkflowService, interceptors,
7};
8use futures_util::future::BoxFuture;
9use temporalio_common::{
10    data_converters::{
11        ActivitySerializationContext, SerializationContext, SerializationContextData,
12        TemporalSerializable,
13    },
14    error::{ApplicationFailure, OutgoingActivityError, OutgoingError},
15    payload_visitor::encode_payloads,
16    protos::{
17        TaskToken,
18        temporal::api::{
19            common::v1::Payloads,
20            workflowservice::v1::{
21                RecordActivityTaskHeartbeatByIdRequest, RecordActivityTaskHeartbeatByIdResponse,
22                RecordActivityTaskHeartbeatRequest, RecordActivityTaskHeartbeatResponse,
23                RespondActivityTaskCanceledByIdRequest, RespondActivityTaskCanceledRequest,
24                RespondActivityTaskCompletedByIdRequest, RespondActivityTaskCompletedRequest,
25                RespondActivityTaskFailedByIdRequest, RespondActivityTaskFailedRequest,
26            },
27        },
28    },
29};
30use tonic::IntoRequest;
31
32async fn encode_optional_value(
33    value: Option<Box<dyn TemporalClientValue>>,
34    data_converter: &temporalio_common::data_converters::DataConverter,
35) -> Result<Option<Payloads>, AsyncActivityError> {
36    let Some(value) = value else {
37        return Ok(None);
38    };
39    let unencoded_payloads = {
40        let payload_converter = data_converter.payload_converter();
41        let context_data = SerializationContextData::Activity(ActivitySerializationContext::new());
42        let context = SerializationContext::new(&context_data, payload_converter);
43        value.serialize_payloads(&context)?
44    };
45    drop(value);
46    let payloads = data_converter
47        .codec()
48        .encode(
49            &SerializationContextData::Activity(ActivitySerializationContext::new()),
50            unencoded_payloads,
51        )
52        .await?;
53    Ok(Some(Payloads { payloads }))
54}
55
56/// Identifies an async activity for completion outside a worker.
57#[derive(Debug, Clone)]
58pub enum ActivityIdentifier {
59    /// Identify activity by its task token
60    TaskToken(TaskToken),
61    /// Identify workflow activity by workflow and activity IDs.
62    ByIdWorkflow {
63        /// ID of the workflow that scheduled this activity.
64        workflow_id: String,
65        /// Run ID of the workflow (optional - if not provided, targets the latest run).
66        run_id: String,
67        /// ID of the activity to complete.
68        activity_id: String,
69    },
70    /// Identify standalone activity by activity ID.
71    ByIdStandalone {
72        /// ID of the activity to complete.
73        activity_id: String,
74        /// Run ID of the activity (optional - if not provided, targets the latest run).
75        run_id: String,
76    },
77}
78
79impl ActivityIdentifier {
80    /// Create an identifier from a task token.
81    pub fn from_task_token(token: TaskToken) -> Self {
82        Self::TaskToken(token)
83    }
84
85    /// Create an identifier from workflow and activity IDs. Use an empty run id to target the
86    /// latest workflow execution.
87    pub fn by_id_workflow(
88        workflow_id: impl Into<String>,
89        run_id: impl Into<String>,
90        activity_id: impl Into<String>,
91    ) -> Self {
92        Self::ByIdWorkflow {
93            workflow_id: workflow_id.into(),
94            run_id: run_id.into(),
95            activity_id: activity_id.into(),
96        }
97    }
98
99    /// Create an identifier from standalone activity ID. Use an empty run id to target the
100    /// latest activity execution.
101    pub fn by_id_standalone(activity_id: impl Into<String>, run_id: impl Into<String>) -> Self {
102        Self::ByIdStandalone {
103            activity_id: activity_id.into(),
104            run_id: run_id.into(),
105        }
106    }
107
108    /// Returns tuple of (workflow_id, run_id, activity_id).
109    fn into_parts(self) -> Option<(String, String, String)> {
110        match self {
111            Self::TaskToken(_) => None,
112            Self::ByIdWorkflow {
113                workflow_id,
114                run_id,
115                activity_id,
116            } => Some((workflow_id, run_id, activity_id)),
117            Self::ByIdStandalone {
118                activity_id,
119                run_id,
120            } => Some((String::new(), run_id, activity_id)),
121        }
122    }
123}
124
125/// Handle for completing activities asynchronously (outside the worker).
126pub struct AsyncActivityHandle<CT> {
127    client: CT,
128    identifier: ActivityIdentifier,
129}
130
131impl<CT> AsyncActivityHandle<CT> {
132    /// Create a new async activity handle.
133    pub fn new(client: CT, identifier: ActivityIdentifier) -> Self {
134        Self { client, identifier }
135    }
136
137    /// Get the identifier for this activity.
138    pub fn identifier(&self) -> &ActivityIdentifier {
139        &self.identifier
140    }
141
142    /// Get a reference to the underlying client.
143    pub fn client(&self) -> &CT {
144        &self.client
145    }
146}
147
148impl<CT: WorkflowService + NamespacedClient + Clone> AsyncActivityHandle<CT> {
149    /// Complete the activity with a successful result.
150    pub async fn complete<T>(
151        &self,
152        result: Option<T>,
153        rpc_options: RpcOptions,
154    ) -> Result<(), AsyncActivityError>
155    where
156        T: TemporalSerializable + Send + 'static,
157    {
158        interceptors::call_complete_async_activity(
159            self.client.client_interceptors(),
160            CompleteAsyncActivityInput::new(self.identifier.clone(), result, rpc_options),
161            Next::new({
162                let mut client = self.client.clone();
163                move |input: CompleteAsyncActivityInput| -> BoxFuture<
164                    '_,
165                    Result<(), AsyncActivityError>,
166                > {
167                    Box::pin(async move {
168                        let (identifier, result, rpc_options) = input.into_parts();
169                        let result = encode_optional_value(result, client.data_converter()).await?;
170                        if let ActivityIdentifier::TaskToken(token) = identifier {
171                            let mut request = RespondActivityTaskCompletedRequest {
172                                task_token: token.into_inner(),
173                                result,
174                                identity: client.identity(),
175                                namespace: client.namespace(),
176                                ..Default::default()
177                            }
178                            .into_request();
179                            rpc_options.apply_to(&mut request);
180                            WorkflowService::respond_activity_task_completed(
181                                &mut client,
182                                request,
183                            )
184                            .await
185                            .map_err(AsyncActivityError::from_status)?;
186                        } else {
187                            let (workflow_id, run_id, activity_id) = identifier.into_parts().unwrap();
188                            let mut request = RespondActivityTaskCompletedByIdRequest {
189                                namespace: client.namespace(),
190                                workflow_id,
191                                run_id,
192                                activity_id,
193                                result,
194                                identity: client.identity(),
195                                resource_id: Default::default(),
196                            }
197                            .into_request();
198                            rpc_options.apply_to(&mut request);
199                            WorkflowService::respond_activity_task_completed_by_id(
200                                &mut client,
201                                request,
202                            )
203                            .await
204                            .map_err(AsyncActivityError::from_status)?;
205                        }
206                        Ok(())
207                    })
208                }
209            }),
210        )
211        .await
212    }
213
214    /// Fail the activity with a failure.
215    pub async fn fail<E, T>(
216        &self,
217        failure: E,
218        last_heartbeat_details: Option<T>,
219        rpc_options: RpcOptions,
220    ) -> Result<(), AsyncActivityError>
221    where
222        E: Into<ApplicationFailure>,
223        T: TemporalSerializable + Send + 'static,
224    {
225        interceptors::call_fail_async_activity(
226            self.client.client_interceptors(),
227            FailAsyncActivityInput::new(
228                self.identifier.clone(),
229                failure.into(),
230                last_heartbeat_details,
231                rpc_options,
232            ),
233            Next::new({
234                let mut client = self.client.clone();
235                move |input: FailAsyncActivityInput| -> BoxFuture<
236                    '_,
237                    Result<(), AsyncActivityError>,
238                > {
239                    Box::pin(async move {
240                        let (identifier, application_failure, details, rpc_options) =
241                            input.into_parts();
242                        let data_converter = client.data_converter().clone();
243                        let mut failure = data_converter.to_failure(
244                            &SerializationContextData::Activity(ActivitySerializationContext::new()),
245                            OutgoingError::Activity(OutgoingActivityError::Application(Box::new(
246                                application_failure,
247                            ))),
248                        );
249                        encode_payloads(
250                            &mut failure,
251                            data_converter.codec(),
252                            &SerializationContextData::Activity(ActivitySerializationContext::new()),
253                        )
254                        .await?;
255                        let last_heartbeat_details =
256                            encode_optional_value(details, &data_converter).await?;
257                        if let ActivityIdentifier::TaskToken(token) = identifier {
258                            let mut request = RespondActivityTaskFailedRequest {
259                                task_token: token.into_inner(),
260                                failure: Some(failure),
261                                identity: client.identity(),
262                                namespace: client.namespace(),
263                                last_heartbeat_details,
264                                ..Default::default()
265                            }
266                            .into_request();
267                            rpc_options.apply_to(&mut request);
268                            WorkflowService::respond_activity_task_failed(
269                                &mut client,
270                                request,
271                            )
272                            .await
273                            .map_err(AsyncActivityError::from_status)?;
274                        } else {
275                            let (workflow_id, run_id, activity_id) = identifier.into_parts().unwrap();
276                            let mut request = RespondActivityTaskFailedByIdRequest {
277                                namespace: client.namespace(),
278                                workflow_id,
279                                run_id,
280                                activity_id,
281                                failure: Some(failure),
282                                identity: client.identity(),
283                                last_heartbeat_details,
284                                resource_id: Default::default(),
285                                ..Default::default()
286                            }
287                            .into_request();
288                            rpc_options.apply_to(&mut request);
289                            WorkflowService::respond_activity_task_failed_by_id(
290                                &mut client,
291                                request,
292                            )
293                            .await
294                            .map_err(AsyncActivityError::from_status)?;
295                        }
296                        Ok(())
297                    })
298                }
299            }),
300        )
301        .await
302    }
303
304    /// Reports the activity as canceled.
305    pub async fn report_cancelation<T>(
306        &self,
307        details: Option<T>,
308        rpc_options: RpcOptions,
309    ) -> Result<(), AsyncActivityError>
310    where
311        T: TemporalSerializable + Send + 'static,
312    {
313        interceptors::call_report_async_activity_cancellation(
314            self.client.client_interceptors(),
315            ReportAsyncActivityCancellationInput::new(
316                self.identifier.clone(),
317                details,
318                rpc_options,
319            ),
320            Next::new({
321                let mut client = self.client.clone();
322                move |input: ReportAsyncActivityCancellationInput| -> BoxFuture<
323                    '_,
324                    Result<(), AsyncActivityError>,
325                > {
326                    Box::pin(async move {
327                        let (identifier, details, rpc_options) = input.into_parts();
328                        let details = encode_optional_value(details, client.data_converter()).await?;
329                        if let ActivityIdentifier::TaskToken(token) = identifier {
330                            let mut request = RespondActivityTaskCanceledRequest {
331                                task_token: token.into_inner(),
332                                details,
333                                identity: client.identity(),
334                                namespace: client.namespace(),
335                                ..Default::default()
336                            }
337                            .into_request();
338                            rpc_options.apply_to(&mut request);
339                            WorkflowService::respond_activity_task_canceled(
340                                &mut client,
341                                request,
342                            )
343                            .await
344                            .map_err(AsyncActivityError::from_status)?;
345                        } else {
346                            let (workflow_id, run_id, activity_id) = identifier.into_parts().unwrap();
347                            let mut request = RespondActivityTaskCanceledByIdRequest {
348                                namespace: client.namespace(),
349                                workflow_id,
350                                run_id,
351                                activity_id,
352                                details,
353                                identity: client.identity(),
354                                ..Default::default()
355                            }
356                            .into_request();
357                            rpc_options.apply_to(&mut request);
358                            WorkflowService::respond_activity_task_canceled_by_id(
359                                &mut client,
360                                request,
361                            )
362                            .await
363                            .map_err(AsyncActivityError::from_status)?;
364                        }
365                        Ok(())
366                    })
367                }
368            }),
369        )
370        .await
371    }
372
373    /// Record a heartbeat for the activity.
374    ///
375    /// Heartbeats let the server know the activity is still running and can carry
376    /// progress information. The response indicates if cancellation has been requested.
377    pub async fn heartbeat<T>(
378        &self,
379        details: Option<T>,
380        rpc_options: RpcOptions,
381    ) -> Result<ActivityHeartbeatResponse, AsyncActivityError>
382    where
383        T: TemporalSerializable + Send + 'static,
384    {
385        interceptors::call_heartbeat_async_activity(
386            self.client.client_interceptors(),
387            HeartbeatAsyncActivityInput::new(self.identifier.clone(), details, rpc_options),
388            Next::new({
389                let mut client = self.client.clone();
390                move |input: HeartbeatAsyncActivityInput| -> BoxFuture<
391                    '_,
392                    Result<ActivityHeartbeatResponse, AsyncActivityError>,
393                > {
394                    Box::pin(async move {
395                        let (identifier, details, rpc_options) = input.into_parts();
396                        let details = encode_optional_value(details, client.data_converter()).await?;
397                        if let ActivityIdentifier::TaskToken(token) = identifier {
398                            let mut request = RecordActivityTaskHeartbeatRequest {
399                                task_token: token.into_inner(),
400                                details,
401                                identity: client.identity(),
402                                namespace: client.namespace(),
403                                resource_id: Default::default(),
404                            }
405                            .into_request();
406                            rpc_options.apply_to(&mut request);
407                            let response = WorkflowService::record_activity_task_heartbeat(
408                                &mut client,
409                                request,
410                            )
411                            .await
412                            .map_err(AsyncActivityError::from_status)?
413                            .into_inner();
414                            Ok(ActivityHeartbeatResponse::from(response))
415                        } else {
416                            let (workflow_id, run_id, activity_id) = identifier.into_parts().unwrap();
417                            let mut request = RecordActivityTaskHeartbeatByIdRequest {
418                                namespace: client.namespace(),
419                                workflow_id,
420                                run_id,
421                                activity_id,
422                                details,
423                                identity: client.identity(),
424                                resource_id: Default::default(),
425                            }
426                            .into_request();
427                            rpc_options.apply_to(&mut request);
428                            let response =
429                                WorkflowService::record_activity_task_heartbeat_by_id(
430                                    &mut client,
431                                    request,
432                                )
433                                .await
434                                .map_err(AsyncActivityError::from_status)?
435                                .into_inner();
436                            Ok(ActivityHeartbeatResponse::from(response))
437                        }
438                    })
439                }
440            }),
441        )
442        .await
443    }
444}
445
446/// Response from a heartbeat call.
447#[derive(Debug, Clone)]
448#[non_exhaustive]
449pub struct ActivityHeartbeatResponse {
450    /// True if the activity has been asked to cancel itself.
451    pub cancel_requested: bool,
452    /// True if the activity is paused.
453    pub activity_paused: bool,
454    /// True if the activity was reset.
455    pub activity_reset: bool,
456}
457
458impl From<RecordActivityTaskHeartbeatResponse> for ActivityHeartbeatResponse {
459    fn from(resp: RecordActivityTaskHeartbeatResponse) -> Self {
460        Self {
461            cancel_requested: resp.cancel_requested,
462            activity_paused: resp.activity_paused,
463            activity_reset: resp.activity_reset,
464        }
465    }
466}
467
468impl From<RecordActivityTaskHeartbeatByIdResponse> for ActivityHeartbeatResponse {
469    fn from(resp: RecordActivityTaskHeartbeatByIdResponse) -> Self {
470        Self {
471            cancel_requested: resp.cancel_requested,
472            activity_paused: resp.activity_paused,
473            activity_reset: resp.activity_reset,
474        }
475    }
476}