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                            }
286                            .into_request();
287                            rpc_options.apply_to(&mut request);
288                            WorkflowService::respond_activity_task_failed_by_id(
289                                &mut client,
290                                request,
291                            )
292                            .await
293                            .map_err(AsyncActivityError::from_status)?;
294                        }
295                        Ok(())
296                    })
297                }
298            }),
299        )
300        .await
301    }
302
303    /// Reports the activity as canceled.
304    pub async fn report_cancelation<T>(
305        &self,
306        details: Option<T>,
307        rpc_options: RpcOptions,
308    ) -> Result<(), AsyncActivityError>
309    where
310        T: TemporalSerializable + Send + 'static,
311    {
312        interceptors::call_report_async_activity_cancellation(
313            self.client.client_interceptors(),
314            ReportAsyncActivityCancellationInput::new(
315                self.identifier.clone(),
316                details,
317                rpc_options,
318            ),
319            Next::new({
320                let mut client = self.client.clone();
321                move |input: ReportAsyncActivityCancellationInput| -> BoxFuture<
322                    '_,
323                    Result<(), AsyncActivityError>,
324                > {
325                    Box::pin(async move {
326                        let (identifier, details, rpc_options) = input.into_parts();
327                        let details = encode_optional_value(details, client.data_converter()).await?;
328                        if let ActivityIdentifier::TaskToken(token) = identifier {
329                            let mut request = RespondActivityTaskCanceledRequest {
330                                task_token: token.into_inner(),
331                                details,
332                                identity: client.identity(),
333                                namespace: client.namespace(),
334                                ..Default::default()
335                            }
336                            .into_request();
337                            rpc_options.apply_to(&mut request);
338                            WorkflowService::respond_activity_task_canceled(
339                                &mut client,
340                                request,
341                            )
342                            .await
343                            .map_err(AsyncActivityError::from_status)?;
344                        } else {
345                            let (workflow_id, run_id, activity_id) = identifier.into_parts().unwrap();
346                            let mut request = RespondActivityTaskCanceledByIdRequest {
347                                namespace: client.namespace(),
348                                workflow_id,
349                                run_id,
350                                activity_id,
351                                details,
352                                identity: client.identity(),
353                                ..Default::default()
354                            }
355                            .into_request();
356                            rpc_options.apply_to(&mut request);
357                            WorkflowService::respond_activity_task_canceled_by_id(
358                                &mut client,
359                                request,
360                            )
361                            .await
362                            .map_err(AsyncActivityError::from_status)?;
363                        }
364                        Ok(())
365                    })
366                }
367            }),
368        )
369        .await
370    }
371
372    /// Record a heartbeat for the activity.
373    ///
374    /// Heartbeats let the server know the activity is still running and can carry
375    /// progress information. The response indicates if cancellation has been requested.
376    pub async fn heartbeat<T>(
377        &self,
378        details: Option<T>,
379        rpc_options: RpcOptions,
380    ) -> Result<ActivityHeartbeatResponse, AsyncActivityError>
381    where
382        T: TemporalSerializable + Send + 'static,
383    {
384        interceptors::call_heartbeat_async_activity(
385            self.client.client_interceptors(),
386            HeartbeatAsyncActivityInput::new(self.identifier.clone(), details, rpc_options),
387            Next::new({
388                let mut client = self.client.clone();
389                move |input: HeartbeatAsyncActivityInput| -> BoxFuture<
390                    '_,
391                    Result<ActivityHeartbeatResponse, AsyncActivityError>,
392                > {
393                    Box::pin(async move {
394                        let (identifier, details, rpc_options) = input.into_parts();
395                        let details = encode_optional_value(details, client.data_converter()).await?;
396                        if let ActivityIdentifier::TaskToken(token) = identifier {
397                            let mut request = RecordActivityTaskHeartbeatRequest {
398                                task_token: token.into_inner(),
399                                details,
400                                identity: client.identity(),
401                                namespace: client.namespace(),
402                                resource_id: Default::default(),
403                            }
404                            .into_request();
405                            rpc_options.apply_to(&mut request);
406                            let response = WorkflowService::record_activity_task_heartbeat(
407                                &mut client,
408                                request,
409                            )
410                            .await
411                            .map_err(AsyncActivityError::from_status)?
412                            .into_inner();
413                            Ok(ActivityHeartbeatResponse::from(response))
414                        } else {
415                            let (workflow_id, run_id, activity_id) = identifier.into_parts().unwrap();
416                            let mut request = RecordActivityTaskHeartbeatByIdRequest {
417                                namespace: client.namespace(),
418                                workflow_id,
419                                run_id,
420                                activity_id,
421                                details,
422                                identity: client.identity(),
423                                resource_id: Default::default(),
424                            }
425                            .into_request();
426                            rpc_options.apply_to(&mut request);
427                            let response =
428                                WorkflowService::record_activity_task_heartbeat_by_id(
429                                    &mut client,
430                                    request,
431                                )
432                                .await
433                                .map_err(AsyncActivityError::from_status)?
434                                .into_inner();
435                            Ok(ActivityHeartbeatResponse::from(response))
436                        }
437                    })
438                }
439            }),
440        )
441        .await
442    }
443}
444
445/// Response from a heartbeat call.
446#[derive(Debug, Clone)]
447#[non_exhaustive]
448pub struct ActivityHeartbeatResponse {
449    /// True if the activity has been asked to cancel itself.
450    pub cancel_requested: bool,
451    /// True if the activity is paused.
452    pub activity_paused: bool,
453    /// True if the activity was reset.
454    pub activity_reset: bool,
455}
456
457impl From<RecordActivityTaskHeartbeatResponse> for ActivityHeartbeatResponse {
458    fn from(resp: RecordActivityTaskHeartbeatResponse) -> Self {
459        Self {
460            cancel_requested: resp.cancel_requested,
461            activity_paused: resp.activity_paused,
462            activity_reset: resp.activity_reset,
463        }
464    }
465}
466
467impl From<RecordActivityTaskHeartbeatByIdResponse> for ActivityHeartbeatResponse {
468    fn from(resp: RecordActivityTaskHeartbeatByIdResponse) -> Self {
469        Self {
470            cancel_requested: resp.cancel_requested,
471            activity_paused: resp.activity_paused,
472            activity_reset: resp.activity_reset,
473        }
474    }
475}