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 ActivitySerializationContext, DataConverter, DecodablePayloads, GenericPayloadConverter,
80 PayloadConversionError, PayloadConverter, RawValue, SerializationContext,
81 SerializationContextData, 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(
136 &SerializationContextData::Activity(ActivitySerializationContext::new()),
137 &details,
138 )
139 .await?;
140 worker.record_activity_heartbeat(ActivityHeartbeat {
141 task_token: task_token.to_vec(),
142 details,
143 });
144 }
145 #[cfg(feature = "testing")]
146 Self::Test {
147 heartbeat_callback, ..
148 } => {
149 if let Some(callback) = heartbeat_callback {
150 callback(Box::new(details));
151 }
152 }
153 }
154 Ok(())
155 }
156
157 fn client(&self) -> Client {
158 match self {
159 Self::Worker {
160 worker,
161 client_options,
162 } => {
163 let connection = worker.get_client_connection().expect(
164 "activity context client is unavailable because the worker was not created from a Temporal client",
165 );
166 Client::new(connection, client_options.clone())
167 .expect("client construction from a worker connection should be infallible")
168 }
169 #[cfg(feature = "testing")]
170 Self::Test { client, .. } => client
171 .as_ref()
172 .expect("ActivityEnvironment was created without a Client. Pass one during construction to have one availalbe at runtime")
173 .clone(),
174 }
175 }
176}
177
178impl ActivityContext {
179 #[cfg(feature = "testing")]
180 pub(crate) fn new_for_test(
181 info: ActivityInfo,
182 header_fields: HashMap<String, Payload>,
183 payload_converter: PayloadConverter,
184 cancellation_token: CancellationToken,
185 heartbeat_details: Vec<Payload>,
186 client: Option<Client>,
187 heartbeat_callback: Option<ActivityHeartbeatCallback>,
188 ) -> Self {
189 let heartbeat_details = ActivityHeartbeatDetails::new(heartbeat_details, payload_converter);
190 Self {
191 backend: ActivityContextBackend::Test {
192 client,
193 heartbeat_callback,
194 },
195 cancellation_token,
196 heartbeat_details,
197 header_fields,
198 info,
199 }
200 }
201
202 pub(crate) fn new(
203 worker: Arc<CoreWorker>,
204 client_options: ClientOptions,
205 cancellation_token: CancellationToken,
206 task_queue: String,
207 task_token: Vec<u8>,
208 task: activity_task::Start,
209 ) -> (Self, Vec<Payload>) {
210 let activity_task::Start {
211 workflow_namespace,
212 workflow_type,
213 workflow_execution,
214 activity_id,
215 activity_type,
216 header_fields,
217 input,
218 heartbeat_details,
219 scheduled_time,
220 current_attempt_scheduled_time,
221 started_time,
222 attempt,
223 schedule_to_close_timeout,
224 start_to_close_timeout,
225 heartbeat_timeout,
226 retry_policy,
227 is_local,
228 priority,
229 run_id,
230 } = task;
231 let deadline = calculate_deadline(
232 scheduled_time.as_ref(),
233 started_time.as_ref(),
234 start_to_close_timeout.as_ref(),
235 schedule_to_close_timeout.as_ref(),
236 );
237 let heartbeat_details = ActivityHeartbeatDetails::new(
238 heartbeat_details,
239 client_options.data_converter.payload_converter().clone(),
240 );
241 let (workflow_id, workflow_run_id) = workflow_execution
242 .map(|we| (we.workflow_id, we.run_id))
243 .unzip();
244 let activity_run_id = (workflow_id.is_none() && !run_id.is_empty()).then_some(run_id);
245
246 (
247 ActivityContext {
248 backend: ActivityContextBackend::Worker {
249 worker,
250 client_options,
251 },
252 cancellation_token,
253 heartbeat_details,
254 header_fields,
255 info: ActivityInfo {
256 task_token,
257 task_queue,
258 workflow_type: (!workflow_type.is_empty()).then_some(workflow_type),
259 namespace: workflow_namespace,
260 workflow_id,
261 workflow_run_id,
262 activity_id,
263 activity_type,
264 heartbeat_timeout: heartbeat_timeout.try_into_or_none(),
265 scheduled_time: scheduled_time.try_into_or_none(),
266 started_time: started_time.try_into_or_none(),
267 deadline,
268 attempt,
269 current_attempt_scheduled_time: current_attempt_scheduled_time
270 .try_into_or_none(),
271 retry_policy: retry_policy.map(Into::into),
272 is_local,
273 priority: priority.map(Into::into).unwrap_or_default(),
274 activity_run_id,
275 },
276 },
277 input,
278 )
279 }
280
281 pub async fn cancelled(&self) {
284 self.cancellation_token.clone().cancelled().await
285 }
286
287 pub fn is_cancelled(&self) -> bool {
289 self.cancellation_token.is_cancelled()
290 }
291
292 pub fn heartbeat_details(&self) -> &ActivityHeartbeatDetails {
295 &self.heartbeat_details
296 }
297
298 pub async fn record_heartbeat<T>(&self, details: T) -> Result<(), PayloadConversionError>
300 where
301 T: TemporalSerializable + 'static,
302 {
303 if !self.info.is_local {
304 self.backend
305 .record_heartbeat(&self.info.task_token, details)
306 .await?;
307 }
308 Ok(())
309 }
310
311 pub fn info(&self) -> &ActivityInfo {
313 &self.info
314 }
315
316 pub fn client(&self) -> Client {
318 self.backend.client()
319 }
320
321 pub fn workflow_handle<W: HasWorkflowDefinition>(&self) -> Option<WorkflowHandle<Client, W>> {
323 let workflow_id = self.info.workflow_id.clone()?;
324 let run_id = self.info.workflow_run_id.clone();
325 let first_execution_run_id = run_id.clone();
326 let client = self.client();
327
328 Some(WorkflowHandle::new(
329 client.clone(),
330 WorkflowExecutionInfo::builder()
331 .namespace(client.options().namespace.clone())
332 .workflow_id(workflow_id)
333 .maybe_run_id(run_id)
334 .maybe_first_execution_run_id(first_execution_run_id)
335 .build(),
336 ))
337 }
338
339 pub fn headers(&self) -> &HashMap<String, Payload> {
341 &self.header_fields
342 }
343
344 pub(crate) fn headers_mut(&mut self) -> &mut HashMap<String, Payload> {
345 &mut self.header_fields
346 }
347}
348
349#[derive(Clone, Debug)]
351#[non_exhaustive]
352pub struct ActivityHeartbeatDetails {
353 payloads: DecodablePayloads,
354}
355
356impl ActivityHeartbeatDetails {
357 fn new(payloads: Vec<Payload>, payload_converter: PayloadConverter) -> Self {
358 Self {
359 payloads: DecodablePayloads::new(
360 payloads,
361 payload_converter,
362 SerializationContextData::Activity(ActivitySerializationContext::new()),
363 ),
364 }
365 }
366
367 pub fn deserialize<T: TemporalDeserializable + 'static>(
369 &self,
370 ) -> Result<Option<T>, PayloadConversionError> {
371 if self.payloads.raw().is_empty() {
372 Ok(None)
373 } else {
374 self.payloads.deserialize().map(Some)
375 }
376 }
377
378 pub fn raw(&self) -> &[Payload] {
380 self.payloads.raw()
381 }
382
383 pub fn into_raw(self) -> RawValue {
385 self.payloads.into_raw()
386 }
387}
388
389#[derive(Clone, Debug)]
391#[non_exhaustive]
392pub struct ActivityInfo {
393 pub task_token: Vec<u8>,
395 pub workflow_type: Option<String>,
397 pub namespace: String,
399 pub workflow_id: Option<String>,
401 pub workflow_run_id: Option<String>,
403 pub activity_id: String,
405 pub activity_type: String,
407 pub task_queue: String,
409 pub heartbeat_timeout: Option<StdDuration>,
411 pub scheduled_time: Option<SystemTime>,
413 pub started_time: Option<SystemTime>,
415 pub deadline: Option<SystemTime>,
417 pub attempt: u32,
419 pub current_attempt_scheduled_time: Option<SystemTime>,
421 pub retry_policy: Option<RetryPolicy>,
423 pub is_local: bool,
425 pub priority: Priority,
427 pub activity_run_id: Option<String>,
429}
430
431fn calculate_deadline(
434 scheduled_time: Option<&Timestamp>,
435 started_time: Option<&Timestamp>,
436 start_to_close_timeout: Option<&Duration>,
437 schedule_to_close_timeout: Option<&Duration>,
438) -> Option<SystemTime> {
439 match (
440 scheduled_time,
441 started_time,
442 start_to_close_timeout,
443 schedule_to_close_timeout,
444 ) {
445 (
446 Some(scheduled),
447 Some(started),
448 Some(start_to_close_timeout),
449 Some(schedule_to_close_timeout),
450 ) => {
451 let scheduled: SystemTime = maybe_convert_timestamp(scheduled)?;
452 let started: SystemTime = maybe_convert_timestamp(started)?;
453 let start_to_close_timeout: StdDuration = (*start_to_close_timeout).try_into().ok()?;
454 let schedule_to_close_timeout: StdDuration =
455 (*schedule_to_close_timeout).try_into().ok()?;
456
457 let start_to_close_deadline: SystemTime =
458 started.checked_add(start_to_close_timeout)?;
459 if schedule_to_close_timeout > StdDuration::ZERO {
460 let schedule_to_close_deadline =
461 scheduled.checked_add(schedule_to_close_timeout)?;
462 if schedule_to_close_deadline < start_to_close_deadline {
464 Some(schedule_to_close_deadline)
465 } else {
466 Some(start_to_close_deadline)
467 }
468 } else {
469 Some(start_to_close_deadline)
470 }
471 }
472 _ => None,
473 }
474}
475
476fn maybe_convert_timestamp(timestamp: &Timestamp) -> Option<SystemTime> {
479 let mut timestamp = *timestamp;
480 timestamp.normalize();
481
482 let system_time = if timestamp.seconds >= 0 {
483 std::time::UNIX_EPOCH.checked_add(StdDuration::from_secs(timestamp.seconds as u64))
484 } else {
485 std::time::UNIX_EPOCH.checked_sub(StdDuration::from_secs((-timestamp.seconds) as u64))
486 };
487
488 system_time.and_then(|system_time| {
489 system_time.checked_add(StdDuration::from_nanos(timestamp.nanos as u64))
490 })
491}
492
493pub(crate) type ActivityInvocation = Arc<
494 dyn Fn(
495 Vec<Payload>,
496 DataConverter,
497 ActivityContext,
498 Vec<Arc<dyn ActivityInboundInterceptor>>,
499 ) -> ExecuteActivityOutput<'static>
500 + Send
501 + Sync,
502>;
503
504fn call_execute_activity<'a>(
505 interceptors: &'a [Arc<dyn ActivityInboundInterceptor>],
506 input: ExecuteActivityInput,
507 next: Next<'a, ExecuteActivityInput, ExecuteActivityOutput<'a>>,
508) -> ExecuteActivityOutput<'a> {
509 if let Some((first, rest)) = interceptors.split_first() {
510 first.execute_activity(
511 input,
512 Next::new(move |input| call_execute_activity(rest, input, next)),
513 )
514 } else {
515 next.run(input)
516 }
517}
518
519pub trait ActivityImplementer {
524 fn register_all(self: Arc<Self>, defs: &mut ActivityDefinitions);
526}
527
528pub trait ExecutableActivity: ActivityDefinition + Sized {
533 type Implementer: ActivityImplementer + Send + Sync + 'static;
535 const REQUIRES_INSTANCE: bool;
537 fn definition() -> Self;
539 fn execute(
541 receiver: Option<Arc<Self::Implementer>>,
542 ctx: ActivityContext,
543 input: Self::Input,
544 ) -> BoxFuture<'static, Result<Self::Output, ActivityError>>;
545}
546
547#[derive(Default, Clone)]
549pub struct ActivityDefinitions {
550 activities: HashMap<String, ActivityInvocation>,
551}
552
553impl ActivityDefinitions {
554 #[cfg(feature = "experimental")]
555 pub(crate) fn extend(&mut self, other: &Self) {
556 self.activities.extend(other.activities.clone());
557 }
558
559 pub fn register_activities<AI: ActivityImplementer>(&mut self, instance: AI) -> &mut Self {
561 let arcd = Arc::new(instance);
562 AI::register_all(arcd, self);
563 self
564 }
565 pub fn register_activity<AD>(&mut self, instance: Arc<AD::Implementer>) -> &mut Self
567 where
568 AD: ActivityDefinition + ExecutableActivity,
569 AD::Input: Send + Sync,
570 AD::Output: Send + Sync,
571 {
572 self.activities.insert(
573 AD::definition().name().to_string(),
574 Arc::new(move |payloads, dc, c, activity_inbound_interceptors| {
575 let instance = instance.clone();
576 async move {
577 let pc = dc.payload_converter();
580 let context_data =
581 SerializationContextData::Activity(ActivitySerializationContext::new());
582 let ctx = SerializationContext::new(&context_data, pc);
583 let input: AD::Input = pc.from_payloads(&ctx, payloads)?;
584 let input = ExecuteActivityInput::new(c, Box::new(input));
585 let leaf = activity_inbound_base::<AD>(instance);
586 let activity_execution =
587 call_execute_activity(&activity_inbound_interceptors, input, leaf);
588 match AssertUnwindSafe(activity_execution).catch_unwind().await {
589 Ok(output) => output,
590 Err(panic) => Err(ApplicationFailure::new(anyhow::anyhow!(
591 "Activity function panicked: {}",
592 panic_formatter(panic)
593 ))
594 .into()),
595 }
596 }
597 .boxed()
598 }),
599 );
600 self
601 }
602
603 pub(crate) fn is_empty(&self) -> bool {
604 self.activities.is_empty()
605 }
606
607 pub(crate) fn get(&self, act_type: &str) -> Option<ActivityInvocation> {
608 self.activities.get(act_type).cloned()
609 }
610
611 pub(crate) fn names(&self) -> Vec<String> {
612 let mut names: Vec<_> = self.activities.keys().cloned().collect();
613 names.sort_unstable();
614 names
615 }
616}
617
618fn activity_inbound_base<'a, AD>(
619 instance: Arc<AD::Implementer>,
620) -> Next<'a, ExecuteActivityInput, ExecuteActivityOutput<'a>>
621where
622 AD: ActivityDefinition + ExecutableActivity,
623 AD::Input: Send + Sync,
624 AD::Output: Send + Sync,
625{
626 Next::new(
627 move |input: ExecuteActivityInput| -> ExecuteActivityOutput<'a> {
628 let (activity_context, args) = input.into_parts();
629 let args = match args.downcast::<AD::Input>() {
630 Ok(args) => args,
631 Err(_) => {
632 return ready(Err(ApplicationFailure::new(anyhow::anyhow!(
633 "Activity inbound interceptor returned arguments with wrong concrete type for activity {}",
634 AD::definition().name()
635 ))
636 .into()))
637 .boxed();
638 }
639 };
640
641 async move {
642 match AssertUnwindSafe(AD::execute(Some(instance), activity_context, *args))
643 .catch_unwind()
644 .await
645 {
646 Ok(result) => {
647 result.map(|output| Box::new(output) as Box<dyn ActivityExecutionValue>)
648 }
649 Err(panic) => Err(ApplicationFailure::new(anyhow::anyhow!(
650 "Activity function panicked: {}",
651 panic_formatter(panic)
652 ))
653 .into()),
654 }
655 }
656 .boxed()
657 },
658 )
659}
660
661pub(crate) fn activity_error_to_core_result(
662 dc: &DataConverter,
663 err: ActivityError,
664) -> ActivityExecutionResult {
665 match err {
666 ActivityError::Application(app) => ActivityExecutionResult::fail(dc.to_failure(
667 &SerializationContextData::Activity(ActivitySerializationContext::new()),
668 OutgoingError::Activity(OutgoingActivityError::Application(app)),
669 )),
670 ActivityError::Cancelled { details } => ActivityExecutionResult::cancel(dc.to_failure(
671 &SerializationContextData::Activity(ActivitySerializationContext::new()),
672 OutgoingError::Activity(OutgoingActivityError::Cancelled { details }),
673 )),
674 ActivityError::WillCompleteAsync => ActivityExecutionResult::will_complete_async(),
675 other => ActivityExecutionResult::fail(dc.to_failure(
676 &SerializationContextData::Activity(ActivitySerializationContext::new()),
677 OutgoingError::Activity(OutgoingActivityError::Application(Box::new(
678 ApplicationFailure::new(anyhow::anyhow!("Unsupported activity error: {other:?}")),
679 ))),
680 )),
681 }
682}
683
684impl Debug for ActivityDefinitions {
685 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
686 f.debug_struct("ActivityDefinitions")
687 .field("activities", &self.activities.keys())
688 .finish()
689 }
690}
691
692#[cfg(test)]
693mod test {
694 use super::*;
695 use rstest::rstest;
696 use temporalio_common::error::{ApplicationErrorCategory, ApplicationFailure};
697
698 #[test]
699 fn activity_heartbeat_details_support_typed_decoding() {
700 let payload_converter = PayloadConverter::default();
701 let payload = payload_converter
702 .to_payload(
703 &SerializationContext::new(
704 &SerializationContextData::Activity(ActivitySerializationContext::new()),
705 &payload_converter,
706 ),
707 &"progress".to_owned(),
708 )
709 .unwrap();
710 let details = ActivityHeartbeatDetails::new(vec![payload.clone()], payload_converter);
711
712 assert_eq!(details.raw(), &[payload]);
713 assert_eq!(
714 details.deserialize::<String>().unwrap(),
715 Some("progress".to_owned())
716 );
717 }
718
719 #[test]
720 fn empty_activity_heartbeat_details_decode_to_none() {
721 let details = ActivityHeartbeatDetails::new(Vec::new(), PayloadConverter::default());
722
723 assert_eq!(details.deserialize::<String>().unwrap(), None);
724 assert!(details.into_raw().payloads.is_empty());
725 }
726
727 #[rstest]
728 #[case(true)]
729 #[case(false)]
730 fn activity_error_conversion_is_not_lossy(#[case] non_retryable: bool) {
731 let original = ApplicationFailure::builder(anyhow::anyhow!("big boom"))
732 .type_name("BigBoom".to_owned())
733 .non_retryable(non_retryable)
734 .next_retry_delay(StdDuration::from_secs(3))
735 .category(ApplicationErrorCategory::Benign)
736 .details("details")
737 .build();
738 let err = ActivityError::from(original);
739 let ActivityError::Application(actual) = err else {
740 panic!("application failure should become app failure")
741 };
742 assert_eq!(actual.type_name(), Some("BigBoom"));
743 assert_eq!(actual.is_non_retryable(), non_retryable);
744 assert_eq!(actual.next_retry_delay(), Some(StdDuration::from_secs(3)));
745 assert_eq!(actual.category(), ApplicationErrorCategory::Benign);
746 assert_eq!(actual.to_string(), "big boom");
747 }
748
749 #[test]
750 fn activity_error_from_special_err_becomes_application() {
751 #[derive(Debug, PartialEq)]
752 struct MyError;
753
754 impl std::error::Error for MyError {}
755 impl std::fmt::Display for MyError {
756 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
757 f.write_str("MyError")
758 }
759 }
760
761 let err = ActivityError::from(MyError);
762 let ActivityError::Application(actual) = err else {
763 panic!("expected application failure, got {err:?}")
764 };
765 assert_eq!(actual.to_string(), "MyError");
766 }
767}