1#[doc(inline)]
50pub use temporalio_macros::activities;
51
52use crate::{
53 OutgoingActivityError, OutgoingError,
54 interceptors::{
55 ActivityExecutionValue, ActivityInboundInterceptor, ExecuteActivityInput,
56 ExecuteActivityOutput, Next,
57 },
58 panic_formatter,
59};
60use futures_util::{
61 FutureExt,
62 future::{BoxFuture, ready},
63};
64use prost_types::{Duration, Timestamp};
65#[cfg(feature = "testing")]
66use std::any::Any;
67use std::{
68 collections::HashMap,
69 fmt::Debug,
70 panic::AssertUnwindSafe,
71 sync::Arc,
72 time::{Duration as StdDuration, SystemTime},
73};
74use temporalio_client::{Client, ClientOptions, Priority, WorkflowExecutionInfo, WorkflowHandle};
75pub use temporalio_common::ActivityError;
76use temporalio_common::{
77 ActivityDefinition, HasWorkflowDefinition, RetryPolicy,
78 data_converters::{
79 DataConverter, DecodablePayloads, GenericPayloadConverter, PayloadConversionError,
80 PayloadConverter, RawValue, SerializationContext, SerializationContextData,
81 TemporalDeserializable, TemporalSerializable,
82 },
83 error::ApplicationFailure,
84 protos::{
85 coresdk::{ActivityHeartbeat, activity_result::ActivityExecutionResult, activity_task},
86 temporal::api::common::v1::Payload,
87 utilities::TryIntoOrNone,
88 },
89};
90use temporalio_sdk_core::Worker as CoreWorker;
91use tokio_util::sync::CancellationToken;
92
93#[cfg(feature = "testing")]
94pub(crate) type ActivityHeartbeatCallback = Arc<dyn Fn(Box<dyn Any>) + Send + Sync>;
95
96#[derive(Clone)]
98pub struct ActivityContext {
99 backend: ActivityContextBackend,
100 cancellation_token: CancellationToken,
101 heartbeat_details: ActivityHeartbeatDetails,
102 header_fields: HashMap<String, Payload>,
103 info: ActivityInfo,
104}
105
106#[derive(Clone)]
107enum ActivityContextBackend {
108 Worker {
109 worker: Arc<CoreWorker>,
110 client_options: ClientOptions,
111 },
112 #[cfg(feature = "testing")]
113 Test {
114 client: Option<Client>,
115 heartbeat_callback: Option<ActivityHeartbeatCallback>,
116 },
117}
118
119impl ActivityContextBackend {
120 async fn record_heartbeat<T>(
121 &self,
122 task_token: &[u8],
123 details: T,
124 ) -> Result<(), PayloadConversionError>
125 where
126 T: TemporalSerializable + 'static,
127 {
128 match self {
129 Self::Worker {
130 worker,
131 client_options,
132 } => {
133 let details = client_options
134 .data_converter
135 .to_payloads(&SerializationContextData::Activity, &details)
136 .await?;
137 worker.record_activity_heartbeat(ActivityHeartbeat {
138 task_token: task_token.to_vec(),
139 details,
140 });
141 }
142 #[cfg(feature = "testing")]
143 Self::Test {
144 heartbeat_callback, ..
145 } => {
146 if let Some(callback) = heartbeat_callback {
147 callback(Box::new(details));
148 }
149 }
150 }
151 Ok(())
152 }
153
154 fn client(&self) -> Client {
155 match self {
156 Self::Worker {
157 worker,
158 client_options,
159 } => {
160 let connection = worker.get_client_connection().expect(
161 "activity context client is unavailable because the worker was not created from a Temporal client",
162 );
163 Client::new(connection, client_options.clone())
164 .expect("client construction from a worker connection should be infallible")
165 }
166 #[cfg(feature = "testing")]
167 Self::Test { client, .. } => client
168 .as_ref()
169 .expect("ActivityEnvironment was created without a Client. Pass one during construction to have one availalbe at runtime")
170 .clone(),
171 }
172 }
173}
174
175impl ActivityContext {
176 #[cfg(feature = "testing")]
177 pub(crate) fn new_for_test(
178 info: ActivityInfo,
179 header_fields: HashMap<String, Payload>,
180 payload_converter: PayloadConverter,
181 cancellation_token: CancellationToken,
182 heartbeat_details: Vec<Payload>,
183 client: Option<Client>,
184 heartbeat_callback: Option<ActivityHeartbeatCallback>,
185 ) -> Self {
186 let heartbeat_details = ActivityHeartbeatDetails::new(heartbeat_details, payload_converter);
187 Self {
188 backend: ActivityContextBackend::Test {
189 client,
190 heartbeat_callback,
191 },
192 cancellation_token,
193 heartbeat_details,
194 header_fields,
195 info,
196 }
197 }
198
199 pub(crate) fn new(
200 worker: Arc<CoreWorker>,
201 client_options: ClientOptions,
202 cancellation_token: CancellationToken,
203 task_queue: String,
204 task_token: Vec<u8>,
205 task: activity_task::Start,
206 ) -> (Self, Vec<Payload>) {
207 let activity_task::Start {
208 workflow_namespace,
209 workflow_type,
210 workflow_execution,
211 activity_id,
212 activity_type,
213 header_fields,
214 input,
215 heartbeat_details,
216 scheduled_time,
217 current_attempt_scheduled_time,
218 started_time,
219 attempt,
220 schedule_to_close_timeout,
221 start_to_close_timeout,
222 heartbeat_timeout,
223 retry_policy,
224 is_local,
225 priority,
226 run_id,
227 } = task;
228 let deadline = calculate_deadline(
229 scheduled_time.as_ref(),
230 started_time.as_ref(),
231 start_to_close_timeout.as_ref(),
232 schedule_to_close_timeout.as_ref(),
233 );
234 let heartbeat_details = ActivityHeartbeatDetails::new(
235 heartbeat_details,
236 client_options.data_converter.payload_converter().clone(),
237 );
238 let (workflow_id, workflow_run_id) = workflow_execution
239 .map(|we| (we.workflow_id, we.run_id))
240 .unzip();
241 let activity_run_id = (workflow_id.is_none() && !run_id.is_empty()).then_some(run_id);
242
243 (
244 ActivityContext {
245 backend: ActivityContextBackend::Worker {
246 worker,
247 client_options,
248 },
249 cancellation_token,
250 heartbeat_details,
251 header_fields,
252 info: ActivityInfo {
253 task_token,
254 task_queue,
255 workflow_type: (!workflow_type.is_empty()).then_some(workflow_type),
256 namespace: workflow_namespace,
257 workflow_id,
258 workflow_run_id,
259 activity_id,
260 activity_type,
261 heartbeat_timeout: heartbeat_timeout.try_into_or_none(),
262 scheduled_time: scheduled_time.try_into_or_none(),
263 started_time: started_time.try_into_or_none(),
264 deadline,
265 attempt,
266 current_attempt_scheduled_time: current_attempt_scheduled_time
267 .try_into_or_none(),
268 retry_policy: retry_policy.map(Into::into),
269 is_local,
270 priority: priority.map(Into::into).unwrap_or_default(),
271 activity_run_id,
272 },
273 },
274 input,
275 )
276 }
277
278 pub async fn cancelled(&self) {
281 self.cancellation_token.clone().cancelled().await
282 }
283
284 pub fn is_cancelled(&self) -> bool {
286 self.cancellation_token.is_cancelled()
287 }
288
289 pub fn heartbeat_details(&self) -> &ActivityHeartbeatDetails {
292 &self.heartbeat_details
293 }
294
295 pub async fn record_heartbeat<T>(&self, details: T) -> Result<(), PayloadConversionError>
297 where
298 T: TemporalSerializable + 'static,
299 {
300 if !self.info.is_local {
301 self.backend
302 .record_heartbeat(&self.info.task_token, details)
303 .await?;
304 }
305 Ok(())
306 }
307
308 pub fn info(&self) -> &ActivityInfo {
310 &self.info
311 }
312
313 pub fn client(&self) -> Client {
315 self.backend.client()
316 }
317
318 pub fn workflow_handle<W: HasWorkflowDefinition>(&self) -> Option<WorkflowHandle<Client, W>> {
320 let workflow_id = self.info.workflow_id.clone()?;
321 let run_id = self.info.workflow_run_id.clone();
322 let first_execution_run_id = run_id.clone();
323 let client = self.client();
324
325 Some(WorkflowHandle::new(
326 client.clone(),
327 WorkflowExecutionInfo {
328 namespace: client.options().namespace.clone(),
329 workflow_id,
330 run_id,
331 first_execution_run_id,
332 },
333 ))
334 }
335
336 pub fn headers(&self) -> &HashMap<String, Payload> {
338 &self.header_fields
339 }
340
341 pub(crate) fn headers_mut(&mut self) -> &mut HashMap<String, Payload> {
342 &mut self.header_fields
343 }
344}
345
346#[derive(Clone, Debug)]
348#[non_exhaustive]
349pub struct ActivityHeartbeatDetails {
350 payloads: DecodablePayloads,
351}
352
353impl ActivityHeartbeatDetails {
354 fn new(payloads: Vec<Payload>, payload_converter: PayloadConverter) -> Self {
355 Self {
356 payloads: DecodablePayloads::new(
357 payloads,
358 payload_converter,
359 SerializationContextData::Activity,
360 ),
361 }
362 }
363
364 pub fn deserialize<T: TemporalDeserializable + 'static>(
366 &self,
367 ) -> Result<Option<T>, PayloadConversionError> {
368 if self.payloads.raw().is_empty() {
369 Ok(None)
370 } else {
371 self.payloads.deserialize().map(Some)
372 }
373 }
374
375 pub fn raw(&self) -> &[Payload] {
377 self.payloads.raw()
378 }
379
380 pub fn into_raw(self) -> RawValue {
382 self.payloads.into_raw()
383 }
384}
385
386#[derive(Clone, Debug)]
388#[non_exhaustive]
389pub struct ActivityInfo {
390 pub task_token: Vec<u8>,
392 pub workflow_type: Option<String>,
394 pub namespace: String,
396 pub workflow_id: Option<String>,
398 pub workflow_run_id: Option<String>,
400 pub activity_id: String,
402 pub activity_type: String,
404 pub task_queue: String,
406 pub heartbeat_timeout: Option<StdDuration>,
408 pub scheduled_time: Option<SystemTime>,
410 pub started_time: Option<SystemTime>,
412 pub deadline: Option<SystemTime>,
414 pub attempt: u32,
416 pub current_attempt_scheduled_time: Option<SystemTime>,
418 pub retry_policy: Option<RetryPolicy>,
420 pub is_local: bool,
422 pub priority: Priority,
424 pub activity_run_id: Option<String>,
426}
427
428fn calculate_deadline(
431 scheduled_time: Option<&Timestamp>,
432 started_time: Option<&Timestamp>,
433 start_to_close_timeout: Option<&Duration>,
434 schedule_to_close_timeout: Option<&Duration>,
435) -> Option<SystemTime> {
436 match (
437 scheduled_time,
438 started_time,
439 start_to_close_timeout,
440 schedule_to_close_timeout,
441 ) {
442 (
443 Some(scheduled),
444 Some(started),
445 Some(start_to_close_timeout),
446 Some(schedule_to_close_timeout),
447 ) => {
448 let scheduled: SystemTime = maybe_convert_timestamp(scheduled)?;
449 let started: SystemTime = maybe_convert_timestamp(started)?;
450 let start_to_close_timeout: StdDuration = (*start_to_close_timeout).try_into().ok()?;
451 let schedule_to_close_timeout: StdDuration =
452 (*schedule_to_close_timeout).try_into().ok()?;
453
454 let start_to_close_deadline: SystemTime =
455 started.checked_add(start_to_close_timeout)?;
456 if schedule_to_close_timeout > StdDuration::ZERO {
457 let schedule_to_close_deadline =
458 scheduled.checked_add(schedule_to_close_timeout)?;
459 if schedule_to_close_deadline < start_to_close_deadline {
461 Some(schedule_to_close_deadline)
462 } else {
463 Some(start_to_close_deadline)
464 }
465 } else {
466 Some(start_to_close_deadline)
467 }
468 }
469 _ => None,
470 }
471}
472
473fn maybe_convert_timestamp(timestamp: &Timestamp) -> Option<SystemTime> {
476 let mut timestamp = *timestamp;
477 timestamp.normalize();
478
479 let system_time = if timestamp.seconds >= 0 {
480 std::time::UNIX_EPOCH.checked_add(StdDuration::from_secs(timestamp.seconds as u64))
481 } else {
482 std::time::UNIX_EPOCH.checked_sub(StdDuration::from_secs((-timestamp.seconds) as u64))
483 };
484
485 system_time.and_then(|system_time| {
486 system_time.checked_add(StdDuration::from_nanos(timestamp.nanos as u64))
487 })
488}
489
490pub(crate) type ActivityInvocation = Arc<
491 dyn Fn(
492 Vec<Payload>,
493 DataConverter,
494 ActivityContext,
495 Vec<Arc<dyn ActivityInboundInterceptor>>,
496 ) -> ExecuteActivityOutput<'static>
497 + Send
498 + Sync,
499>;
500
501fn call_execute_activity<'a>(
502 interceptors: &'a [Arc<dyn ActivityInboundInterceptor>],
503 input: ExecuteActivityInput,
504 next: Next<'a, ExecuteActivityInput, ExecuteActivityOutput<'a>>,
505) -> ExecuteActivityOutput<'a> {
506 if let Some((first, rest)) = interceptors.split_first() {
507 first.execute_activity(
508 input,
509 Next::new(move |input| call_execute_activity(rest, input, next)),
510 )
511 } else {
512 next.run(input)
513 }
514}
515
516pub trait ActivityImplementer {
521 fn register_all(self: Arc<Self>, defs: &mut ActivityDefinitions);
523}
524
525pub trait ExecutableActivity: ActivityDefinition + Sized {
530 type Implementer: ActivityImplementer + Send + Sync + 'static;
532 const REQUIRES_INSTANCE: bool;
534 fn definition() -> Self;
536 fn execute(
538 receiver: Option<Arc<Self::Implementer>>,
539 ctx: ActivityContext,
540 input: Self::Input,
541 ) -> BoxFuture<'static, Result<Self::Output, ActivityError>>;
542}
543
544#[derive(Default, Clone)]
546pub struct ActivityDefinitions {
547 activities: HashMap<String, ActivityInvocation>,
548}
549
550impl ActivityDefinitions {
551 pub(crate) fn extend(&mut self, other: &Self) {
552 self.activities.extend(other.activities.clone());
553 }
554
555 pub fn register_activities<AI: ActivityImplementer>(&mut self, instance: AI) -> &mut Self {
557 let arcd = Arc::new(instance);
558 AI::register_all(arcd, self);
559 self
560 }
561 pub fn register_activity<AD>(&mut self, instance: Arc<AD::Implementer>) -> &mut Self
563 where
564 AD: ActivityDefinition + ExecutableActivity,
565 AD::Input: Send + Sync,
566 AD::Output: Send + Sync,
567 {
568 self.activities.insert(
569 AD::definition().name().to_string(),
570 Arc::new(move |payloads, dc, c, activity_inbound_interceptors| {
571 let instance = instance.clone();
572 async move {
573 let pc = dc.payload_converter();
576 let ctx = SerializationContext {
577 data: &SerializationContextData::Activity,
578 converter: pc,
579 };
580 let input: AD::Input = pc.from_payloads(&ctx, payloads)?;
581 let input = ExecuteActivityInput::new(c, Box::new(input));
582 let leaf = activity_inbound_base::<AD>(instance);
583 let activity_execution =
584 call_execute_activity(&activity_inbound_interceptors, input, leaf);
585 match AssertUnwindSafe(activity_execution).catch_unwind().await {
586 Ok(output) => output,
587 Err(panic) => Err(ApplicationFailure::new(anyhow::anyhow!(
588 "Activity function panicked: {}",
589 panic_formatter(panic)
590 ))
591 .into()),
592 }
593 }
594 .boxed()
595 }),
596 );
597 self
598 }
599
600 pub(crate) fn is_empty(&self) -> bool {
601 self.activities.is_empty()
602 }
603
604 pub(crate) fn get(&self, act_type: &str) -> Option<ActivityInvocation> {
605 self.activities.get(act_type).cloned()
606 }
607
608 pub(crate) fn names(&self) -> Vec<String> {
609 let mut names: Vec<_> = self.activities.keys().cloned().collect();
610 names.sort_unstable();
611 names
612 }
613}
614
615fn activity_inbound_base<'a, AD>(
616 instance: Arc<AD::Implementer>,
617) -> Next<'a, ExecuteActivityInput, ExecuteActivityOutput<'a>>
618where
619 AD: ActivityDefinition + ExecutableActivity,
620 AD::Input: Send + Sync,
621 AD::Output: Send + Sync,
622{
623 Next::new(
624 move |input: ExecuteActivityInput| -> ExecuteActivityOutput<'a> {
625 let (activity_context, args) = input.into_parts();
626 let args = match args.downcast::<AD::Input>() {
627 Ok(args) => args,
628 Err(_) => {
629 return ready(Err(ApplicationFailure::new(anyhow::anyhow!(
630 "Activity inbound interceptor returned arguments with wrong concrete type for activity {}",
631 AD::definition().name()
632 ))
633 .into()))
634 .boxed();
635 }
636 };
637
638 async move {
639 match AssertUnwindSafe(AD::execute(Some(instance), activity_context, *args))
640 .catch_unwind()
641 .await
642 {
643 Ok(result) => {
644 result.map(|output| Box::new(output) as Box<dyn ActivityExecutionValue>)
645 }
646 Err(panic) => Err(ApplicationFailure::new(anyhow::anyhow!(
647 "Activity function panicked: {}",
648 panic_formatter(panic)
649 ))
650 .into()),
651 }
652 }
653 .boxed()
654 },
655 )
656}
657
658pub(crate) fn activity_error_to_core_result(
659 dc: &DataConverter,
660 err: ActivityError,
661) -> ActivityExecutionResult {
662 match err {
663 ActivityError::Application(app) => ActivityExecutionResult::fail(dc.to_failure(
664 &SerializationContextData::Activity,
665 OutgoingError::Activity(OutgoingActivityError::Application(app)),
666 )),
667 ActivityError::Cancelled { details } => ActivityExecutionResult::cancel(dc.to_failure(
668 &SerializationContextData::Activity,
669 OutgoingError::Activity(OutgoingActivityError::Cancelled { details }),
670 )),
671 ActivityError::WillCompleteAsync => ActivityExecutionResult::will_complete_async(),
672 }
673}
674
675impl Debug for ActivityDefinitions {
676 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
677 f.debug_struct("ActivityDefinitions")
678 .field("activities", &self.activities.keys())
679 .finish()
680 }
681}
682
683#[cfg(test)]
684mod test {
685 use super::*;
686 use rstest::rstest;
687 use temporalio_common::error::{ApplicationErrorCategory, ApplicationFailure};
688
689 #[test]
690 fn activity_heartbeat_details_support_typed_decoding() {
691 let payload_converter = PayloadConverter::default();
692 let payload = payload_converter
693 .to_payload(
694 &SerializationContext {
695 data: &SerializationContextData::Activity,
696 converter: &payload_converter,
697 },
698 &"progress".to_owned(),
699 )
700 .unwrap();
701 let details = ActivityHeartbeatDetails::new(vec![payload.clone()], payload_converter);
702
703 assert_eq!(details.raw(), &[payload]);
704 assert_eq!(
705 details.deserialize::<String>().unwrap(),
706 Some("progress".to_owned())
707 );
708 }
709
710 #[test]
711 fn empty_activity_heartbeat_details_decode_to_none() {
712 let details = ActivityHeartbeatDetails::new(Vec::new(), PayloadConverter::default());
713
714 assert_eq!(details.deserialize::<String>().unwrap(), None);
715 assert!(details.into_raw().payloads.is_empty());
716 }
717
718 #[rstest]
719 #[case(true)]
720 #[case(false)]
721 fn activity_error_conversion_is_not_lossy(#[case] non_retryable: bool) {
722 let original = ApplicationFailure::builder(anyhow::anyhow!("big boom"))
723 .type_name("BigBoom".to_owned())
724 .non_retryable(non_retryable)
725 .next_retry_delay(StdDuration::from_secs(3))
726 .category(ApplicationErrorCategory::Benign)
727 .details("details")
728 .build();
729 let err = ActivityError::from(original);
730 let ActivityError::Application(actual) = err else {
731 panic!("application failure should become app failure")
732 };
733 assert_eq!(actual.type_name(), Some("BigBoom"));
734 assert_eq!(actual.is_non_retryable(), non_retryable);
735 assert_eq!(actual.next_retry_delay(), Some(StdDuration::from_secs(3)));
736 assert_eq!(actual.category(), ApplicationErrorCategory::Benign);
737 assert_eq!(actual.to_string(), "big boom");
738 }
739
740 #[test]
741 fn activity_error_from_special_err_becomes_application() {
742 #[derive(Debug, PartialEq)]
743 struct MyError;
744
745 impl std::error::Error for MyError {}
746 impl std::fmt::Display for MyError {
747 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
748 f.write_str("MyError")
749 }
750 }
751
752 let err = ActivityError::from(MyError);
753 let ActivityError::Application(actual) = err else {
754 panic!("expected application failure, got {err:?}")
755 };
756 assert_eq!(actual.to_string(), "MyError");
757 }
758}