@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@temporalio/core-bridge",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Temporal.io SDK Core<>Node bridge",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -20,7 +20,7 @@
20
20
  "license": "MIT",
21
21
  "dependencies": {
22
22
  "@opentelemetry/api": "^1.1.0",
23
- "@temporalio/internal-non-workflow-common": "^1.0.0",
23
+ "@temporalio/internal-non-workflow-common": "^1.0.1",
24
24
  "arg": "^5.0.2",
25
25
  "cargo-cp-artifact": "^0.1.6",
26
26
  "which": "^2.0.2"
@@ -43,5 +43,5 @@
43
43
  "publishConfig": {
44
44
  "access": "public"
45
45
  },
46
- "gitHead": "c4fc4dc608bf58701c11b6ae02d1d63b4457718d"
46
+ "gitHead": "a1dae539e72b6b088b400998d7bef482e8ed52f1"
47
47
  }
@@ -12,7 +12,7 @@ mod raw;
12
12
  mod retry;
13
13
  mod workflow_handle;
14
14
 
15
- pub use crate::retry::{CallType, RetryClient};
15
+ pub use crate::retry::{CallType, RetryClient, RETRYABLE_ERROR_CODES};
16
16
  pub use raw::WorkflowService;
17
17
  pub use workflow_handle::{WorkflowExecutionInfo, WorkflowExecutionResult};
18
18
 
@@ -609,6 +609,20 @@ pub trait WorkflowClientTrait {
609
609
  payloads: Option<Payloads>,
610
610
  ) -> Result<SignalWorkflowExecutionResponse>;
611
611
 
612
+ /// Send signal and start workflow transcationally
613
+ //#TODO maybe lift the Signal type from sdk::workflow_context::options
614
+ #[allow(clippy::too_many_arguments)]
615
+ async fn signal_with_start_workflow_execution(
616
+ &self,
617
+ input: Option<Payloads>,
618
+ task_queue: String,
619
+ workflow_id: String,
620
+ workflow_type: String,
621
+ options: WorkflowOptions,
622
+ signal_name: String,
623
+ signal_input: Option<Payloads>,
624
+ ) -> Result<SignalWithStartWorkflowExecutionResponse>;
625
+
612
626
  /// Request a query of a certain workflow instance
613
627
  async fn query_workflow_execution(
614
628
  &self,
@@ -697,7 +711,7 @@ impl WorkflowClientTrait for Client {
697
711
  }),
698
712
  task_queue: Some(TaskQueue {
699
713
  name: task_queue,
700
- kind: 0,
714
+ kind: TaskQueueKind::Unspecified as i32,
701
715
  }),
702
716
  request_id,
703
717
  workflow_task_timeout: options.task_timeout.map(Into::into),
@@ -928,6 +942,42 @@ impl WorkflowClientTrait for Client {
928
942
  .into_inner())
929
943
  }
930
944
 
945
+ async fn signal_with_start_workflow_execution(
946
+ &self,
947
+ input: Option<Payloads>,
948
+ task_queue: String,
949
+ workflow_id: String,
950
+ workflow_type: String,
951
+ options: WorkflowOptions,
952
+ signal_name: String,
953
+ signal_input: Option<Payloads>,
954
+ ) -> Result<SignalWithStartWorkflowExecutionResponse> {
955
+ let request_id = Uuid::new_v4().to_string();
956
+ Ok(self
957
+ .wf_svc()
958
+ .signal_with_start_workflow_execution(SignalWithStartWorkflowExecutionRequest {
959
+ namespace: self.namespace.clone(),
960
+ workflow_id,
961
+ workflow_type: Some(WorkflowType {
962
+ name: workflow_type,
963
+ }),
964
+ task_queue: Some(TaskQueue {
965
+ name: task_queue,
966
+ kind: TaskQueueKind::Normal as i32,
967
+ }),
968
+ request_id,
969
+ input,
970
+ signal_name,
971
+ signal_input,
972
+ identity: self.inner.options.identity.clone(),
973
+ workflow_task_timeout: options.task_timeout.map(Into::into),
974
+ search_attributes: options.search_attributes.map(Into::into),
975
+ ..Default::default()
976
+ })
977
+ .await?
978
+ .into_inner())
979
+ }
980
+
931
981
  async fn query_workflow_execution(
932
982
  &self,
933
983
  workflow_id: String,
@@ -120,9 +120,9 @@ impl TonicErrorHandler {
120
120
  fn new(cfg: RetryConfig, call_type: CallType, call_name: &'static str) -> Self {
121
121
  Self {
122
122
  max_retries: cfg.max_retries,
123
- backoff: cfg.into(),
124
123
  call_type,
125
124
  call_name,
125
+ backoff: cfg.into(),
126
126
  }
127
127
  }
128
128
 
@@ -346,6 +346,29 @@ where
346
346
  )
347
347
  }
348
348
 
349
+ async fn signal_with_start_workflow_execution(
350
+ &self,
351
+ input: Option<Payloads>,
352
+ task_queue: String,
353
+ workflow_id: String,
354
+ workflow_type: String,
355
+ options: WorkflowOptions,
356
+ signal_name: String,
357
+ signal_input: Option<Payloads>,
358
+ ) -> Result<SignalWithStartWorkflowExecutionResponse> {
359
+ retry_call!(
360
+ self,
361
+ signal_with_start_workflow_execution,
362
+ input.clone(),
363
+ task_queue.clone(),
364
+ workflow_id.clone(),
365
+ workflow_type.clone(),
366
+ options.clone(),
367
+ signal_name.clone(),
368
+ signal_input.clone()
369
+ )
370
+ }
371
+
349
372
  async fn query_workflow_execution(
350
373
  &self,
351
374
  workflow_id: String,
@@ -443,6 +466,17 @@ where
443
466
  }
444
467
  }
445
468
 
469
+ impl<C> RawClientLikeUser for RetryClient<C>
470
+ where
471
+ C: RawClientLikeUser,
472
+ {
473
+ type RawClientT = C::RawClientT;
474
+
475
+ fn wf_svc(&self) -> Self::RawClientT {
476
+ self.client.wf_svc()
477
+ }
478
+ }
479
+
446
480
  #[cfg(test)]
447
481
  mod tests {
448
482
  use super::*;
@@ -597,14 +631,3 @@ mod tests {
597
631
  }
598
632
  }
599
633
  }
600
-
601
- impl<C> RawClientLikeUser for RetryClient<C>
602
- where
603
- C: RawClientLikeUser,
604
- {
605
- type RawClientT = C::RawClientT;
606
-
607
- fn wf_svc(&self) -> Self::RawClientT {
608
- self.client.wf_svc()
609
- }
610
- }
@@ -22,10 +22,13 @@ use std::{
22
22
  };
23
23
  use temporal_client::WorkflowOptions;
24
24
  use temporal_sdk::{ActivityOptions, WfContext};
25
- use temporal_sdk_core_api::Worker as WorkerTrait;
25
+ use temporal_sdk_core_api::{errors::CompleteActivityError, Worker as WorkerTrait};
26
26
  use temporal_sdk_core_protos::{
27
27
  coresdk::{
28
- activity_result::{activity_resolution, ActivityExecutionResult, ActivityResolution},
28
+ activity_result::{
29
+ activity_execution_result, activity_resolution, ActivityExecutionResult,
30
+ ActivityResolution, Success,
31
+ },
29
32
  activity_task::{activity_task, ActivityTask},
30
33
  workflow_activation::{workflow_activation_job, ResolveActivity, WorkflowActivationJob},
31
34
  workflow_commands::{
@@ -801,3 +804,68 @@ async fn activity_tasks_from_completion_reserve_slots() {
801
804
  let run_fut = async { worker.run_until_done().await.unwrap() };
802
805
  tokio::join!(run_fut, act_completer);
803
806
  }
807
+
808
+ #[tokio::test]
809
+ async fn retryable_net_error_exhaustion_is_nonfatal() {
810
+ let mut mock_client = mock_workflow_client();
811
+ mock_client
812
+ .expect_complete_activity_task()
813
+ .times(1)
814
+ .returning(|_, _| Err(tonic::Status::internal("retryable error")));
815
+
816
+ let core = mock_worker(MocksHolder::from_client_with_activities(
817
+ mock_client,
818
+ [PollActivityTaskQueueResponse {
819
+ task_token: vec![1],
820
+ activity_id: "act1".to_string(),
821
+ heartbeat_timeout: Some(Duration::from_secs(10).into()),
822
+ ..Default::default()
823
+ }
824
+ .into()],
825
+ ));
826
+
827
+ let act = core.poll_activity_task().await.unwrap();
828
+ core.complete_activity_task(ActivityTaskCompletion {
829
+ task_token: act.task_token,
830
+ result: Some(ActivityExecutionResult::ok(vec![1].into())),
831
+ })
832
+ .await
833
+ .unwrap();
834
+ core.shutdown().await;
835
+ }
836
+
837
+ #[tokio::test]
838
+ async fn cant_complete_activity_with_unset_result_payload() {
839
+ let mut mock_client = mock_workflow_client();
840
+ mock_client
841
+ .expect_poll_activity_task()
842
+ .returning(move |_, _| {
843
+ Ok(PollActivityTaskQueueResponse {
844
+ task_token: vec![1],
845
+ ..Default::default()
846
+ })
847
+ });
848
+
849
+ let cfg = WorkerConfigBuilder::default()
850
+ .namespace("enchi")
851
+ .task_queue("cat")
852
+ .worker_build_id("enchi_loves_salmon")
853
+ .build()
854
+ .unwrap();
855
+ let worker = Worker::new_test(cfg, mock_client);
856
+ let t = worker.poll_activity_task().await.unwrap();
857
+ let res = worker
858
+ .complete_activity_task(ActivityTaskCompletion {
859
+ task_token: t.task_token,
860
+ result: Some(ActivityExecutionResult {
861
+ status: Some(activity_execution_result::Status::Completed(Success {
862
+ result: None,
863
+ })),
864
+ }),
865
+ })
866
+ .await;
867
+ assert_matches!(
868
+ res,
869
+ Err(CompleteActivityError::MalformedActivityCompletion { .. })
870
+ )
871
+ }
@@ -1,20 +1,38 @@
1
1
  use crate::{
2
2
  replay::{default_wes_attribs, TestHistoryBuilder, DEFAULT_WORKFLOW_TYPE},
3
- test_help::{mock_sdk, mock_sdk_cfg, MockPollCfg, ResponseType},
3
+ test_help::{
4
+ hist_to_poll_resp, mock_sdk, mock_sdk_cfg, mock_worker, single_hist_mock_sg, MockPollCfg,
5
+ ResponseType, TEST_Q,
6
+ },
4
7
  worker::client::mocks::mock_workflow_client,
5
8
  };
6
9
  use anyhow::anyhow;
7
- use futures::future::join_all;
10
+ use futures::{future::join_all, FutureExt};
8
11
  use std::{
9
- sync::atomic::{AtomicUsize, Ordering},
12
+ collections::HashMap,
13
+ sync::{
14
+ atomic::{AtomicUsize, Ordering},
15
+ Arc,
16
+ },
10
17
  time::Duration,
11
18
  };
12
19
  use temporal_client::WorkflowOptions;
13
20
  use temporal_sdk::{ActContext, LocalActivityOptions, WfContext, WorkflowResult};
21
+ use temporal_sdk_core_api::Worker;
14
22
  use temporal_sdk_core_protos::{
15
- coresdk::AsJsonPayloadExt,
16
- temporal::api::{common::v1::RetryPolicy, enums::v1::EventType, failure::v1::Failure},
23
+ coresdk::{
24
+ activity_result::ActivityExecutionResult,
25
+ workflow_activation::{workflow_activation_job, WorkflowActivationJob},
26
+ workflow_commands::{ActivityCancellationType, QueryResult, QuerySuccess},
27
+ workflow_completion::WorkflowActivationCompletion,
28
+ ActivityTaskCompletion, AsJsonPayloadExt,
29
+ },
30
+ temporal::api::{
31
+ common::v1::RetryPolicy, enums::v1::EventType, failure::v1::Failure,
32
+ query::v1::WorkflowQuery,
33
+ },
17
34
  };
35
+ use temporal_sdk_core_test_utils::{schedule_local_activity_cmd, WorkerTestHelpers};
18
36
  use tokio::sync::Barrier;
19
37
 
20
38
  async fn echo(_ctx: ActContext, e: String) -> anyhow::Result<String> {
@@ -117,10 +135,7 @@ async fn local_act_many_concurrent() {
117
135
  let mut worker = mock_sdk(mh);
118
136
 
119
137
  worker.register_wf(DEFAULT_WORKFLOW_TYPE.to_owned(), local_act_fanout_wf);
120
- worker.register_activity(
121
- "echo",
122
- |_ctx: ActContext, str: String| async move { Ok(str) },
123
- );
138
+ worker.register_activity("echo", echo);
124
139
  worker
125
140
  .submit_wf(
126
141
  wf_id.to_owned(),
@@ -343,3 +358,158 @@ async fn local_act_retry_long_backoff_uses_timer() {
343
358
  .unwrap();
344
359
  worker.run_until_done().await.unwrap();
345
360
  }
361
+
362
+ #[tokio::test]
363
+ async fn local_act_null_result() {
364
+ let mut t = TestHistoryBuilder::default();
365
+ t.add_by_type(EventType::WorkflowExecutionStarted);
366
+ t.add_full_wf_task();
367
+ t.add_local_activity_marker(1, "1", None, None, None);
368
+ t.add_workflow_execution_completed();
369
+
370
+ let wf_id = "fakeid";
371
+ let mock = mock_workflow_client();
372
+ let mh = MockPollCfg::from_resp_batches(wf_id, t, [ResponseType::AllHistory], mock);
373
+ let mut worker = mock_sdk_cfg(mh, |w| w.max_cached_workflows = 1);
374
+
375
+ worker.register_wf(
376
+ DEFAULT_WORKFLOW_TYPE.to_owned(),
377
+ |ctx: WfContext| async move {
378
+ ctx.local_activity(LocalActivityOptions {
379
+ activity_type: "nullres".to_string(),
380
+ input: "hi".as_json_payload().expect("serializes fine"),
381
+ ..Default::default()
382
+ })
383
+ .await;
384
+ Ok(().into())
385
+ },
386
+ );
387
+ worker.register_activity("nullres", |_ctx: ActContext, _: String| async { Ok(()) });
388
+ worker
389
+ .submit_wf(
390
+ wf_id.to_owned(),
391
+ DEFAULT_WORKFLOW_TYPE.to_owned(),
392
+ vec![],
393
+ WorkflowOptions::default(),
394
+ )
395
+ .await
396
+ .unwrap();
397
+ worker.run_until_done().await.unwrap();
398
+ }
399
+
400
+ #[tokio::test]
401
+ async fn query_during_wft_heartbeat_doesnt_accidentally_fail_to_continue_heartbeat() {
402
+ crate::telemetry::test_telem_console();
403
+ let wfid = "fake_wf_id";
404
+ let mut t = TestHistoryBuilder::default();
405
+ let mut wes_short_wft_timeout = default_wes_attribs();
406
+ wes_short_wft_timeout.workflow_task_timeout = Some(Duration::from_millis(200).into());
407
+ t.add(
408
+ EventType::WorkflowExecutionStarted,
409
+ wes_short_wft_timeout.into(),
410
+ );
411
+ t.add_full_wf_task();
412
+ // get query here
413
+ t.add_full_wf_task();
414
+ t.add_local_activity_marker(1, "1", None, None, None);
415
+ t.add_workflow_execution_completed();
416
+
417
+ let query_with_hist_task = {
418
+ let mut pr = hist_to_poll_resp(&t, wfid, ResponseType::ToTaskNum(1), TEST_Q);
419
+ pr.queries = HashMap::new();
420
+ pr.queries.insert(
421
+ "the-query".to_string(),
422
+ WorkflowQuery {
423
+ query_type: "query-type".to_string(),
424
+ query_args: Some(b"hi".into()),
425
+ header: None,
426
+ },
427
+ );
428
+ pr
429
+ };
430
+ let after_la_resolved = Arc::new(Barrier::new(2));
431
+ let poll_barr = after_la_resolved.clone();
432
+ let tasks = [
433
+ query_with_hist_task,
434
+ hist_to_poll_resp(
435
+ &t,
436
+ wfid,
437
+ ResponseType::UntilResolved(
438
+ async move {
439
+ poll_barr.wait().await;
440
+ }
441
+ .boxed(),
442
+ 3,
443
+ ),
444
+ TEST_Q,
445
+ ),
446
+ ];
447
+ let mock = mock_workflow_client();
448
+ let mut mock = single_hist_mock_sg(wfid, t, tasks, mock, true);
449
+ mock.worker_cfg(|wc| wc.max_cached_workflows = 1);
450
+ let core = mock_worker(mock);
451
+
452
+ let barrier = Barrier::new(2);
453
+
454
+ let wf_fut = async {
455
+ let task = core.poll_workflow_activation().await.unwrap();
456
+ core.complete_workflow_activation(WorkflowActivationCompletion::from_cmd(
457
+ task.run_id,
458
+ schedule_local_activity_cmd(
459
+ 1,
460
+ "act-id",
461
+ ActivityCancellationType::TryCancel,
462
+ Duration::from_secs(60),
463
+ ),
464
+ ))
465
+ .await
466
+ .unwrap();
467
+ let task = core.poll_workflow_activation().await.unwrap();
468
+ // Get query, and complete it
469
+ let query = assert_matches!(
470
+ task.jobs.as_slice(),
471
+ [WorkflowActivationJob {
472
+ variant: Some(workflow_activation_job::Variant::QueryWorkflow(q)),
473
+ }] => q
474
+ );
475
+ // Now complete the LA
476
+ barrier.wait().await;
477
+ core.complete_workflow_activation(WorkflowActivationCompletion::from_cmd(
478
+ task.run_id,
479
+ QueryResult {
480
+ query_id: query.query_id.clone(),
481
+ variant: Some(
482
+ QuerySuccess {
483
+ response: Some("whatever".into()),
484
+ }
485
+ .into(),
486
+ ),
487
+ }
488
+ .into(),
489
+ ))
490
+ .await
491
+ .unwrap();
492
+ // Activation with it resolving:
493
+ let task = core.poll_workflow_activation().await.unwrap();
494
+ assert_matches!(
495
+ task.jobs.as_slice(),
496
+ [WorkflowActivationJob {
497
+ variant: Some(workflow_activation_job::Variant::ResolveActivity(_)),
498
+ }]
499
+ );
500
+ core.complete_execution(&task.run_id).await;
501
+ };
502
+ let act_fut = async {
503
+ let act_task = core.poll_activity_task().await.unwrap();
504
+ barrier.wait().await;
505
+ core.complete_activity_task(ActivityTaskCompletion {
506
+ task_token: act_task.task_token,
507
+ result: Some(ActivityExecutionResult::ok(vec![1].into())),
508
+ })
509
+ .await
510
+ .unwrap();
511
+ after_la_resolved.wait().await;
512
+ };
513
+
514
+ tokio::join!(wf_fut, act_fut);
515
+ }
@@ -1,7 +1,7 @@
1
1
  use crate::{
2
2
  test_help::{
3
3
  build_fake_worker, build_mock_pollers, canned_histories, mock_manual_poller, mock_worker,
4
- MockPollCfg, MockWorkerInputs, MocksHolder,
4
+ MockPollCfg, MockWorkerInputs, MocksHolder, ResponseType,
5
5
  },
6
6
  worker::client::mocks::mock_workflow_client,
7
7
  PollActivityError, PollWfError,
@@ -224,3 +224,35 @@ async fn complete_eviction_after_shutdown_doesnt_panic() {
224
224
  .await
225
225
  .unwrap();
226
226
  }
227
+
228
+ #[tokio::test]
229
+ async fn worker_does_not_panic_on_retry_exhaustion_of_nonfatal_net_err() {
230
+ let t = canned_histories::single_timer("1");
231
+ let mut mock = mock_workflow_client();
232
+ // Return a failure that counts as retryable, and hence we want to be swallowed
233
+ mock.expect_complete_workflow_task()
234
+ .times(1)
235
+ .returning(|_| Err(tonic::Status::internal("Some retryable error")));
236
+ let mut mh =
237
+ MockPollCfg::from_resp_batches("fakeid", t, [1.into(), ResponseType::AllHistory], mock);
238
+ mh.enforce_correct_number_of_polls = false;
239
+ let mut mock = build_mock_pollers(mh);
240
+ mock.worker_cfg(|w| w.max_cached_workflows = 1);
241
+ let core = mock_worker(mock);
242
+
243
+ let res = core.poll_workflow_activation().await.unwrap();
244
+ assert_eq!(res.jobs.len(), 1);
245
+ // This should not return a fatal error
246
+ core.complete_workflow_activation(WorkflowActivationCompletion::from_cmds(
247
+ res.run_id,
248
+ vec![start_timer_cmd(1, Duration::from_secs(1))],
249
+ ))
250
+ .await
251
+ .unwrap();
252
+ // We should see an eviction
253
+ let res = core.poll_workflow_activation().await.unwrap();
254
+ assert_matches!(
255
+ res.jobs[0].variant,
256
+ Some(workflow_activation_job::Variant::RemoveFromCache(_))
257
+ );
258
+ }
@@ -62,7 +62,17 @@ lazy_static::lazy_static! {
62
62
  };
63
63
  }
64
64
 
65
- /// Initialize a worker bound to a task queue
65
+ /// Initialize a worker bound to a task queue.
66
+ ///
67
+ /// Lang implementations should pass in a a [temporal_client::ConfiguredClient] directly (or a
68
+ /// [RetryClient] wrapping one). When they do so, this function will always overwrite the client
69
+ /// retry configuration, force the client to use the namespace defined in the worker config, and set
70
+ /// the client identity appropriately. IE: Use [ClientOptions::connect_no_namespace], not
71
+ /// [ClientOptions::connect].
72
+ ///
73
+ /// It is also possible to pass in a [WorkflowClientTrait] implementor, but this largely exists to
74
+ /// support testing and mocking. Lang impls should not operate that way, as it may result in
75
+ /// improper retry behavior for a worker.
66
76
  pub fn init_worker<CT>(worker_config: WorkerConfig, client: CT) -> Worker
67
77
  where
68
78
  CT: Into<AnyClient>,
@@ -76,19 +86,18 @@ where
76
86
  if let Some(ref id_override) = worker_config.client_identity_override {
77
87
  client.options_mut().identity = id_override.clone();
78
88
  }
79
- let retry_client = RetryClient::new(client, RetryConfig::default());
89
+ let retry_client = RetryClient::new(client, Default::default());
80
90
  Arc::new(retry_client)
81
91
  }
82
92
  };
83
- let c_opts = client.get_options().clone();
84
93
  if client.namespace() != worker_config.namespace {
85
94
  panic!("Passed in client is not bound to the same namespace as the worker");
86
95
  }
96
+ let sticky_q = sticky_q_name_for_worker(&client.get_options().identity, &worker_config);
87
97
  let client_bag = Arc::new(WorkerClientBag::new(
88
98
  Box::new(client),
89
99
  worker_config.namespace.clone(),
90
100
  ));
91
- let sticky_q = sticky_q_name_for_worker(&c_opts.identity, &worker_config);
92
101
  let metrics = MetricsContext::top_level(worker_config.namespace.clone())
93
102
  .with_task_q(worker_config.task_queue.clone());
94
103
  Worker::new(worker_config, sticky_q, client_bag, metrics)
@@ -6,7 +6,7 @@ use anyhow::anyhow;
6
6
  use std::{
7
7
  collections::HashMap,
8
8
  convert::TryFrom,
9
- fmt::{Display, Formatter},
9
+ fmt::{Debug, Display, Formatter},
10
10
  time::{Duration, SystemTime},
11
11
  };
12
12
  use temporal_sdk_core_protos::{
@@ -39,7 +39,7 @@ use temporal_sdk_core_protos::{
39
39
  };
40
40
 
41
41
  /// A validated version of a [PollWorkflowTaskQueueResponse]
42
- #[derive(Debug, Clone, PartialEq)]
42
+ #[derive(Clone, PartialEq)]
43
43
  #[allow(clippy::manual_non_exhaustive)] // Clippy doesn't understand it's only for *in* this crate
44
44
  pub struct ValidPollWFTQResponse {
45
45
  pub task_token: TaskToken,
@@ -61,6 +61,28 @@ pub struct ValidPollWFTQResponse {
61
61
  _cant_construct_me: (),
62
62
  }
63
63
 
64
+ impl Debug for ValidPollWFTQResponse {
65
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
66
+ write!(
67
+ f,
68
+ "ValidWFT {{ task_token: {}, task_queue: {}, workflow_execution: {:?}, \
69
+ workflow_type: {}, attempt: {}, previous_started_event_id: {}, started_event_id {}, \
70
+ history_length: {}, first_evt_in_hist_id: {:?}, legacy_query: {:?}, queries: {:?} }}",
71
+ self.task_token,
72
+ self.task_queue,
73
+ self.workflow_execution,
74
+ self.workflow_type,
75
+ self.attempt,
76
+ self.previous_started_event_id,
77
+ self.started_event_id,
78
+ self.history.events.len(),
79
+ self.history.events.get(0).map(|e| e.event_id),
80
+ self.legacy_query,
81
+ self.query_requests
82
+ )
83
+ }
84
+ }
85
+
64
86
  impl TryFrom<PollWorkflowTaskQueueResponse> for ValidPollWFTQResponse {
65
87
  /// We return the poll response itself if it was invalid
66
88
  type Error = PollWorkflowTaskQueueResponse;
@@ -204,11 +226,10 @@ impl HistoryEventExt for HistoryEvent {
204
226
  )) if marker_name == LOCAL_ACTIVITY_MARKER_NAME => {
205
227
  let (data, ok_res) = extract_local_activity_marker_details(&mut details);
206
228
  let data = data?;
207
- let result = if let Some(r) = ok_res {
208
- Ok(r)
209
- } else {
210
- let fail = failure?;
211
- Err(fail)
229
+ let result = match (ok_res, failure) {
230
+ (Some(r), None) => Ok(r),
231
+ (None | Some(_), Some(f)) => Err(f),
232
+ (None, None) => Ok(Default::default()),
212
233
  };
213
234
  Some(CompleteLocalActivityData {
214
235
  marker_dat: data,
@@ -228,6 +249,22 @@ pub(crate) struct CompleteLocalActivityData {
228
249
  pub result: Result<Payload, Failure>,
229
250
  }
230
251
 
252
+ pub(crate) fn validate_activity_completion(
253
+ status: &activity_execution_result::Status,
254
+ ) -> Result<(), CompleteActivityError> {
255
+ match status {
256
+ Status::Completed(c) if c.result.is_none() => {
257
+ Err(CompleteActivityError::MalformedActivityCompletion {
258
+ reason: "Activity completions must contain a `result` payload \
259
+ (which may be empty)"
260
+ .to_string(),
261
+ completion: None,
262
+ })
263
+ }
264
+ _ => Ok(()),
265
+ }
266
+ }
267
+
231
268
  impl TryFrom<activity_execution_result::Status> for LocalActivityExecutionResult {
232
269
  type Error = CompleteActivityError;
233
270