temporalio_client/activity/
activity_handle.rs1use 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
26pub 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 pub fn activity_id(&self) -> &str {
60 &self.activity_id
61 }
62
63 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 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 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 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 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 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}