@temporalio/core-bridge 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -73,6 +73,8 @@ pub enum ResponseType {
73
73
  OneTask(usize),
74
74
  /// Waits until the future resolves before responding as `ToTaskNum` with the provided number
75
75
  UntilResolved(BoxFuture<'static, ()>, usize),
76
+ /// Waits until the future resolves before responding with the provided response
77
+ UntilResolvedRaw(BoxFuture<'static, ()>, PollWorkflowTaskQueueResponse),
76
78
  AllHistory,
77
79
  Raw(PollWorkflowTaskQueueResponse),
78
80
  }
@@ -81,6 +83,7 @@ pub enum HashableResponseType {
81
83
  ToTaskNum(usize),
82
84
  OneTask(usize),
83
85
  UntilResolved(usize),
86
+ UntilResolvedRaw(TaskToken),
84
87
  AllHistory,
85
88
  Raw(TaskToken),
86
89
  }
@@ -92,6 +95,9 @@ impl ResponseType {
92
95
  ResponseType::AllHistory => HashableResponseType::AllHistory,
93
96
  ResponseType::Raw(r) => HashableResponseType::Raw(r.task_token.clone().into()),
94
97
  ResponseType::UntilResolved(_, x) => HashableResponseType::UntilResolved(*x),
98
+ ResponseType::UntilResolvedRaw(_, r) => {
99
+ HashableResponseType::UntilResolvedRaw(r.task_token.clone().into())
100
+ }
95
101
  }
96
102
  }
97
103
  }
@@ -618,7 +624,11 @@ impl<T> From<T> for QueueResponse<T> {
618
624
  }
619
625
  impl From<QueueResponse<PollWorkflowTaskQueueResponse>> for ResponseType {
620
626
  fn from(qr: QueueResponse<PollWorkflowTaskQueueResponse>) -> Self {
621
- ResponseType::Raw(qr.resp)
627
+ if let Some(du) = qr.delay_until {
628
+ ResponseType::UntilResolvedRaw(du, qr.resp)
629
+ } else {
630
+ ResponseType::Raw(qr.resp)
631
+ }
622
632
  }
623
633
  }
624
634
  impl<T> Deref for QueueResponse<T> {
@@ -636,13 +646,13 @@ impl<T> DerefMut for QueueResponse<T> {
636
646
 
637
647
  pub fn hist_to_poll_resp(
638
648
  t: &TestHistoryBuilder,
639
- wf_id: String,
649
+ wf_id: impl Into<String>,
640
650
  response_type: ResponseType,
641
651
  task_queue: impl Into<String>,
642
652
  ) -> QueueResponse<PollWorkflowTaskQueueResponse> {
643
653
  let run_id = t.get_orig_run_id();
644
654
  let wf = WorkflowExecution {
645
- workflow_id: wf_id,
655
+ workflow_id: wf_id.into(),
646
656
  run_id: run_id.to_string(),
647
657
  };
648
658
  let mut delay_until = None;
@@ -660,6 +670,12 @@ pub fn hist_to_poll_resp(
660
670
  delay_until = Some(fut);
661
671
  t.get_history_info(tn).unwrap()
662
672
  }
673
+ ResponseType::UntilResolvedRaw(fut, r) => {
674
+ return QueueResponse {
675
+ resp: r,
676
+ delay_until: Some(fut),
677
+ }
678
+ }
663
679
  };
664
680
  let mut resp = hist_info.as_poll_wft_response(task_queue);
665
681
  resp.workflow_execution = Some(wf);
@@ -131,7 +131,7 @@ impl ActivityHeartbeatManager {
131
131
  /// Initiates shutdown procedure by stopping lifecycle loop and awaiting for all in-flight
132
132
  /// heartbeat requests to be flushed to the server.
133
133
  pub(super) async fn shutdown(&self) {
134
- let _ = self.shutdown_token.cancel();
134
+ self.shutdown_token.cancel();
135
135
  let mut handle = self.join_handle.lock().await;
136
136
  if let Some(h) = handle.take() {
137
137
  let handle_r = h.await;
@@ -282,7 +282,7 @@ impl HeartbeatStreamState {
282
282
  ) -> Option<HeartbeatExecutorAction> {
283
283
  if let Some(state) = self.tt_to_state.remove(&tt) {
284
284
  if let Some(cancel_tok) = state.throttled_cancellation_token {
285
- let _ = cancel_tok.cancel();
285
+ cancel_tok.cancel();
286
286
  }
287
287
  if let Some(last_deets) = state.last_recorded_details {
288
288
  self.tt_needs_flush.insert(tt.clone(), on_complete);
@@ -6,7 +6,7 @@ use std::{
6
6
  borrow::Borrow,
7
7
  ops::{Deref, DerefMut},
8
8
  };
9
- use temporal_client::{WorkflowClientTrait, WorkflowTaskCompletion};
9
+ use temporal_client::{WorkflowClientTrait, WorkflowTaskCompletion, RETRYABLE_ERROR_CODES};
10
10
  use temporal_sdk_core_protos::{
11
11
  coresdk::workflow_commands::QueryResult,
12
12
  temporal::api::{
@@ -15,9 +15,17 @@ use temporal_sdk_core_protos::{
15
15
  },
16
16
  TaskToken,
17
17
  };
18
+ use tonic::Code;
18
19
 
19
20
  type Result<T, E = tonic::Status> = std::result::Result<T, E>;
20
21
 
22
+ /// Returns true if the network error should not be reported to lang. This can happen if we've
23
+ /// exceeded the max number of retries, and we prefer to just warn rather than blowing up lang.
24
+ pub(crate) fn should_swallow_net_error(err: &tonic::Status) -> bool {
25
+ RETRYABLE_ERROR_CODES.contains(&err.code())
26
+ || matches!(err.code(), Code::Cancelled | Code::DeadlineExceeded)
27
+ }
28
+
21
29
  /// Contains everything a worker needs to interact with the server
22
30
  pub(crate) struct WorkerClientBag {
23
31
  client: Box<dyn WorkerClient>,
@@ -18,14 +18,14 @@ use crate::{
18
18
  new_activity_task_buffer, new_workflow_task_buffer, BoxedActPoller, Poller,
19
19
  WorkflowTaskPoller,
20
20
  },
21
- protosext::ValidPollWFTQResponse,
21
+ protosext::{validate_activity_completion, ValidPollWFTQResponse},
22
22
  telemetry::metrics::{
23
23
  activity_poller, local_activity_worker_type, workflow_poller, workflow_sticky_poller,
24
24
  MetricsContext,
25
25
  },
26
26
  worker::{
27
27
  activities::{DispatchOrTimeoutLA, LACompleteAction, LocalActivityManager},
28
- client::WorkerClientBag,
28
+ client::{should_swallow_net_error, WorkerClientBag},
29
29
  workflow::{LocalResolution, Workflows},
30
30
  },
31
31
  ActivityHeartbeat, CompleteActivityError, PollActivityError, PollWfError, WorkerTrait,
@@ -398,6 +398,7 @@ impl Worker {
398
398
  task_token: TaskToken,
399
399
  status: activity_execution_result::Status,
400
400
  ) -> Result<(), CompleteActivityError> {
401
+ validate_activity_completion(&status)?;
401
402
  if task_token.is_local_activity_task() {
402
403
  let as_la_res: LocalActivityExecutionResult = status.try_into()?;
403
404
  match self.local_act_mgr.complete(&task_token, &as_la_res) {
@@ -408,7 +409,7 @@ impl Worker {
408
409
  // no other situations where core generates "internal" commands so it is much
409
410
  // simpler for lang to reply with the timer / next LA command than to do it
410
411
  // internally. Plus, this backoff hack we'd like to eliminate eventually.
411
- self.complete_local_act(as_la_res, info, Some(backoff))
412
+ self.complete_local_act(as_la_res, info, Some(backoff));
412
413
  }
413
414
  LACompleteAction::WillBeRetried => {
414
415
  // Nothing to do here
@@ -421,7 +422,13 @@ impl Worker {
421
422
  }
422
423
 
423
424
  if let Some(atm) = &self.at_task_mgr {
424
- atm.complete(task_token, status, &**self.wf_client).await
425
+ match atm.complete(task_token, status, &**self.wf_client).await {
426
+ Err(CompleteActivityError::TonicError(e)) if should_swallow_net_error(&e) => {
427
+ warn!(error=?e, "Network error while completing activity");
428
+ Ok(())
429
+ }
430
+ o => o,
431
+ }
425
432
  } else {
426
433
  error!(
427
434
  "Tried to complete activity {} on a worker that does not have an activity manager",
@@ -1,5 +1,4 @@
1
1
  use crate::worker::workflow::{WFCommand, WorkflowStartedInfo};
2
- use std::collections::VecDeque;
3
2
  use temporal_sdk_core_protos::{
4
3
  coresdk::workflow_activation::{
5
4
  start_workflow_from_attribs, workflow_activation_job, CancelWorkflow, SignalWorkflow,
@@ -15,7 +14,7 @@ pub struct DrivenWorkflow {
15
14
  started_attrs: Option<WorkflowStartedInfo>,
16
15
  fetcher: Box<dyn WorkflowFetcher>,
17
16
  /// Outgoing activation jobs that need to be sent to the lang sdk
18
- outgoing_wf_activation_jobs: VecDeque<workflow_activation_job::Variant>,
17
+ outgoing_wf_activation_jobs: Vec<workflow_activation_job::Variant>,
19
18
  }
20
19
 
21
20
  impl<WF> From<Box<WF>> for DrivenWorkflow
@@ -58,12 +57,12 @@ impl DrivenWorkflow {
58
57
 
59
58
  /// Enqueue a new job to be sent to the driven workflow
60
59
  pub fn send_job(&mut self, job: workflow_activation_job::Variant) {
61
- self.outgoing_wf_activation_jobs.push_back(job);
60
+ self.outgoing_wf_activation_jobs.push(job);
62
61
  }
63
62
 
64
- /// Check if there are pending jobs
65
- pub fn has_pending_jobs(&self) -> bool {
66
- !self.outgoing_wf_activation_jobs.is_empty()
63
+ /// Observe pending jobs
64
+ pub fn peek_pending_jobs(&self) -> &[workflow_activation_job::Variant] {
65
+ self.outgoing_wf_activation_jobs.as_slice()
67
66
  }
68
67
 
69
68
  /// Drain all pending jobs, so that they may be sent to the driven workflow
@@ -584,7 +584,7 @@ impl WorkflowMachines {
584
584
  }
585
585
 
586
586
  pub(crate) fn has_pending_jobs(&self) -> bool {
587
- self.drive_me.has_pending_jobs()
587
+ !self.drive_me.peek_pending_jobs().is_empty()
588
588
  }
589
589
 
590
590
  fn set_current_time(&mut self, time: SystemTime) -> SystemTime {
@@ -111,6 +111,7 @@ impl ManagedRun {
111
111
  .fold((self, heartbeat_tx), |(mut me, heartbeat_tx), action| {
112
112
  let span = action.trace_span;
113
113
  let action = action.action;
114
+ let mut no_wft = false;
114
115
  async move {
115
116
  let res = match action {
116
117
  RunActions::NewIncomingWFT(wft) => me
@@ -124,10 +125,15 @@ impl ManagedRun {
124
125
  RunActions::CheckMoreWork {
125
126
  want_to_evict,
126
127
  has_pending_queries,
127
- } => me
128
- .check_more_work(want_to_evict, has_pending_queries)
129
- .await
130
- .map(RunActionOutcome::AfterCheckWork),
128
+ has_wft,
129
+ } => {
130
+ if !has_wft {
131
+ no_wft = true;
132
+ }
133
+ me.check_more_work(want_to_evict, has_pending_queries, has_wft)
134
+ .await
135
+ .map(RunActionOutcome::AfterCheckWork)
136
+ }
131
137
  RunActions::LocalResolution(r) => me
132
138
  .local_resolution(r)
133
139
  .await
@@ -145,7 +151,7 @@ impl ManagedRun {
145
151
  };
146
152
  match res {
147
153
  Ok(outcome) => {
148
- me.send_update_response(outcome);
154
+ me.send_update_response(outcome, no_wft);
149
155
  }
150
156
  Err(e) => {
151
157
  error!(error=?e, "Error in run machines");
@@ -308,7 +314,12 @@ impl ManagedRun {
308
314
  &mut self,
309
315
  want_to_evict: Option<RequestEvictMsg>,
310
316
  has_pending_queries: bool,
317
+ has_wft: bool,
311
318
  ) -> Result<Option<ActivationOrAuto>, RunUpdateErr> {
319
+ if !has_wft {
320
+ // It doesn't make sense to do work unless we have a WFT
321
+ return Ok(None);
322
+ }
312
323
  if self.wfm.machines.has_pending_jobs() && !self.am_broken {
313
324
  Ok(Some(ActivationOrAuto::LangActivation(
314
325
  self.wfm.get_next_activation().await?,
@@ -427,7 +438,7 @@ impl ManagedRun {
427
438
  false
428
439
  }
429
440
 
430
- fn send_update_response(&self, outcome: RunActionOutcome) {
441
+ fn send_update_response(&self, outcome: RunActionOutcome, no_wft: bool) {
431
442
  let mut in_response_to_wft = false;
432
443
  let (outgoing_activation, fulfillable_complete) = match outcome {
433
444
  RunActionOutcome::AfterNewWFT(a) => {
@@ -439,7 +450,15 @@ impl ManagedRun {
439
450
  RunActionOutcome::AfterCompletion(f) => (None, f),
440
451
  RunActionOutcome::AfterHeartbeatTimeout(a) => (a, None),
441
452
  };
442
-
453
+ let mut more_pending_work = self.wfm.machines.has_pending_jobs();
454
+ // We don't want to consider there to be more local-only work to be done if there is no
455
+ // workflow task associated with the run right now. This can happen if, ex, we complete
456
+ // a local activity while waiting for server to send us the next WFT. Activating lang would
457
+ // be harmful at this stage, as there might be work returned in that next WFT which should
458
+ // be part of the next activation.
459
+ if no_wft {
460
+ more_pending_work = false;
461
+ }
443
462
  self.update_tx
444
463
  .send(RunUpdateResponse {
445
464
  kind: RunUpdateResponseKind::Good(GoodRunUpdate {
@@ -447,7 +466,7 @@ impl ManagedRun {
447
466
  outgoing_activation,
448
467
  fulfillable_complete,
449
468
  have_seen_terminal_event: self.wfm.machines.have_seen_terminal_event,
450
- more_pending_work: self.wfm.machines.has_pending_jobs(),
469
+ more_pending_work,
451
470
  most_recently_processed_event_number: self.wfm.machines.last_processed_event
452
471
  as usize,
453
472
  in_response_to_wft,
@@ -24,6 +24,7 @@ use crate::{
24
24
  telemetry::VecDisplayer,
25
25
  worker::{
26
26
  activities::{ActivitiesFromWFTsHandle, PermittedTqResp},
27
+ client::should_swallow_net_error,
27
28
  workflow::{
28
29
  managed_run::{ManagedRun, WorkflowManager},
29
30
  wft_poller::validate_wft,
@@ -234,7 +235,7 @@ impl Workflows {
234
235
  let reserved_act_permits =
235
236
  self.reserve_activity_slots_for_outgoing_commands(commands.as_mut_slice());
236
237
  debug!(commands=%commands.display(), query_responses=%query_responses.display(),
237
- "Sending responses to server");
238
+ force_new_wft, "Sending responses to server");
238
239
  let mut completion = WorkflowTaskCompletion {
239
240
  task_token,
240
241
  commands,
@@ -362,20 +363,26 @@ impl Workflows {
362
363
  let mut should_evict = None;
363
364
  let res = match completer().await {
364
365
  Err(err) => {
365
- match err.code() {
366
- // Silence unhandled command errors since the lang SDK cannot do anything about
367
- // them besides poll again, which it will do anyway.
368
- tonic::Code::InvalidArgument if err.message() == "UnhandledCommand" => {
369
- debug!(error = %err, run_id, "Unhandled command response when completing");
370
- should_evict = Some(EvictionReason::UnhandledCommand);
371
- Ok(())
372
- }
373
- tonic::Code::NotFound => {
374
- warn!(error = %err, run_id, "Task not found when completing");
375
- should_evict = Some(EvictionReason::TaskNotFound);
376
- Ok(())
366
+ if should_swallow_net_error(&err) {
367
+ warn!(error= %err, "Network error while completing workflow activation");
368
+ should_evict = Some(EvictionReason::Fatal);
369
+ Ok(())
370
+ } else {
371
+ match err.code() {
372
+ // Silence unhandled command errors since the lang SDK cannot do anything
373
+ // about them besides poll again, which it will do anyway.
374
+ tonic::Code::InvalidArgument if err.message() == "UnhandledCommand" => {
375
+ debug!(error = %err, run_id, "Unhandled command response when completing");
376
+ should_evict = Some(EvictionReason::UnhandledCommand);
377
+ Ok(())
378
+ }
379
+ tonic::Code::NotFound => {
380
+ warn!(error = %err, run_id, "Task not found when completing");
381
+ should_evict = Some(EvictionReason::TaskNotFound);
382
+ Ok(())
383
+ }
384
+ _ => Err(err),
377
385
  }
378
- _ => Err(err),
379
386
  }
380
387
  }
381
388
  _ => Ok(()),
@@ -486,8 +493,12 @@ impl Workflows {
486
493
  ) -> Result<(), tonic::Status> {
487
494
  match self.client.respond_legacy_query(tt, res).await {
488
495
  Ok(_) => Ok(()),
496
+ Err(e) if should_swallow_net_error(&e) => {
497
+ warn!(error= %e, "Network error while responding to legacy query");
498
+ Ok(())
499
+ }
489
500
  Err(e) if e.code() == tonic::Code::NotFound => {
490
- warn!(error=?e,"Query not found when attempting to respond to it");
501
+ warn!(error=?e, "Query not found when attempting to respond to it");
491
502
  Ok(())
492
503
  }
493
504
  Err(e) => Err(e),
@@ -497,7 +508,20 @@ impl Workflows {
497
508
 
498
509
  /// Manages access to a specific workflow run, and contains various bookkeeping information that the
499
510
  /// [WFStream] may need to access quickly.
500
- #[derive(Debug)]
511
+ #[derive(derive_more::DebugCustom)]
512
+ #[debug(
513
+ fmt = "ManagedRunHandle {{ wft: {:?}, activation: {:?}, buffered_resp: {:?} \
514
+ have_seen_terminal_event: {}, most_recently_processed_event: {}, more_pending_work: {}, \
515
+ trying_to_evict: {}, last_action_acked: {} }}",
516
+ wft,
517
+ activation,
518
+ buffered_resp,
519
+ have_seen_terminal_event,
520
+ most_recently_processed_event_number,
521
+ more_pending_work,
522
+ "trying_to_evict.is_some()",
523
+ last_action_acked
524
+ )]
501
525
  struct ManagedRunHandle {
502
526
  /// If set, the WFT this run is currently/will be processing.
503
527
  wft: Option<OutstandingTask>,
@@ -567,6 +591,7 @@ impl ManagedRunHandle {
567
591
  .as_ref()
568
592
  .map(|wft| !wft.pending_queries.is_empty())
569
593
  .unwrap_or_default(),
594
+ has_wft: self.wft.is_some(),
570
595
  });
571
596
  }
572
597
  }
@@ -677,7 +702,8 @@ impl ActivationOrAuto {
677
702
  }
678
703
  }
679
704
 
680
- #[derive(Debug)]
705
+ #[derive(derive_more::DebugCustom)]
706
+ #[debug(fmt = "PermittedWft {{ {:?} }}", wft)]
681
707
  pub(crate) struct PermittedWFT {
682
708
  wft: ValidPollWFTQResponse,
683
709
  permit: OwnedMeteredSemPermit,
@@ -924,6 +950,7 @@ enum RunActions {
924
950
  CheckMoreWork {
925
951
  want_to_evict: Option<RequestEvictMsg>,
926
952
  has_pending_queries: bool,
953
+ has_wft: bool,
927
954
  },
928
955
  LocalResolution(LocalResolution),
929
956
  HeartbeatTimeout,
@@ -45,7 +45,7 @@ pub(crate) struct WFStream {
45
45
  metrics: MetricsContext,
46
46
  }
47
47
  /// All possible inputs to the [WFStream]
48
- #[derive(derive_more::From)]
48
+ #[derive(derive_more::From, Debug)]
49
49
  enum WFStreamInput {
50
50
  NewWft(PermittedWFT),
51
51
  Local(LocalInput),
@@ -64,6 +64,8 @@ impl From<RunUpdateResponse> for WFStreamInput {
64
64
  }
65
65
  }
66
66
  /// A non-poller-received input to the [WFStream]
67
+ #[derive(derive_more::DebugCustom)]
68
+ #[debug(fmt = "LocalInput {{ {:?} }}", input)]
67
69
  pub(super) struct LocalInput {
68
70
  pub input: LocalInputs,
69
71
  pub span: Span,
@@ -167,7 +169,7 @@ impl WFStream {
167
169
  };
168
170
  all_inputs
169
171
  .map(move |action| {
170
- let span = span!(Level::DEBUG, "new_stream_input");
172
+ let span = span!(Level::DEBUG, "new_stream_input", action=?action);
171
173
  let _span_g = span.enter();
172
174
 
173
175
  let maybe_activation = match action {
@@ -209,7 +211,7 @@ impl WFStream {
209
211
  }
210
212
  }
211
213
  WFStreamInput::PollerDead => {
212
- warn!("WFT poller died, shutting down");
214
+ debug!("WFT poller died, shutting down");
213
215
  state.shutdown_token.cancel();
214
216
  None
215
217
  }
@@ -259,17 +261,15 @@ impl WFStream {
259
261
  debug!(resp=%resp, "Processing run update response from machines");
260
262
  match resp {
261
263
  RunUpdateResponseKind::Good(mut resp) => {
262
- if let Some(r) = self.runs.get_mut(&resp.run_id) {
263
- r.have_seen_terminal_event = resp.have_seen_terminal_event;
264
- r.more_pending_work = resp.more_pending_work;
265
- r.last_action_acked = true;
266
- r.most_recently_processed_event_number =
267
- resp.most_recently_processed_event_number;
268
- }
269
264
  let run_handle = self
270
265
  .runs
271
266
  .get_mut(&resp.run_id)
272
267
  .expect("Workflow must exist, it just sent us an update response");
268
+ run_handle.have_seen_terminal_event = resp.have_seen_terminal_event;
269
+ run_handle.more_pending_work = resp.more_pending_work;
270
+ run_handle.last_action_acked = true;
271
+ run_handle.most_recently_processed_event_number =
272
+ resp.most_recently_processed_event_number;
273
273
 
274
274
  let r = match resp.outgoing_activation {
275
275
  Some(ActivationOrAuto::LangActivation(mut activation)) => {
@@ -304,18 +304,20 @@ impl WFStream {
304
304
  None => {
305
305
  // If the response indicates there is no activation to send yet but there
306
306
  // is more pending work, we should check again.
307
- if resp.more_pending_work {
307
+ if run_handle.more_pending_work {
308
308
  run_handle.check_more_activations();
309
309
  None
310
310
  } else if let Some(reason) = run_handle.trying_to_evict.as_ref() {
311
311
  // If a run update came back and had nothing to do, but we're trying to
312
312
  // evict, just do that now as long as there's no other outstanding work.
313
313
  if run_handle.activation.is_none() && !run_handle.more_pending_work {
314
- let evict_act = create_evict_activation(
314
+ let mut evict_act = create_evict_activation(
315
315
  resp.run_id,
316
316
  reason.message.clone(),
317
317
  reason.reason,
318
318
  );
319
+ evict_act.history_length =
320
+ run_handle.most_recently_processed_event_number as u32;
319
321
  Some(ActivationOrAuto::LangActivation(evict_act))
320
322
  } else {
321
323
  None
@@ -205,10 +205,10 @@ impl Worker {
205
205
 
206
206
  /// Register an Activity function to invoke when the Worker is asked to run an activity of
207
207
  /// `activity_type`
208
- pub fn register_activity<A, R>(
208
+ pub fn register_activity<A, R, O>(
209
209
  &mut self,
210
210
  activity_type: impl Into<String>,
211
- act_function: impl IntoActivityFunc<A, R>,
211
+ act_function: impl IntoActivityFunc<A, R, O>,
212
212
  ) {
213
213
  self.activity_half.activity_fns.insert(
214
214
  activity_type.into(),
@@ -485,7 +485,10 @@ impl ActivityHalf {
485
485
  tokio::spawn(async move {
486
486
  let output = (act_fn.act_func)(ctx, arg).await;
487
487
  let result = match output {
488
- Ok(res) => ActivityExecutionResult::ok(res),
488
+ Ok(ActExitValue::Normal(p)) => ActivityExecutionResult::ok(p),
489
+ Ok(ActExitValue::WillCompleteAsync) => {
490
+ ActivityExecutionResult::will_complete_async()
491
+ }
489
492
  Err(err) => match err.downcast::<ActivityCancelledError>() {
490
493
  Ok(ce) => ActivityExecutionResult::cancel_from_details(ce.details),
491
494
  Err(other_err) => ActivityExecutionResult::fail(other_err.into()),
@@ -497,7 +500,7 @@ impl ActivityHalf {
497
500
  result: Some(result),
498
501
  })
499
502
  .await?;
500
- Result::<_, anyhow::Error>::Ok(())
503
+ Ok::<_, anyhow::Error>(())
501
504
  });
502
505
  }
503
506
  Some(activity_task::Variant::Cancel(_)) => {
@@ -716,9 +719,22 @@ impl<T: Debug> WfExitValue<T> {
716
719
  }
717
720
  }
718
721
 
722
+ /// Activity functions may return these values when exiting
723
+ #[derive(Debug, derive_more::From)]
724
+ pub enum ActExitValue<T: Debug> {
725
+ /// Completion requires an asynchronous callback
726
+ #[from(ignore)]
727
+ WillCompleteAsync,
728
+ /// Finish with a result
729
+ Normal(T),
730
+ }
731
+
719
732
  type BoxActFn = Arc<
720
- dyn Fn(ActContext, Payload) -> BoxFuture<'static, Result<Payload, anyhow::Error>> + Send + Sync,
733
+ dyn Fn(ActContext, Payload) -> BoxFuture<'static, Result<ActExitValue<Payload>, anyhow::Error>>
734
+ + Send
735
+ + Sync,
721
736
  >;
737
+
722
738
  /// Container for user-defined activity functions
723
739
  #[derive(Clone)]
724
740
  pub struct ActivityFunction {
@@ -738,24 +754,35 @@ impl Display for ActivityCancelledError {
738
754
  }
739
755
 
740
756
  /// Closures / functions which can be turned into activity functions implement this trait
741
- pub trait IntoActivityFunc<Args, Res> {
757
+ pub trait IntoActivityFunc<Args, Res, Out> {
742
758
  /// Consume the closure or fn pointer and turned it into a boxed activity function
743
759
  fn into_activity_fn(self) -> BoxActFn;
744
760
  }
745
761
 
746
- impl<A, Rf, R, F> IntoActivityFunc<A, Rf> for F
762
+ impl<A, Rf, R, O, F> IntoActivityFunc<A, Rf, O> for F
747
763
  where
748
764
  F: (Fn(ActContext, A) -> Rf) + Sync + Send + 'static,
749
765
  A: FromJsonPayloadExt + Send,
750
766
  Rf: Future<Output = Result<R, anyhow::Error>> + Send + 'static,
751
- R: AsJsonPayloadExt,
767
+ R: Into<ActExitValue<O>>,
768
+ O: AsJsonPayloadExt + Debug,
752
769
  {
753
770
  fn into_activity_fn(self) -> BoxActFn {
754
771
  let wrapper = move |ctx: ActContext, input: Payload| {
755
772
  // Some minor gymnastics are required to avoid needing to clone the function
756
773
  match A::from_json_payload(&input) {
757
774
  Ok(deser) => (self)(ctx, deser)
758
- .map(|r| r.map(|r| r.as_json_payload())?)
775
+ .map(|r| {
776
+ r.and_then(|r| {
777
+ let exit_val: ActExitValue<O> = r.into();
778
+ Ok(match exit_val {
779
+ ActExitValue::WillCompleteAsync => ActExitValue::WillCompleteAsync,
780
+ ActExitValue::Normal(x) => {
781
+ ActExitValue::Normal(x.as_json_payload()?)
782
+ }
783
+ })
784
+ })
785
+ })
759
786
  .boxed(),
760
787
  Err(e) => async move { Err(e.into()) }.boxed(),
761
788
  }
@@ -264,7 +264,7 @@ impl TestHistoryBuilder {
264
264
  self.build_and_push_event(EventType::MarkerRecorded, attrs.into());
265
265
  }
266
266
 
267
- fn add_local_activity_marker(
267
+ pub fn add_local_activity_marker(
268
268
  &mut self,
269
269
  seq: u32,
270
270
  activity_id: &str,
@@ -29,7 +29,7 @@ use temporal_sdk_core_protos::{
29
29
  coresdk::{
30
30
  workflow_commands::{
31
31
  workflow_command, ActivityCancellationType, CompleteWorkflowExecution,
32
- ScheduleActivity, StartTimer,
32
+ ScheduleActivity, ScheduleLocalActivity, StartTimer,
33
33
  },
34
34
  workflow_completion::WorkflowActivationCompletion,
35
35
  },
@@ -290,10 +290,10 @@ impl TestWorker {
290
290
  self.inner.register_wf(workflow_type, wf_function)
291
291
  }
292
292
 
293
- pub fn register_activity<A, R>(
293
+ pub fn register_activity<A, R, O>(
294
294
  &mut self,
295
295
  activity_type: impl Into<String>,
296
- act_function: impl IntoActivityFunc<A, R>,
296
+ act_function: impl IntoActivityFunc<A, R, O>,
297
297
  ) {
298
298
  self.inner.register_activity(activity_type, act_function)
299
299
  }
@@ -493,6 +493,25 @@ pub fn schedule_activity_cmd(
493
493
  .into()
494
494
  }
495
495
 
496
+ pub fn schedule_local_activity_cmd(
497
+ seq: u32,
498
+ activity_id: &str,
499
+ cancellation_type: ActivityCancellationType,
500
+ activity_timeout: Duration,
501
+ ) -> workflow_command::Variant {
502
+ ScheduleLocalActivity {
503
+ seq,
504
+ activity_id: activity_id.to_string(),
505
+ activity_type: "test_activity".to_string(),
506
+ schedule_to_start_timeout: Some(activity_timeout.into()),
507
+ start_to_close_timeout: Some(activity_timeout.into()),
508
+ schedule_to_close_timeout: Some(activity_timeout.into()),
509
+ cancellation_type: cancellation_type as i32,
510
+ ..Default::default()
511
+ }
512
+ .into()
513
+ }
514
+
496
515
  pub fn start_timer_cmd(seq: u32, duration: Duration) -> workflow_command::Variant {
497
516
  StartTimer {
498
517
  seq,