1#![doc = include_str!("../README.md")]
2
3use std::{
4 any::{Any, TypeId},
5 collections::{BTreeMap, HashMap},
6 future::Future,
7 pin::Pin,
8 sync::{
9 atomic::{AtomicBool, Ordering},
10 Arc, Mutex, OnceLock,
11 },
12 task::{Context as TaskContext, Poll},
13 time::{Duration, Instant, SystemTime, UNIX_EPOCH},
14};
15
16use apache_avro::{from_avro_datum, from_value, to_avro_datum, to_value, Schema};
17use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
18use futures_util::{future::OptionFuture, task::noop_waker_ref};
19use serde::{de::DeserializeOwned, Deserialize, Serialize};
20pub use serde_json::{json, Value};
21use thiserror::Error;
22pub use uuid::Uuid;
23
24pub const WORKER_PROTOCOL_VERSION: &str = "1.2";
25pub const CONTROL_PLANE_VERSION: &str = "2";
26pub const DEFAULT_CODEC: &str = "avro";
27pub const JSON_CODEC: &str = "json";
28pub const SDK_VERSION: &str = concat!("durable-workflow-rust/", env!("CARGO_PKG_VERSION"));
29pub const QUERY_TASKS_CAPABILITY: &str = "query_tasks";
31pub const QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION: &str = "1.8";
33
34const MAX_LONG_POLL_TIMEOUT_SECONDS: u64 = 60;
35const WORKFLOW_TASK_WAITING_FOR_HISTORY_MESSAGE: &str =
36 "Workflow task waiting for scheduled history.";
37const WORKFLOW_TASK_WAITING_FOR_HISTORY_TYPE: &str = "WorkflowTaskWaitingForHistory";
38
39const QUERY_TASK_FINAL_REJECTION_REASONS: &[&str] = &[
40 "lease_expired",
41 "query_task_not_found",
42 "query_task_not_leased",
43 "query_task_timed_out",
44];
45
46const AVRO_PAYLOAD_SCHEMA_JSON: &str = r#"{"type":"record","name":"Payload","namespace":"durable_workflow","fields":[{"name":"json","type":"string"},{"name":"version","type":"int","default":1}]}"#;
47const AVRO_PAYLOAD_VERSION: i32 = 1;
48
49static AVRO_PAYLOAD_SCHEMA: OnceLock<std::result::Result<Schema, String>> = OnceLock::new();
50
51#[derive(Clone, Copy)]
52enum RequestProtocol {
53 ControlPlane,
54 Worker(&'static str),
55}
56
57impl RequestProtocol {
58 fn is_worker(self) -> bool {
59 matches!(self, Self::Worker(_))
60 }
61}
62
63pub type Result<T> = std::result::Result<T, Error>;
64
65#[derive(Debug, Error)]
66pub enum Error {
67 #[error("transport error: {0}")]
68 Transport(#[from] reqwest::Error),
69 #[error("json error: {0}")]
70 Json(#[from] serde_json::Error),
71 #[error("http {status}: {body}")]
72 Http {
73 status: reqwest::StatusCode,
74 body: String,
75 },
76 #[error("codec error: {0}")]
77 Codec(String),
78 #[error(transparent)]
79 QueryFailed(QueryFailure),
80 #[error(transparent)]
81 Protocol(ProtocolFailure),
82 #[error(transparent)]
83 NonDeterministicReplay(ReplayFailure),
84 #[error(transparent)]
85 ChildWorkflowFailed(ChildWorkflowFailure),
86 #[error(transparent)]
87 ActivityFailed(ActivityFailure),
88 #[error(transparent)]
89 WorkflowCommandRejected(WorkflowCommandRejection),
90 #[error(transparent)]
91 WorkflowFailed(WorkflowTerminalOutcome),
92 #[error(transparent)]
93 WorkflowCancelled(WorkflowTerminalOutcome),
94 #[error(transparent)]
95 WorkflowTerminated(WorkflowTerminalOutcome),
96 #[error(transparent)]
97 WorkflowTimedOut(WorkflowTerminalOutcome),
98 #[error(transparent)]
99 ActivityTaskRejected(ActivityTaskRejection),
100 #[error("workflow handler {0:?} is not registered")]
101 WorkflowNotRegistered(String),
102 #[error("activity handler {0:?} is not registered")]
103 ActivityNotRegistered(String),
104 #[error("workflow future yielded without emitting a durable command")]
105 WorkflowYieldedWithoutCommand,
106 #[error("workflow state lock is poisoned")]
107 WorkflowStatePoisoned,
108 #[error("timer duration is too large for the worker protocol")]
109 TimerDurationOverflow,
110 #[error("operation timed out")]
111 Timeout,
112 #[error("worker loop error: {0}")]
113 WorkerLoop(String),
114 #[error("invalid child workflow options: {0}")]
115 InvalidChildWorkflowOptions(String),
116 #[error(transparent)]
117 InvalidActivityOptions(ActivityOptionsError),
118 #[error(transparent)]
119 InvalidContinueAsNewOptions(#[from] ContinueAsNewOptionsError),
120 #[doc(hidden)]
121 #[error("workflow requested continue as new")]
122 ContinueAsNew(ContinueAsNewRequest),
123}
124
125#[derive(Clone, Copy, Debug, PartialEq, Eq)]
127pub enum WorkflowCommandKind {
128 Cancel,
129 Terminate,
130}
131
132impl WorkflowCommandKind {
133 fn as_str(self) -> &'static str {
134 match self {
135 Self::Cancel => "cancel",
136 Self::Terminate => "terminate",
137 }
138 }
139}
140
141#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
143pub struct WorkflowCommandOptions {
144 #[serde(skip_serializing_if = "Option::is_none")]
145 pub reason: Option<String>,
146 #[serde(skip_serializing_if = "Option::is_none")]
147 pub request_id: Option<String>,
148}
149
150#[derive(Clone, Debug, PartialEq, Eq)]
157pub struct WorkflowStartOptions {
158 pub execution_timeout_seconds: u64,
159 pub run_timeout_seconds: u64,
160}
161
162impl Default for WorkflowStartOptions {
163 fn default() -> Self {
164 Self {
165 execution_timeout_seconds: 3600,
166 run_timeout_seconds: 600,
167 }
168 }
169}
170
171impl WorkflowStartOptions {
172 pub fn new() -> Self {
173 Self::default()
174 }
175
176 pub fn execution_timeout_seconds(mut self, seconds: u64) -> Self {
177 self.execution_timeout_seconds = seconds;
178 self
179 }
180
181 pub fn run_timeout_seconds(mut self, seconds: u64) -> Self {
182 self.run_timeout_seconds = seconds;
183 self
184 }
185
186 fn validate(&self) -> Result<()> {
187 if self.execution_timeout_seconds == 0 {
188 return Err(Error::Codec(
189 "execution_timeout_seconds must be at least 1".to_string(),
190 ));
191 }
192 if self.run_timeout_seconds == 0 {
193 return Err(Error::Codec(
194 "run_timeout_seconds must be at least 1".to_string(),
195 ));
196 }
197 if self.run_timeout_seconds > self.execution_timeout_seconds {
198 return Err(Error::Codec(
199 "run_timeout_seconds cannot exceed execution_timeout_seconds".to_string(),
200 ));
201 }
202
203 Ok(())
204 }
205}
206
207#[derive(Clone, Debug, Default, PartialEq, Eq)]
212pub struct ContinueAsNewOptions {
213 pub workflow_type: Option<String>,
214 pub task_queue: Option<String>,
215}
216
217impl ContinueAsNewOptions {
218 pub fn new() -> Self {
219 Self::default()
220 }
221
222 pub fn workflow_type(mut self, workflow_type: impl Into<String>) -> Self {
223 self.workflow_type = Some(workflow_type.into());
224 self
225 }
226
227 pub fn task_queue(mut self, task_queue: impl Into<String>) -> Self {
228 self.task_queue = Some(task_queue.into());
229 self
230 }
231
232 fn validate(&self) -> std::result::Result<(), ContinueAsNewOptionsError> {
233 for (field, value) in [
234 ("workflow_type", self.workflow_type.as_deref()),
235 ("task_queue", self.task_queue.as_deref()),
236 ] {
237 if value.is_some_and(|value| value.trim().is_empty()) {
238 return Err(ContinueAsNewOptionsError {
239 field,
240 message: format!("{field} must not be empty"),
241 });
242 }
243 }
244 Ok(())
245 }
246}
247
248#[derive(Clone, Debug, Error, PartialEq, Eq)]
250#[error("invalid continue-as-new option {field}: {message}")]
251pub struct ContinueAsNewOptionsError {
252 pub field: &'static str,
253 pub message: String,
254}
255
256#[derive(Clone, Debug, Default, PartialEq, Eq)]
258pub struct WorkflowHistoryBudget {
259 pub event_count: u64,
260 pub size_bytes: Option<u64>,
261 pub continue_as_new_recommended: bool,
262 pub pressure: Option<String>,
263}
264
265#[doc(hidden)]
266#[derive(Clone, Debug)]
267pub struct ContinueAsNewRequest {
268 arguments: Value,
269 options: ContinueAsNewOptions,
270}
271
272impl WorkflowCommandOptions {
273 pub fn new() -> Self {
274 Self::default()
275 }
276
277 pub fn reason(mut self, reason: impl Into<String>) -> Self {
278 self.reason = Some(reason.into());
279 self
280 }
281
282 pub fn request_id(mut self, request_id: impl Into<String>) -> Self {
283 self.request_id = Some(request_id.into());
284 self
285 }
286}
287
288#[derive(Clone, Debug, PartialEq)]
290pub struct WorkflowCommandResult {
291 pub command: WorkflowCommandKind,
292 pub workflow_id: String,
293 pub run_id: Option<String>,
294 pub outcome: Option<String>,
295 pub reason: Option<String>,
296 pub command_status: Option<String>,
297 pub raw: Value,
298}
299
300#[derive(Clone, Debug, Error)]
302#[error("workflow {command:?} rejected ({reason}, HTTP {status}): {message}")]
303pub struct WorkflowCommandRejection {
304 pub command: WorkflowCommandKind,
305 pub status: u16,
306 pub reason: String,
307 pub message: String,
308 pub workflow_id: String,
309 pub run_id: Option<String>,
310 pub target_scope: Option<String>,
311 pub body: Value,
312}
313
314#[derive(Clone, Copy, Debug, PartialEq, Eq)]
316pub enum WorkflowTerminalKind {
317 Failed,
318 Cancelled,
319 Terminated,
320 TimedOut,
321}
322
323#[derive(Clone, Debug, Error)]
329#[error("workflow {workflow_id} run {run_id:?} ended as {kind:?} ({reason})")]
330pub struct WorkflowTerminalOutcome {
331 pub kind: WorkflowTerminalKind,
332 pub workflow_id: String,
333 pub run_id: Option<String>,
334 pub reason: String,
335 pub failure_category: Option<String>,
336 pub failure_id: Option<String>,
337 pub exception_type: Option<String>,
338 pub exception_class: Option<String>,
339 pub non_retryable: Option<bool>,
340 pub message: Option<String>,
341 pub exception: Option<Value>,
342 pub raw: Value,
343}
344
345#[derive(Clone, Debug, Error)]
347#[error("activity task {operation} rejected ({reason}, HTTP {status})")]
348pub struct ActivityTaskRejection {
349 pub operation: String,
350 pub status: u16,
351 pub reason: String,
352 pub task_id: String,
353 pub activity_attempt_id: String,
354 pub cancel_requested: bool,
355 pub can_continue: Option<bool>,
356 pub run_closed_reason: Option<String>,
357 pub body: Value,
358}
359
360#[derive(Clone, Copy, Debug, PartialEq, Eq)]
362pub enum ActivityOptionsErrorKind {
363 EmptyTaskQueue,
364 EmptyRetryPolicy,
365 InvalidMaxAttempts,
366 BackoffWithoutRetryBudget,
367 TooManyBackoffIntervals,
368 InvalidBackoffCoefficient,
369 BackoffGenerationTooLarge,
370 BackoffOverflow,
371 EmptyNonRetryableErrorType,
372 TimeoutNotPositive,
373 TimeoutOverflow,
374 TimeoutOrder,
375}
376
377#[derive(Clone, Debug, Error, PartialEq, Eq)]
379#[error("invalid activity options ({kind:?}, {field:?}): {message}")]
380pub struct ActivityOptionsError {
381 pub kind: ActivityOptionsErrorKind,
382 pub field: Option<&'static str>,
383 pub message: String,
384}
385
386impl ActivityOptionsError {
387 fn new(
388 kind: ActivityOptionsErrorKind,
389 field: Option<&'static str>,
390 message: impl Into<String>,
391 ) -> Self {
392 Self {
393 kind,
394 field,
395 message: message.into(),
396 }
397 }
398}
399
400#[derive(Clone, Copy, Debug, PartialEq, Eq)]
402pub enum ActivityFailureKind {
403 Failed,
404 Cancelled,
405 TimedOut,
406}
407
408#[derive(Clone, Debug, Error)]
413#[error("activity failed ({reason}): {message}")]
414pub struct ActivityFailure {
415 pub kind: ActivityFailureKind,
416 pub reason: String,
417 pub message: String,
418 pub activity_execution_id: Option<String>,
419 pub activity_attempt_id: Option<String>,
420 pub activity_type: Option<String>,
421 pub activity_class: Option<String>,
422 pub attempt_number: Option<u64>,
423 pub failure_id: Option<String>,
424 pub failure_category: Option<String>,
425 pub timeout_kind: Option<String>,
426 pub non_retryable: bool,
427 pub exception_type: Option<String>,
428 pub exception_class: Option<String>,
429 pub code: Option<Value>,
430 pub exception: Option<Value>,
431}
432
433#[derive(Clone, Copy, Debug, PartialEq, Eq)]
435pub enum ChildWorkflowFailureKind {
436 Failed,
437 Cancelled,
438 Terminated,
439}
440
441#[derive(Clone, Debug, Error)]
447#[error("child workflow failed ({reason}): {message}")]
448pub struct ChildWorkflowFailure {
449 pub kind: ChildWorkflowFailureKind,
450 pub reason: String,
451 pub message: String,
452 pub parent_workflow_id: Option<String>,
453 pub parent_workflow_run_id: Option<String>,
454 pub child_workflow_id: Option<String>,
455 pub child_workflow_run_id: Option<String>,
456 pub child_workflow_type: Option<String>,
457 pub failure_id: Option<String>,
458 pub failure_category: Option<String>,
459 pub exception_type: Option<String>,
460 pub exception_class: Option<String>,
461 pub non_retryable: bool,
462 pub code: Option<Value>,
463 pub exception: Option<Value>,
464}
465
466#[derive(Clone, Debug, PartialEq, Eq)]
468pub struct WorkflowIdentity {
469 pub workflow_id: Option<String>,
470 pub run_id: Option<String>,
471}
472
473#[derive(Clone, Debug, PartialEq)]
475pub struct ChildWorkflowResult {
476 pub parent: WorkflowIdentity,
477 pub child: WorkflowIdentity,
478 pub child_workflow_type: Option<String>,
479 pub result: Value,
480}
481
482#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
484pub enum ParentClosePolicy {
485 #[default]
486 Abandon,
487 RequestCancel,
488 Terminate,
489}
490
491impl ParentClosePolicy {
492 fn as_str(self) -> &'static str {
493 match self {
494 Self::Abandon => "abandon",
495 Self::RequestCancel => "request_cancel",
496 Self::Terminate => "terminate",
497 }
498 }
499}
500
501#[derive(Clone, Debug, Default, PartialEq, Eq)]
503pub struct ChildWorkflowRetryPolicy {
504 pub max_attempts: Option<u32>,
505 pub backoff_seconds: Vec<u64>,
506 pub non_retryable_error_types: Vec<String>,
507}
508
509#[derive(Clone, Debug, PartialEq, Eq)]
513pub struct ChildWorkflowOptions {
514 pub task_queue: String,
515 pub parent_close_policy: ParentClosePolicy,
516 pub retry_policy: Option<ChildWorkflowRetryPolicy>,
517 pub execution_timeout_seconds: Option<u64>,
518 pub run_timeout_seconds: Option<u64>,
519}
520
521impl ChildWorkflowOptions {
522 pub fn new(task_queue: impl Into<String>) -> Self {
523 Self {
524 task_queue: task_queue.into(),
525 parent_close_policy: ParentClosePolicy::Abandon,
526 retry_policy: None,
527 execution_timeout_seconds: None,
528 run_timeout_seconds: None,
529 }
530 }
531
532 pub fn parent_close_policy(mut self, policy: ParentClosePolicy) -> Self {
533 self.parent_close_policy = policy;
534 self
535 }
536
537 pub fn retry_policy(mut self, policy: ChildWorkflowRetryPolicy) -> Self {
538 self.retry_policy = Some(policy);
539 self
540 }
541
542 pub fn execution_timeout_seconds(mut self, seconds: u64) -> Self {
543 self.execution_timeout_seconds = Some(seconds);
544 self
545 }
546
547 pub fn run_timeout_seconds(mut self, seconds: u64) -> Self {
548 self.run_timeout_seconds = Some(seconds);
549 self
550 }
551}
552
553#[derive(Clone, Debug, PartialEq, Eq)]
555pub enum ActivityBackoff {
556 Explicit(Vec<Duration>),
559 Exponential {
561 initial_interval: Duration,
562 coefficient: u32,
563 maximum_interval: Option<Duration>,
564 },
565}
566
567#[derive(Clone, Debug, Default, PartialEq, Eq)]
569pub struct ActivityRetryPolicy {
570 pub max_attempts: Option<u32>,
571 pub backoff: Option<ActivityBackoff>,
572 pub non_retryable_error_types: Vec<String>,
573}
574
575impl ActivityRetryPolicy {
576 pub fn new(max_attempts: u32) -> Self {
578 Self {
579 max_attempts: Some(max_attempts),
580 ..Self::default()
581 }
582 }
583
584 pub fn backoff_intervals(mut self, intervals: impl IntoIterator<Item = Duration>) -> Self {
585 self.backoff = Some(ActivityBackoff::Explicit(intervals.into_iter().collect()));
586 self
587 }
588
589 pub fn exponential_backoff(
590 mut self,
591 initial_interval: Duration,
592 coefficient: u32,
593 maximum_interval: Option<Duration>,
594 ) -> Self {
595 self.backoff = Some(ActivityBackoff::Exponential {
596 initial_interval,
597 coefficient,
598 maximum_interval,
599 });
600 self
601 }
602
603 pub fn non_retryable_error_type(mut self, error_type: impl Into<String>) -> Self {
604 self.non_retryable_error_types.push(error_type.into());
605 self
606 }
607
608 pub fn non_retryable_error_types(
609 mut self,
610 error_types: impl IntoIterator<Item = impl Into<String>>,
611 ) -> Self {
612 self.non_retryable_error_types
613 .extend(error_types.into_iter().map(Into::into));
614 self
615 }
616}
617
618#[derive(Clone, Debug, Default, PartialEq, Eq)]
623pub struct ActivityOptions {
624 pub task_queue: Option<String>,
625 pub retry_policy: Option<ActivityRetryPolicy>,
626 pub start_to_close_timeout: Option<Duration>,
627 pub schedule_to_start_timeout: Option<Duration>,
628 pub schedule_to_close_timeout: Option<Duration>,
629 pub heartbeat_timeout: Option<Duration>,
630}
631
632impl ActivityOptions {
633 pub fn new() -> Self {
634 Self::default()
635 }
636
637 pub fn task_queue(mut self, task_queue: impl Into<String>) -> Self {
638 self.task_queue = Some(task_queue.into());
639 self
640 }
641
642 pub fn retry_policy(mut self, policy: ActivityRetryPolicy) -> Self {
643 self.retry_policy = Some(policy);
644 self
645 }
646
647 pub fn start_to_close_timeout(mut self, timeout: Duration) -> Self {
648 self.start_to_close_timeout = Some(timeout);
649 self
650 }
651
652 pub fn schedule_to_start_timeout(mut self, timeout: Duration) -> Self {
653 self.schedule_to_start_timeout = Some(timeout);
654 self
655 }
656
657 pub fn schedule_to_close_timeout(mut self, timeout: Duration) -> Self {
658 self.schedule_to_close_timeout = Some(timeout);
659 self
660 }
661
662 pub fn heartbeat_timeout(mut self, timeout: Duration) -> Self {
663 self.heartbeat_timeout = Some(timeout);
664 self
665 }
666
667 fn validate(&self) -> std::result::Result<ValidatedActivityOptions, ActivityOptionsError> {
668 if self
669 .task_queue
670 .as_deref()
671 .is_some_and(|queue| queue.trim().is_empty())
672 {
673 return Err(ActivityOptionsError::new(
674 ActivityOptionsErrorKind::EmptyTaskQueue,
675 Some("task_queue"),
676 "task_queue must not be empty",
677 ));
678 }
679
680 for (field, value) in [
681 ("start_to_close_timeout", self.start_to_close_timeout),
682 ("schedule_to_start_timeout", self.schedule_to_start_timeout),
683 ("schedule_to_close_timeout", self.schedule_to_close_timeout),
684 ("heartbeat_timeout", self.heartbeat_timeout),
685 ] {
686 if value.is_some_and(|value| value.is_zero()) {
687 return Err(ActivityOptionsError::new(
688 ActivityOptionsErrorKind::TimeoutNotPositive,
689 Some(field),
690 format!("{field} must be positive"),
691 ));
692 }
693 }
694
695 validate_timeout_order(
696 "heartbeat_timeout",
697 self.heartbeat_timeout,
698 "start_to_close_timeout",
699 self.start_to_close_timeout,
700 )?;
701 validate_timeout_order(
702 "start_to_close_timeout",
703 self.start_to_close_timeout,
704 "schedule_to_close_timeout",
705 self.schedule_to_close_timeout,
706 )?;
707 validate_timeout_order(
708 "schedule_to_start_timeout",
709 self.schedule_to_start_timeout,
710 "schedule_to_close_timeout",
711 self.schedule_to_close_timeout,
712 )?;
713
714 Ok(ValidatedActivityOptions {
715 task_queue: self.task_queue.clone(),
716 retry_policy: self
717 .retry_policy
718 .as_ref()
719 .map(validate_activity_retry_policy)
720 .transpose()?,
721 start_to_close_timeout: timeout_seconds(
722 "start_to_close_timeout",
723 self.start_to_close_timeout,
724 )?,
725 schedule_to_start_timeout: timeout_seconds(
726 "schedule_to_start_timeout",
727 self.schedule_to_start_timeout,
728 )?,
729 schedule_to_close_timeout: timeout_seconds(
730 "schedule_to_close_timeout",
731 self.schedule_to_close_timeout,
732 )?,
733 heartbeat_timeout: timeout_seconds("heartbeat_timeout", self.heartbeat_timeout)?,
734 })
735 }
736}
737
738#[derive(Clone, Debug)]
739struct ValidatedActivityOptions {
740 task_queue: Option<String>,
741 retry_policy: Option<Value>,
742 start_to_close_timeout: Option<u64>,
743 schedule_to_start_timeout: Option<u64>,
744 schedule_to_close_timeout: Option<u64>,
745 heartbeat_timeout: Option<u64>,
746}
747
748fn validate_timeout_order(
749 smaller_name: &'static str,
750 smaller: Option<Duration>,
751 larger_name: &'static str,
752 larger: Option<Duration>,
753) -> std::result::Result<(), ActivityOptionsError> {
754 if matches!((smaller, larger), (Some(smaller), Some(larger)) if smaller > larger) {
755 return Err(ActivityOptionsError::new(
756 ActivityOptionsErrorKind::TimeoutOrder,
757 Some(smaller_name),
758 format!("{smaller_name} must be <= {larger_name}"),
759 ));
760 }
761 Ok(())
762}
763
764fn timeout_seconds(
765 field: &'static str,
766 value: Option<Duration>,
767) -> std::result::Result<Option<u64>, ActivityOptionsError> {
768 value
769 .map(|value| {
770 activity_protocol_seconds(value).ok_or_else(|| {
771 ActivityOptionsError::new(
772 ActivityOptionsErrorKind::TimeoutOverflow,
773 Some(field),
774 format!("{field} is too large for the worker protocol"),
775 )
776 })
777 })
778 .transpose()
779}
780
781fn duration_seconds_ceil(value: Duration) -> Option<u64> {
782 value
783 .as_secs()
784 .checked_add(u64::from(value.subsec_nanos() > 0))
785}
786
787fn activity_protocol_seconds(value: Duration) -> Option<u64> {
788 duration_seconds_ceil(value).filter(|seconds| *seconds <= i64::MAX as u64)
789}
790
791fn validate_activity_retry_policy(
792 policy: &ActivityRetryPolicy,
793) -> std::result::Result<Value, ActivityOptionsError> {
794 if policy.max_attempts.is_none()
795 && policy.backoff.is_none()
796 && policy.non_retryable_error_types.is_empty()
797 {
798 return Err(ActivityOptionsError::new(
799 ActivityOptionsErrorKind::EmptyRetryPolicy,
800 Some("retry_policy"),
801 "retry_policy must configure at least one field",
802 ));
803 }
804 if policy.max_attempts == Some(0) {
805 return Err(ActivityOptionsError::new(
806 ActivityOptionsErrorKind::InvalidMaxAttempts,
807 Some("retry_policy.max_attempts"),
808 "max_attempts must be >= 1",
809 ));
810 }
811 if policy
812 .non_retryable_error_types
813 .iter()
814 .any(|error_type| error_type.trim().is_empty())
815 {
816 return Err(ActivityOptionsError::new(
817 ActivityOptionsErrorKind::EmptyNonRetryableErrorType,
818 Some("retry_policy.non_retryable_error_types"),
819 "non_retryable_error_types must not contain empty values",
820 ));
821 }
822
823 let backoff_seconds = match &policy.backoff {
824 None => None,
825 Some(backoff) => {
826 let max_attempts = policy.max_attempts.ok_or_else(|| {
827 ActivityOptionsError::new(
828 ActivityOptionsErrorKind::BackoffWithoutRetryBudget,
829 Some("retry_policy.backoff"),
830 "backoff requires max_attempts",
831 )
832 })?;
833 let retry_count = max_attempts.saturating_sub(1) as usize;
834 let intervals = match backoff {
835 ActivityBackoff::Explicit(intervals) => {
836 if intervals.len() > retry_count {
837 return Err(ActivityOptionsError::new(
838 ActivityOptionsErrorKind::TooManyBackoffIntervals,
839 Some("retry_policy.backoff"),
840 "backoff interval count must not exceed max_attempts - 1",
841 ));
842 }
843 intervals.clone()
844 }
845 ActivityBackoff::Exponential {
846 initial_interval,
847 coefficient,
848 maximum_interval,
849 } => {
850 if *coefficient < 1 {
851 return Err(ActivityOptionsError::new(
852 ActivityOptionsErrorKind::InvalidBackoffCoefficient,
853 Some("retry_policy.backoff.coefficient"),
854 "backoff coefficient must be >= 1",
855 ));
856 }
857 if retry_count > 10_000 {
858 return Err(ActivityOptionsError::new(
859 ActivityOptionsErrorKind::BackoffGenerationTooLarge,
860 Some("retry_policy.max_attempts"),
861 "generated backoff supports at most 10000 retry intervals",
862 ));
863 }
864 let mut current = *initial_interval;
865 let mut intervals = Vec::with_capacity(retry_count);
866 for _ in 0..retry_count {
867 let interval = maximum_interval
868 .map(|maximum| current.min(maximum))
869 .unwrap_or(current);
870 intervals.push(interval);
871 if maximum_interval.is_some_and(|maximum| interval == maximum) {
872 break;
873 }
874 current = current.checked_mul(*coefficient).ok_or_else(|| {
875 ActivityOptionsError::new(
876 ActivityOptionsErrorKind::BackoffOverflow,
877 Some("retry_policy.backoff"),
878 "generated backoff interval overflowed",
879 )
880 })?;
881 }
882 intervals
883 }
884 };
885 Some(
886 intervals
887 .into_iter()
888 .map(|interval| {
889 activity_protocol_seconds(interval).ok_or_else(|| {
890 ActivityOptionsError::new(
891 ActivityOptionsErrorKind::BackoffOverflow,
892 Some("retry_policy.backoff"),
893 "backoff interval is too large for the worker protocol",
894 )
895 })
896 })
897 .collect::<std::result::Result<Vec<_>, _>>()?,
898 )
899 }
900 };
901
902 let mut encoded = serde_json::Map::new();
903 if let Some(max_attempts) = policy.max_attempts {
904 encoded.insert("max_attempts".to_string(), json!(max_attempts));
905 }
906 if let Some(backoff_seconds) = backoff_seconds {
907 encoded.insert("backoff_seconds".to_string(), json!(backoff_seconds));
908 }
909 if !policy.non_retryable_error_types.is_empty() {
910 let mut canonical_error_types = Vec::new();
911 for error_type in policy
912 .non_retryable_error_types
913 .iter()
914 .map(|error_type| error_type.trim())
915 {
916 if !canonical_error_types.contains(&error_type) {
917 canonical_error_types.push(error_type);
918 }
919 }
920 encoded.insert(
921 "non_retryable_error_types".to_string(),
922 json!(canonical_error_types),
923 );
924 }
925 Ok(Value::Object(encoded))
926}
927
928#[derive(Clone, Debug, Error)]
931#[error("non-deterministic workflow replay ({reason}) at sequence {sequence:?}: {message}")]
932pub struct ReplayFailure {
933 pub reason: String,
934 pub sequence: Option<u64>,
935 pub expected: Option<String>,
936 pub actual: Option<String>,
937 pub message: String,
938}
939
940impl ReplayFailure {
941 fn new(
942 reason: impl Into<String>,
943 sequence: Option<u64>,
944 expected: Option<String>,
945 actual: Option<String>,
946 message: impl Into<String>,
947 ) -> Self {
948 Self {
949 reason: reason.into(),
950 sequence,
951 expected,
952 actual,
953 message: message.into(),
954 }
955 }
956}
957
958#[derive(Clone, Debug, Error)]
960#[error("query failed ({reason}, HTTP {status}): {message}")]
961pub struct QueryFailure {
962 pub status: u16,
963 pub reason: String,
964 pub message: String,
965 pub body: Value,
966}
967
968#[derive(Clone, Debug, Error)]
970#[error("protocol rejected ({reason}, HTTP {status}): {message}")]
971pub struct ProtocolFailure {
972 pub status: u16,
973 pub reason: String,
974 pub message: String,
975 pub supported_version: Option<String>,
976 pub requested_version: Option<String>,
977 pub body: Value,
978}
979
980#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
981pub struct PayloadEnvelope {
982 pub codec: String,
983 pub blob: String,
984}
985
986impl PayloadEnvelope {
987 pub fn avro<T: Serialize>(value: &T) -> Result<Self> {
988 encode_payload(value, DEFAULT_CODEC)
989 }
990
991 pub fn json<T: Serialize>(value: &T) -> Result<Self> {
992 encode_payload(value, JSON_CODEC)
993 }
994}
995
996pub fn encode_payload<T: Serialize>(value: &T, codec: &str) -> Result<PayloadEnvelope> {
997 let value = serde_json::to_value(value)?;
998 let blob = encode_value_blob(&value, codec)?;
999
1000 Ok(PayloadEnvelope {
1001 codec: codec.to_string(),
1002 blob,
1003 })
1004}
1005
1006pub fn decode_payload<T: DeserializeOwned>(envelope: &PayloadEnvelope) -> Result<T> {
1007 let value = decode_blob(&envelope.blob, &envelope.codec)?;
1008 Ok(serde_json::from_value(value)?)
1009}
1010
1011fn encode_value_envelope(value: &Value, codec: &str) -> Result<Value> {
1012 Ok(serde_json::to_value(encode_payload(value, codec)?)?)
1013}
1014
1015fn encode_value_blob(value: &Value, codec: &str) -> Result<String> {
1016 match codec {
1017 JSON_CODEC => Ok(serde_json::to_string(value)?),
1018 DEFAULT_CODEC => encode_avro_generic(value),
1019 other => Err(Error::Codec(format!("unsupported payload codec {other:?}"))),
1020 }
1021}
1022
1023fn decode_wire_value(value: &Value, fallback_codec: &str) -> Result<Value> {
1024 if value.is_null() {
1025 return Ok(Value::Null);
1026 }
1027
1028 if let Some(object) = value.as_object() {
1029 if let (Some(codec), Some(blob)) = (
1030 object.get("codec").and_then(Value::as_str),
1031 object.get("blob").and_then(Value::as_str),
1032 ) {
1033 return decode_blob(blob, codec);
1034 }
1035 }
1036
1037 if let Some(blob) = value.as_str() {
1038 return decode_blob(blob, fallback_codec);
1039 }
1040
1041 Ok(value.clone())
1042}
1043
1044fn decode_blob(blob: &str, codec: &str) -> Result<Value> {
1045 match codec {
1046 JSON_CODEC => Ok(serde_json::from_str(blob)?),
1047 DEFAULT_CODEC => decode_avro_generic(blob),
1048 other => Err(Error::Codec(format!("unsupported payload codec {other:?}"))),
1049 }
1050}
1051
1052fn encode_avro_generic(value: &Value) -> Result<String> {
1053 let json = serde_json::to_string(value)?;
1054 let datum = to_value(AvroPayload {
1055 json,
1056 version: AVRO_PAYLOAD_VERSION,
1057 })
1058 .map_err(|err| Error::Codec(format!("could not convert avro generic wrapper: {err}")))?;
1059 let datum = to_avro_datum(avro_payload_schema()?, datum)
1060 .map_err(|err| Error::Codec(format!("could not encode avro generic wrapper: {err}")))?;
1061
1062 let mut bytes = Vec::with_capacity(datum.len() + 1);
1063 bytes.push(0x00);
1064 bytes.extend_from_slice(&datum);
1065 Ok(BASE64.encode(bytes))
1066}
1067
1068fn decode_avro_generic(blob: &str) -> Result<Value> {
1069 let bytes = BASE64
1070 .decode(blob)
1071 .map_err(|err| Error::Codec(format!("invalid avro base64 payload: {err}")))?;
1072
1073 if bytes.is_empty() {
1074 return Err(Error::Codec("avro payload is empty".to_string()));
1075 }
1076
1077 match bytes[0] {
1078 0x00 => {}
1079 0x01 => {
1080 return Err(Error::Codec(
1081 "typed avro payloads require a schema context; v1 supports the generic wrapper"
1082 .to_string(),
1083 ));
1084 }
1085 other => {
1086 return Err(Error::Codec(format!(
1087 "unknown avro payload prefix 0x{other:02x}"
1088 )));
1089 }
1090 }
1091
1092 let mut datum = &bytes[1..];
1093 let datum = from_avro_datum(avro_payload_schema()?, &mut datum, None)
1094 .map_err(|err| Error::Codec(format!("could not decode avro generic wrapper: {err}")))?;
1095 let payload: AvroPayload = from_value(&datum)
1096 .map_err(|err| Error::Codec(format!("invalid avro generic wrapper record: {err}")))?;
1097
1098 if payload.version != AVRO_PAYLOAD_VERSION {
1099 return Err(Error::Codec(format!(
1100 "unsupported avro generic wrapper version {}",
1101 payload.version
1102 )));
1103 }
1104
1105 Ok(serde_json::from_str(&payload.json)?)
1106}
1107
1108#[derive(Debug, Serialize, Deserialize)]
1109struct AvroPayload {
1110 json: String,
1111 version: i32,
1112}
1113
1114fn avro_payload_schema() -> Result<&'static Schema> {
1115 match AVRO_PAYLOAD_SCHEMA.get_or_init(|| {
1116 Schema::parse_str(AVRO_PAYLOAD_SCHEMA_JSON)
1117 .map_err(|err| format!("could not parse avro payload schema: {err}"))
1118 }) {
1119 Ok(schema) => Ok(schema),
1120 Err(message) => Err(Error::Codec(message.clone())),
1121 }
1122}
1123
1124#[derive(Clone, Debug)]
1125pub struct Client {
1126 http: reqwest::Client,
1127 base_url: String,
1128 token: Option<String>,
1129 control_token: Option<String>,
1130 worker_token: Option<String>,
1131 namespace: String,
1132}
1133
1134impl Client {
1135 pub fn new(base_url: impl Into<String>) -> Result<Self> {
1136 Self::builder(base_url).build()
1137 }
1138
1139 pub fn builder(base_url: impl Into<String>) -> ClientBuilder {
1140 ClientBuilder {
1141 base_url: base_url.into(),
1142 token: None,
1143 control_token: None,
1144 worker_token: None,
1145 namespace: "default".to_string(),
1146 timeout: Duration::from_secs(60),
1147 }
1148 }
1149
1150 pub async fn health(&self) -> Result<Value> {
1151 self.request_json(
1152 reqwest::Method::GET,
1153 "/health",
1154 RequestProtocol::ControlPlane,
1155 Option::<&Value>::None,
1156 )
1157 .await
1158 }
1159
1160 pub async fn cluster_info(&self) -> Result<Value> {
1161 self.request_json(
1162 reqwest::Method::GET,
1163 "/cluster/info",
1164 RequestProtocol::ControlPlane,
1165 Option::<&Value>::None,
1166 )
1167 .await
1168 }
1169
1170 pub async fn start_workflow<T: Serialize>(
1171 &self,
1172 workflow_type: &str,
1173 task_queue: &str,
1174 workflow_id: &str,
1175 input: T,
1176 ) -> Result<WorkflowHandle> {
1177 self.start_workflow_with_options(
1178 workflow_type,
1179 task_queue,
1180 workflow_id,
1181 WorkflowStartOptions::default(),
1182 input,
1183 )
1184 .await
1185 }
1186
1187 pub async fn start_workflow_with_options<T: Serialize>(
1190 &self,
1191 workflow_type: &str,
1192 task_queue: &str,
1193 workflow_id: &str,
1194 options: WorkflowStartOptions,
1195 input: T,
1196 ) -> Result<WorkflowHandle> {
1197 options.validate()?;
1198 let input = serde_json::to_value(input)?;
1199 let input_envelope = encode_value_envelope(&normalize_arguments(input), DEFAULT_CODEC)?;
1200 let body = json!({
1201 "workflow_id": workflow_id,
1202 "workflow_type": workflow_type,
1203 "task_queue": task_queue,
1204 "input": input_envelope,
1205 "execution_timeout_seconds": options.execution_timeout_seconds,
1206 "run_timeout_seconds": options.run_timeout_seconds
1207 });
1208
1209 let data: Value = self
1210 .request_json(
1211 reqwest::Method::POST,
1212 "/workflows",
1213 RequestProtocol::ControlPlane,
1214 Some(&body),
1215 )
1216 .await?;
1217
1218 Ok(WorkflowHandle {
1219 client: self.clone(),
1220 workflow_id: data
1221 .get("workflow_id")
1222 .and_then(Value::as_str)
1223 .unwrap_or(workflow_id)
1224 .to_string(),
1225 run_id: data
1226 .get("run_id")
1227 .and_then(Value::as_str)
1228 .map(str::to_string),
1229 workflow_type: data
1230 .get("workflow_type")
1231 .and_then(Value::as_str)
1232 .unwrap_or(workflow_type)
1233 .to_string(),
1234 })
1235 }
1236
1237 pub async fn signal_workflow<T: Serialize>(
1238 &self,
1239 workflow_id: &str,
1240 signal_name: &str,
1241 input: T,
1242 ) -> Result<Value> {
1243 self.signal_workflow_target(workflow_id, None, signal_name, input)
1244 .await
1245 }
1246
1247 pub async fn signal_workflow_run<T: Serialize>(
1249 &self,
1250 workflow_id: &str,
1251 run_id: &str,
1252 signal_name: &str,
1253 input: T,
1254 ) -> Result<Value> {
1255 self.signal_workflow_target(workflow_id, Some(run_id), signal_name, input)
1256 .await
1257 }
1258
1259 async fn signal_workflow_target<T: Serialize>(
1260 &self,
1261 workflow_id: &str,
1262 run_id: Option<&str>,
1263 signal_name: &str,
1264 input: T,
1265 ) -> Result<Value> {
1266 let input = serde_json::to_value(input)?;
1267 let input_envelope = encode_value_envelope(&normalize_arguments(input), DEFAULT_CODEC)?;
1268 let body = json!({
1269 "input": input_envelope
1270 });
1271 let path = match run_id {
1272 Some(run_id) => {
1273 format!("/workflows/{workflow_id}/runs/{run_id}/signal/{signal_name}")
1274 }
1275 None => format!("/workflows/{workflow_id}/signal/{signal_name}"),
1276 };
1277 self.request_json(
1278 reqwest::Method::POST,
1279 &path,
1280 RequestProtocol::ControlPlane,
1281 Some(&body),
1282 )
1283 .await
1284 }
1285
1286 pub async fn cancel_workflow(
1288 &self,
1289 workflow_id: &str,
1290 options: WorkflowCommandOptions,
1291 ) -> Result<WorkflowCommandResult> {
1292 self.workflow_command(workflow_id, None, WorkflowCommandKind::Cancel, options)
1293 .await
1294 }
1295
1296 pub async fn cancel_workflow_run(
1298 &self,
1299 workflow_id: &str,
1300 run_id: &str,
1301 options: WorkflowCommandOptions,
1302 ) -> Result<WorkflowCommandResult> {
1303 self.workflow_command(
1304 workflow_id,
1305 Some(run_id),
1306 WorkflowCommandKind::Cancel,
1307 options,
1308 )
1309 .await
1310 }
1311
1312 pub async fn terminate_workflow(
1314 &self,
1315 workflow_id: &str,
1316 options: WorkflowCommandOptions,
1317 ) -> Result<WorkflowCommandResult> {
1318 self.workflow_command(workflow_id, None, WorkflowCommandKind::Terminate, options)
1319 .await
1320 }
1321
1322 pub async fn terminate_workflow_run(
1324 &self,
1325 workflow_id: &str,
1326 run_id: &str,
1327 options: WorkflowCommandOptions,
1328 ) -> Result<WorkflowCommandResult> {
1329 self.workflow_command(
1330 workflow_id,
1331 Some(run_id),
1332 WorkflowCommandKind::Terminate,
1333 options,
1334 )
1335 .await
1336 }
1337
1338 async fn workflow_command(
1339 &self,
1340 workflow_id: &str,
1341 run_id: Option<&str>,
1342 command: WorkflowCommandKind,
1343 options: WorkflowCommandOptions,
1344 ) -> Result<WorkflowCommandResult> {
1345 let path = match run_id {
1346 Some(run_id) => format!(
1347 "/workflows/{workflow_id}/runs/{run_id}/{}",
1348 command.as_str()
1349 ),
1350 None => format!("/workflows/{workflow_id}/{}", command.as_str()),
1351 };
1352 let data = match self
1353 .request_json(
1354 reqwest::Method::POST,
1355 &path,
1356 RequestProtocol::ControlPlane,
1357 Some(&options),
1358 )
1359 .await
1360 {
1361 Ok(data) => data,
1362 Err(Error::Http { status, body }) => {
1363 return Err(Error::WorkflowCommandRejected(workflow_command_rejection(
1364 command,
1365 status,
1366 body,
1367 workflow_id,
1368 run_id,
1369 )));
1370 }
1371 Err(error) => return Err(error),
1372 };
1373
1374 Ok(workflow_command_result(command, data, workflow_id, run_id))
1375 }
1376
1377 pub async fn query_workflow<T: Serialize>(
1383 &self,
1384 workflow_id: &str,
1385 query_name: &str,
1386 input: T,
1387 ) -> Result<Value> {
1388 self.query_workflow_target(workflow_id, None, query_name, input)
1389 .await
1390 }
1391
1392 pub async fn query_workflow_run<T: Serialize>(
1394 &self,
1395 workflow_id: &str,
1396 run_id: &str,
1397 query_name: &str,
1398 input: T,
1399 ) -> Result<Value> {
1400 self.query_workflow_target(workflow_id, Some(run_id), query_name, input)
1401 .await
1402 }
1403
1404 async fn query_workflow_target<T: Serialize>(
1405 &self,
1406 workflow_id: &str,
1407 run_id: Option<&str>,
1408 query_name: &str,
1409 input: T,
1410 ) -> Result<Value> {
1411 let input = serde_json::to_value(input)?;
1412 let input_envelope = encode_value_envelope(&normalize_arguments(input), DEFAULT_CODEC)?;
1413 let body = json!({
1414 "input": input_envelope
1415 });
1416 let path = match run_id {
1417 Some(run_id) => {
1418 format!("/workflows/{workflow_id}/runs/{run_id}/query/{query_name}")
1419 }
1420 None => format!("/workflows/{workflow_id}/query/{query_name}"),
1421 };
1422 let response: Value = match self
1423 .request_json(
1424 reqwest::Method::POST,
1425 &path,
1426 RequestProtocol::ControlPlane,
1427 Some(&body),
1428 )
1429 .await
1430 {
1431 Ok(response) => response,
1432 Err(Error::Http { status, body }) => {
1433 return Err(Error::QueryFailed(query_failure(status, body)));
1434 }
1435 Err(error) => return Err(error),
1436 };
1437
1438 if let Some(envelope) = response
1439 .get("result_envelope")
1440 .filter(|envelope| !envelope.is_null())
1441 {
1442 return decode_wire_value(envelope, DEFAULT_CODEC);
1443 }
1444
1445 Ok(response.get("result").cloned().unwrap_or(Value::Null))
1446 }
1447
1448 pub async fn describe_workflow(&self, workflow_id: &str) -> Result<WorkflowDescription> {
1449 let path = format!("/workflows/{workflow_id}");
1450 let mut data: WorkflowDescription = self
1451 .request_json(
1452 reqwest::Method::GET,
1453 &path,
1454 RequestProtocol::ControlPlane,
1455 Option::<&Value>::None,
1456 )
1457 .await?;
1458 data.decode_payloads()?;
1459 Ok(data)
1460 }
1461
1462 pub async fn describe_workflow_run(
1464 &self,
1465 workflow_id: &str,
1466 run_id: &str,
1467 ) -> Result<WorkflowDescription> {
1468 let path = format!("/workflows/{workflow_id}/runs/{run_id}");
1469 let mut data: WorkflowDescription = self
1470 .request_json(
1471 reqwest::Method::GET,
1472 &path,
1473 RequestProtocol::ControlPlane,
1474 Option::<&Value>::None,
1475 )
1476 .await?;
1477 data.decode_payloads()?;
1478 Ok(data)
1479 }
1480
1481 pub async fn register_worker(
1482 &self,
1483 worker_id: &str,
1484 task_queue: &str,
1485 supported_workflow_types: Vec<String>,
1486 supported_activity_types: Vec<String>,
1487 max_concurrent_workflow_tasks: usize,
1488 max_concurrent_activity_tasks: usize,
1489 ) -> Result<RegisterWorkerResponse> {
1490 self.register_worker_with_capabilities(
1491 worker_id,
1492 task_queue,
1493 supported_workflow_types,
1494 supported_activity_types,
1495 max_concurrent_workflow_tasks,
1496 max_concurrent_activity_tasks,
1497 Vec::new(),
1498 )
1499 .await
1500 }
1501
1502 pub async fn register_worker_with_capabilities(
1504 &self,
1505 worker_id: &str,
1506 task_queue: &str,
1507 supported_workflow_types: Vec<String>,
1508 supported_activity_types: Vec<String>,
1509 max_concurrent_workflow_tasks: usize,
1510 max_concurrent_activity_tasks: usize,
1511 capabilities: Vec<String>,
1512 ) -> Result<RegisterWorkerResponse> {
1513 let body = json!({
1514 "worker_id": worker_id,
1515 "task_queue": task_queue,
1516 "runtime": "rust",
1517 "sdk_version": SDK_VERSION,
1518 "supported_workflow_types": supported_workflow_types,
1519 "supported_activity_types": supported_activity_types,
1520 "capabilities": capabilities,
1521 "max_concurrent_workflow_tasks": max_concurrent_workflow_tasks,
1522 "max_concurrent_activity_tasks": max_concurrent_activity_tasks
1523 });
1524
1525 self.request_json(
1526 reqwest::Method::POST,
1527 "/worker/register",
1528 RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
1529 Some(&body),
1530 )
1531 .await
1532 }
1533
1534 pub async fn poll_query_task(
1536 &self,
1537 worker_id: &str,
1538 task_queue: &str,
1539 timeout: Duration,
1540 ) -> Result<Option<QueryTask>> {
1541 Ok(self
1542 .poll_query_task_response(worker_id, task_queue, timeout)
1543 .await?
1544 .task)
1545 }
1546
1547 pub async fn poll_query_task_response(
1549 &self,
1550 worker_id: &str,
1551 task_queue: &str,
1552 timeout: Duration,
1553 ) -> Result<PollQueryTaskResponse> {
1554 let timeout_seconds = long_poll_timeout_seconds(timeout);
1555 let body = json!({
1556 "worker_id": worker_id,
1557 "task_queue": task_queue,
1558 "poll_request_id": unique_request_id("rust-query-poll"),
1559 "timeout_seconds": timeout_seconds,
1560 });
1561 worker_poll_response(
1562 self.request_json_with_timeout(
1563 reqwest::Method::POST,
1564 "/worker/query-tasks/poll",
1565 RequestProtocol::Worker(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION),
1566 Some(&body),
1567 timeout + Duration::from_secs(5),
1568 )
1569 .await,
1570 )
1571 }
1572
1573 pub async fn complete_query_task(
1575 &self,
1576 query_task_id: &str,
1577 lease_owner: &str,
1578 query_task_attempt: u64,
1579 result: Value,
1580 codec: &str,
1581 ) -> Result<Value> {
1582 let result_envelope = encode_value_envelope(&result, codec)?;
1583 self.complete_query_task_with_envelope(
1584 query_task_id,
1585 lease_owner,
1586 query_task_attempt,
1587 result,
1588 result_envelope,
1589 )
1590 .await
1591 }
1592
1593 async fn complete_query_task_with_envelope(
1594 &self,
1595 query_task_id: &str,
1596 lease_owner: &str,
1597 query_task_attempt: u64,
1598 result: Value,
1599 result_envelope: Value,
1600 ) -> Result<Value> {
1601 let body = json!({
1602 "lease_owner": lease_owner,
1603 "query_task_attempt": query_task_attempt,
1604 "result": result,
1605 "result_envelope": result_envelope,
1606 });
1607 let path = format!("/worker/query-tasks/{query_task_id}/complete");
1608 let response = self
1609 .request_json(
1610 reqwest::Method::POST,
1611 &path,
1612 RequestProtocol::Worker(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION),
1613 Some(&body),
1614 )
1615 .await;
1616 query_task_response(response)
1617 }
1618
1619 pub async fn fail_query_task(
1621 &self,
1622 query_task_id: &str,
1623 lease_owner: &str,
1624 query_task_attempt: u64,
1625 message: impl Into<String>,
1626 reason: impl Into<String>,
1627 failure_type: impl Into<String>,
1628 ) -> Result<Value> {
1629 let body = json!({
1630 "lease_owner": lease_owner,
1631 "query_task_attempt": query_task_attempt,
1632 "failure": {
1633 "message": message.into(),
1634 "reason": reason.into(),
1635 "type": failure_type.into(),
1636 }
1637 });
1638 let path = format!("/worker/query-tasks/{query_task_id}/fail");
1639 let response = self
1640 .request_json(
1641 reqwest::Method::POST,
1642 &path,
1643 RequestProtocol::Worker(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION),
1644 Some(&body),
1645 )
1646 .await;
1647 query_task_response(response)
1648 }
1649
1650 pub async fn heartbeat_worker(
1651 &self,
1652 worker_id: &str,
1653 workflow_available: usize,
1654 activity_available: usize,
1655 ) -> Result<Value> {
1656 let body = json!({
1657 "worker_id": worker_id,
1658 "task_slots": {
1659 "workflow_available": workflow_available,
1660 "activity_available": activity_available
1661 },
1662 "process_metrics": {
1663 "process_id": std::process::id(),
1664 "process_uptime_seconds": 0
1665 }
1666 });
1667
1668 self.request_json(
1669 reqwest::Method::POST,
1670 "/worker/heartbeat",
1671 RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
1672 Some(&body),
1673 )
1674 .await
1675 }
1676
1677 pub async fn poll_workflow_task(
1678 &self,
1679 worker_id: &str,
1680 task_queue: &str,
1681 timeout: Duration,
1682 ) -> Result<Option<WorkflowTask>> {
1683 Ok(self
1684 .poll_workflow_task_response(worker_id, task_queue, timeout)
1685 .await?
1686 .task)
1687 }
1688
1689 pub async fn poll_workflow_task_response(
1690 &self,
1691 worker_id: &str,
1692 task_queue: &str,
1693 timeout: Duration,
1694 ) -> Result<PollWorkflowTaskResponse> {
1695 let body = json!({
1696 "worker_id": worker_id,
1697 "task_queue": task_queue,
1698 "timeout_seconds": long_poll_timeout_seconds(timeout),
1699 });
1700 let mut data: PollWorkflowTaskResponse = worker_poll_response(
1701 self.request_json_with_timeout(
1702 reqwest::Method::POST,
1703 "/worker/workflow-tasks/poll",
1704 RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
1705 Some(&body),
1706 timeout + Duration::from_secs(5),
1707 )
1708 .await,
1709 )?;
1710
1711 if let Some(task) = data.task.as_mut() {
1712 self.fetch_remaining_workflow_history(worker_id, task)
1713 .await?;
1714 }
1715
1716 Ok(data)
1717 }
1718
1719 async fn fetch_remaining_workflow_history(
1720 &self,
1721 worker_id: &str,
1722 task: &mut WorkflowTask,
1723 ) -> Result<()> {
1724 let mut next_token = task.next_history_page_token.clone();
1725
1726 while let Some(token) = next_token.take().filter(|token| !token.is_empty()) {
1727 let lease_owner = task
1728 .lease_owner
1729 .clone()
1730 .unwrap_or_else(|| worker_id.to_string());
1731 let page = self
1732 .workflow_task_history_page(
1733 &task.task_id,
1734 &lease_owner,
1735 task.workflow_task_attempt,
1736 &token,
1737 )
1738 .await?;
1739
1740 task.append_history_page(page);
1741
1742 if task.next_history_page_token.as_deref() == Some(token.as_str()) {
1743 return Err(Error::Codec(
1744 "workflow history pagination returned the same page token".to_string(),
1745 ));
1746 }
1747
1748 next_token = task.next_history_page_token.clone();
1749 }
1750
1751 Ok(())
1752 }
1753
1754 async fn workflow_task_history_page(
1755 &self,
1756 task_id: &str,
1757 lease_owner: &str,
1758 workflow_task_attempt: u64,
1759 next_history_page_token: &str,
1760 ) -> Result<WorkflowTaskHistoryPage> {
1761 let body = json!({
1762 "lease_owner": lease_owner,
1763 "workflow_task_attempt": workflow_task_attempt,
1764 "next_history_page_token": next_history_page_token
1765 });
1766 let path = format!("/worker/workflow-tasks/{task_id}/history");
1767
1768 self.request_json(
1769 reqwest::Method::POST,
1770 &path,
1771 RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
1772 Some(&body),
1773 )
1774 .await
1775 }
1776
1777 pub async fn complete_workflow_task(
1778 &self,
1779 task_id: &str,
1780 lease_owner: &str,
1781 workflow_task_attempt: u64,
1782 commands: Vec<Value>,
1783 ) -> Result<Value> {
1784 let body = json!({
1785 "lease_owner": lease_owner,
1786 "workflow_task_attempt": workflow_task_attempt,
1787 "commands": commands
1788 });
1789 let path = format!("/worker/workflow-tasks/{task_id}/complete");
1790 self.request_json(
1791 reqwest::Method::POST,
1792 &path,
1793 RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
1794 Some(&body),
1795 )
1796 .await
1797 }
1798
1799 pub async fn fail_workflow_task(
1800 &self,
1801 task_id: &str,
1802 lease_owner: &str,
1803 workflow_task_attempt: u64,
1804 message: impl Into<String>,
1805 ) -> Result<Value> {
1806 self.fail_workflow_task_with_type(
1807 task_id,
1808 lease_owner,
1809 workflow_task_attempt,
1810 message,
1811 "RustWorkflowTaskFailure",
1812 )
1813 .await
1814 }
1815
1816 async fn fail_workflow_task_with_type(
1817 &self,
1818 task_id: &str,
1819 lease_owner: &str,
1820 workflow_task_attempt: u64,
1821 message: impl Into<String>,
1822 failure_type: &str,
1823 ) -> Result<Value> {
1824 let body = json!({
1825 "lease_owner": lease_owner,
1826 "workflow_task_attempt": workflow_task_attempt,
1827 "failure": {
1828 "message": message.into(),
1829 "type": failure_type
1830 }
1831 });
1832 let path = format!("/worker/workflow-tasks/{task_id}/fail");
1833 self.request_json(
1834 reqwest::Method::POST,
1835 &path,
1836 RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
1837 Some(&body),
1838 )
1839 .await
1840 }
1841
1842 pub async fn poll_activity_task(
1843 &self,
1844 worker_id: &str,
1845 task_queue: &str,
1846 timeout: Duration,
1847 ) -> Result<Option<ActivityTask>> {
1848 Ok(self
1849 .poll_activity_task_response(worker_id, task_queue, timeout)
1850 .await?
1851 .task)
1852 }
1853
1854 pub async fn poll_activity_task_response(
1856 &self,
1857 worker_id: &str,
1858 task_queue: &str,
1859 timeout: Duration,
1860 ) -> Result<PollActivityTaskResponse> {
1861 let body = json!({
1862 "worker_id": worker_id,
1863 "task_queue": task_queue,
1864 "timeout_seconds": long_poll_timeout_seconds(timeout),
1865 });
1866 let data: PollActivityTaskResponse = worker_poll_response(
1867 self.request_json_with_timeout(
1868 reqwest::Method::POST,
1869 "/worker/activity-tasks/poll",
1870 RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
1871 Some(&body),
1872 timeout + Duration::from_secs(5),
1873 )
1874 .await,
1875 )?;
1876 Ok(data)
1877 }
1878
1879 pub async fn complete_activity_task(
1880 &self,
1881 task_id: &str,
1882 activity_attempt_id: &str,
1883 lease_owner: &str,
1884 result: Value,
1885 codec: &str,
1886 ) -> Result<Value> {
1887 let result = encode_value_envelope(&result, codec)?;
1888 let body = json!({
1889 "activity_attempt_id": activity_attempt_id,
1890 "lease_owner": lease_owner,
1891 "result": result
1892 });
1893 let path = format!("/worker/activity-tasks/{task_id}/complete");
1894 activity_task_response(
1895 self.request_json(
1896 reqwest::Method::POST,
1897 &path,
1898 RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
1899 Some(&body),
1900 )
1901 .await,
1902 "complete",
1903 task_id,
1904 activity_attempt_id,
1905 )
1906 }
1907
1908 pub async fn fail_activity_task(
1909 &self,
1910 task_id: &str,
1911 activity_attempt_id: &str,
1912 lease_owner: &str,
1913 message: impl Into<String>,
1914 non_retryable: bool,
1915 ) -> Result<Value> {
1916 let body = json!({
1917 "activity_attempt_id": activity_attempt_id,
1918 "lease_owner": lease_owner,
1919 "failure": {
1920 "message": message.into(),
1921 "type": "RustActivityFailure",
1922 "non_retryable": non_retryable
1923 }
1924 });
1925 let path = format!("/worker/activity-tasks/{task_id}/fail");
1926 activity_task_response(
1927 self.request_json(
1928 reqwest::Method::POST,
1929 &path,
1930 RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
1931 Some(&body),
1932 )
1933 .await,
1934 "fail",
1935 task_id,
1936 activity_attempt_id,
1937 )
1938 }
1939
1940 pub async fn heartbeat_activity_task(
1941 &self,
1942 task_id: &str,
1943 activity_attempt_id: &str,
1944 lease_owner: &str,
1945 details: Value,
1946 ) -> Result<ActivityHeartbeatResponse> {
1947 let body = json!({
1948 "activity_attempt_id": activity_attempt_id,
1949 "lease_owner": lease_owner,
1950 "details": details
1951 });
1952 let path = format!("/worker/activity-tasks/{task_id}/heartbeat");
1953 activity_task_response(
1954 self.request_json(
1955 reqwest::Method::POST,
1956 &path,
1957 RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
1958 Some(&body),
1959 )
1960 .await,
1961 "heartbeat",
1962 task_id,
1963 activity_attempt_id,
1964 )
1965 }
1966
1967 async fn request_json<T: DeserializeOwned, B: Serialize + ?Sized>(
1968 &self,
1969 method: reqwest::Method,
1970 path: &str,
1971 protocol: RequestProtocol,
1972 body: Option<&B>,
1973 ) -> Result<T> {
1974 self.request_json_with_timeout(method, path, protocol, body, Duration::from_secs(60))
1975 .await
1976 }
1977
1978 async fn request_json_with_timeout<T: DeserializeOwned, B: Serialize + ?Sized>(
1979 &self,
1980 method: reqwest::Method,
1981 path: &str,
1982 protocol: RequestProtocol,
1983 body: Option<&B>,
1984 timeout: Duration,
1985 ) -> Result<T> {
1986 let mut request = self
1987 .http
1988 .request(method, format!("{}/api{}", self.base_url, path))
1989 .timeout(timeout)
1990 .header(reqwest::header::ACCEPT, "application/json")
1991 .header(reqwest::header::CONTENT_TYPE, "application/json")
1992 .header("X-Namespace", &self.namespace);
1993
1994 match protocol {
1995 RequestProtocol::Worker(version) => {
1996 request = request.header("X-Durable-Workflow-Protocol-Version", version);
1997 }
1998 RequestProtocol::ControlPlane => {
1999 request = request.header(
2000 "X-Durable-Workflow-Control-Plane-Version",
2001 CONTROL_PLANE_VERSION,
2002 );
2003 }
2004 }
2005
2006 if let Some(token) = self.auth_token(protocol.is_worker()) {
2007 request = request.bearer_auth(token);
2008 }
2009
2010 if let Some(body) = body {
2011 request = request.json(body);
2012 }
2013
2014 let response = request.send().await?;
2015 let status = response.status();
2016 let bytes = response.bytes().await?;
2017
2018 if !status.is_success() {
2019 let body = String::from_utf8_lossy(&bytes).to_string();
2020 if let Some(protocol) = protocol_failure(status, &body) {
2021 return Err(Error::Protocol(protocol));
2022 }
2023 return Err(Error::Http { status, body });
2024 }
2025
2026 if bytes.is_empty() {
2027 return Ok(serde_json::from_value(Value::Null)?);
2028 }
2029
2030 Ok(serde_json::from_slice(&bytes)?)
2031 }
2032
2033 fn auth_token(&self, worker: bool) -> Option<&str> {
2034 if worker {
2035 self.worker_token
2036 .as_deref()
2037 .or(self.token.as_deref())
2038 .or(self.control_token.as_deref())
2039 } else {
2040 self.control_token
2041 .as_deref()
2042 .or(self.token.as_deref())
2043 .or(self.worker_token.as_deref())
2044 }
2045 }
2046}
2047
2048fn query_failure(status: reqwest::StatusCode, raw_body: String) -> QueryFailure {
2049 let body = serde_json::from_str(&raw_body).unwrap_or_else(|_| json!({"message": raw_body}));
2050 let reason = body
2051 .get("reason")
2052 .and_then(Value::as_str)
2053 .unwrap_or("query_rejected")
2054 .to_string();
2055 let message = body
2056 .get("message")
2057 .or_else(|| body.get("error"))
2058 .and_then(Value::as_str)
2059 .unwrap_or("workflow query was rejected")
2060 .to_string();
2061
2062 QueryFailure {
2063 status: status.as_u16(),
2064 reason,
2065 message,
2066 body,
2067 }
2068}
2069
2070fn workflow_command_result(
2071 command: WorkflowCommandKind,
2072 data: Value,
2073 workflow_id: &str,
2074 run_id: Option<&str>,
2075) -> WorkflowCommandResult {
2076 WorkflowCommandResult {
2077 command,
2078 workflow_id: data
2079 .get("workflow_id")
2080 .and_then(Value::as_str)
2081 .unwrap_or(workflow_id)
2082 .to_string(),
2083 run_id: data
2084 .get("run_id")
2085 .and_then(Value::as_str)
2086 .or(run_id)
2087 .map(str::to_string),
2088 outcome: data
2089 .get("outcome")
2090 .and_then(Value::as_str)
2091 .map(str::to_string),
2092 reason: data
2093 .get("reason")
2094 .and_then(Value::as_str)
2095 .map(str::to_string),
2096 command_status: data
2097 .get("command_status")
2098 .and_then(Value::as_str)
2099 .map(str::to_string),
2100 raw: data,
2101 }
2102}
2103
2104fn workflow_command_rejection(
2105 command: WorkflowCommandKind,
2106 status: reqwest::StatusCode,
2107 raw_body: String,
2108 workflow_id: &str,
2109 run_id: Option<&str>,
2110) -> WorkflowCommandRejection {
2111 let body = serde_json::from_str(&raw_body).unwrap_or_else(|_| json!({"message": raw_body}));
2112 WorkflowCommandRejection {
2113 command,
2114 status: status.as_u16(),
2115 reason: body
2116 .get("reason")
2117 .and_then(Value::as_str)
2118 .unwrap_or("workflow_command_rejected")
2119 .to_string(),
2120 message: body
2121 .get("message")
2122 .or_else(|| body.get("error"))
2123 .and_then(Value::as_str)
2124 .unwrap_or("workflow lifecycle command was rejected")
2125 .to_string(),
2126 workflow_id: body
2127 .get("workflow_id")
2128 .and_then(Value::as_str)
2129 .unwrap_or(workflow_id)
2130 .to_string(),
2131 run_id: body
2132 .get("run_id")
2133 .and_then(Value::as_str)
2134 .or(run_id)
2135 .map(str::to_string),
2136 target_scope: body
2137 .get("target_scope")
2138 .and_then(Value::as_str)
2139 .map(str::to_string),
2140 body,
2141 }
2142}
2143
2144fn query_task_response(response: Result<Value>) -> Result<Value> {
2145 match response {
2146 Err(Error::Http { status, body }) => Err(Error::QueryFailed(query_failure(status, body))),
2147 response => response,
2148 }
2149}
2150
2151fn worker_poll_response<T: DeserializeOwned>(response: Result<T>) -> Result<T> {
2152 match response {
2153 Err(Error::Http { status, body })
2154 if status == reqwest::StatusCode::CONFLICT && worker_poll_body_is_stop(&body) =>
2155 {
2156 Ok(serde_json::from_str(&body)?)
2157 }
2158 response => response,
2159 }
2160}
2161
2162fn worker_poll_body_is_stop(body: &str) -> bool {
2163 serde_json::from_str::<Value>(body)
2164 .ok()
2165 .is_some_and(|body| {
2166 worker_poll_is_stop(
2167 body.get("poll_status").and_then(Value::as_str),
2168 body.get("reason").and_then(Value::as_str),
2169 )
2170 })
2171}
2172
2173fn worker_poll_is_stop(poll_status: Option<&str>, reason: Option<&str>) -> bool {
2174 matches!(poll_status, Some("draining" | "stopped"))
2175 || matches!(reason, Some("worker_draining" | "worker_stopped"))
2176}
2177
2178fn query_task_rejection_is_final(error: &Error) -> bool {
2179 matches!(
2180 error,
2181 Error::QueryFailed(failure)
2182 if QUERY_TASK_FINAL_REJECTION_REASONS.contains(&failure.reason.as_str())
2183 )
2184}
2185
2186fn activity_task_response<T>(
2187 response: Result<T>,
2188 operation: &str,
2189 task_id: &str,
2190 activity_attempt_id: &str,
2191) -> Result<T> {
2192 match response {
2193 Err(Error::Http { status, body }) => {
2194 let body = serde_json::from_str(&body).unwrap_or_else(|_| json!({"message": body}));
2195 Err(Error::ActivityTaskRejected(ActivityTaskRejection {
2196 operation: operation.to_string(),
2197 status: status.as_u16(),
2198 reason: body
2199 .get("reason")
2200 .and_then(Value::as_str)
2201 .unwrap_or("activity_task_rejected")
2202 .to_string(),
2203 task_id: body
2204 .get("task_id")
2205 .and_then(Value::as_str)
2206 .unwrap_or(task_id)
2207 .to_string(),
2208 activity_attempt_id: body
2209 .get("activity_attempt_id")
2210 .and_then(Value::as_str)
2211 .unwrap_or(activity_attempt_id)
2212 .to_string(),
2213 cancel_requested: body
2214 .get("cancel_requested")
2215 .and_then(Value::as_bool)
2216 .unwrap_or(false),
2217 can_continue: body.get("can_continue").and_then(Value::as_bool),
2218 run_closed_reason: body
2219 .get("run_closed_reason")
2220 .and_then(Value::as_str)
2221 .map(str::to_string),
2222 body,
2223 }))
2224 }
2225 response => response,
2226 }
2227}
2228
2229fn activity_task_rejection_is_final(error: &Error) -> bool {
2230 matches!(
2231 error,
2232 Error::ActivityTaskRejected(rejection)
2233 if matches!(
2234 rejection.reason.as_str(),
2235 "run_cancelled"
2236 | "run_terminated"
2237 | "attempt_closed"
2238 | "stale_attempt"
2239 | "activity_cancelled"
2240 | "task_cancelled"
2241 | "run_closed"
2242 | "activity_not_running"
2243 | "attempt_not_found"
2244 )
2245 )
2246}
2247
2248fn workflow_task_completion_is_terminal_timeout(
2249 error: &Error,
2250 task_id: &str,
2251 workflow_task_attempt: u64,
2252 run_id: Option<&str>,
2253) -> bool {
2254 let Error::Http { status, body } = error else {
2255 return false;
2256 };
2257 if *status != reqwest::StatusCode::CONFLICT {
2258 return false;
2259 }
2260
2261 let Some(run_id) = run_id else {
2262 return false;
2263 };
2264 let Ok(body) = serde_json::from_str::<Value>(body) else {
2265 return false;
2266 };
2267
2268 body.get("recorded").and_then(Value::as_bool) == Some(false)
2269 && body.get("reason").and_then(Value::as_str) == Some("run_timed_out")
2270 && body.get("run_status").and_then(Value::as_str) == Some("failed")
2271 && body.get("run_id").and_then(Value::as_str) == Some(run_id)
2272 && body.get("task_id").and_then(Value::as_str) == Some(task_id)
2273 && body.get("workflow_task_attempt").and_then(Value::as_u64) == Some(workflow_task_attempt)
2274}
2275
2276fn protocol_failure(status: reqwest::StatusCode, raw_body: &str) -> Option<ProtocolFailure> {
2277 let body: Value = serde_json::from_str(raw_body).ok()?;
2278 let reason = body.get("reason")?.as_str()?;
2279 if !matches!(
2280 reason,
2281 "missing_protocol_version"
2282 | "unsupported_protocol_version"
2283 | "missing_control_plane_version"
2284 | "unsupported_control_plane_version"
2285 ) {
2286 return None;
2287 }
2288
2289 Some(ProtocolFailure {
2290 status: status.as_u16(),
2291 reason: reason.to_string(),
2292 message: body
2293 .get("message")
2294 .or_else(|| body.get("error"))
2295 .and_then(Value::as_str)
2296 .unwrap_or("protocol version rejected")
2297 .to_string(),
2298 supported_version: body
2299 .get("supported_version")
2300 .and_then(Value::as_str)
2301 .map(str::to_string),
2302 requested_version: body
2303 .get("requested_version")
2304 .and_then(Value::as_str)
2305 .map(str::to_string),
2306 body,
2307 })
2308}
2309
2310fn long_poll_timeout_seconds(timeout: Duration) -> u64 {
2311 timeout
2312 .as_secs()
2313 .saturating_add(u64::from(timeout.subsec_nanos() > 0))
2314 .min(MAX_LONG_POLL_TIMEOUT_SECONDS)
2315}
2316
2317fn worker_operation_is_retryable(error: &Error) -> bool {
2318 match error {
2319 Error::Transport(error) => {
2320 error.is_timeout() || error.is_connect() || error.is_request() || error.is_body()
2321 }
2322 Error::Http { status, .. } => {
2323 matches!(
2324 *status,
2325 reqwest::StatusCode::REQUEST_TIMEOUT | reqwest::StatusCode::TOO_MANY_REQUESTS
2326 ) || status.is_server_error()
2327 }
2328 _ => false,
2329 }
2330}
2331
2332fn worker_retry_delay(policy: WorkerRetryPolicy, retry: usize) -> Duration {
2333 let exponent = retry.saturating_sub(1).min(31) as u32;
2334 policy
2335 .initial_backoff
2336 .saturating_mul(1_u32 << exponent)
2337 .min(policy.max_backoff)
2338}
2339
2340#[derive(Debug)]
2341pub struct ClientBuilder {
2342 base_url: String,
2343 token: Option<String>,
2344 control_token: Option<String>,
2345 worker_token: Option<String>,
2346 namespace: String,
2347 timeout: Duration,
2348}
2349
2350impl ClientBuilder {
2351 pub fn token(mut self, token: Option<String>) -> Self {
2352 self.token = token;
2353 self
2354 }
2355
2356 pub fn control_token(mut self, token: Option<String>) -> Self {
2357 self.control_token = token;
2358 self
2359 }
2360
2361 pub fn worker_token(mut self, token: Option<String>) -> Self {
2362 self.worker_token = token;
2363 self
2364 }
2365
2366 pub fn namespace(mut self, namespace: impl Into<String>) -> Self {
2367 self.namespace = namespace.into();
2368 self
2369 }
2370
2371 pub fn timeout(mut self, timeout: Duration) -> Self {
2372 self.timeout = timeout;
2373 self
2374 }
2375
2376 pub fn build(self) -> Result<Client> {
2377 Ok(Client {
2378 http: reqwest::Client::builder().timeout(self.timeout).build()?,
2379 base_url: self.base_url.trim_end_matches('/').to_string(),
2380 token: self.token,
2381 control_token: self.control_token,
2382 worker_token: self.worker_token,
2383 namespace: self.namespace,
2384 })
2385 }
2386}
2387
2388#[derive(Clone, Debug)]
2389pub struct WorkflowHandle {
2390 client: Client,
2391 pub workflow_id: String,
2392 pub run_id: Option<String>,
2393 pub workflow_type: String,
2394}
2395
2396impl WorkflowHandle {
2397 pub async fn describe(&self) -> Result<WorkflowDescription> {
2399 self.client.describe_workflow(&self.workflow_id).await
2400 }
2401
2402 pub async fn describe_selected_run(&self) -> Result<WorkflowDescription> {
2404 let run_id = self.run_id.as_deref().ok_or_else(|| {
2405 Error::Codec("run_id is required for selected-run description".to_string())
2406 })?;
2407 self.client
2408 .describe_workflow_run(&self.workflow_id, run_id)
2409 .await
2410 }
2411
2412 pub async fn signal<T: Serialize>(&self, signal_name: &str, input: T) -> Result<Value> {
2413 self.client
2414 .signal_workflow(&self.workflow_id, signal_name, input)
2415 .await
2416 }
2417
2418 pub async fn signal_selected_run<T: Serialize>(
2420 &self,
2421 signal_name: &str,
2422 input: T,
2423 ) -> Result<Value> {
2424 let run_id = self.run_id.as_deref().ok_or_else(|| {
2425 Error::Codec("run_id is required for selected-run signaling".to_string())
2426 })?;
2427 self.client
2428 .signal_workflow_run(&self.workflow_id, run_id, signal_name, input)
2429 .await
2430 }
2431
2432 pub async fn cancel(&self, options: WorkflowCommandOptions) -> Result<WorkflowCommandResult> {
2434 self.client
2435 .cancel_workflow(&self.workflow_id, options)
2436 .await
2437 }
2438
2439 pub async fn cancel_selected_run(
2441 &self,
2442 options: WorkflowCommandOptions,
2443 ) -> Result<WorkflowCommandResult> {
2444 let run_id = self.run_id.as_deref().ok_or_else(|| {
2445 Error::Codec("run_id is required for selected-run cancellation".to_string())
2446 })?;
2447 self.client
2448 .cancel_workflow_run(&self.workflow_id, run_id, options)
2449 .await
2450 }
2451
2452 pub async fn terminate(
2454 &self,
2455 options: WorkflowCommandOptions,
2456 ) -> Result<WorkflowCommandResult> {
2457 self.client
2458 .terminate_workflow(&self.workflow_id, options)
2459 .await
2460 }
2461
2462 pub async fn terminate_selected_run(
2464 &self,
2465 options: WorkflowCommandOptions,
2466 ) -> Result<WorkflowCommandResult> {
2467 let run_id = self.run_id.as_deref().ok_or_else(|| {
2468 Error::Codec("run_id is required for selected-run termination".to_string())
2469 })?;
2470 self.client
2471 .terminate_workflow_run(&self.workflow_id, run_id, options)
2472 .await
2473 }
2474
2475 pub async fn query<T: Serialize>(&self, query_name: &str, input: T) -> Result<Value> {
2477 self.client
2478 .query_workflow(&self.workflow_id, query_name, input)
2479 .await
2480 }
2481
2482 pub async fn query_selected_run<T: Serialize>(
2484 &self,
2485 query_name: &str,
2486 input: T,
2487 ) -> Result<Value> {
2488 let run_id = self
2489 .run_id
2490 .as_deref()
2491 .ok_or_else(|| Error::Codec("run_id is required for selected-run query".to_string()))?;
2492 self.client
2493 .query_workflow_run(&self.workflow_id, run_id, query_name, input)
2494 .await
2495 }
2496
2497 pub async fn result(&self, options: WorkflowResultOptions) -> Result<Value> {
2499 self.result_target(options, None).await
2500 }
2501
2502 pub async fn result_selected_run(&self, options: WorkflowResultOptions) -> Result<Value> {
2504 let run_id = self.run_id.as_deref().ok_or_else(|| {
2505 Error::Codec("run_id is required for selected-run result".to_string())
2506 })?;
2507 self.result_target(options, Some(run_id)).await
2508 }
2509
2510 async fn result_target(
2511 &self,
2512 options: WorkflowResultOptions,
2513 selected_run_id: Option<&str>,
2514 ) -> Result<Value> {
2515 let started = Instant::now();
2516
2517 loop {
2518 let description = match selected_run_id {
2519 Some(run_id) => {
2520 self.client
2521 .describe_workflow_run(&self.workflow_id, run_id)
2522 .await?
2523 }
2524 None => self.describe().await?,
2525 };
2526 if description.is_completed() {
2527 return Ok(description.output.unwrap_or(Value::Null));
2528 }
2529
2530 if description.is_terminal() {
2531 let outcome =
2532 workflow_terminal_outcome(&description, &self.workflow_id, selected_run_id);
2533 return Err(match outcome.kind {
2534 WorkflowTerminalKind::Failed => Error::WorkflowFailed(outcome),
2535 WorkflowTerminalKind::Cancelled => Error::WorkflowCancelled(outcome),
2536 WorkflowTerminalKind::Terminated => Error::WorkflowTerminated(outcome),
2537 WorkflowTerminalKind::TimedOut => Error::WorkflowTimedOut(outcome),
2538 });
2539 }
2540
2541 if started.elapsed() >= options.timeout {
2542 return Err(Error::WorkflowTimedOut(WorkflowTerminalOutcome {
2543 kind: WorkflowTerminalKind::TimedOut,
2544 workflow_id: description
2545 .workflow_id
2546 .clone()
2547 .unwrap_or_else(|| self.workflow_id.clone()),
2548 run_id: description
2549 .run_id
2550 .clone()
2551 .or_else(|| selected_run_id.map(str::to_string)),
2552 reason: "result_wait_timeout".to_string(),
2553 failure_category: Some("client_timeout".to_string()),
2554 failure_id: None,
2555 exception_type: None,
2556 exception_class: None,
2557 non_retryable: None,
2558 message: Some(format!(
2559 "workflow result was not terminal within {:?}",
2560 options.timeout
2561 )),
2562 exception: None,
2563 raw: description.raw_value(),
2564 }));
2565 }
2566
2567 tokio::time::sleep(options.poll_interval).await;
2568 }
2569 }
2570}
2571
2572#[derive(Clone, Copy, Debug)]
2573pub struct WorkflowResultOptions {
2574 pub poll_interval: Duration,
2575 pub timeout: Duration,
2576}
2577
2578impl Default for WorkflowResultOptions {
2579 fn default() -> Self {
2580 Self {
2581 poll_interval: Duration::from_millis(500),
2582 timeout: Duration::from_secs(30),
2583 }
2584 }
2585}
2586
2587#[derive(Clone, Debug, Deserialize)]
2588pub struct WorkflowDescription {
2589 pub workflow_id: Option<String>,
2590 pub run_id: Option<String>,
2591 pub workflow_type: Option<String>,
2592 pub status: Option<String>,
2593 #[serde(default)]
2594 pub closed_reason: Option<String>,
2595 #[serde(default)]
2596 pub error: Option<String>,
2597 #[serde(default)]
2598 pub failure: Option<Value>,
2599 #[serde(default)]
2600 pub exception: Option<Value>,
2601 #[serde(default)]
2602 pub failures: Vec<Value>,
2603 #[serde(default)]
2604 pub output: Option<Value>,
2605 #[serde(default)]
2606 pub output_envelope: Option<Value>,
2607 #[serde(flatten)]
2608 pub raw: HashMap<String, Value>,
2609}
2610
2611impl WorkflowDescription {
2612 pub fn is_completed(&self) -> bool {
2613 matches!(self.status.as_deref(), Some("completed" | "Completed"))
2614 }
2615
2616 pub fn is_terminal(&self) -> bool {
2617 matches!(
2618 self.status.as_deref(),
2619 Some(
2620 "completed"
2621 | "Completed"
2622 | "failed"
2623 | "Failed"
2624 | "cancelled"
2625 | "Cancelled"
2626 | "terminated"
2627 | "Terminated"
2628 | "timed_out"
2629 | "TimedOut",
2630 )
2631 )
2632 }
2633
2634 fn decode_payloads(&mut self) -> Result<()> {
2635 if let Some(envelope) = &self.output_envelope {
2636 self.output = Some(decode_wire_value(envelope, DEFAULT_CODEC)?);
2637 }
2638
2639 Ok(())
2640 }
2641
2642 fn raw_value(&self) -> Value {
2643 let mut data = self.raw.clone();
2644 data.insert(
2645 "workflow_id".to_string(),
2646 self.workflow_id
2647 .clone()
2648 .map(Value::String)
2649 .unwrap_or(Value::Null),
2650 );
2651 data.insert(
2652 "run_id".to_string(),
2653 self.run_id
2654 .clone()
2655 .map(Value::String)
2656 .unwrap_or(Value::Null),
2657 );
2658 data.insert(
2659 "workflow_type".to_string(),
2660 self.workflow_type
2661 .clone()
2662 .map(Value::String)
2663 .unwrap_or(Value::Null),
2664 );
2665 data.insert(
2666 "status".to_string(),
2667 self.status
2668 .clone()
2669 .map(Value::String)
2670 .unwrap_or(Value::Null),
2671 );
2672 data.insert(
2673 "closed_reason".to_string(),
2674 self.closed_reason
2675 .clone()
2676 .map(Value::String)
2677 .unwrap_or(Value::Null),
2678 );
2679 if let Some(failure) = &self.failure {
2680 data.insert("failure".to_string(), failure.clone());
2681 }
2682 if let Some(exception) = &self.exception {
2683 data.insert("exception".to_string(), exception.clone());
2684 }
2685 Value::Object(data.into_iter().collect())
2686 }
2687}
2688
2689fn workflow_terminal_outcome(
2690 description: &WorkflowDescription,
2691 workflow_id: &str,
2692 run_id: Option<&str>,
2693) -> WorkflowTerminalOutcome {
2694 let terminal_kind = description
2695 .closed_reason
2696 .as_deref()
2697 .or(description.status.as_deref())
2698 .unwrap_or("failed")
2699 .to_ascii_lowercase();
2700 let kind = match terminal_kind.as_str() {
2701 "cancelled" | "canceled" => WorkflowTerminalKind::Cancelled,
2702 "terminated" => WorkflowTerminalKind::Terminated,
2703 "timed_out" | "timedout" => WorkflowTerminalKind::TimedOut,
2704 _ => WorkflowTerminalKind::Failed,
2705 };
2706 let default_reason = match kind {
2707 WorkflowTerminalKind::Failed => "workflow_failed",
2708 WorkflowTerminalKind::Cancelled => "cancelled",
2709 WorkflowTerminalKind::Terminated => "terminated",
2710 WorkflowTerminalKind::TimedOut => "timed_out",
2711 };
2712 let failure = description
2713 .failure
2714 .as_ref()
2715 .filter(|value| value.is_object());
2716 let nested_failure = failure
2717 .and_then(|value| value.get("failures"))
2718 .and_then(Value::as_array)
2719 .and_then(|failures| failures.last())
2720 .or_else(|| description.failures.last());
2721 let exception = description
2722 .exception
2723 .clone()
2724 .or_else(|| failure.and_then(|value| value.get("exception")).cloned())
2725 .or_else(|| {
2726 nested_failure
2727 .and_then(|value| value.get("exception_payload"))
2728 .cloned()
2729 });
2730 let string_field = |name: &str| {
2731 failure
2732 .and_then(|value| value.get(name))
2733 .and_then(Value::as_str)
2734 .or_else(|| {
2735 nested_failure
2736 .and_then(|value| value.get(name))
2737 .and_then(Value::as_str)
2738 })
2739 .map(str::to_string)
2740 };
2741 let exception_field = |name: &str| {
2742 exception
2743 .as_ref()
2744 .and_then(|value| value.get(name))
2745 .and_then(Value::as_str)
2746 .map(str::to_string)
2747 };
2748 let message = description
2749 .error
2750 .clone()
2751 .or_else(|| string_field("message"))
2752 .or_else(|| exception_field("message"));
2753 let reason = description
2754 .raw
2755 .get("reason")
2756 .and_then(Value::as_str)
2757 .map(str::to_string)
2758 .or_else(|| {
2759 failure
2760 .and_then(|value| value.get("reason"))
2761 .and_then(Value::as_str)
2762 .map(str::to_string)
2763 })
2764 .or_else(|| description.closed_reason.clone())
2765 .unwrap_or_else(|| default_reason.to_string());
2766 let failure_id = string_field("failure_id").or_else(|| {
2767 nested_failure
2768 .and_then(|value| value.get("id"))
2769 .and_then(Value::as_str)
2770 .map(str::to_string)
2771 });
2772
2773 WorkflowTerminalOutcome {
2774 kind,
2775 workflow_id: description
2776 .workflow_id
2777 .clone()
2778 .unwrap_or_else(|| workflow_id.to_string()),
2779 run_id: description
2780 .run_id
2781 .clone()
2782 .or_else(|| run_id.map(str::to_string)),
2783 reason,
2784 failure_category: string_field("failure_category")
2785 .or_else(|| Some(default_reason.to_string())),
2786 failure_id,
2787 exception_type: string_field("exception_type").or_else(|| exception_field("type")),
2788 exception_class: string_field("exception_class").or_else(|| exception_field("class")),
2789 non_retryable: failure
2790 .and_then(|value| value.get("non_retryable"))
2791 .and_then(Value::as_bool)
2792 .or_else(|| {
2793 nested_failure
2794 .and_then(|value| value.get("non_retryable"))
2795 .and_then(Value::as_bool)
2796 }),
2797 message,
2798 exception,
2799 raw: description.raw_value(),
2800 }
2801}
2802
2803#[derive(Clone, Debug, Deserialize)]
2804pub struct RegisterWorkerResponse {
2805 pub worker_id: String,
2806 pub registered: bool,
2807 #[serde(default)]
2808 pub heartbeat_interval_seconds: Option<u64>,
2809 #[serde(default)]
2810 pub protocol_version: Option<String>,
2811 #[serde(default)]
2812 pub server_capabilities: Option<Value>,
2813}
2814
2815#[derive(Clone, Debug, Deserialize)]
2816pub struct PollWorkflowTaskResponse {
2817 #[serde(default)]
2818 pub task: Option<WorkflowTask>,
2819 #[serde(default)]
2820 pub poll_status: Option<String>,
2821 #[serde(default)]
2822 pub reason: Option<String>,
2823 #[serde(default)]
2824 pub protocol_version: Option<String>,
2825 #[serde(default)]
2826 pub server_capabilities: Option<Value>,
2827}
2828
2829impl PollWorkflowTaskResponse {
2830 pub fn outcome(&self) -> WorkerPollOutcome {
2832 worker_poll_outcome(
2833 self.task.is_some(),
2834 self.poll_status.as_deref(),
2835 self.reason.as_deref(),
2836 )
2837 }
2838}
2839
2840#[derive(Clone, Debug, Deserialize)]
2841pub struct PollActivityTaskResponse {
2842 #[serde(default)]
2843 pub task: Option<ActivityTask>,
2844 #[serde(default)]
2845 pub poll_status: Option<String>,
2846 #[serde(default)]
2847 pub reason: Option<String>,
2848}
2849
2850impl PollActivityTaskResponse {
2851 pub fn outcome(&self) -> WorkerPollOutcome {
2853 worker_poll_outcome(
2854 self.task.is_some(),
2855 self.poll_status.as_deref(),
2856 self.reason.as_deref(),
2857 )
2858 }
2859}
2860
2861#[derive(Clone, Debug, Deserialize)]
2862pub struct PollQueryTaskResponse {
2863 #[serde(default)]
2864 pub task: Option<QueryTask>,
2865 #[serde(default)]
2866 pub poll_status: Option<String>,
2867 #[serde(default)]
2868 pub reason: Option<String>,
2869}
2870
2871impl PollQueryTaskResponse {
2872 pub fn outcome(&self) -> WorkerPollOutcome {
2874 worker_poll_outcome(
2875 self.task.is_some(),
2876 self.poll_status.as_deref(),
2877 self.reason.as_deref(),
2878 )
2879 }
2880}
2881
2882#[derive(Clone, Debug, PartialEq, Eq)]
2884pub enum WorkerPollOutcome {
2885 Task,
2887 Idle {
2889 poll_status: Option<String>,
2890 reason: Option<String>,
2891 },
2892 Stop {
2894 poll_status: Option<String>,
2895 reason: Option<String>,
2896 },
2897}
2898
2899impl WorkerPollOutcome {
2900 pub fn should_stop(&self) -> bool {
2901 matches!(self, Self::Stop { .. })
2902 }
2903}
2904
2905fn worker_poll_outcome(
2906 has_task: bool,
2907 poll_status: Option<&str>,
2908 reason: Option<&str>,
2909) -> WorkerPollOutcome {
2910 if worker_poll_is_stop(poll_status, reason) {
2911 return WorkerPollOutcome::Stop {
2912 poll_status: poll_status.map(str::to_string),
2913 reason: reason.map(str::to_string),
2914 };
2915 }
2916
2917 if has_task {
2918 WorkerPollOutcome::Task
2919 } else {
2920 WorkerPollOutcome::Idle {
2921 poll_status: poll_status.map(str::to_string),
2922 reason: reason.map(str::to_string),
2923 }
2924 }
2925}
2926
2927#[derive(Clone, Debug, Deserialize)]
2929pub struct QueryTask {
2930 pub query_task_id: String,
2931 #[serde(default = "default_workflow_task_attempt")]
2932 pub query_task_attempt: u64,
2933 #[serde(default)]
2934 pub lease_owner: Option<String>,
2935 #[serde(default)]
2936 pub workflow_id: Option<String>,
2937 #[serde(default)]
2938 pub run_id: Option<String>,
2939 pub workflow_type: String,
2940 pub query_name: String,
2941 #[serde(default = "default_payload_codec")]
2942 pub payload_codec: String,
2943 #[serde(default)]
2944 pub workflow_arguments: Option<Value>,
2945 #[serde(default)]
2946 pub query_arguments: Option<Value>,
2947 #[serde(default)]
2948 pub history_events: Vec<HistoryEvent>,
2949 #[serde(default)]
2950 pub history_export: Option<Value>,
2951 #[serde(default)]
2952 pub run_status: Option<String>,
2953}
2954
2955#[derive(Clone, Debug, Deserialize)]
2956pub struct WorkflowTask {
2957 pub task_id: String,
2958 #[serde(default)]
2959 pub workflow_id: Option<String>,
2960 #[serde(default)]
2961 pub run_id: Option<String>,
2962 pub workflow_type: String,
2963 #[serde(default = "default_payload_codec")]
2964 pub payload_codec: String,
2965 #[serde(default)]
2966 pub arguments: Option<Value>,
2967 #[serde(default)]
2968 pub history_events: Vec<HistoryEvent>,
2969 #[serde(default)]
2970 pub total_history_events: Option<u64>,
2971 #[serde(default)]
2972 pub history_size_bytes: Option<u64>,
2973 #[serde(default)]
2974 pub continue_as_new_recommended: Option<bool>,
2975 #[serde(default)]
2976 pub history_budget_pressure: Option<String>,
2977 #[serde(default)]
2978 pub next_history_page_token: Option<String>,
2979 #[serde(default = "default_workflow_task_attempt")]
2980 pub workflow_task_attempt: u64,
2981 #[serde(default)]
2982 pub workflow_signal_id: Option<String>,
2983 #[serde(default)]
2984 pub signal_name: Option<String>,
2985 #[serde(default)]
2986 pub signal_arguments: Option<Value>,
2987 #[serde(default)]
2988 pub lease_owner: Option<String>,
2989}
2990
2991impl WorkflowTask {
2992 fn append_history_page(&mut self, page: WorkflowTaskHistoryPage) {
2993 self.history_events.extend(page.history_events);
2994
2995 if page.total_history_events.is_some() {
2996 self.total_history_events = page.total_history_events;
2997 }
2998
2999 self.next_history_page_token = page
3000 .next_history_page_token
3001 .filter(|token| !token.is_empty());
3002 }
3003}
3004
3005#[derive(Clone, Debug, Deserialize)]
3006struct WorkflowTaskHistoryPage {
3007 #[serde(default)]
3008 history_events: Vec<HistoryEvent>,
3009 #[serde(default)]
3010 total_history_events: Option<u64>,
3011 #[serde(default)]
3012 next_history_page_token: Option<String>,
3013}
3014
3015#[derive(Clone, Debug, Deserialize)]
3016pub struct ActivityTask {
3017 pub task_id: String,
3018 #[serde(default)]
3019 pub activity_attempt_id: Option<String>,
3020 #[serde(default)]
3021 pub attempt_id: Option<String>,
3022 pub activity_type: String,
3023 #[serde(default = "default_payload_codec")]
3024 pub payload_codec: String,
3025 #[serde(default)]
3026 pub arguments: Option<Value>,
3027 #[serde(default = "default_attempt_number")]
3028 pub attempt_number: u64,
3029 #[serde(default)]
3030 pub lease_owner: Option<String>,
3031}
3032
3033#[derive(Clone, Debug, Deserialize)]
3034pub struct HistoryEvent {
3035 #[serde(alias = "type")]
3036 pub event_type: String,
3037 #[serde(default)]
3038 pub payload: Value,
3039 #[serde(flatten)]
3040 pub raw: HashMap<String, Value>,
3041}
3042
3043#[derive(Clone, Debug, PartialEq)]
3045pub struct QuerySignal {
3046 pub id: Option<String>,
3047 pub name: String,
3048 pub arguments: Vec<Value>,
3049 pub workflow_sequence: Option<u64>,
3050}
3051
3052#[derive(Clone, Debug)]
3058pub struct QueryContext {
3059 pub workflow_id: Option<String>,
3060 pub run_id: Option<String>,
3061 pub workflow_type: String,
3062 pub run_status: Option<String>,
3063 workflow_input: Value,
3064 history_events: Arc<Vec<HistoryEvent>>,
3065 signal_events: Arc<Vec<QuerySignal>>,
3066}
3067
3068impl QueryContext {
3069 pub fn workflow_input(&self) -> &Value {
3071 &self.workflow_input
3072 }
3073
3074 pub fn history_events(&self) -> &[HistoryEvent] {
3076 self.history_events.as_slice()
3077 }
3078
3079 pub fn signal_events(&self) -> &[QuerySignal] {
3081 self.signal_events.as_slice()
3082 }
3083
3084 pub fn signals(&self, signal_name: &str) -> Vec<Vec<Value>> {
3086 self.signal_events
3087 .iter()
3088 .filter(|signal| signal.name == signal_name)
3089 .map(|signal| signal.arguments.clone())
3090 .collect()
3091 }
3092}
3093
3094#[derive(Clone, Debug, Deserialize)]
3095pub struct ActivityHeartbeatResponse {
3096 #[serde(default)]
3097 pub cancel_requested: bool,
3098 #[serde(default)]
3099 pub heartbeat_recorded: bool,
3100 #[serde(default)]
3101 pub can_continue: Option<bool>,
3102 #[serde(default)]
3103 pub reason: Option<String>,
3104 #[serde(default)]
3105 pub run_closed_reason: Option<String>,
3106 #[serde(default)]
3107 pub run_closed_at: Option<String>,
3108 #[serde(default)]
3109 pub lease_expires_at: Option<String>,
3110 #[serde(default)]
3111 pub last_heartbeat_at: Option<String>,
3112}
3113
3114impl ActivityHeartbeatResponse {
3115 pub fn should_stop(&self) -> bool {
3117 self.cancel_requested || self.can_continue == Some(false)
3118 }
3119}
3120
3121fn default_payload_codec() -> String {
3122 DEFAULT_CODEC.to_string()
3123}
3124
3125fn default_workflow_task_attempt() -> u64 {
3126 1
3127}
3128
3129fn default_attempt_number() -> u64 {
3130 1
3131}
3132
3133type WorkflowFuture = Pin<Box<dyn Future<Output = Result<Value>> + Send + 'static>>;
3134type WorkflowHandler = Arc<dyn Fn(WorkflowContext, Value) -> WorkflowFuture + Send + Sync>;
3135type ErasedWorkflowState = Arc<dyn Any + Send + Sync>;
3136type WorkflowStateSnapshot = Arc<dyn Fn() -> Result<ErasedWorkflowState> + Send + Sync>;
3137type ReplayedWorkflowHandler =
3138 Arc<dyn Fn(WorkflowContext, Value) -> ReplayedWorkflowInvocation + Send + Sync>;
3139type ActivityFuture = Pin<Box<dyn Future<Output = Result<Value>> + Send + 'static>>;
3140type ActivityHandler = Arc<dyn Fn(ActivityContext, Value) -> ActivityFuture + Send + Sync>;
3141type QueryFuture = Pin<Box<dyn Future<Output = Result<Value>> + Send + 'static>>;
3142type QueryHandler = Arc<dyn Fn(QueryContext, Value) -> QueryFuture + Send + Sync>;
3143type ReplayedQueryHandler = Arc<
3144 dyn Fn(QueryContext, ErasedWorkflowState, Value) -> std::result::Result<QueryFuture, String>
3145 + Send
3146 + Sync,
3147>;
3148type WorkerHeartbeatObserver = Arc<dyn Fn(&WorkerHeartbeatObservation) + Send + Sync>;
3149
3150struct ReplayedWorkflowInvocation {
3151 future: WorkflowFuture,
3152 snapshot: WorkflowStateSnapshot,
3153}
3154
3155#[derive(Clone)]
3156struct RegisteredWorkflow {
3157 execute: WorkflowHandler,
3158 replay: Option<ReplayedWorkflowHandler>,
3159 state_type: Option<TypeId>,
3160}
3161
3162#[derive(Clone)]
3163enum RegisteredQuery {
3164 Snapshot(QueryHandler),
3165 Replayed {
3166 state_type: TypeId,
3167 handler: ReplayedQueryHandler,
3168 },
3169}
3170
3171#[derive(Clone, Debug)]
3172pub struct WorkerHeartbeatObservation {
3173 pub worker_id: String,
3174 pub task_queue: String,
3175 pub acknowledged_at_unix_millis: u64,
3176 pub acknowledgement: Value,
3177}
3178
3179#[derive(Clone, Copy, Debug)]
3186pub struct WorkerRetryPolicy {
3187 pub max_retries: usize,
3189 pub initial_backoff: Duration,
3191 pub max_backoff: Duration,
3193}
3194
3195impl Default for WorkerRetryPolicy {
3196 fn default() -> Self {
3197 Self {
3198 max_retries: 5,
3199 initial_backoff: Duration::from_millis(100),
3200 max_backoff: Duration::from_secs(5),
3201 }
3202 }
3203}
3204
3205#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3206enum ManagedPollOutcome {
3207 Idle,
3208 Handled,
3209 Stop,
3210}
3211
3212#[derive(Clone)]
3213pub struct Worker {
3214 client: Client,
3215 worker_id: String,
3216 task_queue: String,
3217 workflows: HashMap<String, RegisteredWorkflow>,
3218 activities: HashMap<String, ActivityHandler>,
3219 queries: HashMap<String, HashMap<String, RegisteredQuery>>,
3220 max_concurrent_workflow_tasks: usize,
3221 max_concurrent_activity_tasks: usize,
3222 poll_timeout: Duration,
3223 heartbeat_interval: Duration,
3224 retry_policy: WorkerRetryPolicy,
3225 heartbeat_observer: Option<WorkerHeartbeatObserver>,
3226}
3227
3228impl Worker {
3229 pub fn new(client: Client, task_queue: impl Into<String>) -> Self {
3230 Self {
3231 client,
3232 worker_id: default_worker_id(),
3233 task_queue: task_queue.into(),
3234 workflows: HashMap::new(),
3235 activities: HashMap::new(),
3236 queries: HashMap::new(),
3237 max_concurrent_workflow_tasks: 10,
3238 max_concurrent_activity_tasks: 10,
3239 poll_timeout: Duration::from_secs(30),
3240 heartbeat_interval: Duration::from_secs(60),
3241 retry_policy: WorkerRetryPolicy::default(),
3242 heartbeat_observer: None,
3243 }
3244 }
3245
3246 pub fn worker_id(mut self, worker_id: impl Into<String>) -> Self {
3247 self.worker_id = worker_id.into();
3248 self
3249 }
3250
3251 pub fn poll_timeout(mut self, timeout: Duration) -> Self {
3252 self.poll_timeout = timeout;
3253 self
3254 }
3255
3256 pub fn heartbeat_interval(mut self, interval: Duration) -> Self {
3257 self.heartbeat_interval = interval;
3258 self
3259 }
3260
3261 pub fn retry_policy(mut self, policy: WorkerRetryPolicy) -> Self {
3263 self.retry_policy = policy;
3264 self
3265 }
3266
3267 pub fn on_worker_heartbeat<F>(mut self, observer: F) -> Self
3268 where
3269 F: Fn(&WorkerHeartbeatObservation) + Send + Sync + 'static,
3270 {
3271 self.heartbeat_observer = Some(Arc::new(observer));
3272 self
3273 }
3274
3275 pub fn max_concurrent_workflow_tasks(mut self, count: usize) -> Self {
3276 self.max_concurrent_workflow_tasks = count.max(1);
3277 self
3278 }
3279
3280 pub fn max_concurrent_activity_tasks(mut self, count: usize) -> Self {
3281 self.max_concurrent_activity_tasks = count.max(1);
3282 self
3283 }
3284
3285 pub fn register_workflow<F, Fut>(&mut self, workflow_type: impl Into<String>, handler: F)
3292 where
3293 F: Fn(WorkflowContext, Value) -> Fut + Send + Sync + 'static,
3294 Fut: Future<Output = Result<Value>> + Send + 'static,
3295 {
3296 self.workflows.insert(
3297 workflow_type.into(),
3298 RegisteredWorkflow {
3299 execute: Arc::new(move |ctx, input| Box::pin(handler(ctx, input))),
3300 replay: None,
3301 state_type: None,
3302 },
3303 );
3304 }
3305
3306 pub fn register_replayed_workflow<S, Factory, F, Fut>(
3314 &mut self,
3315 workflow_type: impl Into<String>,
3316 state_factory: Factory,
3317 handler: F,
3318 ) where
3319 S: Clone + Send + Sync + 'static,
3320 Factory: Fn() -> S + Send + Sync + 'static,
3321 F: Fn(WorkflowContext, Value, WorkflowInstance<S>) -> Fut + Send + Sync + 'static,
3322 Fut: Future<Output = Result<Value>> + Send + 'static,
3323 {
3324 let state_factory = Arc::new(state_factory);
3325 let handler = Arc::new(handler);
3326
3327 let execute_factory = Arc::clone(&state_factory);
3328 let execute_handler = Arc::clone(&handler);
3329 let execute = Arc::new(move |ctx: WorkflowContext, input: Value| {
3330 let state = WorkflowInstance::new(execute_factory());
3331 let future = execute_handler(ctx, input, state);
3332 Box::pin(future) as WorkflowFuture
3333 });
3334
3335 let replay = Arc::new(move |ctx: WorkflowContext, input: Value| {
3336 let state = WorkflowInstance::new(state_factory());
3337 let snapshot_state = state.clone();
3338 let snapshot: WorkflowStateSnapshot =
3339 Arc::new(move || Ok(Arc::new(snapshot_state.snapshot()?) as ErasedWorkflowState));
3340 let future = handler(ctx, input, state);
3341 ReplayedWorkflowInvocation {
3342 future: Box::pin(future),
3343 snapshot,
3344 }
3345 });
3346
3347 self.workflows.insert(
3348 workflow_type.into(),
3349 RegisteredWorkflow {
3350 execute,
3351 replay: Some(replay),
3352 state_type: Some(TypeId::of::<S>()),
3353 },
3354 );
3355 }
3356
3357 pub fn register_activity<F, Fut>(&mut self, activity_type: impl Into<String>, handler: F)
3358 where
3359 F: Fn(ActivityContext, Value) -> Fut + Send + Sync + 'static,
3360 Fut: Future<Output = Result<Value>> + Send + 'static,
3361 {
3362 self.activities.insert(
3363 activity_type.into(),
3364 Arc::new(move |ctx, args| Box::pin(handler(ctx, args))),
3365 );
3366 }
3367
3368 pub fn register_query<F, Fut>(
3374 &mut self,
3375 workflow_type: impl Into<String>,
3376 query_name: impl Into<String>,
3377 handler: F,
3378 ) where
3379 F: Fn(QueryContext, Value) -> Fut + Send + Sync + 'static,
3380 Fut: Future<Output = Result<Value>> + Send + 'static,
3381 {
3382 self.queries
3383 .entry(workflow_type.into())
3384 .or_default()
3385 .insert(
3386 query_name.into(),
3387 RegisteredQuery::Snapshot(Arc::new(move |ctx, args| Box::pin(handler(ctx, args)))),
3388 );
3389 }
3390
3391 pub fn register_replayed_query<S, F, Fut>(
3398 &mut self,
3399 workflow_type: impl Into<String>,
3400 query_name: impl Into<String>,
3401 handler: F,
3402 ) where
3403 S: Clone + Send + Sync + 'static,
3404 F: Fn(QueryContext, Arc<S>, Value) -> Fut + Send + Sync + 'static,
3405 Fut: Future<Output = Result<Value>> + Send + 'static,
3406 {
3407 let handler = Arc::new(handler);
3408 let erased_handler: ReplayedQueryHandler = Arc::new(move |ctx, state, args| {
3409 let state = state.downcast::<S>().map_err(|_| {
3410 "registered query state type does not match the replayed workflow state".to_string()
3411 })?;
3412 Ok(Box::pin(handler(ctx, state, args)))
3413 });
3414
3415 self.queries
3416 .entry(workflow_type.into())
3417 .or_default()
3418 .insert(
3419 query_name.into(),
3420 RegisteredQuery::Replayed {
3421 state_type: TypeId::of::<S>(),
3422 handler: erased_handler,
3423 },
3424 );
3425 }
3426
3427 pub async fn register(&self) -> Result<RegisterWorkerResponse> {
3428 self.client
3429 .register_worker_with_capabilities(
3430 &self.worker_id,
3431 &self.task_queue,
3432 self.workflows.keys().cloned().collect(),
3433 self.activities.keys().cloned().collect(),
3434 self.max_concurrent_workflow_tasks,
3435 self.max_concurrent_activity_tasks,
3436 (!self.queries.is_empty())
3437 .then(|| QUERY_TASKS_CAPABILITY.to_string())
3438 .into_iter()
3439 .collect(),
3440 )
3441 .await
3442 }
3443
3444 pub async fn run(&self) -> Result<()> {
3450 self.run_until(std::future::pending::<()>()).await
3451 }
3452
3453 pub async fn run_until<F>(&self, shutdown: F) -> Result<()>
3457 where
3458 F: Future<Output = ()>,
3459 {
3460 let registration = self.register().await?;
3461 let heartbeat_interval = Duration::from_secs(
3462 registration
3463 .heartbeat_interval_seconds
3464 .unwrap_or(self.heartbeat_interval.as_secs().max(1)),
3465 );
3466 let heartbeat = tokio::time::sleep(Duration::ZERO);
3472 tokio::pin!(heartbeat);
3473 tokio::pin!(shutdown);
3474 let stop = Arc::new(AtomicBool::new(false));
3475 let mut workflow_poller = (!self.workflows.is_empty()).then(|| {
3479 let worker = self.clone();
3480 let stop = Arc::clone(&stop);
3481 tokio::spawn(async move { worker.poll_workflows_until_stopped(stop).await })
3482 });
3483 let mut activity_poller = (!self.activities.is_empty()).then(|| {
3484 let worker = self.clone();
3485 let stop = Arc::clone(&stop);
3486 tokio::spawn(async move { worker.poll_activities_until_stopped(stop).await })
3487 });
3488 let mut query_poller = (!self.queries.is_empty()).then(|| {
3489 let worker = self.clone();
3490 let stop = Arc::clone(&stop);
3491 tokio::spawn(async move { worker.poll_queries_until_stopped(stop).await })
3492 });
3493
3494 loop {
3495 tokio::select! {
3496 _ = &mut shutdown => {
3497 stop.store(true, Ordering::SeqCst);
3498 break;
3499 }
3500 _ = &mut heartbeat => {
3501 let result = self.retry_worker_operation(|| {
3502 self.client.heartbeat_worker(
3503 &self.worker_id,
3504 self.max_concurrent_workflow_tasks,
3505 self.max_concurrent_activity_tasks,
3506 )
3507 }).await;
3508 heartbeat
3509 .as_mut()
3510 .reset(tokio::time::Instant::now() + heartbeat_interval);
3511 match result {
3512 Ok(acknowledgement) => {
3513 if let Some(observer) = &self.heartbeat_observer {
3514 observer(&WorkerHeartbeatObservation {
3515 worker_id: self.worker_id.clone(),
3516 task_queue: self.task_queue.clone(),
3517 acknowledged_at_unix_millis: SystemTime::now()
3518 .duration_since(UNIX_EPOCH)
3519 .unwrap_or_default()
3520 .as_millis()
3521 .min(u64::MAX as u128)
3522 as u64,
3523 acknowledgement,
3524 });
3525 }
3526 }
3527 Err(error) => {
3528 stop.store(true, Ordering::SeqCst);
3529 join_pollers(workflow_poller.take(), activity_poller.take(), query_poller.take()).await?;
3530 return Err(error);
3531 }
3532 }
3533 }
3534 result = OptionFuture::from(workflow_poller.as_mut()), if workflow_poller.is_some() => {
3535 workflow_poller = None;
3536 let stopped_by_server = stop.load(Ordering::SeqCst);
3537 stop.store(true, Ordering::SeqCst);
3538 let poller_result = optional_poller_result("workflow", result);
3539 let join_result =
3540 join_pollers(workflow_poller.take(), activity_poller.take(), query_poller.take()).await;
3541 poller_result?;
3542 join_result?;
3543 if stopped_by_server {
3544 return Ok(());
3545 }
3546 return Err(Error::WorkerLoop(
3547 "workflow poller stopped unexpectedly".to_string(),
3548 ));
3549 }
3550 result = OptionFuture::from(activity_poller.as_mut()), if activity_poller.is_some() => {
3551 activity_poller = None;
3552 let stopped_by_server = stop.load(Ordering::SeqCst);
3553 stop.store(true, Ordering::SeqCst);
3554 let poller_result = optional_poller_result("activity", result);
3555 let join_result =
3556 join_pollers(workflow_poller.take(), activity_poller.take(), query_poller.take()).await;
3557 poller_result?;
3558 join_result?;
3559 if stopped_by_server {
3560 return Ok(());
3561 }
3562 return Err(Error::WorkerLoop(
3563 "activity poller stopped unexpectedly".to_string(),
3564 ));
3565 }
3566 result = OptionFuture::from(query_poller.as_mut()), if query_poller.is_some() => {
3567 query_poller = None;
3568 let stopped_by_server = stop.load(Ordering::SeqCst);
3569 stop.store(true, Ordering::SeqCst);
3570 let poller_result = optional_poller_result("query", result);
3571 let join_result =
3572 join_pollers(workflow_poller.take(), activity_poller.take(), query_poller.take()).await;
3573 poller_result?;
3574 join_result?;
3575 if stopped_by_server {
3576 return Ok(());
3577 }
3578 return Err(Error::WorkerLoop(
3579 "query poller stopped unexpectedly".to_string(),
3580 ));
3581 }
3582 }
3583 }
3584
3585 join_pollers(
3586 workflow_poller.take(),
3587 activity_poller.take(),
3588 query_poller.take(),
3589 )
3590 .await
3591 }
3592
3593 pub async fn run_once(&self) -> Result<usize> {
3607 let mut handled = 0;
3608 match self.poll_workflow_once().await? {
3609 ManagedPollOutcome::Handled => handled += 1,
3610 ManagedPollOutcome::Stop => return Ok(handled),
3611 ManagedPollOutcome::Idle => {}
3612 }
3613 match self.poll_activity_once().await? {
3614 ManagedPollOutcome::Handled => handled += 1,
3615 ManagedPollOutcome::Stop => return Ok(handled),
3616 ManagedPollOutcome::Idle => {}
3617 }
3618 if !self.queries.is_empty() {
3619 match self.poll_query_once().await? {
3620 ManagedPollOutcome::Handled => handled += 1,
3621 ManagedPollOutcome::Stop => return Ok(handled),
3622 ManagedPollOutcome::Idle => {}
3623 }
3624 }
3625 Ok(handled)
3626 }
3627
3628 async fn poll_workflow_once(&self) -> Result<ManagedPollOutcome> {
3629 let response = self
3630 .retry_worker_operation(|| {
3631 self.client.poll_workflow_task_response(
3632 &self.worker_id,
3633 &self.task_queue,
3634 self.poll_timeout,
3635 )
3636 })
3637 .await?;
3638 if response.outcome().should_stop() {
3639 return Ok(ManagedPollOutcome::Stop);
3640 }
3641 let Some(task) = response.task else {
3642 return Ok(ManagedPollOutcome::Idle);
3643 };
3644
3645 let task_id = task.task_id.clone();
3646 let attempt = task.workflow_task_attempt;
3647 let run_id = task.run_id.clone();
3648 let lease_owner = task
3649 .lease_owner
3650 .clone()
3651 .unwrap_or_else(|| self.worker_id.clone());
3652
3653 match self.execute_workflow_task(task) {
3654 Ok(commands) if commands.is_empty() => {
3655 self.client
3661 .fail_workflow_task_with_type(
3662 &task_id,
3663 &lease_owner,
3664 attempt,
3665 WORKFLOW_TASK_WAITING_FOR_HISTORY_MESSAGE,
3666 WORKFLOW_TASK_WAITING_FOR_HISTORY_TYPE,
3667 )
3668 .await?;
3669 }
3670 Ok(commands) => {
3671 let completion = self
3672 .client
3673 .complete_workflow_task(&task_id, &lease_owner, attempt, commands)
3674 .await;
3675 if let Err(error) = completion {
3676 if !workflow_task_completion_is_terminal_timeout(
3677 &error,
3678 &task_id,
3679 attempt,
3680 run_id.as_deref(),
3681 ) {
3682 return Err(error);
3683 }
3684 }
3685 }
3686 Err(error) => {
3687 self.client
3688 .fail_workflow_task(&task_id, &lease_owner, attempt, error.to_string())
3689 .await?;
3690 }
3691 }
3692
3693 Ok(ManagedPollOutcome::Handled)
3694 }
3695
3696 async fn poll_workflows_until_stopped(self, stop: Arc<AtomicBool>) -> Result<()> {
3697 while !stop.load(Ordering::SeqCst) {
3698 if self.poll_workflow_once().await? == ManagedPollOutcome::Stop {
3699 stop.store(true, Ordering::SeqCst);
3700 break;
3701 }
3702 }
3703
3704 Ok(())
3705 }
3706
3707 async fn poll_activity_once(&self) -> Result<ManagedPollOutcome> {
3708 let response = self
3709 .retry_worker_operation(|| {
3710 self.client.poll_activity_task_response(
3711 &self.worker_id,
3712 &self.task_queue,
3713 self.poll_timeout,
3714 )
3715 })
3716 .await?;
3717 if response.outcome().should_stop() {
3718 return Ok(ManagedPollOutcome::Stop);
3719 }
3720 let Some(task) = response.task else {
3721 return Ok(ManagedPollOutcome::Idle);
3722 };
3723
3724 let task_id = task.task_id.clone();
3725 let attempt_id = task
3726 .activity_attempt_id
3727 .clone()
3728 .or(task.attempt_id.clone())
3729 .unwrap_or_default();
3730 let lease_owner = task
3731 .lease_owner
3732 .clone()
3733 .unwrap_or_else(|| self.worker_id.clone());
3734 let codec = task.payload_codec.clone();
3735 let result = self.execute_activity_task(task).await;
3736 match result {
3737 Ok(value) => {
3738 let completion = self
3739 .client
3740 .complete_activity_task(&task_id, &attempt_id, &lease_owner, value, &codec)
3741 .await;
3742 if let Err(error) = completion {
3743 if !activity_task_rejection_is_final(&error) {
3744 return Err(error);
3745 }
3746 }
3747 }
3748 Err(error) => {
3749 let failure = self
3750 .client
3751 .fail_activity_task(
3752 &task_id,
3753 &attempt_id,
3754 &lease_owner,
3755 error.to_string(),
3756 false,
3757 )
3758 .await;
3759 if let Err(error) = failure {
3760 if !activity_task_rejection_is_final(&error) {
3761 return Err(error);
3762 }
3763 }
3764 }
3765 }
3766
3767 Ok(ManagedPollOutcome::Handled)
3768 }
3769
3770 async fn poll_activities_until_stopped(self, stop: Arc<AtomicBool>) -> Result<()> {
3771 while !stop.load(Ordering::SeqCst) {
3772 if self.poll_activity_once().await? == ManagedPollOutcome::Stop {
3773 stop.store(true, Ordering::SeqCst);
3774 break;
3775 }
3776 }
3777
3778 Ok(())
3779 }
3780
3781 async fn poll_query_once(&self) -> Result<ManagedPollOutcome> {
3782 let response = self
3783 .retry_worker_operation(|| {
3784 self.client.poll_query_task_response(
3785 &self.worker_id,
3786 &self.task_queue,
3787 self.poll_timeout,
3788 )
3789 })
3790 .await?;
3791 if response.outcome().should_stop() {
3792 return Ok(ManagedPollOutcome::Stop);
3793 }
3794 let Some(task) = response.task else {
3795 return Ok(ManagedPollOutcome::Idle);
3796 };
3797
3798 let query_task_id = task.query_task_id.clone();
3799 let attempt = task.query_task_attempt;
3800 let lease_owner = task
3801 .lease_owner
3802 .clone()
3803 .unwrap_or_else(|| self.worker_id.clone());
3804 let codec = task.payload_codec.clone();
3805
3806 match self.execute_query_task(task).await {
3807 Ok(value) => {
3808 let result_envelope = match encode_value_envelope(&value, &codec) {
3809 Ok(result_envelope) => result_envelope,
3810 Err(error) => {
3811 let failure = self
3812 .client
3813 .fail_query_task(
3814 &query_task_id,
3815 &lease_owner,
3816 attempt,
3817 error.to_string(),
3818 "query_result_encode_failed",
3819 "QueryResultEncodeFailed",
3820 )
3821 .await;
3822 if let Err(error) = failure {
3823 if !query_task_rejection_is_final(&error) {
3824 return Err(error);
3825 }
3826 }
3827 return Ok(ManagedPollOutcome::Handled);
3828 }
3829 };
3830
3831 if let Err(error) = self
3832 .client
3833 .complete_query_task_with_envelope(
3834 &query_task_id,
3835 &lease_owner,
3836 attempt,
3837 value,
3838 result_envelope,
3839 )
3840 .await
3841 {
3842 if !query_task_rejection_is_final(&error) {
3843 return Err(error);
3844 }
3845 }
3846 }
3847 Err(failure) => {
3848 let result = self
3849 .client
3850 .fail_query_task(
3851 &query_task_id,
3852 &lease_owner,
3853 attempt,
3854 failure.message,
3855 failure.reason,
3856 failure.failure_type,
3857 )
3858 .await;
3859 if let Err(error) = result {
3860 if !query_task_rejection_is_final(&error) {
3861 return Err(error);
3862 }
3863 }
3864 }
3865 }
3866
3867 Ok(ManagedPollOutcome::Handled)
3868 }
3869
3870 async fn poll_queries_until_stopped(self, stop: Arc<AtomicBool>) -> Result<()> {
3871 while !stop.load(Ordering::SeqCst) {
3872 if self.poll_query_once().await? == ManagedPollOutcome::Stop {
3873 stop.store(true, Ordering::SeqCst);
3874 break;
3875 }
3876 }
3877
3878 Ok(())
3879 }
3880
3881 async fn retry_worker_operation<T, F, Fut>(&self, mut operation: F) -> Result<T>
3882 where
3883 F: FnMut() -> Fut,
3884 Fut: Future<Output = Result<T>>,
3885 {
3886 let mut retries = 0;
3887
3888 loop {
3889 match operation().await {
3890 Err(error)
3891 if worker_operation_is_retryable(&error)
3892 && retries < self.retry_policy.max_retries =>
3893 {
3894 retries += 1;
3895 tokio::time::sleep(worker_retry_delay(self.retry_policy, retries)).await;
3896 }
3897 result => return result,
3898 }
3899 }
3900 }
3901
3902 async fn execute_query_task(
3903 &self,
3904 mut task: QueryTask,
3905 ) -> std::result::Result<Value, QueryTaskExecutionFailure> {
3906 if !matches!(task.payload_codec.as_str(), DEFAULT_CODEC | JSON_CODEC) {
3907 return Err(QueryTaskExecutionFailure::new(
3908 "query_payload_decode_failed",
3909 format!(
3910 "cannot decode query payload with unsupported codec {:?}",
3911 task.payload_codec
3912 ),
3913 "QueryPayloadDecodeFailed",
3914 ));
3915 }
3916
3917 if !self.workflows.contains_key(&task.workflow_type) {
3918 return Err(QueryTaskExecutionFailure::new(
3919 "query_workflow_type_not_registered",
3920 format!("no workflow registered for type {:?}", task.workflow_type),
3921 "WorkflowTypeNotRegistered",
3922 ));
3923 }
3924
3925 let Some(handlers) = self.queries.get(&task.workflow_type) else {
3926 return Err(QueryTaskExecutionFailure::new(
3927 "query_handler_unavailable",
3928 format!(
3929 "query handlers are unavailable for workflow type {:?}",
3930 task.workflow_type
3931 ),
3932 "QueryHandlerUnavailable",
3933 ));
3934 };
3935 let Some(query) = handlers.get(&task.query_name) else {
3936 return Err(QueryTaskExecutionFailure::new(
3937 "rejected_unknown_query",
3938 format!("unknown query {:?}", task.query_name),
3939 "QueryFailed",
3940 ));
3941 };
3942
3943 let args = decode_task_arguments(task.query_arguments.as_ref(), &task.payload_codec)
3944 .map_err(|error| {
3945 QueryTaskExecutionFailure::new(
3946 "query_payload_decode_failed",
3947 format!("cannot decode query arguments: {error}"),
3948 "QueryPayloadDecodeFailed",
3949 )
3950 })?;
3951 let workflow_input =
3952 decode_task_arguments(task.workflow_arguments.as_ref(), &task.payload_codec).map_err(
3953 |error| {
3954 QueryTaskExecutionFailure::new(
3955 "query_workflow_state_unavailable",
3956 format!("cannot decode workflow start input: {error}"),
3957 "QueryWorkflowStateUnavailable",
3958 )
3959 },
3960 )?;
3961 hydrate_query_history_from_export(&mut task).map_err(|error| {
3962 QueryTaskExecutionFailure::new(
3963 "query_workflow_state_unavailable",
3964 format!("cannot restore query history snapshot: {error}"),
3965 "QueryWorkflowStateUnavailable",
3966 )
3967 })?;
3968 enrich_query_history_from_export(&mut task).map_err(|error| {
3969 QueryTaskExecutionFailure::new(
3970 "query_workflow_state_unavailable",
3971 format!("cannot restore compact query history payloads: {error}"),
3972 "QueryWorkflowStateUnavailable",
3973 )
3974 })?;
3975 let signal_events = query_signal_events(&task).map_err(|error| {
3976 QueryTaskExecutionFailure::new(
3977 "query_workflow_state_unavailable",
3978 format!("cannot decode committed workflow signals: {error}"),
3979 "QueryWorkflowStateUnavailable",
3980 )
3981 })?;
3982 let history_events = Arc::new(std::mem::take(&mut task.history_events));
3983 let context = QueryContext {
3984 workflow_id: task.workflow_id,
3985 run_id: task.run_id,
3986 workflow_type: task.workflow_type.clone(),
3987 run_status: task.run_status,
3988 workflow_input,
3989 history_events: Arc::clone(&history_events),
3990 signal_events: Arc::new(signal_events),
3991 };
3992
3993 let future = match query {
3994 RegisteredQuery::Snapshot(handler) => handler(context, args),
3995 RegisteredQuery::Replayed {
3996 state_type,
3997 handler,
3998 } => {
3999 let workflow = self
4000 .workflows
4001 .get(&task.workflow_type)
4002 .expect("workflow registration was checked above");
4003 if workflow.state_type != Some(*state_type) {
4004 return Err(QueryTaskExecutionFailure::new(
4005 "query_workflow_state_unavailable",
4006 "replayed query state type does not match its workflow registration",
4007 "QueryWorkflowStateUnavailable",
4008 ));
4009 }
4010 let replay = workflow.replay.as_ref().ok_or_else(|| {
4011 QueryTaskExecutionFailure::new(
4012 "query_workflow_state_unavailable",
4013 format!(
4014 "workflow type {:?} is not registered for instance-state replay",
4015 task.workflow_type
4016 ),
4017 "QueryWorkflowStateUnavailable",
4018 )
4019 })?;
4020 let workflow_state = Arc::new(Mutex::new(
4021 WorkflowState::new_with_identity(
4022 history_events.as_ref().clone(),
4023 context.workflow_id.clone(),
4024 context.run_id.clone(),
4025 self.task_queue.clone(),
4026 task.payload_codec,
4027 None,
4028 )
4029 .map_err(|error| {
4030 QueryTaskExecutionFailure::new(
4031 "query_workflow_state_unavailable",
4032 format!("workflow replay failed before query: {error}"),
4033 "QueryWorkflowStateUnavailable",
4034 )
4035 })?,
4036 ));
4037 let workflow_context = WorkflowContext {
4038 state: workflow_state,
4039 };
4040 let mut invocation =
4041 replay(workflow_context.clone(), context.workflow_input.clone());
4042 let mut cx = TaskContext::from_waker(noop_waker_ref());
4043 match invocation.future.as_mut().poll(&mut cx) {
4044 Poll::Ready(Ok(_)) => {
4045 workflow_context
4046 .ensure_history_consumed()
4047 .map_err(|error| {
4048 QueryTaskExecutionFailure::new(
4049 "query_workflow_state_unavailable",
4050 format!("workflow replay failed before query: {error}"),
4051 "QueryWorkflowStateUnavailable",
4052 )
4053 })?;
4054 }
4055 Poll::Ready(Err(error)) => {
4056 return Err(QueryTaskExecutionFailure::new(
4057 "query_workflow_state_unavailable",
4058 format!("workflow replay failed before query: {error}"),
4059 "QueryWorkflowStateUnavailable",
4060 ));
4061 }
4062 Poll::Pending => {
4063 let commands = workflow_context.take_commands().map_err(|error| {
4064 QueryTaskExecutionFailure::new(
4065 "query_workflow_state_unavailable",
4066 format!("workflow replay failed before query: {error}"),
4067 "QueryWorkflowStateUnavailable",
4068 )
4069 })?;
4070 if commands.is_empty()
4071 && !workflow_context
4072 .matched_recorded_pending()
4073 .map_err(|error| {
4074 QueryTaskExecutionFailure::new(
4075 "query_workflow_state_unavailable",
4076 format!("workflow replay failed before query: {error}"),
4077 "QueryWorkflowStateUnavailable",
4078 )
4079 })?
4080 {
4081 return Err(QueryTaskExecutionFailure::new(
4082 "query_workflow_state_unavailable",
4083 "workflow replay yielded without a durable command",
4084 "QueryWorkflowStateUnavailable",
4085 ));
4086 }
4087 }
4088 }
4089 let state = (invocation.snapshot)().map_err(|error| {
4090 QueryTaskExecutionFailure::new(
4091 "query_workflow_state_unavailable",
4092 format!("cannot snapshot replayed workflow state: {error}"),
4093 "QueryWorkflowStateUnavailable",
4094 )
4095 })?;
4096 handler(context, state, args).map_err(|message| {
4097 QueryTaskExecutionFailure::new(
4098 "query_workflow_state_unavailable",
4099 message,
4100 "QueryWorkflowStateUnavailable",
4101 )
4102 })?
4103 }
4104 };
4105
4106 future.await.map_err(|error| {
4107 QueryTaskExecutionFailure::new("query_rejected", error.to_string(), "QueryFailed")
4108 })
4109 }
4110
4111 fn execute_workflow_task(&self, task: WorkflowTask) -> Result<Vec<Value>> {
4112 let workflow = self
4113 .workflows
4114 .get(&task.workflow_type)
4115 .ok_or_else(|| Error::WorkflowNotRegistered(task.workflow_type.clone()))?;
4116 let input = decode_task_arguments(task.arguments.as_ref(), &task.payload_codec)?;
4117 let resume_signal = decode_resume_signal(&task)?;
4118 let history_budget = WorkflowHistoryBudget {
4119 event_count: task
4120 .total_history_events
4121 .unwrap_or_else(|| u64::try_from(task.history_events.len()).unwrap_or(u64::MAX)),
4122 size_bytes: task.history_size_bytes,
4123 continue_as_new_recommended: task.continue_as_new_recommended.unwrap_or(false),
4124 pressure: task.history_budget_pressure.clone(),
4125 };
4126 let mut workflow_state = WorkflowState::new_with_identity(
4127 task.history_events,
4128 task.workflow_id,
4129 task.run_id,
4130 self.task_queue.clone(),
4131 task.payload_codec.clone(),
4132 resume_signal,
4133 )?;
4134 workflow_state.history_budget = history_budget;
4135 let state = Arc::new(Mutex::new(workflow_state));
4136 let ctx = WorkflowContext { state };
4137 let mut future = (workflow.execute)(ctx.clone(), input);
4138 let mut cx = TaskContext::from_waker(noop_waker_ref());
4139
4140 match future.as_mut().poll(&mut cx) {
4141 Poll::Ready(Ok(result)) => {
4142 ctx.ensure_history_consumed()?;
4143 let result = encode_value_envelope(&result, &task.payload_codec)?;
4144 let mut commands = ctx.take_commands()?;
4145 commands.push(json!({
4146 "type": "complete_workflow",
4147 "result": result
4148 }));
4149 Ok(commands)
4150 }
4151 Poll::Ready(Err(error)) => {
4152 if let Error::ContinueAsNew(request) = error {
4153 let mut commands = ctx.take_commands()?;
4154 if let Some(command) = ctx.continue_as_new_command(request)? {
4155 commands.push(command);
4156 }
4157 ctx.ensure_history_consumed()?;
4158 return Ok(commands);
4159 }
4160 ctx.ensure_history_consumed()?;
4163 if workflow_task_integrity_error(&error) {
4164 return Err(error);
4169 }
4170 let mut commands = ctx.take_commands()?;
4171 commands.push(workflow_failure_command(&error));
4172 Ok(commands)
4173 }
4174 Poll::Pending => {
4175 let commands = ctx.take_commands()?;
4176 if commands.is_empty() && !ctx.matched_recorded_pending()? {
4177 Err(Error::WorkflowYieldedWithoutCommand)
4178 } else {
4179 Ok(commands)
4180 }
4181 }
4182 }
4183 }
4184
4185 async fn execute_activity_task(&self, task: ActivityTask) -> Result<Value> {
4186 let handler = self
4187 .activities
4188 .get(&task.activity_type)
4189 .ok_or_else(|| Error::ActivityNotRegistered(task.activity_type.clone()))?;
4190 let args = decode_task_arguments(task.arguments.as_ref(), &task.payload_codec)?;
4191 let attempt_id = task
4192 .activity_attempt_id
4193 .clone()
4194 .or(task.attempt_id.clone())
4195 .unwrap_or_default();
4196 let lease_owner = task
4197 .lease_owner
4198 .clone()
4199 .unwrap_or_else(|| self.worker_id.clone());
4200 let ctx = ActivityContext {
4201 client: self.client.clone(),
4202 task_id: task.task_id,
4203 activity_attempt_id: attempt_id,
4204 lease_owner,
4205 activity_type: task.activity_type,
4206 attempt_number: task.attempt_number,
4207 task_queue: self.task_queue.clone(),
4208 worker_id: self.worker_id.clone(),
4209 };
4210
4211 handler(ctx, args).await
4212 }
4213}
4214
4215fn poller_result(
4216 kind: &str,
4217 result: std::result::Result<Result<()>, tokio::task::JoinError>,
4218) -> Result<()> {
4219 match result {
4220 Ok(result) => result,
4221 Err(error) => Err(Error::WorkerLoop(format!(
4222 "{kind} poller join error: {error}"
4223 ))),
4224 }
4225}
4226
4227fn optional_poller_result(
4228 kind: &str,
4229 result: Option<std::result::Result<Result<()>, tokio::task::JoinError>>,
4230) -> Result<()> {
4231 match result {
4232 Some(result) => poller_result(kind, result),
4233 None => Ok(()),
4234 }
4235}
4236
4237async fn join_pollers(
4238 workflow_poller: Option<tokio::task::JoinHandle<Result<()>>>,
4239 activity_poller: Option<tokio::task::JoinHandle<Result<()>>>,
4240 query_poller: Option<tokio::task::JoinHandle<Result<()>>>,
4241) -> Result<()> {
4242 let mut first_error = None;
4243
4244 if let Some(handle) = workflow_poller {
4245 if let Err(error) = poller_result("workflow", handle.await) {
4246 first_error.get_or_insert(error);
4247 }
4248 }
4249
4250 if let Some(handle) = activity_poller {
4251 if let Err(error) = poller_result("activity", handle.await) {
4252 first_error.get_or_insert(error);
4253 }
4254 }
4255
4256 if let Some(handle) = query_poller {
4257 if let Err(error) = poller_result("query", handle.await) {
4258 first_error.get_or_insert(error);
4259 }
4260 }
4261
4262 if let Some(error) = first_error {
4263 Err(error)
4264 } else {
4265 Ok(())
4266 }
4267}
4268
4269fn default_worker_id() -> String {
4270 let millis = SystemTime::now()
4271 .duration_since(UNIX_EPOCH)
4272 .unwrap_or_default()
4273 .as_millis();
4274 format!("rust-worker-{}-{millis}", std::process::id())
4275}
4276
4277fn unique_request_id(prefix: &str) -> String {
4278 let nanos = SystemTime::now()
4279 .duration_since(UNIX_EPOCH)
4280 .unwrap_or_default()
4281 .as_nanos();
4282 format!("{prefix}-{}-{nanos}", std::process::id())
4283}
4284
4285#[derive(Debug)]
4286struct QueryTaskExecutionFailure {
4287 reason: String,
4288 message: String,
4289 failure_type: String,
4290}
4291
4292impl QueryTaskExecutionFailure {
4293 fn new(
4294 reason: impl Into<String>,
4295 message: impl Into<String>,
4296 failure_type: impl Into<String>,
4297 ) -> Self {
4298 Self {
4299 reason: reason.into(),
4300 message: message.into(),
4301 failure_type: failure_type.into(),
4302 }
4303 }
4304}
4305
4306#[derive(Clone, Debug)]
4312pub struct WorkflowInstance<S> {
4313 state: Arc<Mutex<S>>,
4314}
4315
4316impl<S> WorkflowInstance<S> {
4317 fn new(state: S) -> Self {
4318 Self {
4319 state: Arc::new(Mutex::new(state)),
4320 }
4321 }
4322
4323 pub fn read<R>(&self, reader: impl FnOnce(&S) -> R) -> Result<R> {
4325 let state = self
4326 .state
4327 .lock()
4328 .map_err(|_| Error::WorkflowStatePoisoned)?;
4329 Ok(reader(&state))
4330 }
4331
4332 pub fn update<R>(&self, transition: impl FnOnce(&mut S) -> R) -> Result<R> {
4334 let mut state = self
4335 .state
4336 .lock()
4337 .map_err(|_| Error::WorkflowStatePoisoned)?;
4338 Ok(transition(&mut state))
4339 }
4340}
4341
4342impl<S: Clone> WorkflowInstance<S> {
4343 fn snapshot(&self) -> Result<S> {
4344 self.read(Clone::clone)
4345 }
4346}
4347
4348#[derive(Clone, Debug)]
4349pub struct WorkflowContext {
4350 state: Arc<Mutex<WorkflowState>>,
4351}
4352
4353impl WorkflowContext {
4354 pub fn workflow_identity(&self) -> Result<WorkflowIdentity> {
4356 let state = self
4357 .state
4358 .lock()
4359 .map_err(|_| Error::WorkflowStatePoisoned)?;
4360 Ok(WorkflowIdentity {
4361 workflow_id: state.workflow_id.clone(),
4362 run_id: state.run_id.clone(),
4363 })
4364 }
4365
4366 pub fn history_budget(&self) -> Result<WorkflowHistoryBudget> {
4368 let state = self
4369 .state
4370 .lock()
4371 .map_err(|_| Error::WorkflowStatePoisoned)?;
4372 Ok(state.history_budget.clone())
4373 }
4374
4375 pub fn continue_as_new<T: Serialize>(&self, args: T) -> Result<Value> {
4381 self.continue_as_new_with_options(ContinueAsNewOptions::new(), args)
4382 }
4383
4384 pub fn continue_as_new_with_options<T: Serialize>(
4386 &self,
4387 options: ContinueAsNewOptions,
4388 args: T,
4389 ) -> Result<Value> {
4390 options.validate()?;
4391 Err(Error::ContinueAsNew(ContinueAsNewRequest {
4392 arguments: normalize_arguments(serde_json::to_value(args)?),
4393 options,
4394 }))
4395 }
4396
4397 pub fn activity<T: Serialize>(
4398 &self,
4399 activity_type: impl Into<String>,
4400 args: T,
4401 ) -> ActivityCall {
4402 self.activity_with_options(activity_type, ActivityOptions::new(), args)
4403 }
4404
4405 pub fn activity_on_queue<T, Q>(
4406 &self,
4407 activity_type: impl Into<String>,
4408 task_queue: Option<Q>,
4409 args: T,
4410 ) -> ActivityCall
4411 where
4412 T: Serialize,
4413 Q: Into<String>,
4414 {
4415 let mut options = ActivityOptions::new();
4416 options.task_queue = task_queue.map(Into::into);
4417 self.activity_with_options(activity_type, options, args)
4418 }
4419
4420 pub fn activity_with_options<T: Serialize>(
4458 &self,
4459 activity_type: impl Into<String>,
4460 options: ActivityOptions,
4461 args: T,
4462 ) -> ActivityCall {
4463 ActivityCall {
4464 ctx: self.clone(),
4465 activity_type: activity_type.into(),
4466 options,
4467 args: Some(serde_json::to_value(args).map_err(Error::from)),
4468 scheduled: false,
4469 }
4470 }
4471
4472 pub fn wait_signal(&self, signal_name: impl Into<String>) -> SignalCall {
4473 SignalCall {
4474 ctx: self.clone(),
4475 signal_name: signal_name.into(),
4476 opened_wait: false,
4477 matched_pending: false,
4478 }
4479 }
4480
4481 pub fn sleep(&self, duration: Duration) -> TimerCall {
4502 let delay_seconds = duration
4503 .as_secs()
4504 .checked_add(u64::from(duration.subsec_nanos() > 0));
4505 TimerCall {
4506 ctx: self.clone(),
4507 delay_seconds,
4508 scheduled: false,
4509 matched_pending: false,
4510 }
4511 }
4512
4513 pub fn start_timer(&self, duration: Duration) -> TimerCall {
4515 self.sleep(duration)
4516 }
4517
4518 pub fn side_effect<T, F>(&self, callback: F) -> Result<T>
4525 where
4526 T: Serialize + DeserializeOwned,
4527 F: FnOnce() -> T,
4528 {
4529 {
4530 let mut state = self
4531 .state
4532 .lock()
4533 .map_err(|_| Error::WorkflowStatePoisoned)?;
4534 if let Some(recorded) = state.recorded_commands.get(state.command_cursor).cloned() {
4535 return match recorded {
4536 RecordedCommand::SideEffect { sequence, value } => {
4537 state.command_cursor += 1;
4538 serde_json::from_value(value).map_err(|error| {
4539 Error::NonDeterministicReplay(ReplayFailure::new(
4540 "side_effect_type_mismatch",
4541 Some(sequence),
4542 Some(std::any::type_name::<T>().to_string()),
4543 Some(error.to_string()),
4544 "recorded side-effect value is incompatible with the requested Rust type",
4545 ))
4546 })
4547 }
4548 other => Err(command_mismatch(&other, "side effect")),
4549 };
4550 }
4551 }
4552
4553 let value = callback();
4554 let json_value = serde_json::to_value(&value)?;
4555 let mut state = self
4556 .state
4557 .lock()
4558 .map_err(|_| Error::WorkflowStatePoisoned)?;
4559 let result = encode_value_envelope(&json_value, &state.payload_codec)?;
4560 state.commands.push(json!({
4561 "type": "record_side_effect",
4562 "result": result,
4563 }));
4564 Ok(value)
4565 }
4566
4567 pub fn uuid_v4(&self) -> Result<Uuid> {
4569 self.side_effect(Uuid::new_v4)
4570 }
4571
4572 pub fn get_version(
4575 &self,
4576 change_id: impl Into<String>,
4577 min_supported: i32,
4578 max_supported: i32,
4579 ) -> Result<i32> {
4580 let change_id = change_id.into();
4581 if change_id.trim().is_empty() {
4582 return Err(Error::NonDeterministicReplay(ReplayFailure::new(
4583 "version_change_id_invalid",
4584 None,
4585 Some("non-empty change ID".to_string()),
4586 Some(change_id),
4587 "version markers require a stable non-empty change ID",
4588 )));
4589 }
4590 if min_supported > max_supported {
4591 return Err(Error::NonDeterministicReplay(ReplayFailure::new(
4592 "version_range_invalid",
4593 None,
4594 Some("min_supported <= max_supported".to_string()),
4595 Some(format!("{min_supported}..={max_supported}")),
4596 "version marker supported range is invalid",
4597 )));
4598 }
4599
4600 let mut state = self
4601 .state
4602 .lock()
4603 .map_err(|_| Error::WorkflowStatePoisoned)?;
4604 if let Some((version, sequence)) = state.version_markers.get(&change_id).copied() {
4605 ensure_version_supported(&change_id, version, min_supported, max_supported, sequence)?;
4606 return Ok(version);
4607 }
4608
4609 if let Some(recorded) = state.recorded_commands.get(state.command_cursor).cloned() {
4610 return match recorded {
4611 RecordedCommand::VersionMarker {
4612 sequence,
4613 change_id: recorded_change_id,
4614 version,
4615 ..
4616 } => {
4617 if recorded_change_id != change_id {
4618 return Err(Error::NonDeterministicReplay(ReplayFailure::new(
4619 "version_change_id_mismatch",
4620 Some(sequence),
4621 Some(recorded_change_id),
4622 Some(change_id),
4623 "recorded version marker change ID differs from current workflow code",
4624 )));
4625 }
4626 ensure_version_supported(
4627 &change_id,
4628 version,
4629 min_supported,
4630 max_supported,
4631 sequence,
4632 )?;
4633 state.command_cursor += 1;
4634 state.version_markers.insert(change_id, (version, sequence));
4635 Ok(version)
4636 }
4637 other => Err(command_mismatch(
4638 &other,
4639 format!("version marker:{change_id}"),
4640 )),
4641 };
4642 }
4643
4644 let version = max_supported;
4645 state.commands.push(json!({
4646 "type": "record_version_marker",
4647 "change_id": change_id,
4648 "version": version,
4649 "min_supported": min_supported,
4650 "max_supported": max_supported,
4651 }));
4652 state.version_markers.insert(change_id, (version, 0));
4655 Ok(version)
4656 }
4657
4658 pub fn patched(&self, change_id: impl Into<String>) -> Result<bool> {
4660 Ok(self.get_version(change_id, -1, 1)? == 1)
4661 }
4662
4663 pub fn deprecate_patch(&self, change_id: impl Into<String>) -> Result<()> {
4665 self.get_version(change_id, -1, 1).map(|_| ())
4666 }
4667
4668 pub fn start_child_workflow<T: Serialize>(
4695 &self,
4696 workflow_type: impl Into<String>,
4697 options: ChildWorkflowOptions,
4698 args: T,
4699 ) -> ChildWorkflowCall {
4700 ChildWorkflowCall {
4701 ctx: self.clone(),
4702 workflow_type: workflow_type.into(),
4703 options,
4704 args: Some(serde_json::to_value(args).map_err(Error::from)),
4705 scheduled: false,
4706 matched_pending: false,
4707 }
4708 }
4709
4710 fn take_commands(&self) -> Result<Vec<Value>> {
4711 let mut state = self
4712 .state
4713 .lock()
4714 .map_err(|_| Error::WorkflowStatePoisoned)?;
4715 Ok(std::mem::take(&mut state.commands))
4716 }
4717
4718 fn continue_as_new_command(&self, request: ContinueAsNewRequest) -> Result<Option<Value>> {
4719 let mut state = self
4720 .state
4721 .lock()
4722 .map_err(|_| Error::WorkflowStatePoisoned)?;
4723
4724 if let Some(recorded) = state.recorded_commands.get(state.command_cursor).cloned() {
4725 return Err(command_mismatch(&recorded, "continue as new"));
4726 }
4727 if state.recorded_continue_as_new_sequence.is_some() {
4728 state.continue_as_new_consumed = true;
4729 return Ok(None);
4730 }
4731
4732 let arguments = encode_value_envelope(&request.arguments, &state.payload_codec)?;
4733 let mut command = serde_json::Map::from_iter([
4734 ("type".to_string(), json!("continue_as_new")),
4735 ("arguments".to_string(), arguments),
4736 ("queue".to_string(), json!(state.task_queue.clone())),
4737 ]);
4738 if let Some(workflow_type) = request.options.workflow_type {
4739 command.insert("workflow_type".to_string(), json!(workflow_type));
4740 }
4741 if let Some(task_queue) = request.options.task_queue {
4742 command.insert("queue".to_string(), json!(task_queue));
4743 }
4744 Ok(Some(Value::Object(command)))
4745 }
4746
4747 fn matched_recorded_pending(&self) -> Result<bool> {
4748 let state = self
4749 .state
4750 .lock()
4751 .map_err(|_| Error::WorkflowStatePoisoned)?;
4752 Ok(state.matched_recorded_pending)
4753 }
4754
4755 fn ensure_history_consumed(&self) -> Result<()> {
4756 let state = self
4757 .state
4758 .lock()
4759 .map_err(|_| Error::WorkflowStatePoisoned)?;
4760 if let Some(command) = state.recorded_commands.get(state.command_cursor) {
4761 return Err(Error::NonDeterministicReplay(ReplayFailure::new(
4762 "recorded_commands_unconsumed",
4763 Some(command.sequence()),
4764 Some(command.shape().to_string()),
4765 Some("workflow completion".to_string()),
4766 "workflow completed before consuming all recorded durable commands",
4767 )));
4768 }
4769 if let Some(sequence) = state
4770 .recorded_continue_as_new_sequence
4771 .filter(|_| !state.continue_as_new_consumed)
4772 {
4773 return Err(Error::NonDeterministicReplay(ReplayFailure::new(
4774 "recorded_continue_as_new_unconsumed",
4775 Some(sequence),
4776 Some("continue as new".to_string()),
4777 Some("workflow completion".to_string()),
4778 "workflow completed without consuming its recorded continue-as-new transition",
4779 )));
4780 }
4781 Ok(())
4782 }
4783}
4784
4785#[derive(Debug)]
4786struct WorkflowState {
4787 history: Vec<HistoryEvent>,
4788 workflow_id: Option<String>,
4789 run_id: Option<String>,
4790 task_queue: String,
4791 payload_codec: String,
4792 history_budget: WorkflowHistoryBudget,
4793 resume_signal: Option<ResumeSignal>,
4794 recorded_commands: Vec<RecordedCommand>,
4795 recorded_continue_as_new_sequence: Option<u64>,
4796 continue_as_new_consumed: bool,
4797 command_cursor: usize,
4798 matched_recorded_pending: bool,
4799 signal_cursors: HashMap<String, usize>,
4800 version_markers: HashMap<String, (i32, u64)>,
4801 commands: Vec<Value>,
4802}
4803
4804impl WorkflowState {
4805 #[cfg(test)]
4806 fn new(
4807 history: Vec<HistoryEvent>,
4808 task_queue: String,
4809 payload_codec: String,
4810 resume_signal: Option<ResumeSignal>,
4811 ) -> Result<Self> {
4812 Self::new_with_identity(
4813 history,
4814 None,
4815 None,
4816 task_queue,
4817 payload_codec,
4818 resume_signal,
4819 )
4820 }
4821
4822 fn new_with_identity(
4823 history: Vec<HistoryEvent>,
4824 workflow_id: Option<String>,
4825 run_id: Option<String>,
4826 task_queue: String,
4827 payload_codec: String,
4828 resume_signal: Option<ResumeSignal>,
4829 ) -> Result<Self> {
4830 let recorded_commands = recorded_commands(
4831 &history,
4832 &payload_codec,
4833 WorkflowIdentity {
4834 workflow_id: workflow_id.clone(),
4835 run_id: run_id.clone(),
4836 },
4837 )?;
4838 let recorded_continue_as_new = history
4839 .iter()
4840 .filter(|event| event.event_type == "WorkflowContinuedAsNew")
4841 .collect::<Vec<_>>();
4842 if recorded_continue_as_new.len() > 1 {
4843 return Err(invalid_recorded_history(
4844 "duplicate_continue_as_new_transition",
4845 recorded_continue_as_new
4846 .last()
4847 .and_then(|event| durable_event_sequence(event))
4848 .unwrap_or(0),
4849 "one WorkflowContinuedAsNew event",
4850 &format!(
4851 "{} WorkflowContinuedAsNew events",
4852 recorded_continue_as_new.len()
4853 ),
4854 "workflow history records one continue-as-new transition more than once",
4855 ));
4856 }
4857 let recorded_continue_as_new_sequence = recorded_continue_as_new
4858 .first()
4859 .map(|event| {
4860 durable_event_sequence(event).ok_or_else(|| {
4861 Error::NonDeterministicReplay(ReplayFailure::new(
4862 "continue_as_new_sequence_missing",
4863 None,
4864 Some("recorded transition sequence".to_string()),
4865 Some("missing sequence".to_string()),
4866 "WorkflowContinuedAsNew history is missing its recorded sequence",
4867 ))
4868 })
4869 })
4870 .transpose()?;
4871 let event_count = u64::try_from(history.len()).unwrap_or(u64::MAX);
4872 Ok(Self {
4873 history,
4874 workflow_id,
4875 run_id,
4876 task_queue,
4877 payload_codec,
4878 history_budget: WorkflowHistoryBudget {
4879 event_count,
4880 ..WorkflowHistoryBudget::default()
4881 },
4882 resume_signal,
4883 recorded_commands,
4884 recorded_continue_as_new_sequence,
4885 continue_as_new_consumed: false,
4886 command_cursor: 0,
4887 matched_recorded_pending: false,
4888 signal_cursors: HashMap::new(),
4889 version_markers: HashMap::new(),
4890 commands: Vec::new(),
4891 })
4892 }
4893}
4894
4895#[derive(Clone, Debug)]
4896enum RecordedCommand {
4897 Activity {
4898 sequence: u64,
4899 activity_type: Option<String>,
4900 options: Option<RecordedActivityOptions>,
4901 outcome: Option<ActivityOutcome>,
4902 },
4903 Timer {
4904 sequence: u64,
4905 delay_seconds: u64,
4906 fired: bool,
4907 },
4908 ChildWorkflow {
4909 sequence: u64,
4910 workflow_type: Option<String>,
4911 outcome: Option<ChildWorkflowOutcome>,
4912 },
4913 SignalWait {
4914 sequence: u64,
4915 signal_name: Option<String>,
4916 },
4917 SideEffect {
4918 sequence: u64,
4919 value: Value,
4920 },
4921 VersionMarker {
4922 sequence: u64,
4923 change_id: String,
4924 version: i32,
4925 },
4926}
4927
4928#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
4929struct RecordedActivityOptions {
4930 task_queue: RecordedSnapshotValue<Option<String>>,
4931 execution_mode: RecordedSnapshotValue<Option<String>>,
4932 retry_policy: ActivityRetrySnapshot,
4933}
4934
4935#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
4936enum RecordedSnapshotValue<T> {
4937 Unknown,
4939 Known(T),
4940}
4941
4942impl<T: PartialEq> RecordedSnapshotValue<T> {
4943 fn matches_current(&self, current: &Self) -> bool {
4944 match self {
4945 Self::Unknown => true,
4946 Self::Known(recorded) => matches!(current, Self::Known(value) if value == recorded),
4947 }
4948 }
4949}
4950
4951#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
4952struct ActivityRetrySnapshot {
4953 snapshot_version: RecordedSnapshotValue<Option<u64>>,
4954 max_attempts: RecordedSnapshotValue<Option<u64>>,
4955 backoff_seconds: RecordedSnapshotValue<Vec<u64>>,
4956 start_to_close_timeout: RecordedSnapshotValue<Option<u64>>,
4957 schedule_to_start_timeout: RecordedSnapshotValue<Option<u64>>,
4958 schedule_to_close_timeout: RecordedSnapshotValue<Option<u64>>,
4959 heartbeat_timeout: RecordedSnapshotValue<Option<u64>>,
4960 non_retryable_error_types: RecordedSnapshotValue<Vec<String>>,
4961}
4962
4963impl ActivityRetrySnapshot {
4964 fn matches_current(&self, current: &Self) -> bool {
4965 self.snapshot_version
4966 .matches_current(¤t.snapshot_version)
4967 && self.max_attempts.matches_current(¤t.max_attempts)
4968 && self
4969 .backoff_seconds
4970 .matches_current(¤t.backoff_seconds)
4971 && self
4972 .start_to_close_timeout
4973 .matches_current(¤t.start_to_close_timeout)
4974 && self
4975 .schedule_to_start_timeout
4976 .matches_current(¤t.schedule_to_start_timeout)
4977 && self
4978 .schedule_to_close_timeout
4979 .matches_current(¤t.schedule_to_close_timeout)
4980 && self
4981 .heartbeat_timeout
4982 .matches_current(¤t.heartbeat_timeout)
4983 && self
4984 .non_retryable_error_types
4985 .matches_current(¤t.non_retryable_error_types)
4986 }
4987}
4988
4989fn recorded_optional_u64(
4990 object: Option<&serde_json::Map<String, Value>>,
4991 field: &str,
4992) -> RecordedSnapshotValue<Option<u64>> {
4993 match object.and_then(|object| object.get(field)) {
4994 None => RecordedSnapshotValue::Unknown,
4995 Some(Value::Null) => RecordedSnapshotValue::Known(None),
4996 Some(value) => RecordedSnapshotValue::Known(value_as_u64(value)),
4997 }
4998}
4999
5000fn recorded_optional_string(
5001 object: &serde_json::Map<String, Value>,
5002 field: &str,
5003) -> RecordedSnapshotValue<Option<String>> {
5004 match object.get(field) {
5005 None => RecordedSnapshotValue::Unknown,
5006 Some(Value::Null) => RecordedSnapshotValue::Known(None),
5007 Some(value) => RecordedSnapshotValue::Known(value.as_str().map(str::to_string)),
5008 }
5009}
5010
5011fn recorded_activity_retry_snapshot(policy: Option<&Value>) -> ActivityRetrySnapshot {
5012 let policy = policy.and_then(Value::as_object);
5013 let backoff_seconds = policy
5014 .and_then(|policy| policy.get("backoff_seconds"))
5015 .and_then(Value::as_array)
5016 .map(|intervals| intervals.iter().filter_map(value_as_u64).collect())
5017 .map_or(RecordedSnapshotValue::Unknown, RecordedSnapshotValue::Known);
5018 let mut non_retryable_error_types = Vec::new();
5019 for error_type in policy
5020 .and_then(|policy| policy.get("non_retryable_error_types"))
5021 .and_then(Value::as_array)
5022 .into_iter()
5023 .flatten()
5024 .filter_map(Value::as_str)
5025 .map(str::trim)
5026 .filter(|error_type| !error_type.is_empty())
5027 {
5028 if !non_retryable_error_types
5029 .iter()
5030 .any(|recorded| recorded == error_type)
5031 {
5032 non_retryable_error_types.push(error_type.to_string());
5033 }
5034 }
5035
5036 ActivityRetrySnapshot {
5037 snapshot_version: recorded_optional_u64(policy, "snapshot_version"),
5038 max_attempts: recorded_optional_u64(policy, "max_attempts"),
5039 backoff_seconds,
5040 start_to_close_timeout: recorded_optional_u64(policy, "start_to_close_timeout"),
5041 schedule_to_start_timeout: recorded_optional_u64(policy, "schedule_to_start_timeout"),
5042 schedule_to_close_timeout: recorded_optional_u64(policy, "schedule_to_close_timeout"),
5043 heartbeat_timeout: recorded_optional_u64(policy, "heartbeat_timeout"),
5044 non_retryable_error_types: if policy
5045 .is_some_and(|policy| policy.contains_key("non_retryable_error_types"))
5046 {
5047 RecordedSnapshotValue::Known(non_retryable_error_types)
5048 } else {
5049 RecordedSnapshotValue::Unknown
5050 },
5051 }
5052}
5053
5054fn current_activity_retry_snapshot(options: &ValidatedActivityOptions) -> ActivityRetrySnapshot {
5055 let policy = options.retry_policy.as_ref();
5056 let max_attempts = match policy.and_then(|policy| policy.get("max_attempts")) {
5057 Some(Value::Null) => None,
5058 Some(value) => value_as_u64(value),
5059 None => Some(1),
5060 };
5061 let backoff_seconds = policy
5062 .and_then(|policy| policy.get("backoff_seconds"))
5063 .and_then(Value::as_array)
5064 .map(|intervals| intervals.iter().filter_map(value_as_u64).collect())
5065 .unwrap_or_default();
5066 let non_retryable_error_types = policy
5067 .and_then(|policy| policy.get("non_retryable_error_types"))
5068 .and_then(Value::as_array)
5069 .into_iter()
5070 .flatten()
5071 .filter_map(Value::as_str)
5072 .map(str::to_string)
5073 .collect();
5074
5075 ActivityRetrySnapshot {
5076 snapshot_version: RecordedSnapshotValue::Known(Some(1)),
5077 max_attempts: RecordedSnapshotValue::Known(max_attempts),
5078 backoff_seconds: RecordedSnapshotValue::Known(backoff_seconds),
5079 start_to_close_timeout: RecordedSnapshotValue::Known(options.start_to_close_timeout),
5080 schedule_to_start_timeout: RecordedSnapshotValue::Known(options.schedule_to_start_timeout),
5081 schedule_to_close_timeout: RecordedSnapshotValue::Known(options.schedule_to_close_timeout),
5082 heartbeat_timeout: RecordedSnapshotValue::Known(options.heartbeat_timeout),
5083 non_retryable_error_types: RecordedSnapshotValue::Known(non_retryable_error_types),
5084 }
5085}
5086
5087fn activity_options_description(options: &RecordedActivityOptions) -> String {
5088 serde_json::to_string(options).unwrap_or_else(|_| format!("{options:?}"))
5089}
5090
5091impl RecordedCommand {
5092 fn sequence(&self) -> u64 {
5093 match self {
5094 Self::Activity { sequence, .. }
5095 | Self::Timer { sequence, .. }
5096 | Self::ChildWorkflow { sequence, .. }
5097 | Self::SignalWait { sequence, .. }
5098 | Self::SideEffect { sequence, .. }
5099 | Self::VersionMarker { sequence, .. } => *sequence,
5100 }
5101 }
5102
5103 fn shape(&self) -> &'static str {
5104 match self {
5105 Self::Activity { .. } => "activity",
5106 Self::Timer { .. } => "timer",
5107 Self::ChildWorkflow { .. } => "child workflow",
5108 Self::SignalWait { .. } => "signal wait",
5109 Self::SideEffect { .. } => "side effect",
5110 Self::VersionMarker { .. } => "version marker",
5111 }
5112 }
5113}
5114
5115fn ensure_version_supported(
5116 change_id: &str,
5117 version: i32,
5118 min_supported: i32,
5119 max_supported: i32,
5120 sequence: u64,
5121) -> Result<()> {
5122 if (min_supported..=max_supported).contains(&version) {
5123 return Ok(());
5124 }
5125 Err(Error::NonDeterministicReplay(ReplayFailure::new(
5126 "version_marker_incompatible_range",
5127 (sequence != 0).then_some(sequence),
5128 Some(format!("{min_supported}..={max_supported}")),
5129 Some(format!("{change_id}:{version}")),
5130 "recorded workflow version is outside the range supported by current code",
5131 )))
5132}
5133
5134#[derive(Clone, Debug)]
5135struct ResumeSignal {
5136 signal_id: Option<String>,
5137 signal_name: String,
5138 arguments: Vec<Value>,
5139}
5140
5141pub struct ActivityCall {
5142 ctx: WorkflowContext,
5143 activity_type: String,
5144 options: ActivityOptions,
5145 args: Option<Result<Value>>,
5146 scheduled: bool,
5147}
5148
5149impl Future for ActivityCall {
5150 type Output = Result<Value>;
5151
5152 fn poll(mut self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
5153 let ctx = self.ctx.clone();
5154 let mut state = match ctx.state.lock() {
5155 Ok(state) => state,
5156 Err(_) => return Poll::Ready(Err(Error::WorkflowStatePoisoned)),
5157 };
5158
5159 if self.scheduled {
5160 return Poll::Pending;
5161 }
5162
5163 let options = match self.options.validate() {
5164 Ok(options) => options,
5165 Err(error) => {
5166 return Poll::Ready(Err(Error::InvalidActivityOptions(error)));
5167 }
5168 };
5169 let task_queue = options
5170 .task_queue
5171 .clone()
5172 .unwrap_or_else(|| state.task_queue.clone());
5173 let current_recorded_options = RecordedActivityOptions {
5174 task_queue: RecordedSnapshotValue::Known(Some(task_queue.clone())),
5175 execution_mode: RecordedSnapshotValue::Known(None),
5178 retry_policy: current_activity_retry_snapshot(&options),
5179 };
5180
5181 if let Some(recorded) = state.recorded_commands.get(state.command_cursor).cloned() {
5182 let sequence = recorded.sequence();
5183 match recorded {
5184 RecordedCommand::Activity {
5185 activity_type,
5186 options: recorded_options,
5187 outcome,
5188 ..
5189 } => {
5190 if let Some(recorded_type) = activity_type {
5191 if recorded_type != self.activity_type {
5192 return Poll::Ready(Err(Error::NonDeterministicReplay(
5193 ReplayFailure::new(
5194 "recorded_command_detail_mismatch",
5195 Some(sequence),
5196 Some(format!("activity:{recorded_type}")),
5197 Some(format!("activity:{}", self.activity_type)),
5198 "recorded activity type differs from the current workflow command",
5199 ),
5200 )));
5201 }
5202 }
5203 if let Some(recorded_options) = recorded_options {
5204 if !recorded_options
5205 .task_queue
5206 .matches_current(¤t_recorded_options.task_queue)
5207 {
5208 return Poll::Ready(Err(Error::NonDeterministicReplay(
5209 ReplayFailure::new(
5210 "activity_task_queue_mismatch",
5211 Some(sequence),
5212 Some(activity_options_description(&recorded_options)),
5213 Some(activity_options_description(¤t_recorded_options)),
5214 "recorded activity task queue differs from the current workflow command",
5215 ),
5216 )));
5217 }
5218 if !recorded_options
5219 .execution_mode
5220 .matches_current(¤t_recorded_options.execution_mode)
5221 {
5222 return Poll::Ready(Err(Error::NonDeterministicReplay(
5223 ReplayFailure::new(
5224 "activity_execution_mode_mismatch",
5225 Some(sequence),
5226 Some(activity_options_description(&recorded_options)),
5227 Some(activity_options_description(¤t_recorded_options)),
5228 "recorded activity execution mode differs from the current workflow command",
5229 ),
5230 )));
5231 }
5232 if !recorded_options
5233 .retry_policy
5234 .matches_current(¤t_recorded_options.retry_policy)
5235 {
5236 return Poll::Ready(Err(Error::NonDeterministicReplay(
5237 ReplayFailure::new(
5238 "activity_retry_policy_mismatch",
5239 Some(sequence),
5240 Some(activity_options_description(&recorded_options)),
5241 Some(activity_options_description(¤t_recorded_options)),
5242 "recorded activity retry policy differs from the current workflow command",
5243 ),
5244 )));
5245 }
5246 }
5247 state.command_cursor += 1;
5248 if let Some(outcome) = outcome {
5249 return Poll::Ready(outcome.map_err(Error::ActivityFailed));
5250 }
5251 state.matched_recorded_pending = true;
5252 self.scheduled = true;
5253 return Poll::Pending;
5254 }
5255 other => {
5256 return Poll::Ready(Err(command_mismatch(
5257 &other,
5258 format!("activity:{}", self.activity_type),
5259 )));
5260 }
5261 }
5262 }
5263
5264 if !self.scheduled {
5265 let args = match self.args.take().unwrap_or(Ok(Value::Null)) {
5266 Ok(args) => args,
5267 Err(error) => return Poll::Ready(Err(error)),
5268 };
5269 let arguments = normalize_arguments(args);
5270 let envelope = match encode_value_envelope(&arguments, &state.payload_codec) {
5271 Ok(envelope) => envelope,
5272 Err(error) => return Poll::Ready(Err(error)),
5273 };
5274
5275 let mut command = serde_json::Map::from_iter([
5276 ("type".to_string(), json!("schedule_activity")),
5277 (
5278 "activity_type".to_string(),
5279 json!(self.activity_type.clone()),
5280 ),
5281 ("queue".to_string(), json!(task_queue)),
5282 ("arguments".to_string(), envelope),
5283 ]);
5284 for (field, value) in [
5285 ("start_to_close_timeout", options.start_to_close_timeout),
5286 (
5287 "schedule_to_start_timeout",
5288 options.schedule_to_start_timeout,
5289 ),
5290 (
5291 "schedule_to_close_timeout",
5292 options.schedule_to_close_timeout,
5293 ),
5294 ("heartbeat_timeout", options.heartbeat_timeout),
5295 ] {
5296 if let Some(value) = value {
5297 command.insert(field.to_string(), json!(value));
5298 }
5299 }
5300 if let Some(retry_policy) = options.retry_policy {
5301 command.insert("retry_policy".to_string(), retry_policy);
5302 }
5303 state.commands.push(Value::Object(command));
5304 self.scheduled = true;
5305 }
5306
5307 Poll::Pending
5308 }
5309}
5310
5311pub struct TimerCall {
5313 ctx: WorkflowContext,
5314 delay_seconds: Option<u64>,
5315 scheduled: bool,
5316 matched_pending: bool,
5317}
5318
5319impl Future for TimerCall {
5320 type Output = Result<()>;
5321
5322 fn poll(mut self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
5323 if self.matched_pending {
5324 return Poll::Pending;
5325 }
5326
5327 let ctx = self.ctx.clone();
5328 let Some(requested_delay) = self.delay_seconds else {
5329 return Poll::Ready(Err(Error::TimerDurationOverflow));
5330 };
5331 let mut state = match ctx.state.lock() {
5332 Ok(state) => state,
5333 Err(_) => return Poll::Ready(Err(Error::WorkflowStatePoisoned)),
5334 };
5335
5336 if let Some(recorded) = state.recorded_commands.get(state.command_cursor).cloned() {
5337 match recorded {
5338 RecordedCommand::Timer {
5339 sequence,
5340 delay_seconds,
5341 fired,
5342 ..
5343 } => {
5344 if delay_seconds != requested_delay {
5345 return Poll::Ready(Err(Error::NonDeterministicReplay(
5346 ReplayFailure::new(
5347 "timer_delay_mismatch",
5348 Some(sequence),
5349 Some(format!("timer:{delay_seconds}s")),
5350 Some(format!("timer:{requested_delay}s")),
5351 "recorded timer delay differs from the current workflow command",
5352 ),
5353 )));
5354 }
5355 state.command_cursor += 1;
5356 if fired {
5357 return Poll::Ready(Ok(()));
5358 }
5359 state.matched_recorded_pending = true;
5360 self.scheduled = true;
5361 self.matched_pending = true;
5362 return Poll::Pending;
5363 }
5364 other => return Poll::Ready(Err(command_mismatch(&other, "timer"))),
5365 }
5366 }
5367
5368 if !self.scheduled {
5369 state.commands.push(json!({
5370 "type": "start_timer",
5371 "delay_seconds": requested_delay,
5372 }));
5373 self.scheduled = true;
5374 }
5375
5376 Poll::Pending
5377 }
5378}
5379
5380pub struct ChildWorkflowCall {
5382 ctx: WorkflowContext,
5383 workflow_type: String,
5384 options: ChildWorkflowOptions,
5385 args: Option<Result<Value>>,
5386 scheduled: bool,
5387 matched_pending: bool,
5388}
5389
5390impl Future for ChildWorkflowCall {
5391 type Output = Result<ChildWorkflowResult>;
5392
5393 fn poll(mut self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
5394 if self.matched_pending {
5395 return Poll::Pending;
5396 }
5397
5398 let ctx = self.ctx.clone();
5399 let mut state = match ctx.state.lock() {
5400 Ok(state) => state,
5401 Err(_) => return Poll::Ready(Err(Error::WorkflowStatePoisoned)),
5402 };
5403
5404 if let Some(recorded) = state.recorded_commands.get(state.command_cursor).cloned() {
5405 let sequence = recorded.sequence();
5406 match recorded {
5407 RecordedCommand::ChildWorkflow {
5408 workflow_type,
5409 outcome,
5410 ..
5411 } => {
5412 if let Some(recorded_type) = workflow_type {
5413 if recorded_type != self.workflow_type {
5414 return Poll::Ready(Err(Error::NonDeterministicReplay(
5415 ReplayFailure::new(
5416 "recorded_command_detail_mismatch",
5417 Some(sequence),
5418 Some(format!("child workflow:{recorded_type}")),
5419 Some(format!("child workflow:{}", self.workflow_type)),
5420 "recorded child workflow type differs from the current workflow command",
5421 ),
5422 )));
5423 }
5424 }
5425 state.command_cursor += 1;
5426 if let Some(outcome) = outcome {
5427 return Poll::Ready(outcome.map_err(Error::ChildWorkflowFailed));
5428 }
5429 state.matched_recorded_pending = true;
5430 self.scheduled = true;
5431 self.matched_pending = true;
5432 return Poll::Pending;
5433 }
5434 other => {
5435 return Poll::Ready(Err(command_mismatch(
5436 &other,
5437 format!("child workflow:{}", self.workflow_type),
5438 )));
5439 }
5440 }
5441 }
5442
5443 if !self.scheduled {
5444 if self.options.task_queue.trim().is_empty() {
5445 return Poll::Ready(Err(Error::InvalidChildWorkflowOptions(
5446 "task_queue must not be empty".to_string(),
5447 )));
5448 }
5449 for (name, value) in [
5450 (
5451 "execution_timeout_seconds",
5452 self.options.execution_timeout_seconds,
5453 ),
5454 ("run_timeout_seconds", self.options.run_timeout_seconds),
5455 ] {
5456 if value == Some(0) {
5457 return Poll::Ready(Err(Error::InvalidChildWorkflowOptions(format!(
5458 "{name} must be at least 1"
5459 ))));
5460 }
5461 }
5462
5463 let args = match self.args.take().unwrap_or(Ok(Value::Null)) {
5464 Ok(args) => args,
5465 Err(error) => return Poll::Ready(Err(error)),
5466 };
5467 let arguments =
5468 match encode_value_envelope(&normalize_arguments(args), &state.payload_codec) {
5469 Ok(arguments) => arguments,
5470 Err(error) => return Poll::Ready(Err(error)),
5471 };
5472 let mut command = json!({
5473 "type": "start_child_workflow",
5474 "workflow_type": self.workflow_type,
5475 "queue": self.options.task_queue,
5476 "parent_close_policy": self.options.parent_close_policy.as_str(),
5477 "arguments": arguments,
5478 });
5479 let object = command
5480 .as_object_mut()
5481 .expect("child workflow command is always an object");
5482 if let Some(policy) = &self.options.retry_policy {
5483 let mut retry_policy = serde_json::Map::new();
5484 if let Some(max_attempts) = policy.max_attempts {
5485 if max_attempts == 0 {
5486 return Poll::Ready(Err(Error::InvalidChildWorkflowOptions(
5487 "retry_policy.max_attempts must be at least 1".to_string(),
5488 )));
5489 }
5490 retry_policy.insert("max_attempts".to_string(), json!(max_attempts));
5491 }
5492 if !policy.backoff_seconds.is_empty() {
5493 retry_policy
5494 .insert("backoff_seconds".to_string(), json!(policy.backoff_seconds));
5495 }
5496 if !policy.non_retryable_error_types.is_empty() {
5497 retry_policy.insert(
5498 "non_retryable_error_types".to_string(),
5499 json!(policy.non_retryable_error_types),
5500 );
5501 }
5502 if retry_policy.is_empty() {
5503 return Poll::Ready(Err(Error::InvalidChildWorkflowOptions(
5504 "retry_policy must configure at least one field".to_string(),
5505 )));
5506 }
5507 object.insert("retry_policy".to_string(), Value::Object(retry_policy));
5508 }
5509 if let Some(seconds) = self.options.execution_timeout_seconds {
5510 object.insert("execution_timeout_seconds".to_string(), json!(seconds));
5511 }
5512 if let Some(seconds) = self.options.run_timeout_seconds {
5513 object.insert("run_timeout_seconds".to_string(), json!(seconds));
5514 }
5515 state.commands.push(command);
5516 self.scheduled = true;
5517 }
5518
5519 Poll::Pending
5520 }
5521}
5522
5523fn command_mismatch(recorded: &RecordedCommand, actual: impl Into<String>) -> Error {
5524 Error::NonDeterministicReplay(ReplayFailure::new(
5525 "recorded_command_mismatch",
5526 Some(recorded.sequence()),
5527 Some(recorded.shape().to_string()),
5528 Some(actual.into()),
5529 "current workflow command does not match the recorded durable command sequence",
5530 ))
5531}
5532
5533pub struct SignalCall {
5534 ctx: WorkflowContext,
5535 signal_name: String,
5536 opened_wait: bool,
5537 matched_pending: bool,
5538}
5539
5540impl Future for SignalCall {
5541 type Output = Result<Vec<Value>>;
5542
5543 fn poll(mut self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
5544 if self.matched_pending {
5545 return Poll::Pending;
5546 }
5547
5548 let ctx = self.ctx.clone();
5549 let mut state = match ctx.state.lock() {
5550 Ok(state) => state,
5551 Err(_) => return Poll::Ready(Err(Error::WorkflowStatePoisoned)),
5552 };
5553
5554 let signals = match signal_values(
5555 &state.history,
5556 &self.signal_name,
5557 &state.payload_codec,
5558 state.resume_signal.as_ref(),
5559 ) {
5560 Ok(signals) => signals,
5561 Err(error) => return Poll::Ready(Err(error)),
5562 };
5563 let cursor = *state.signal_cursors.get(&self.signal_name).unwrap_or(&0);
5564
5565 if let Some(recorded) = state.recorded_commands.get(state.command_cursor).cloned() {
5566 match recorded {
5567 RecordedCommand::SignalWait {
5568 sequence,
5569 signal_name,
5570 } => {
5571 if let Some(recorded_name) = signal_name {
5572 if recorded_name != self.signal_name {
5573 return Poll::Ready(Err(Error::NonDeterministicReplay(
5574 ReplayFailure::new(
5575 "recorded_command_detail_mismatch",
5576 Some(sequence),
5577 Some(format!("signal wait:{recorded_name}")),
5578 Some(format!("signal wait:{}", self.signal_name)),
5579 "recorded signal name differs from the current workflow command",
5580 ),
5581 )));
5582 }
5583 }
5584
5585 state.command_cursor += 1;
5586 if cursor < signals.len() {
5587 state
5588 .signal_cursors
5589 .insert(self.signal_name.clone(), cursor + 1);
5590 return Poll::Ready(Ok(signals[cursor].clone()));
5591 }
5592
5593 state.matched_recorded_pending = true;
5594 self.opened_wait = true;
5595 self.matched_pending = true;
5596 return Poll::Pending;
5597 }
5598 other => {
5599 return Poll::Ready(Err(command_mismatch(
5600 &other,
5601 format!("signal wait:{}", self.signal_name),
5602 )));
5603 }
5604 }
5605 }
5606
5607 if cursor < signals.len() {
5608 state
5609 .signal_cursors
5610 .insert(self.signal_name.clone(), cursor + 1);
5611 return Poll::Ready(Ok(signals[cursor].clone()));
5612 }
5613
5614 if !self.opened_wait {
5615 state.commands.push(json!({
5616 "type": "open_condition_wait",
5617 "condition_key": format!("signal:{}", self.signal_name)
5618 }));
5619 self.opened_wait = true;
5620 }
5621
5622 Poll::Pending
5623 }
5624}
5625
5626#[derive(Clone, Debug)]
5627pub struct ActivityContext {
5628 client: Client,
5629 pub task_id: String,
5630 pub activity_attempt_id: String,
5631 pub lease_owner: String,
5632 pub activity_type: String,
5633 pub attempt_number: u64,
5634 pub task_queue: String,
5635 pub worker_id: String,
5636}
5637
5638impl ActivityContext {
5639 pub async fn heartbeat<T: Serialize>(&self, details: T) -> Result<ActivityHeartbeatResponse> {
5640 self.client
5641 .heartbeat_activity_task(
5642 &self.task_id,
5643 &self.activity_attempt_id,
5644 &self.lease_owner,
5645 serde_json::to_value(details)?,
5646 )
5647 .await
5648 }
5649}
5650
5651fn decode_task_arguments(value: Option<&Value>, codec: &str) -> Result<Value> {
5652 match value {
5653 Some(value) => Ok(normalize_arguments(decode_wire_value(value, codec)?)),
5654 None => Ok(Value::Array(Vec::new())),
5655 }
5656}
5657
5658fn decode_resume_signal(task: &WorkflowTask) -> Result<Option<ResumeSignal>> {
5659 let Some(signal_name) = task
5660 .signal_name
5661 .as_deref()
5662 .filter(|value| !value.is_empty())
5663 else {
5664 return Ok(None);
5665 };
5666 let Some(arguments) = task.signal_arguments.as_ref() else {
5667 return Ok(None);
5668 };
5669
5670 let decoded = normalize_arguments(decode_wire_value(arguments, &task.payload_codec)?);
5671 let Value::Array(arguments) = decoded else {
5672 unreachable!("normalize_arguments always returns an array");
5673 };
5674
5675 Ok(Some(ResumeSignal {
5676 signal_id: task.workflow_signal_id.clone(),
5677 signal_name: signal_name.to_string(),
5678 arguments,
5679 }))
5680}
5681
5682fn normalize_arguments(value: Value) -> Value {
5683 match value {
5684 Value::Null => Value::Array(Vec::new()),
5685 Value::Array(_) => value,
5686 other => Value::Array(vec![other]),
5687 }
5688}
5689
5690fn recorded_commands(
5691 events: &[HistoryEvent],
5692 fallback_codec: &str,
5693 parent: WorkflowIdentity,
5694) -> Result<Vec<RecordedCommand>> {
5695 let mut events_by_sequence: BTreeMap<u64, Vec<&HistoryEvent>> = BTreeMap::new();
5696 let mut last_new_sequence = None;
5697
5698 for event in events {
5699 let is_activity = matches!(
5700 event.event_type.as_str(),
5701 "ActivityScheduled"
5702 | "ActivityStarted"
5703 | "ActivityHeartbeatRecorded"
5704 | "ActivityRetryScheduled"
5705 | "ActivityCompleted"
5706 | "ActivityFailed"
5707 | "ActivityCancelled"
5708 | "ActivityTimedOut"
5709 );
5710 let is_workflow_timer = matches!(
5711 event.event_type.as_str(),
5712 "TimerScheduled" | "TimerCancelled" | "TimerFired"
5713 ) && !is_internal_timer_event(event);
5714 let is_child_workflow = matches!(
5715 event.event_type.as_str(),
5716 "ChildWorkflowScheduled"
5717 | "ChildRunCompleted"
5718 | "ChildRunFailed"
5719 | "ChildRunCancelled"
5720 | "ChildRunTerminated"
5721 );
5722 let is_signal_wait = is_recorded_signal_wait_event(event);
5723 let is_side_effect = event.event_type == "SideEffectRecorded";
5724 let is_version_marker = event.event_type == "VersionMarkerRecorded";
5725 if !is_activity
5726 && !is_workflow_timer
5727 && !is_child_workflow
5728 && !is_signal_wait
5729 && !is_side_effect
5730 && !is_version_marker
5731 {
5732 continue;
5733 }
5734
5735 let sequence = durable_event_sequence(event).ok_or_else(|| {
5736 Error::NonDeterministicReplay(ReplayFailure::new(
5737 "durable_command_sequence_missing",
5738 None,
5739 Some("positive workflow sequence".to_string()),
5740 Some(event.event_type.clone()),
5741 "durable command history event has no workflow sequence",
5742 ))
5743 })?;
5744 if sequence == 0 {
5745 return Err(Error::NonDeterministicReplay(ReplayFailure::new(
5746 "durable_command_sequence_invalid",
5747 Some(sequence),
5748 Some("positive workflow sequence".to_string()),
5749 Some(sequence.to_string()),
5750 "durable command history uses an invalid workflow sequence",
5751 )));
5752 }
5753 if !events_by_sequence.contains_key(&sequence) {
5754 if let Some(previous) = last_new_sequence {
5755 if sequence < previous {
5756 return Err(invalid_recorded_history(
5757 "durable_command_sequence_mismatch",
5758 sequence,
5759 &format!("workflow sequence greater than {previous}"),
5760 &sequence.to_string(),
5761 "durable commands are not strictly ordered by their recorded workflow sequence",
5762 ));
5763 }
5764 }
5765 last_new_sequence = Some(sequence);
5766 }
5767 events_by_sequence.entry(sequence).or_default().push(event);
5768 }
5769
5770 let commands: Vec<RecordedCommand> = events_by_sequence
5771 .into_iter()
5772 .map(|(sequence, sequence_events)| {
5773 let activity_events: Vec<_> = sequence_events
5774 .iter()
5775 .copied()
5776 .filter(|event| event.event_type.starts_with("Activity"))
5777 .collect();
5778 let timer_events: Vec<_> = sequence_events
5779 .iter()
5780 .copied()
5781 .filter(|event| event.event_type.starts_with("Timer"))
5782 .collect();
5783 let child_events: Vec<_> = sequence_events
5784 .iter()
5785 .copied()
5786 .filter(|event| {
5787 event.event_type == "ChildWorkflowScheduled"
5788 || event.event_type.starts_with("ChildRun")
5789 })
5790 .collect();
5791 let signal_wait_events: Vec<_> = sequence_events
5792 .iter()
5793 .copied()
5794 .filter(|event| is_recorded_signal_wait_event(event))
5795 .collect();
5796 let side_effect_events: Vec<_> = sequence_events
5797 .iter()
5798 .copied()
5799 .filter(|event| event.event_type == "SideEffectRecorded")
5800 .collect();
5801 let version_marker_events: Vec<_> = sequence_events
5802 .iter()
5803 .copied()
5804 .filter(|event| event.event_type == "VersionMarkerRecorded")
5805 .collect();
5806
5807 let command_kind_count = usize::from(!activity_events.is_empty())
5808 + usize::from(!timer_events.is_empty())
5809 + usize::from(!child_events.is_empty())
5810 + usize::from(!signal_wait_events.is_empty())
5811 + usize::from(!side_effect_events.is_empty())
5812 + usize::from(!version_marker_events.is_empty());
5813 if command_kind_count > 1 {
5814 let actual = [
5815 (!activity_events.is_empty()).then_some("activity"),
5816 (!timer_events.is_empty()).then_some("timer"),
5817 (!child_events.is_empty()).then_some("child workflow"),
5818 (!signal_wait_events.is_empty()).then_some("signal wait"),
5819 (!side_effect_events.is_empty()).then_some("side effect"),
5820 (!version_marker_events.is_empty()).then_some("version marker"),
5821 ]
5822 .into_iter()
5823 .flatten()
5824 .collect::<Vec<_>>()
5825 .join(" and ");
5826 return Err(invalid_recorded_history(
5827 "durable_command_sequence_collision",
5828 sequence,
5829 "one durable command kind",
5830 &actual,
5831 "one workflow sequence records more than one durable command kind",
5832 ));
5833 }
5834
5835 if !activity_events.is_empty() {
5836 let scheduled_count = activity_events
5837 .iter()
5838 .filter(|event| event.event_type == "ActivityScheduled")
5839 .count();
5840 if scheduled_count > 1 {
5841 return Err(invalid_recorded_history(
5842 "duplicate_activity_schedule",
5843 sequence,
5844 "at most one ActivityScheduled event",
5845 "multiple ActivityScheduled events",
5846 "activity history schedules more than one command at one workflow sequence",
5847 ));
5848 }
5849 let activity_type = activity_events.iter().find_map(|event| {
5850 event
5851 .payload
5852 .get("activity_type")
5853 .or_else(|| event.payload.get("activity_name"))
5854 .and_then(Value::as_str)
5855 .map(str::to_string)
5856 });
5857 if activity_events.iter().filter_map(|event| {
5858 event
5859 .payload
5860 .get("activity_type")
5861 .or_else(|| event.payload.get("activity_name"))
5862 .and_then(Value::as_str)
5863 }).any(|candidate| Some(candidate) != activity_type.as_deref()) {
5864 return Err(invalid_recorded_history(
5865 "activity_identity_mismatch",
5866 sequence,
5867 activity_type.as_deref().unwrap_or("one activity identity"),
5868 "conflicting activity identities",
5869 "activity lifecycle events at one workflow sequence disagree on identity",
5870 ));
5871 }
5872 let terminal: Vec<_> = activity_events
5873 .iter()
5874 .copied()
5875 .filter(|event| {
5876 matches!(
5877 event.event_type.as_str(),
5878 "ActivityCompleted"
5879 | "ActivityFailed"
5880 | "ActivityCancelled"
5881 | "ActivityTimedOut"
5882 )
5883 })
5884 .collect();
5885 if terminal.len() > 1 {
5886 return Err(invalid_recorded_history(
5887 "duplicate_activity_terminal_event",
5888 sequence,
5889 "at most one terminal activity event",
5890 "multiple terminal activity events",
5891 "activity history settles one command more than once",
5892 ));
5893 }
5894 let outcome = terminal
5895 .first()
5896 .map(|event| activity_outcome(event, fallback_codec, activity_type.clone()))
5897 .transpose()?;
5898 let options = activity_events
5899 .iter()
5900 .find(|event| event.event_type == "ActivityScheduled")
5901 .and_then(|event| event.payload.get("activity"))
5902 .and_then(Value::as_object)
5903 .map(|activity| RecordedActivityOptions {
5904 task_queue: recorded_optional_string(activity, "queue"),
5905 execution_mode: recorded_optional_string(activity, "execution_mode"),
5906 retry_policy: recorded_activity_retry_snapshot(
5907 activity.get("retry_policy"),
5908 ),
5909 });
5910 return Ok(RecordedCommand::Activity {
5911 sequence,
5912 activity_type,
5913 options,
5914 outcome,
5915 });
5916 }
5917
5918 if !child_events.is_empty() {
5919 let scheduled: Vec<_> = child_events
5920 .iter()
5921 .copied()
5922 .filter(|event| event.event_type == "ChildWorkflowScheduled")
5923 .collect();
5924 if scheduled.len() != 1 {
5925 return Err(invalid_recorded_history(
5926 "child_workflow_schedule_missing_or_duplicate",
5927 sequence,
5928 "one ChildWorkflowScheduled event",
5929 &format!("{} ChildWorkflowScheduled events", scheduled.len()),
5930 "child workflow replay requires exactly one recorded schedule event",
5931 ));
5932 }
5933 let workflow_type = child_events.iter().find_map(|event| {
5934 event
5935 .payload
5936 .get("child_workflow_type")
5937 .or_else(|| event.payload.get("workflow_type"))
5938 .and_then(Value::as_str)
5939 .filter(|value| !value.is_empty())
5940 .map(str::to_string)
5941 });
5942 if child_events
5943 .iter()
5944 .filter_map(|event| {
5945 event
5946 .payload
5947 .get("child_workflow_type")
5948 .or_else(|| event.payload.get("workflow_type"))
5949 .and_then(Value::as_str)
5950 })
5951 .any(|candidate| Some(candidate) != workflow_type.as_deref())
5952 {
5953 return Err(invalid_recorded_history(
5954 "child_workflow_identity_mismatch",
5955 sequence,
5956 workflow_type
5957 .as_deref()
5958 .unwrap_or("one child workflow type"),
5959 "conflicting child workflow types",
5960 "child workflow lifecycle events at one sequence disagree on type",
5961 ));
5962 }
5963 let mut outcomes = child_workflow_outcomes(
5964 &child_events.iter().map(|event| (*event).clone()).collect::<Vec<_>>(),
5965 fallback_codec,
5966 parent.clone(),
5967 )?;
5968 if outcomes.len() > 1 {
5969 return Err(invalid_recorded_history(
5970 "duplicate_child_workflow_terminal_event",
5971 sequence,
5972 "at most one terminal child event",
5973 "multiple terminal child events",
5974 "child workflow history settles one command more than once",
5975 ));
5976 }
5977 return Ok(RecordedCommand::ChildWorkflow {
5978 sequence,
5979 workflow_type,
5980 outcome: outcomes.pop(),
5981 });
5982 }
5983
5984 if !signal_wait_events.is_empty() {
5985 let opened_count = signal_wait_events
5986 .iter()
5987 .filter(|event| {
5988 matches!(
5989 event.event_type.as_str(),
5990 "SignalWaitOpened" | "ConditionWaitOpened"
5991 )
5992 })
5993 .count();
5994 if opened_count > 1 {
5995 return Err(invalid_recorded_history(
5996 "duplicate_signal_wait_open",
5997 sequence,
5998 "at most one signal wait open event",
5999 "multiple signal wait open events",
6000 "signal history opens more than one durable wait at one workflow sequence",
6001 ));
6002 }
6003
6004 let signal_name = signal_wait_events
6005 .iter()
6006 .find_map(|event| recorded_signal_wait_name(event));
6007 if signal_wait_events
6008 .iter()
6009 .filter_map(|event| recorded_signal_wait_name(event))
6010 .any(|candidate| Some(candidate.as_str()) != signal_name.as_deref())
6011 {
6012 return Err(invalid_recorded_history(
6013 "signal_wait_identity_mismatch",
6014 sequence,
6015 signal_name.as_deref().unwrap_or("one signal name"),
6016 "conflicting signal names",
6017 "signal wait lifecycle events at one workflow sequence disagree on identity",
6018 ));
6019 }
6020 return Ok(RecordedCommand::SignalWait {
6021 sequence,
6022 signal_name,
6023 });
6024 }
6025
6026 if !side_effect_events.is_empty() {
6027 if side_effect_events.len() != 1 {
6028 return Err(invalid_recorded_history(
6029 "duplicate_side_effect_record",
6030 sequence,
6031 "one SideEffectRecorded event",
6032 &format!("{} SideEffectRecorded events", side_effect_events.len()),
6033 "side-effect history records one workflow command more than once",
6034 ));
6035 }
6036 let event = side_effect_events[0];
6037 let result = event.payload.get("result").ok_or_else(|| {
6038 invalid_recorded_history(
6039 "side_effect_result_missing",
6040 sequence,
6041 "recorded result payload",
6042 "missing result",
6043 "side-effect history is missing its recorded value",
6044 )
6045 })?;
6046 let has_published_envelope = result.as_str().is_some()
6047 || result.as_object().is_some_and(|envelope| {
6048 envelope.get("codec").and_then(Value::as_str).is_some()
6049 && envelope.get("blob").and_then(Value::as_str).is_some()
6050 });
6051 if !has_published_envelope {
6052 return Err(invalid_recorded_history(
6053 "side_effect_payload_malformed",
6054 sequence,
6055 "payload blob or {codec, blob} envelope",
6056 &result.to_string(),
6057 "side-effect history result does not use a published payload envelope",
6058 ));
6059 }
6060 let codec = event
6061 .payload
6062 .get("payload_codec")
6063 .and_then(Value::as_str)
6064 .unwrap_or(fallback_codec);
6065 let value = decode_wire_value(result, codec).map_err(|error| {
6066 invalid_recorded_history(
6067 "side_effect_payload_incompatible",
6068 sequence,
6069 &format!("valid {codec} payload envelope"),
6070 &error.to_string(),
6071 "side-effect history payload cannot be decoded with its recorded codec",
6072 )
6073 })?;
6074 return Ok(RecordedCommand::SideEffect { sequence, value });
6075 }
6076
6077 if !version_marker_events.is_empty() {
6078 if version_marker_events.len() != 1 {
6079 return Err(invalid_recorded_history(
6080 "duplicate_version_marker_record",
6081 sequence,
6082 "one VersionMarkerRecorded event",
6083 &format!("{} VersionMarkerRecorded events", version_marker_events.len()),
6084 "version-marker history records one workflow command more than once",
6085 ));
6086 }
6087 let payload = &version_marker_events[0].payload;
6088 let change_id = payload
6089 .get("change_id")
6090 .and_then(Value::as_str)
6091 .filter(|value| !value.is_empty())
6092 .map(str::to_string)
6093 .ok_or_else(|| {
6094 invalid_recorded_history(
6095 "version_marker_field_missing",
6096 sequence,
6097 "non-empty change_id",
6098 "missing or invalid change_id",
6099 "version-marker history is missing its stable change ID",
6100 )
6101 })?;
6102 let version = required_version_i32(payload, "version", sequence)?;
6103 let min_supported = required_version_i32(payload, "min_supported", sequence)?;
6104 let max_supported = required_version_i32(payload, "max_supported", sequence)?;
6105 if min_supported > max_supported || version < min_supported || version > max_supported {
6106 return Err(invalid_recorded_history(
6107 "version_marker_history_range_invalid",
6108 sequence,
6109 "min_supported <= version <= max_supported",
6110 &format!("{min_supported} <= {version} <= {max_supported}"),
6111 "recorded version marker contains an internally incompatible range",
6112 ));
6113 }
6114 return Ok(RecordedCommand::VersionMarker {
6115 sequence,
6116 change_id,
6117 version,
6118 });
6119 }
6120
6121 let scheduled: Vec<_> = timer_events
6122 .iter()
6123 .copied()
6124 .filter(|event| event.event_type == "TimerScheduled")
6125 .collect();
6126 let fired: Vec<_> = timer_events
6127 .iter()
6128 .copied()
6129 .filter(|event| event.event_type == "TimerFired")
6130 .collect();
6131 if scheduled.len() != 1 {
6132 return Err(invalid_recorded_history(
6133 "timer_schedule_missing_or_duplicate",
6134 sequence,
6135 "one TimerScheduled event",
6136 &format!("{} TimerScheduled events", scheduled.len()),
6137 "timer replay requires exactly one recorded schedule event",
6138 ));
6139 }
6140 if fired.len() > 1 {
6141 return Err(invalid_recorded_history(
6142 "duplicate_timer_fire",
6143 sequence,
6144 "at most one TimerFired event",
6145 "multiple TimerFired events",
6146 "timer history contains more than one fire event for a workflow sequence",
6147 ));
6148 }
6149
6150 let scheduled = scheduled[0];
6151 let timer_id = required_history_string(scheduled, "timer_id", sequence)?;
6152 let delay_seconds = required_history_u64(scheduled, "delay_seconds", sequence)?;
6153 if let Some(fired) = fired.first() {
6154 let fired_timer_id = required_history_string(fired, "timer_id", sequence)?;
6155 if fired_timer_id != timer_id {
6156 return Err(invalid_recorded_history(
6157 "timer_identity_mismatch",
6158 sequence,
6159 &timer_id,
6160 &fired_timer_id,
6161 "TimerFired does not correspond to the recorded TimerScheduled event",
6162 ));
6163 }
6164 let fired_delay = required_history_u64(fired, "delay_seconds", sequence)?;
6165 if fired_delay != delay_seconds {
6166 return Err(invalid_recorded_history(
6167 "timer_history_delay_mismatch",
6168 sequence,
6169 &delay_seconds.to_string(),
6170 &fired_delay.to_string(),
6171 "TimerScheduled and TimerFired record different delays",
6172 ));
6173 }
6174 }
6175
6176 Ok(RecordedCommand::Timer {
6177 sequence,
6178 delay_seconds,
6179 fired: !fired.is_empty(),
6180 })
6181 })
6182 .collect::<Result<_>>()?;
6183
6184 let mut marker_sequences = HashMap::new();
6185 for command in &commands {
6186 if let RecordedCommand::VersionMarker {
6187 sequence,
6188 change_id,
6189 ..
6190 } = command
6191 {
6192 if let Some(first_sequence) = marker_sequences.insert(change_id.clone(), *sequence) {
6193 return Err(invalid_recorded_history(
6194 "duplicate_version_marker",
6195 *sequence,
6196 &format!("one marker for change ID {change_id:?}"),
6197 &format!("markers at sequences {first_sequence} and {sequence}"),
6198 "workflow history contains duplicate markers for one stable change ID",
6199 ));
6200 }
6201 }
6202 }
6203
6204 Ok(commands)
6205}
6206
6207fn required_version_i32(payload: &Value, field: &str, sequence: u64) -> Result<i32> {
6208 payload
6209 .get(field)
6210 .and_then(Value::as_i64)
6211 .and_then(|value| i32::try_from(value).ok())
6212 .ok_or_else(|| {
6213 invalid_recorded_history(
6214 "version_marker_field_missing",
6215 sequence,
6216 &format!("integer {field}"),
6217 "missing or out-of-range integer",
6218 "version-marker history is missing a required integer field",
6219 )
6220 })
6221}
6222
6223fn durable_event_sequence(event: &HistoryEvent) -> Option<u64> {
6224 event
6225 .payload
6226 .get("sequence")
6227 .or_else(|| event.payload.get("workflow_sequence"))
6228 .or_else(|| event.raw.get("sequence"))
6229 .or_else(|| event.raw.get("workflow_sequence"))
6230 .and_then(value_as_u64)
6231}
6232
6233fn is_internal_timer_event(event: &HistoryEvent) -> bool {
6234 matches!(
6235 event
6236 .payload
6237 .get("timer_kind")
6238 .or_else(|| event.raw.get("timer_kind"))
6239 .and_then(Value::as_str),
6240 Some("condition_timeout" | "signal_timeout")
6241 )
6242}
6243
6244fn recorded_signal_wait_name(event: &HistoryEvent) -> Option<String> {
6245 match event.event_type.as_str() {
6246 "SignalWaitOpened" | "SignalApplied" => event
6247 .payload
6248 .get("signal_name")
6249 .or_else(|| event.raw.get("signal_name"))
6250 .and_then(Value::as_str)
6251 .filter(|value| !value.is_empty())
6252 .map(str::to_string),
6253 "ConditionWaitOpened" | "ConditionWaitSatisfied" | "ConditionWaitTimedOut" => event
6254 .payload
6255 .get("condition_key")
6256 .or_else(|| event.raw.get("condition_key"))
6257 .and_then(Value::as_str)
6258 .and_then(|key| key.strip_prefix("signal:"))
6259 .filter(|value| !value.is_empty())
6260 .map(str::to_string),
6261 _ => None,
6262 }
6263}
6264
6265fn is_recorded_signal_wait_event(event: &HistoryEvent) -> bool {
6266 match event.event_type.as_str() {
6267 "SignalWaitOpened" | "SignalApplied" => true,
6268 "ConditionWaitOpened" | "ConditionWaitSatisfied" | "ConditionWaitTimedOut" => event
6269 .payload
6270 .get("condition_key")
6271 .or_else(|| event.raw.get("condition_key"))
6272 .and_then(Value::as_str)
6273 .is_some_and(|key| key.starts_with("signal:")),
6274 _ => false,
6275 }
6276}
6277
6278fn required_history_string(event: &HistoryEvent, field: &str, sequence: u64) -> Result<String> {
6279 event
6280 .payload
6281 .get(field)
6282 .and_then(Value::as_str)
6283 .filter(|value| !value.is_empty())
6284 .map(str::to_string)
6285 .ok_or_else(|| {
6286 invalid_recorded_history(
6287 "timer_history_field_missing",
6288 sequence,
6289 field,
6290 &event.event_type,
6291 "timer history is missing a required identity field",
6292 )
6293 })
6294}
6295
6296fn required_history_u64(event: &HistoryEvent, field: &str, sequence: u64) -> Result<u64> {
6297 event
6298 .payload
6299 .get(field)
6300 .and_then(value_as_u64)
6301 .ok_or_else(|| {
6302 invalid_recorded_history(
6303 "timer_history_field_missing",
6304 sequence,
6305 field,
6306 &event.event_type,
6307 "timer history is missing a required numeric field",
6308 )
6309 })
6310}
6311
6312fn invalid_recorded_history(
6313 reason: &str,
6314 sequence: u64,
6315 expected: &str,
6316 actual: &str,
6317 message: &str,
6318) -> Error {
6319 Error::NonDeterministicReplay(ReplayFailure::new(
6320 reason,
6321 Some(sequence),
6322 Some(expected.to_string()),
6323 Some(actual.to_string()),
6324 message,
6325 ))
6326}
6327
6328type ActivityOutcome = std::result::Result<Value, ActivityFailure>;
6329
6330fn activity_outcome(
6331 event: &HistoryEvent,
6332 fallback_codec: &str,
6333 recorded_activity_type: Option<String>,
6334) -> Result<ActivityOutcome> {
6335 if event.event_type == "ActivityCompleted" {
6336 let codec = event
6337 .payload
6338 .get("payload_codec")
6339 .and_then(Value::as_str)
6340 .unwrap_or(fallback_codec);
6341 return Ok(Ok(decode_wire_value(
6342 event.payload.get("result").unwrap_or(&Value::Null),
6343 codec,
6344 )?));
6345 }
6346
6347 let payload = &event.payload;
6348 let (kind, fallback_reason, fallback_message) = match event.event_type.as_str() {
6349 "ActivityFailed" => (ActivityFailureKind::Failed, "activity", "activity failed"),
6350 "ActivityCancelled" => (
6351 ActivityFailureKind::Cancelled,
6352 "cancelled",
6353 "activity was cancelled",
6354 ),
6355 "ActivityTimedOut" => (
6356 ActivityFailureKind::TimedOut,
6357 "timeout",
6358 "activity timed out",
6359 ),
6360 _ => unreachable!("activity_outcome is called only for terminal activity events"),
6361 };
6362 let exception = payload
6363 .get("exception")
6364 .filter(|value| !value.is_null())
6365 .cloned();
6366 let failure_category = payload_string(payload, "failure_category");
6367 let timeout_kind = payload_string(payload, "timeout_kind");
6368 let reason = payload_string(payload, "reason").unwrap_or_else(|| match kind {
6369 ActivityFailureKind::Failed => failure_category
6370 .clone()
6371 .unwrap_or_else(|| fallback_reason.to_string()),
6372 ActivityFailureKind::Cancelled => fallback_reason.to_string(),
6373 ActivityFailureKind::TimedOut => timeout_kind
6374 .clone()
6375 .unwrap_or_else(|| fallback_reason.to_string()),
6376 });
6377 let message = payload_string(payload, "message")
6378 .or_else(|| {
6379 exception
6380 .as_ref()
6381 .and_then(|value| payload_string(value, "message"))
6382 })
6383 .unwrap_or_else(|| fallback_message.to_string());
6384
6385 Ok(Err(ActivityFailure {
6386 kind,
6387 reason,
6388 message,
6389 activity_execution_id: payload_string(payload, "activity_execution_id"),
6390 activity_attempt_id: payload_string(payload, "activity_attempt_id"),
6391 activity_type: payload_string(payload, "activity_type")
6392 .or_else(|| payload_string(payload, "activity_name"))
6393 .or(recorded_activity_type),
6394 activity_class: payload_string(payload, "activity_class"),
6395 attempt_number: payload.get("attempt_number").and_then(value_as_u64),
6396 failure_id: payload_string(payload, "failure_id"),
6397 failure_category,
6398 timeout_kind,
6399 non_retryable: payload
6400 .get("non_retryable")
6401 .and_then(Value::as_bool)
6402 .unwrap_or(false),
6403 exception_type: payload_string(payload, "exception_type").or_else(|| {
6404 exception
6405 .as_ref()
6406 .and_then(|value| payload_string(value, "type"))
6407 }),
6408 exception_class: payload_string(payload, "exception_class").or_else(|| {
6409 exception
6410 .as_ref()
6411 .and_then(|value| payload_string(value, "class"))
6412 }),
6413 code: payload
6414 .get("code")
6415 .filter(|value| !value.is_null())
6416 .cloned(),
6417 exception,
6418 }))
6419}
6420
6421type ChildWorkflowOutcome = std::result::Result<ChildWorkflowResult, ChildWorkflowFailure>;
6422
6423fn child_workflow_outcomes(
6424 events: &[HistoryEvent],
6425 fallback_codec: &str,
6426 parent: WorkflowIdentity,
6427) -> Result<Vec<ChildWorkflowOutcome>> {
6428 let mut outcomes = Vec::new();
6429
6430 for event in events {
6431 let kind = match event.event_type.as_str() {
6432 "ChildRunCompleted" => None,
6433 "ChildRunFailed" => Some((
6434 ChildWorkflowFailureKind::Failed,
6435 "child_workflow",
6436 "child workflow failed",
6437 )),
6438 "ChildRunCancelled" => Some((
6439 ChildWorkflowFailureKind::Cancelled,
6440 "cancelled",
6441 "child workflow was cancelled",
6442 )),
6443 "ChildRunTerminated" => Some((
6444 ChildWorkflowFailureKind::Terminated,
6445 "terminated",
6446 "child workflow was terminated",
6447 )),
6448 _ => continue,
6449 };
6450 let payload = &event.payload;
6451 let child_workflow_id = payload_string(payload, "child_workflow_instance_id");
6452 let child_workflow_run_id = payload_string(payload, "child_workflow_run_id");
6453 let child_workflow_type = payload_string(payload, "child_workflow_type");
6454
6455 if let Some((kind, reason, fallback_message)) = kind {
6456 let exception = payload
6457 .get("exception")
6458 .filter(|value| !value.is_null())
6459 .cloned();
6460 let message = payload_string(payload, "message")
6461 .or_else(|| {
6462 exception
6463 .as_ref()
6464 .and_then(|value| payload_string(value, "message"))
6465 })
6466 .unwrap_or_else(|| fallback_message.to_string());
6467 let exception_type = payload_string(payload, "exception_type").or_else(|| {
6468 exception
6469 .as_ref()
6470 .and_then(|value| payload_string(value, "type"))
6471 });
6472 let exception_class = payload_string(payload, "exception_class").or_else(|| {
6473 exception
6474 .as_ref()
6475 .and_then(|value| payload_string(value, "class"))
6476 });
6477 outcomes.push(Err(ChildWorkflowFailure {
6478 kind,
6479 reason: reason.to_string(),
6480 message,
6481 parent_workflow_id: parent.workflow_id.clone(),
6482 parent_workflow_run_id: parent.run_id.clone(),
6483 child_workflow_id,
6484 child_workflow_run_id,
6485 child_workflow_type,
6486 failure_id: payload_string(payload, "failure_id"),
6487 failure_category: payload_string(payload, "failure_category"),
6488 exception_type,
6489 exception_class,
6490 non_retryable: payload
6491 .get("non_retryable")
6492 .and_then(Value::as_bool)
6493 .unwrap_or(false),
6494 code: payload
6495 .get("code")
6496 .filter(|value| !value.is_null())
6497 .cloned(),
6498 exception,
6499 }));
6500 continue;
6501 }
6502
6503 let codec = payload
6504 .get("payload_codec")
6505 .and_then(Value::as_str)
6506 .unwrap_or(fallback_codec);
6507 let result = payload
6508 .get("result")
6509 .or_else(|| payload.get("output"))
6510 .unwrap_or(&Value::Null);
6511 outcomes.push(Ok(ChildWorkflowResult {
6512 parent: parent.clone(),
6513 child: WorkflowIdentity {
6514 workflow_id: child_workflow_id,
6515 run_id: child_workflow_run_id,
6516 },
6517 child_workflow_type,
6518 result: decode_wire_value(result, codec)?,
6519 }));
6520 }
6521
6522 Ok(outcomes)
6523}
6524
6525fn payload_string(payload: &Value, key: &str) -> Option<String> {
6526 payload
6527 .get(key)
6528 .and_then(Value::as_str)
6529 .filter(|value| !value.is_empty())
6530 .map(str::to_string)
6531}
6532
6533fn workflow_failure_command(error: &Error) -> Value {
6534 let (exception_type, exception_class, properties) = match error {
6535 Error::ActivityFailed(failure) => (
6536 match failure.kind {
6537 ActivityFailureKind::Failed => "ActivityFailed",
6538 ActivityFailureKind::Cancelled => "ActivityCancelled",
6539 ActivityFailureKind::TimedOut => "ActivityTimedOut",
6540 },
6541 "durable_workflow::ActivityFailure",
6542 json!({
6543 "reason": failure.reason,
6544 "activity_execution_id": failure.activity_execution_id,
6545 "activity_attempt_id": failure.activity_attempt_id,
6546 "activity_type": failure.activity_type,
6547 "activity_class": failure.activity_class,
6548 "attempt_number": failure.attempt_number,
6549 "failure_id": failure.failure_id,
6550 "failure_category": failure.failure_category,
6551 "timeout_kind": failure.timeout_kind,
6552 "activity_non_retryable": failure.non_retryable,
6553 "activity_exception_type": failure.exception_type,
6554 "activity_exception_class": failure.exception_class,
6555 "activity_code": failure.code,
6556 "activity_exception": failure.exception,
6557 }),
6558 ),
6559 Error::ChildWorkflowFailed(failure) => (
6560 match failure.kind {
6561 ChildWorkflowFailureKind::Failed => "ChildWorkflowFailed",
6562 ChildWorkflowFailureKind::Cancelled => "ChildWorkflowCancelled",
6563 ChildWorkflowFailureKind::Terminated => "ChildWorkflowTerminated",
6564 },
6565 "durable_workflow::ChildWorkflowFailure",
6566 json!({
6567 "reason": failure.reason,
6568 "parent_workflow_id": failure.parent_workflow_id,
6569 "parent_workflow_run_id": failure.parent_workflow_run_id,
6570 "child_workflow_id": failure.child_workflow_id,
6571 "child_workflow_run_id": failure.child_workflow_run_id,
6572 "child_workflow_type": failure.child_workflow_type,
6573 "failure_id": failure.failure_id,
6574 "failure_category": failure.failure_category,
6575 "child_exception_type": failure.exception_type,
6576 "child_exception_class": failure.exception_class,
6577 "child_non_retryable": failure.non_retryable,
6578 "child_code": failure.code,
6579 "child_exception": failure.exception,
6580 }),
6581 ),
6582 Error::NonDeterministicReplay(_) => (
6583 "NonDeterministicReplay",
6584 "durable_workflow::Error",
6585 Value::Null,
6586 ),
6587 _ => ("RustWorkflowError", "durable_workflow::Error", Value::Null),
6588 };
6589 let non_retryable = match error {
6590 Error::ActivityFailed(failure) => failure.non_retryable,
6591 Error::ChildWorkflowFailed(failure) => failure.non_retryable,
6592 Error::NonDeterministicReplay(_) => true,
6593 _ => false,
6594 };
6595
6596 json!({
6597 "type": "fail_workflow",
6598 "message": error.to_string(),
6599 "exception_type": exception_type,
6600 "exception_class": exception_class,
6601 "non_retryable": non_retryable,
6602 "exception": {
6603 "type": exception_type,
6604 "class": exception_class,
6605 "message": error.to_string(),
6606 "properties": properties,
6607 }
6608 })
6609}
6610
6611fn workflow_task_integrity_error(error: &Error) -> bool {
6612 matches!(
6613 error,
6614 Error::NonDeterministicReplay(_) | Error::Protocol(_) | Error::WorkflowStatePoisoned
6615 )
6616}
6617
6618fn signal_values(
6619 events: &[HistoryEvent],
6620 signal_name: &str,
6621 fallback_codec: &str,
6622 resume_signal: Option<&ResumeSignal>,
6623) -> Result<Vec<Vec<Value>>> {
6624 let mut signals = Vec::new();
6625
6626 for event in events {
6627 if event.event_type != "SignalApplied" && event.event_type != "SignalReceived" {
6628 continue;
6629 }
6630
6631 if event.payload.get("signal_name").and_then(Value::as_str) != Some(signal_name) {
6632 continue;
6633 }
6634
6635 let codec = event
6636 .payload
6637 .get("payload_codec")
6638 .and_then(Value::as_str)
6639 .unwrap_or(fallback_codec);
6640 let raw = event
6641 .payload
6642 .get("value")
6643 .or_else(|| event.payload.get("input"))
6644 .or_else(|| event.payload.get("arguments"));
6645 let decoded = match raw.filter(|value| !value.is_null()) {
6646 Some(value) => decode_wire_value(value, codec)?,
6647 None => resume_signal
6648 .filter(|signal| resume_signal_matches_event(signal, event, signal_name))
6649 .map(|signal| Value::Array(signal.arguments.clone()))
6650 .unwrap_or_else(|| Value::Array(Vec::new())),
6651 };
6652 let args = match normalize_arguments(decoded) {
6653 Value::Array(values) => values,
6654 _ => unreachable!("normalize_arguments always returns an array"),
6655 };
6656 signals.push(args);
6657 }
6658
6659 Ok(signals)
6660}
6661
6662fn hydrate_query_history_from_export(task: &mut QueryTask) -> Result<()> {
6663 let Some(export_events) = task
6664 .history_export
6665 .as_ref()
6666 .and_then(|export| export.get("history_events"))
6667 .and_then(Value::as_array)
6668 else {
6669 return Ok(());
6670 };
6671
6672 if export_events.len() > task.history_events.len() {
6673 task.history_events = serde_json::from_value(Value::Array(export_events.clone()))?;
6674 }
6675
6676 Ok(())
6677}
6678
6679fn enrich_query_history_from_export(task: &mut QueryTask) -> Result<()> {
6680 let Some(export) = task.history_export.as_ref() else {
6681 return Ok(());
6682 };
6683 let signals = export
6684 .get("signals")
6685 .and_then(Value::as_array)
6686 .cloned()
6687 .unwrap_or_default();
6688 let activities = export
6689 .get("activities")
6690 .and_then(Value::as_array)
6691 .cloned()
6692 .unwrap_or_default();
6693 let export_codec = export
6694 .get("payloads")
6695 .and_then(|payloads| payloads.get("codec"))
6696 .and_then(Value::as_str)
6697 .unwrap_or(&task.payload_codec)
6698 .to_string();
6699 let mut signal_name_offsets: HashMap<String, usize> = HashMap::new();
6700
6701 for event in &mut task.history_events {
6702 if event.event_type == "ActivityCompleted" {
6703 let sequence = event
6704 .payload
6705 .get("sequence")
6706 .or_else(|| event.payload.get("workflow_sequence"))
6707 .and_then(value_as_u64);
6708 let Some(activity) = sequence.and_then(|sequence| {
6709 activities.iter().find(|activity| {
6710 activity.get("sequence").and_then(value_as_u64) == Some(sequence)
6711 })
6712 }) else {
6713 continue;
6714 };
6715 let Some(payload) = event.payload.as_object_mut() else {
6716 continue;
6717 };
6718 if missing_payload(payload.get("result")) {
6719 if let Some(result) = activity
6720 .get("result")
6721 .filter(|value| !missing_payload(Some(value)))
6722 {
6723 payload.insert("result".to_string(), result.clone());
6724 }
6725 }
6726 for field in ["payload_codec", "activity_type"] {
6727 if payload
6728 .get(field)
6729 .and_then(Value::as_str)
6730 .unwrap_or_default()
6731 .is_empty()
6732 {
6733 if let Some(value) = activity.get(field) {
6734 payload.insert(field.to_string(), value.clone());
6735 }
6736 }
6737 }
6738 continue;
6739 }
6740
6741 if event.event_type != "SignalReceived" && event.event_type != "SignalApplied" {
6742 continue;
6743 }
6744 let signal_id = event.payload.get("signal_id").and_then(Value::as_str);
6745 let command_id = event
6746 .payload
6747 .get("workflow_command_id")
6748 .or_else(|| event.raw.get("workflow_command_id"))
6749 .and_then(Value::as_str);
6750 let signal_name = event
6751 .payload
6752 .get("signal_name")
6753 .and_then(Value::as_str)
6754 .unwrap_or_default()
6755 .to_string();
6756 let matched = signals
6757 .iter()
6758 .find(|signal| {
6759 signal_id.is_some() && signal.get("id").and_then(Value::as_str) == signal_id
6760 })
6761 .or_else(|| {
6762 signals.iter().find(|signal| {
6763 command_id.is_some()
6764 && signal.get("command_id").and_then(Value::as_str) == command_id
6765 })
6766 })
6767 .or_else(|| {
6768 let offset = signal_name_offsets.entry(signal_name.clone()).or_default();
6769 let signal = signals
6770 .iter()
6771 .filter(|signal| {
6772 signal.get("name").and_then(Value::as_str) == Some(signal_name.as_str())
6773 })
6774 .nth(*offset);
6775 if signal.is_some() {
6776 *offset += 1;
6777 }
6778 signal
6779 });
6780 let Some(signal) = matched else {
6781 continue;
6782 };
6783 let signal_codec = signal
6784 .get("payload_codec")
6785 .and_then(Value::as_str)
6786 .unwrap_or(&export_codec);
6787 let Some(payload) = event.payload.as_object_mut() else {
6788 continue;
6789 };
6790 if missing_payload(payload.get("arguments")) {
6791 if let Some(arguments) = signal
6792 .get("arguments")
6793 .filter(|value| !missing_payload(Some(value)))
6794 {
6795 let envelope = match arguments {
6796 Value::String(blob) => json!({"codec": signal_codec, "blob": blob}),
6797 other => other.clone(),
6798 };
6799 payload.insert("arguments".to_string(), envelope);
6800 }
6801 }
6802 if payload
6803 .get("payload_codec")
6804 .and_then(Value::as_str)
6805 .unwrap_or_default()
6806 .is_empty()
6807 {
6808 payload.insert("payload_codec".to_string(), json!(signal_codec));
6809 }
6810 }
6811
6812 Ok(())
6813}
6814
6815fn missing_payload(value: Option<&Value>) -> bool {
6816 match value {
6817 None | Some(Value::Null) => true,
6818 Some(Value::String(value)) => value.is_empty(),
6819 Some(_) => false,
6820 }
6821}
6822
6823fn query_signal_events(task: &QueryTask) -> Result<Vec<QuerySignal>> {
6824 let export_signals = task
6825 .history_export
6826 .as_ref()
6827 .and_then(|export| export.get("signals"))
6828 .and_then(Value::as_array)
6829 .cloned()
6830 .unwrap_or_default();
6831 let export_codec = task
6832 .history_export
6833 .as_ref()
6834 .and_then(|export| export.get("payloads"))
6835 .and_then(|payloads| payloads.get("codec"))
6836 .and_then(Value::as_str)
6837 .unwrap_or(&task.payload_codec);
6838 let mut name_offsets: HashMap<String, usize> = HashMap::new();
6839 let mut signals = Vec::new();
6840
6841 for event in &task.history_events {
6842 if event.event_type != "SignalApplied" && event.event_type != "SignalReceived" {
6843 continue;
6844 }
6845
6846 let name = event
6847 .payload
6848 .get("signal_name")
6849 .and_then(Value::as_str)
6850 .unwrap_or_default();
6851 if name.is_empty() {
6852 continue;
6853 }
6854 let signal_id = event.payload.get("signal_id").and_then(Value::as_str);
6855 let command_id = event
6856 .payload
6857 .get("workflow_command_id")
6858 .or_else(|| event.raw.get("workflow_command_id"))
6859 .and_then(Value::as_str);
6860 let matched_export = export_signals
6861 .iter()
6862 .find(|candidate| {
6863 signal_id.is_some() && candidate.get("id").and_then(Value::as_str) == signal_id
6864 })
6865 .or_else(|| {
6866 export_signals.iter().find(|candidate| {
6867 command_id.is_some()
6868 && candidate.get("command_id").and_then(Value::as_str) == command_id
6869 })
6870 })
6871 .or_else(|| {
6872 let offset = name_offsets.entry(name.to_string()).or_default();
6873 let candidate = export_signals
6874 .iter()
6875 .filter(|candidate| candidate.get("name").and_then(Value::as_str) == Some(name))
6876 .nth(*offset);
6877 if candidate.is_some() {
6878 *offset += 1;
6879 }
6880 candidate
6881 });
6882 let codec = event
6883 .payload
6884 .get("payload_codec")
6885 .and_then(Value::as_str)
6886 .or_else(|| {
6887 matched_export
6888 .and_then(|signal| signal.get("payload_codec"))
6889 .and_then(Value::as_str)
6890 })
6891 .unwrap_or(export_codec);
6892 let raw_arguments = event
6893 .payload
6894 .get("value")
6895 .or_else(|| event.payload.get("input"))
6896 .or_else(|| event.payload.get("arguments"))
6897 .filter(|value| !value.is_null())
6898 .or_else(|| matched_export.and_then(|signal| signal.get("arguments")));
6899 let arguments = decode_query_signal_arguments(raw_arguments, codec)?;
6900 let workflow_sequence = event
6901 .payload
6902 .get("workflow_sequence")
6903 .and_then(value_as_u64)
6904 .or_else(|| {
6905 matched_export
6906 .and_then(|signal| signal.get("workflow_sequence"))
6907 .and_then(value_as_u64)
6908 });
6909
6910 signals.push(QuerySignal {
6911 id: signal_id.map(str::to_string).or_else(|| {
6912 matched_export
6913 .and_then(|signal| signal.get("id"))
6914 .and_then(Value::as_str)
6915 .map(str::to_string)
6916 }),
6917 name: name.to_string(),
6918 arguments,
6919 workflow_sequence,
6920 });
6921 }
6922
6923 if signals.is_empty() {
6924 for signal in export_signals {
6925 if signal.get("status").and_then(Value::as_str) == Some("rejected") {
6926 continue;
6927 }
6928 let Some(name) = signal.get("name").and_then(Value::as_str) else {
6929 continue;
6930 };
6931 let codec = signal
6932 .get("payload_codec")
6933 .and_then(Value::as_str)
6934 .unwrap_or(export_codec);
6935 let arguments = decode_query_signal_arguments(signal.get("arguments"), codec)?;
6936 signals.push(QuerySignal {
6937 id: signal.get("id").and_then(Value::as_str).map(str::to_string),
6938 name: name.to_string(),
6939 arguments,
6940 workflow_sequence: signal.get("workflow_sequence").and_then(value_as_u64),
6941 });
6942 }
6943 signals.sort_by_key(|signal| signal.workflow_sequence.unwrap_or(u64::MAX));
6944 }
6945
6946 Ok(signals)
6947}
6948
6949fn decode_query_signal_arguments(raw: Option<&Value>, codec: &str) -> Result<Vec<Value>> {
6950 let decoded = match raw.filter(|value| !value.is_null()) {
6951 Some(value) => decode_wire_value(value, codec)?,
6952 None => Value::Array(Vec::new()),
6953 };
6954 let Value::Array(arguments) = normalize_arguments(decoded) else {
6955 unreachable!("normalize_arguments always returns an array");
6956 };
6957 Ok(arguments)
6958}
6959
6960fn value_as_u64(value: &Value) -> Option<u64> {
6961 value
6962 .as_u64()
6963 .or_else(|| value.as_str().and_then(|value| value.parse().ok()))
6964}
6965
6966fn resume_signal_matches_event(
6967 resume_signal: &ResumeSignal,
6968 event: &HistoryEvent,
6969 signal_name: &str,
6970) -> bool {
6971 if resume_signal.signal_name != signal_name {
6972 return false;
6973 }
6974
6975 match (
6976 resume_signal.signal_id.as_deref(),
6977 event.payload.get("signal_id").and_then(Value::as_str),
6978 ) {
6979 (Some(resume_id), Some(event_id)) => resume_id == event_id,
6980 _ => true,
6981 }
6982}
6983
6984#[cfg(test)]
6985mod tests {
6986 use super::*;
6987 use std::{
6988 io::{Read, Write},
6989 net::{SocketAddr, TcpListener, TcpStream},
6990 sync::atomic::AtomicUsize,
6991 thread,
6992 };
6993
6994 #[derive(Clone, Debug, Default, PartialEq)]
6995 struct ReplayCounterState {
6996 loaded: Option<String>,
6997 count: i64,
6998 finished: bool,
6999 }
7000
7001 fn replay_counter_worker() -> Worker {
7002 let client = Client::new("http://127.0.0.1:8080").expect("client");
7003 let mut worker = Worker::new(client, "rust-workers");
7004 worker.register_replayed_workflow(
7005 "replay-counter",
7006 ReplayCounterState::default,
7007 |ctx, _input, state| async move {
7008 let loaded = ctx.activity("load-counter", json!([])).await?;
7009 state.update(|current| {
7010 current.loaded = loaded.as_str().map(str::to_string);
7011 })?;
7012 for _ in 0..2 {
7013 let signal = ctx.wait_signal("increment").await?;
7014 let amount = signal.first().and_then(Value::as_i64).unwrap_or_default();
7015 state.update(|current| current.count += amount)?;
7016 }
7017 state.update(|current| current.finished = true)?;
7018 state.read(|current| Ok(json!(current.count)))?
7019 },
7020 );
7021 worker.register_replayed_query::<ReplayCounterState, _, _>(
7022 "replay-counter",
7023 "current",
7024 |_ctx, state, _args| async move {
7025 Ok(json!({
7026 "loaded": state.loaded,
7027 "count": state.count,
7028 "finished": state.finished,
7029 }))
7030 },
7031 );
7032 worker.register_replayed_query::<ReplayCounterState, _, _>(
7033 "replay-counter",
7034 "detached-mutation",
7035 |_ctx, state, _args| async move {
7036 let mut detached = (*state).clone();
7037 detached.count = 999;
7038 Ok(json!(detached.count))
7039 },
7040 );
7041 worker.register_replayed_query::<ReplayCounterState, _, _>(
7042 "replay-counter",
7043 "failed-mutation",
7044 |_ctx, state, _args| async move {
7045 let mut detached = (*state).clone();
7046 detached.count = 999;
7047 Err(Error::WorkerLoop("query refused".to_string()))
7048 },
7049 );
7050 worker
7051 }
7052
7053 fn replay_counter_query(
7054 query_name: &str,
7055 history_events: Value,
7056 run_status: &str,
7057 ) -> QueryTask {
7058 serde_json::from_value(json!({
7059 "query_task_id": format!("query-{query_name}"),
7060 "workflow_type": "replay-counter",
7061 "query_name": query_name,
7062 "payload_codec": "json",
7063 "workflow_arguments": {"codec": "json", "blob": "[]"},
7064 "query_arguments": {"codec": "json", "blob": "[]"},
7065 "history_events": history_events,
7066 "run_status": run_status,
7067 }))
7068 .expect("query task")
7069 }
7070
7071 fn workflow_context(history: Vec<HistoryEvent>) -> WorkflowContext {
7072 workflow_context_with_codec(history, JSON_CODEC)
7073 }
7074
7075 fn workflow_context_with_codec(
7076 history: Vec<HistoryEvent>,
7077 payload_codec: &str,
7078 ) -> WorkflowContext {
7079 WorkflowContext {
7080 state: Arc::new(Mutex::new(
7081 WorkflowState::new_with_identity(
7082 history,
7083 None,
7084 None,
7085 "rust-workers".to_string(),
7086 payload_codec.to_string(),
7087 None,
7088 )
7089 .expect("valid workflow history"),
7090 )),
7091 }
7092 }
7093
7094 fn history_event(event_type: &str, payload: Value) -> HistoryEvent {
7095 HistoryEvent {
7096 event_type: event_type.to_string(),
7097 payload,
7098 raw: HashMap::new(),
7099 }
7100 }
7101
7102 fn workflow_task(
7103 workflow_type: &str,
7104 history_events: Vec<HistoryEvent>,
7105 payload_codec: &str,
7106 ) -> WorkflowTask {
7107 WorkflowTask {
7108 task_id: format!("wft-{workflow_type}"),
7109 workflow_id: Some(format!("wf-{workflow_type}")),
7110 run_id: Some(format!("run-{workflow_type}")),
7111 workflow_type: workflow_type.to_string(),
7112 payload_codec: payload_codec.to_string(),
7113 arguments: Some(
7114 encode_value_envelope(&json!([]), payload_codec).expect("workflow arguments"),
7115 ),
7116 total_history_events: Some(history_events.len() as u64),
7117 history_size_bytes: None,
7118 continue_as_new_recommended: None,
7119 history_budget_pressure: None,
7120 history_events,
7121 next_history_page_token: None,
7122 workflow_task_attempt: 1,
7123 workflow_signal_id: None,
7124 signal_name: None,
7125 signal_arguments: None,
7126 lease_owner: Some("rust-worker".to_string()),
7127 }
7128 }
7129
7130 #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
7131 struct SideEffectProbe {
7132 request_id: String,
7133 attempt: u32,
7134 }
7135
7136 #[test]
7137 fn typed_side_effect_runs_callback_once_and_replay_skips_it() {
7138 let calls = AtomicUsize::new(0);
7139 let ctx = workflow_context(Vec::new());
7140 let value = ctx
7141 .side_effect(|| {
7142 calls.fetch_add(1, Ordering::SeqCst);
7143 SideEffectProbe {
7144 request_id: "request-42".to_string(),
7145 attempt: 3,
7146 }
7147 })
7148 .expect("first side effect");
7149 assert_eq!(value.attempt, 3);
7150 assert_eq!(calls.load(Ordering::SeqCst), 1);
7151 let commands = ctx.take_commands().expect("commands");
7152 assert_eq!(commands.len(), 1);
7153 assert_eq!(commands[0]["type"], "record_side_effect");
7154 assert_eq!(
7155 decode_wire_value(&commands[0]["result"], JSON_CODEC).expect("JSON result"),
7156 serde_json::to_value(&value).expect("value")
7157 );
7158
7159 let replay = workflow_context(vec![history_event(
7160 "SideEffectRecorded",
7161 json!({"sequence": 1, "result": commands[0]["result"].clone()}),
7162 )]);
7163 let replayed: SideEffectProbe = replay
7164 .side_effect(|| {
7165 calls.fetch_add(1, Ordering::SeqCst);
7166 panic!("committed side-effect callbacks must not run during replay")
7167 })
7168 .expect("replayed side effect");
7169 assert_eq!(replayed, value);
7170 assert_eq!(calls.load(Ordering::SeqCst), 1);
7171 assert!(replay.take_commands().expect("commands").is_empty());
7172 replay.ensure_history_consumed().expect("history consumed");
7173 }
7174
7175 #[test]
7176 fn side_effect_uses_avro_envelope_and_uuid_is_replay_stable() {
7177 let ctx = workflow_context_with_codec(Vec::new(), DEFAULT_CODEC);
7178 let value = ctx
7179 .side_effect(|| SideEffectProbe {
7180 request_id: "avro-request".to_string(),
7181 attempt: 1,
7182 })
7183 .expect("Avro side effect");
7184 let uuid = ctx.uuid_v4().expect("deterministic UUID");
7185 let commands = ctx.take_commands().expect("commands");
7186 assert_eq!(commands.len(), 2);
7187 assert_eq!(commands[0]["result"]["codec"], DEFAULT_CODEC);
7188 assert_eq!(commands[1]["result"]["codec"], DEFAULT_CODEC);
7189 assert_eq!(
7190 decode_wire_value(&commands[0]["result"], DEFAULT_CODEC).expect("Avro result"),
7191 serde_json::to_value(&value).expect("value")
7192 );
7193
7194 let replay = workflow_context_with_codec(
7195 vec![
7196 history_event(
7197 "SideEffectRecorded",
7198 json!({"sequence": 1, "result": commands[0]["result"].clone()}),
7199 ),
7200 history_event(
7201 "SideEffectRecorded",
7202 json!({"sequence": 2, "result": commands[1]["result"].clone()}),
7203 ),
7204 ],
7205 DEFAULT_CODEC,
7206 );
7207 let replayed: SideEffectProbe = replay
7208 .side_effect(|| panic!("Avro callback must not run"))
7209 .expect("replayed Avro value");
7210 let replayed_uuid = replay.uuid_v4().expect("replayed UUID");
7211 assert_eq!(replayed, value);
7212 assert_eq!(replayed_uuid, uuid);
7213 assert!(replay.take_commands().expect("commands").is_empty());
7214 }
7215
7216 #[test]
7217 fn ordered_side_effects_share_the_durable_command_stream() {
7218 let first = encode_value_envelope(&json!("first"), JSON_CODEC).expect("first");
7219 let second = encode_value_envelope(&json!(29), JSON_CODEC).expect("second");
7220 let ctx = workflow_context(vec![
7221 history_event(
7222 "SideEffectRecorded",
7223 json!({"sequence": 1, "result": first}),
7224 ),
7225 history_event(
7226 "SideEffectRecorded",
7227 json!({"sequence": 2, "result": second}),
7228 ),
7229 ]);
7230 let first: String = ctx
7231 .side_effect(|| panic!("first callback must not run"))
7232 .expect("first replay");
7233 let second: i32 = ctx
7234 .side_effect(|| panic!("second callback must not run"))
7235 .expect("second replay");
7236 assert_eq!(first, "first");
7237 assert_eq!(second, 29);
7238 ctx.ensure_history_consumed().expect("ordered history");
7239
7240 let reordered = workflow_context(vec![history_event(
7241 "VersionMarkerRecorded",
7242 json!({
7243 "sequence": 1,
7244 "change_id": "before-side-effect",
7245 "version": 1,
7246 "min_supported": 1,
7247 "max_supported": 1,
7248 }),
7249 )]);
7250 let error = reordered
7251 .side_effect(|| "new".to_string())
7252 .expect_err("command reordering must fail");
7253 assert!(matches!(
7254 error,
7255 Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
7256 if reason == "recorded_command_mismatch"
7257 ));
7258 }
7259
7260 #[test]
7261 fn version_markers_replay_across_upgrades_and_do_not_duplicate() {
7262 let ctx = workflow_context(Vec::new());
7263 assert_eq!(ctx.get_version("checkout-v2", 1, 2).expect("version"), 2);
7264 assert_eq!(ctx.get_version("checkout-v2", 1, 3).expect("cached"), 2);
7265 assert!(ctx.patched("new-search").expect("patch"));
7266 ctx.deprecate_patch("new-search").expect("deprecate patch");
7267 let commands = ctx.take_commands().expect("commands");
7268 assert_eq!(commands.len(), 2);
7269 assert_eq!(commands[0]["type"], "record_version_marker");
7270 assert_eq!(commands[0]["version"], 2);
7271 assert_eq!(commands[1]["change_id"], "new-search");
7272
7273 let replay = workflow_context(vec![history_event(
7274 "VersionMarkerRecorded",
7275 json!({
7276 "sequence": 1,
7277 "change_id": "checkout-v2",
7278 "version": 2,
7279 "min_supported": 1,
7280 "max_supported": 2,
7281 }),
7282 )]);
7283 assert_eq!(replay.get_version("checkout-v2", 1, 4).expect("upgrade"), 2);
7284 assert_eq!(replay.get_version("checkout-v2", 2, 5).expect("repeat"), 2);
7285 assert!(replay.take_commands().expect("commands").is_empty());
7286 replay.ensure_history_consumed().expect("history consumed");
7287 }
7288
7289 #[test]
7290 fn version_markers_reject_incompatible_or_malformed_history() {
7291 let incompatible = workflow_context(vec![history_event(
7292 "VersionMarkerRecorded",
7293 json!({
7294 "sequence": 1,
7295 "change_id": "checkout-v2",
7296 "version": 1,
7297 "min_supported": 1,
7298 "max_supported": 2,
7299 }),
7300 )]);
7301 let error = incompatible
7302 .get_version("checkout-v2", 2, 3)
7303 .expect_err("old version is unsupported");
7304 assert!(matches!(
7305 error,
7306 Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
7307 if reason == "version_marker_incompatible_range"
7308 ));
7309
7310 for (history, reason) in [
7311 (
7312 vec![history_event("SideEffectRecorded", json!({"sequence": 1}))],
7313 "side_effect_result_missing",
7314 ),
7315 (
7316 vec![history_event(
7317 "SideEffectRecorded",
7318 json!({
7319 "sequence": 1,
7320 "result": {"codec": "avro", "blob": "not-base64"},
7321 }),
7322 )],
7323 "side_effect_payload_incompatible",
7324 ),
7325 (
7326 vec![history_event(
7327 "SideEffectRecorded",
7328 json!({"sequence": 1, "result": {"unwrapped": true}}),
7329 )],
7330 "side_effect_payload_malformed",
7331 ),
7332 (
7333 vec![history_event(
7334 "VersionMarkerRecorded",
7335 json!({
7336 "sequence": 1,
7337 "change_id": "change",
7338 "version": 1,
7339 "min_supported": 2,
7340 "max_supported": 1,
7341 }),
7342 )],
7343 "version_marker_history_range_invalid",
7344 ),
7345 ] {
7346 let error = WorkflowState::new(
7347 history,
7348 "rust-workers".to_string(),
7349 JSON_CODEC.to_string(),
7350 None,
7351 )
7352 .expect_err("malformed history must fail");
7353 assert!(matches!(
7354 error,
7355 Error::NonDeterministicReplay(ReplayFailure { reason: actual, .. })
7356 if actual == reason
7357 ));
7358 }
7359 }
7360
7361 #[test]
7362 fn duplicate_side_effects_and_version_markers_are_rejected() {
7363 let duplicate_side_effect = WorkflowState::new(
7364 vec![
7365 history_event(
7366 "SideEffectRecorded",
7367 json!({"sequence": 1, "result": {"codec": "json", "blob": "1"}}),
7368 ),
7369 history_event(
7370 "SideEffectRecorded",
7371 json!({"sequence": 1, "result": {"codec": "json", "blob": "2"}}),
7372 ),
7373 ],
7374 "rust-workers".to_string(),
7375 JSON_CODEC.to_string(),
7376 None,
7377 )
7378 .expect_err("duplicate side effect");
7379 assert!(matches!(
7380 duplicate_side_effect,
7381 Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
7382 if reason == "duplicate_side_effect_record"
7383 ));
7384
7385 let marker = |sequence| {
7386 history_event(
7387 "VersionMarkerRecorded",
7388 json!({
7389 "sequence": sequence,
7390 "change_id": "same-change",
7391 "version": 1,
7392 "min_supported": 1,
7393 "max_supported": 1,
7394 }),
7395 )
7396 };
7397 let duplicate_marker = WorkflowState::new(
7398 vec![marker(1), marker(3)],
7399 "rust-workers".to_string(),
7400 JSON_CODEC.to_string(),
7401 None,
7402 )
7403 .expect_err("duplicate marker");
7404 assert!(matches!(
7405 duplicate_marker,
7406 Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
7407 if reason == "duplicate_version_marker"
7408 ));
7409 }
7410
7411 #[test]
7412 fn cold_worker_replay_does_not_repeat_committed_side_effects_or_markers() {
7413 fn worker(calls: Arc<AtomicUsize>) -> Worker {
7414 let client = Client::new("http://127.0.0.1:8080").expect("client");
7415 let mut worker = Worker::new(client, "rust-workers");
7416 worker.register_workflow("rust.side-effect-version", move |ctx, _input| {
7417 let calls = Arc::clone(&calls);
7418 async move {
7419 let captured = ctx.side_effect(|| {
7420 calls.fetch_add(1, Ordering::SeqCst);
7421 "captured-once".to_string()
7422 })?;
7423 let version = ctx.get_version("cold-restart", 1, 2)?;
7424 Ok(json!({"captured": captured, "version": version}))
7425 }
7426 });
7427 worker
7428 }
7429
7430 fn task(history_events: Vec<HistoryEvent>) -> WorkflowTask {
7431 WorkflowTask {
7432 task_id: "wft-side-effect-version".to_string(),
7433 workflow_id: Some("wf-side-effect-version".to_string()),
7434 run_id: Some("run-side-effect-version".to_string()),
7435 workflow_type: "rust.side-effect-version".to_string(),
7436 payload_codec: JSON_CODEC.to_string(),
7437 arguments: Some(encode_value_envelope(&json!([]), JSON_CODEC).expect("arguments")),
7438 history_events,
7439 total_history_events: None,
7440 history_size_bytes: None,
7441 continue_as_new_recommended: None,
7442 history_budget_pressure: None,
7443 next_history_page_token: None,
7444 workflow_task_attempt: 1,
7445 workflow_signal_id: None,
7446 signal_name: None,
7447 signal_arguments: None,
7448 lease_owner: Some("rust-worker".to_string()),
7449 }
7450 }
7451
7452 let calls = Arc::new(AtomicUsize::new(0));
7453 let initial = worker(Arc::clone(&calls))
7454 .execute_workflow_task(task(Vec::new()))
7455 .expect("initial execution");
7456 assert_eq!(
7457 initial
7458 .iter()
7459 .map(|command| &command["type"])
7460 .collect::<Vec<_>>(),
7461 vec![
7462 "record_side_effect",
7463 "record_version_marker",
7464 "complete_workflow"
7465 ]
7466 );
7467 assert_eq!(calls.load(Ordering::SeqCst), 1);
7468
7469 let restarted = worker(Arc::clone(&calls));
7470 let replayed = restarted
7471 .execute_workflow_task(task(vec![
7472 history_event(
7473 "SideEffectRecorded",
7474 json!({"sequence": 1, "result": initial[0]["result"].clone()}),
7475 ),
7476 history_event(
7477 "VersionMarkerRecorded",
7478 json!({
7479 "sequence": 2,
7480 "change_id": "cold-restart",
7481 "version": 2,
7482 "min_supported": 1,
7483 "max_supported": 2,
7484 }),
7485 ),
7486 ]))
7487 .expect("cold replay");
7488 assert_eq!(replayed.len(), 1);
7489 assert_eq!(replayed[0]["type"], "complete_workflow");
7490 assert_eq!(calls.load(Ordering::SeqCst), 1);
7491 }
7492
7493 #[test]
7494 fn side_effect_replay_rejects_changed_rust_value_type() {
7495 let result = encode_value_envelope(&json!({"value": 42}), JSON_CODEC).expect("result");
7496 let ctx = workflow_context(vec![history_event(
7497 "SideEffectRecorded",
7498 json!({"sequence": 1, "result": result}),
7499 )]);
7500 let error = ctx
7501 .side_effect::<Vec<String>, _>(|| panic!("callback must not run"))
7502 .expect_err("changed type must fail replay");
7503 assert!(matches!(
7504 error,
7505 Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
7506 if reason == "side_effect_type_mismatch"
7507 ));
7508 }
7509
7510 fn completed_retry_activity_history() -> Vec<HistoryEvent> {
7511 vec![
7512 history_event(
7513 "ActivityScheduled",
7514 json!({
7515 "sequence": 1,
7516 "activity_type": "flaky",
7517 "activity_execution_id": "act-1",
7518 "activity": {
7519 "id": "act-1",
7520 "sequence": 1,
7521 "type": "flaky",
7522 "queue": "critical-activities",
7523 "execution_mode": null,
7524 "retry_policy": {
7525 "snapshot_version": 1,
7526 "max_attempts": 3,
7527 "backoff_seconds": [2, 4],
7528 "start_to_close_timeout": 30,
7529 "schedule_to_start_timeout": 5,
7530 "schedule_to_close_timeout": 90,
7531 "heartbeat_timeout": 10,
7532 "non_retryable_error_types": ["PermanentError"]
7533 }
7534 }
7535 }),
7536 ),
7537 history_event(
7538 "ActivityStarted",
7539 json!({
7540 "sequence": 1,
7541 "activity_type": "flaky",
7542 "activity_execution_id": "act-1",
7543 "activity_attempt_id": "attempt-1",
7544 "attempt_number": 1
7545 }),
7546 ),
7547 history_event(
7548 "ActivityRetryScheduled",
7549 json!({
7550 "sequence": 1,
7551 "activity_type": "flaky",
7552 "activity_execution_id": "act-1",
7553 "activity_attempt_id": "attempt-1",
7554 "attempt_number": 1,
7555 "retry_after_attempt": 1,
7556 "retry_backoff_seconds": 2,
7557 "failure_category": "activity",
7558 "exception_type": "TransientError"
7559 }),
7560 ),
7561 history_event(
7562 "ActivityStarted",
7563 json!({
7564 "sequence": 1,
7565 "activity_type": "flaky",
7566 "activity_execution_id": "act-1",
7567 "activity_attempt_id": "attempt-2",
7568 "attempt_number": 2
7569 }),
7570 ),
7571 history_event(
7572 "ActivityCompleted",
7573 json!({
7574 "sequence": 1,
7575 "activity_type": "flaky",
7576 "activity_execution_id": "act-1",
7577 "activity_attempt_id": "attempt-2",
7578 "attempt_number": 2,
7579 "payload_codec": "json",
7580 "result": {"codec": "json", "blob": "{\"status\":\"recovered\"}"}
7581 }),
7582 ),
7583 ]
7584 }
7585
7586 fn retry_activity_options() -> ActivityOptions {
7587 ActivityOptions::new()
7588 .task_queue("critical-activities")
7589 .retry_policy(
7590 ActivityRetryPolicy::new(3)
7591 .backoff_intervals([Duration::from_secs(2), Duration::from_secs(4)])
7592 .non_retryable_error_type("PermanentError"),
7593 )
7594 .start_to_close_timeout(Duration::from_secs(30))
7595 .schedule_to_start_timeout(Duration::from_secs(5))
7596 .schedule_to_close_timeout(Duration::from_secs(90))
7597 .heartbeat_timeout(Duration::from_secs(10))
7598 }
7599
7600 #[test]
7601 fn avro_generic_wrapper_round_trips_json_values() {
7602 let value = json!({"greeting": "hello", "count": 3, "ok": true});
7603 let envelope = PayloadEnvelope::avro(&value).expect("encode");
7604 assert_eq!(envelope.codec, DEFAULT_CODEC);
7605 assert_eq!(decode_payload::<Value>(&envelope).expect("decode"), value);
7606 }
7607
7608 #[test]
7609 fn json_codec_remains_plain_json() {
7610 let value = json!({"greeting": "hello", "count": 3, "ok": true});
7611 let envelope = PayloadEnvelope::json(&value).expect("encode");
7612
7613 assert_eq!(envelope.codec, JSON_CODEC);
7614 assert_eq!(envelope.blob, serde_json::to_string(&value).expect("json"));
7615 assert_eq!(decode_payload::<Value>(&envelope).expect("decode"), value);
7616 }
7617
7618 #[test]
7619 fn typed_avro_payload_without_schema_context_keeps_diagnostic() {
7620 let envelope = PayloadEnvelope {
7621 codec: DEFAULT_CODEC.to_string(),
7622 blob: BASE64.encode([0x01]),
7623 };
7624
7625 let error = decode_payload::<Value>(&envelope).expect_err("typed payload must fail");
7626 assert_eq!(
7627 error.to_string(),
7628 "codec error: typed avro payloads require a schema context; v1 supports the generic wrapper"
7629 );
7630 }
7631
7632 #[test]
7633 fn workflow_context_schedules_activity_until_completion_is_in_history() {
7634 let ctx = WorkflowContext {
7635 state: Arc::new(Mutex::new(
7636 WorkflowState::new_with_identity(
7637 Vec::new(),
7638 Some("wf-parent".to_string()),
7639 Some("run-parent".to_string()),
7640 "rust-workers".to_string(),
7641 DEFAULT_CODEC.to_string(),
7642 None,
7643 )
7644 .expect("workflow state"),
7645 )),
7646 };
7647
7648 let mut call = Box::pin(ctx.activity("hello.activity", json!(["Ada"])));
7649 let mut task_context = TaskContext::from_waker(noop_waker_ref());
7650 assert!(matches!(
7651 call.as_mut().poll(&mut task_context),
7652 Poll::Pending
7653 ));
7654
7655 let commands = ctx.take_commands().expect("commands");
7656 assert_eq!(commands[0]["type"], "schedule_activity");
7657 assert_eq!(commands[0]["activity_type"], "hello.activity");
7658 }
7659
7660 #[test]
7661 fn activity_options_encode_retry_policy_queue_and_every_timeout() {
7662 let ctx = workflow_context(Vec::new());
7663 let options = ActivityOptions::new()
7664 .task_queue("payments")
7665 .retry_policy(
7666 ActivityRetryPolicy::new(4)
7667 .exponential_backoff(Duration::from_secs(1), 3, Some(Duration::from_secs(10)))
7668 .non_retryable_error_type("ValidationError"),
7669 )
7670 .start_to_close_timeout(Duration::from_secs(120))
7671 .schedule_to_start_timeout(Duration::from_secs(10))
7672 .schedule_to_close_timeout(Duration::from_secs(300))
7673 .heartbeat_timeout(Duration::from_secs(15));
7674 let mut call = Box::pin(ctx.activity_with_options(
7675 "charge-card",
7676 options,
7677 json!([{"order_id": "o-1"}]),
7678 ));
7679 let mut task_context = TaskContext::from_waker(noop_waker_ref());
7680
7681 assert!(matches!(
7682 call.as_mut().poll(&mut task_context),
7683 Poll::Pending
7684 ));
7685 assert!(matches!(
7686 call.as_mut().poll(&mut task_context),
7687 Poll::Pending
7688 ));
7689
7690 let commands = ctx.take_commands().expect("activity command");
7691 assert_eq!(commands.len(), 1, "one future emits one logical schedule");
7692 assert_eq!(commands[0]["queue"], "payments");
7693 assert_eq!(
7694 commands[0]["retry_policy"],
7695 json!({
7696 "max_attempts": 4,
7697 "backoff_seconds": [1, 3, 9],
7698 "non_retryable_error_types": ["ValidationError"],
7699 })
7700 );
7701 assert_eq!(commands[0]["start_to_close_timeout"], 120);
7702 assert_eq!(commands[0]["schedule_to_start_timeout"], 10);
7703 assert_eq!(commands[0]["schedule_to_close_timeout"], 300);
7704 assert_eq!(commands[0]["heartbeat_timeout"], 15);
7705 }
7706
7707 #[test]
7708 fn activity_options_encode_explicit_and_rounded_backoff_intervals() {
7709 let ctx = workflow_context(Vec::new());
7710 let options = ActivityOptions::new().retry_policy(
7711 ActivityRetryPolicy::new(3)
7712 .backoff_intervals([Duration::from_millis(1), Duration::from_millis(1_001)]),
7713 );
7714 let mut call = Box::pin(ctx.activity_with_options("work", options, json!([])));
7715 let mut task_context = TaskContext::from_waker(noop_waker_ref());
7716
7717 assert!(matches!(
7718 call.as_mut().poll(&mut task_context),
7719 Poll::Pending
7720 ));
7721 assert_eq!(
7722 ctx.take_commands().expect("command")[0]["retry_policy"]["backoff_seconds"],
7723 json!([1, 2])
7724 );
7725 }
7726
7727 #[test]
7728 fn invalid_activity_options_return_typed_errors_before_emitting_commands() {
7729 let cases = [
7730 (
7731 ActivityOptions::new().task_queue(" "),
7732 ActivityOptionsErrorKind::EmptyTaskQueue,
7733 ),
7734 (
7735 ActivityOptions::new().retry_policy(ActivityRetryPolicy::default()),
7736 ActivityOptionsErrorKind::EmptyRetryPolicy,
7737 ),
7738 (
7739 ActivityOptions::new().retry_policy(ActivityRetryPolicy::new(0)),
7740 ActivityOptionsErrorKind::InvalidMaxAttempts,
7741 ),
7742 (
7743 ActivityOptions::new().retry_policy(ActivityRetryPolicy {
7744 max_attempts: None,
7745 backoff: Some(ActivityBackoff::Explicit(vec![Duration::from_secs(1)])),
7746 non_retryable_error_types: Vec::new(),
7747 }),
7748 ActivityOptionsErrorKind::BackoffWithoutRetryBudget,
7749 ),
7750 (
7751 ActivityOptions::new().retry_policy(
7752 ActivityRetryPolicy::new(2)
7753 .backoff_intervals([Duration::from_secs(1), Duration::from_secs(2)]),
7754 ),
7755 ActivityOptionsErrorKind::TooManyBackoffIntervals,
7756 ),
7757 (
7758 ActivityOptions::new().retry_policy(
7759 ActivityRetryPolicy::new(2).exponential_backoff(
7760 Duration::from_secs(1),
7761 0,
7762 None,
7763 ),
7764 ),
7765 ActivityOptionsErrorKind::InvalidBackoffCoefficient,
7766 ),
7767 (
7768 ActivityOptions::new()
7769 .retry_policy(ActivityRetryPolicy::new(2).non_retryable_error_type(" ")),
7770 ActivityOptionsErrorKind::EmptyNonRetryableErrorType,
7771 ),
7772 (
7773 ActivityOptions::new().retry_policy(
7774 ActivityRetryPolicy::new(10_002).exponential_backoff(
7775 Duration::from_secs(1),
7776 1,
7777 None,
7778 ),
7779 ),
7780 ActivityOptionsErrorKind::BackoffGenerationTooLarge,
7781 ),
7782 (
7783 ActivityOptions::new().retry_policy(
7784 ActivityRetryPolicy::new(2)
7785 .backoff_intervals([Duration::from_secs(i64::MAX as u64 + 1)]),
7786 ),
7787 ActivityOptionsErrorKind::BackoffOverflow,
7788 ),
7789 ];
7790
7791 for (options, expected_kind) in cases {
7792 let ctx = workflow_context(Vec::new());
7793 let mut call = Box::pin(ctx.activity_with_options("work", options, json!([])));
7794 let mut task_context = TaskContext::from_waker(noop_waker_ref());
7795 let Poll::Ready(Err(Error::InvalidActivityOptions(error))) =
7796 call.as_mut().poll(&mut task_context)
7797 else {
7798 panic!("expected typed activity validation error");
7799 };
7800 assert_eq!(error.kind, expected_kind);
7801 assert!(ctx.take_commands().expect("commands").is_empty());
7802 }
7803 }
7804
7805 #[test]
7806 fn activity_options_validate_positive_and_ordered_timeouts() {
7807 let zero_timeout_cases = [
7808 ActivityOptions::new().start_to_close_timeout(Duration::ZERO),
7809 ActivityOptions::new().schedule_to_start_timeout(Duration::ZERO),
7810 ActivityOptions::new().schedule_to_close_timeout(Duration::ZERO),
7811 ActivityOptions::new().heartbeat_timeout(Duration::ZERO),
7812 ];
7813 for options in zero_timeout_cases {
7814 assert_eq!(
7815 options.validate().expect_err("zero timeout").kind,
7816 ActivityOptionsErrorKind::TimeoutNotPositive
7817 );
7818 }
7819
7820 let ordering_cases = [
7821 ActivityOptions::new()
7822 .heartbeat_timeout(Duration::from_secs(11))
7823 .start_to_close_timeout(Duration::from_secs(10)),
7824 ActivityOptions::new()
7825 .start_to_close_timeout(Duration::from_secs(31))
7826 .schedule_to_close_timeout(Duration::from_secs(30)),
7827 ActivityOptions::new()
7828 .schedule_to_start_timeout(Duration::from_secs(31))
7829 .schedule_to_close_timeout(Duration::from_secs(30)),
7830 ];
7831 for options in ordering_cases {
7832 assert_eq!(
7833 options.validate().expect_err("timeout order").kind,
7834 ActivityOptionsErrorKind::TimeoutOrder
7835 );
7836 }
7837
7838 assert_eq!(
7839 ActivityOptions::new()
7840 .start_to_close_timeout(Duration::from_secs(i64::MAX as u64 + 1))
7841 .validate()
7842 .expect_err("protocol integer overflow")
7843 .kind,
7844 ActivityOptionsErrorKind::TimeoutOverflow
7845 );
7846 }
7847
7848 #[test]
7849 fn replayed_activity_retry_history_completes_without_duplicate_schedule() {
7850 let ctx = workflow_context(completed_retry_activity_history());
7851 let mut call =
7852 Box::pin(ctx.activity_with_options("flaky", retry_activity_options(), json!([])));
7853 let mut task_context = TaskContext::from_waker(noop_waker_ref());
7854
7855 assert!(matches!(
7856 call.as_mut().poll(&mut task_context),
7857 Poll::Ready(Ok(result)) if result == json!({"status": "recovered"})
7858 ));
7859 assert!(ctx.take_commands().expect("commands").is_empty());
7860 ctx.ensure_history_consumed().expect("history consumed");
7861 }
7862
7863 #[test]
7864 fn duplicate_non_retryable_types_use_one_command_and_replay_representation() {
7865 let mut options = retry_activity_options();
7866 options
7867 .retry_policy
7868 .as_mut()
7869 .expect("retry policy")
7870 .non_retryable_error_types
7871 .extend([" PermanentError ".to_string(), "PermanentError".to_string()]);
7872
7873 let new_ctx = workflow_context(Vec::new());
7874 let mut new_call =
7875 Box::pin(new_ctx.activity_with_options("flaky", options.clone(), json!([])));
7876 let mut task_context = TaskContext::from_waker(noop_waker_ref());
7877 assert!(matches!(
7878 new_call.as_mut().poll(&mut task_context),
7879 Poll::Pending
7880 ));
7881 let commands = new_ctx.take_commands().expect("commands");
7882 assert_eq!(commands.len(), 1);
7883 assert_eq!(
7884 commands[0]["retry_policy"]["non_retryable_error_types"],
7885 json!(["PermanentError"])
7886 );
7887
7888 let replay_ctx = workflow_context(completed_retry_activity_history());
7889 let mut replay_call =
7890 Box::pin(replay_ctx.activity_with_options("flaky", options, json!([])));
7891 assert!(matches!(
7892 replay_call.as_mut().poll(&mut task_context),
7893 Poll::Ready(Ok(result)) if result == json!({"status": "recovered"})
7894 ));
7895 assert!(replay_ctx.take_commands().expect("commands").is_empty());
7896 replay_ctx
7897 .ensure_history_consumed()
7898 .expect("history consumed");
7899 }
7900
7901 #[test]
7902 fn replayed_intermediate_retry_remains_pending_across_restarts() {
7903 let history = completed_retry_activity_history()
7904 .into_iter()
7905 .take(3)
7906 .collect::<Vec<_>>();
7907
7908 for _restart in 0..2 {
7909 let ctx = workflow_context(history.clone());
7910 let mut call =
7911 Box::pin(ctx.activity_with_options("flaky", retry_activity_options(), json!([])));
7912 let mut task_context = TaskContext::from_waker(noop_waker_ref());
7913 assert!(matches!(
7914 call.as_mut().poll(&mut task_context),
7915 Poll::Pending
7916 ));
7917 assert!(ctx.take_commands().expect("commands").is_empty());
7918 }
7919 }
7920
7921 #[test]
7922 fn replayed_activity_rejects_changed_queue_retry_and_every_timeout_field() {
7923 let mut changed_queue = retry_activity_options();
7924 changed_queue.task_queue = Some("different-queue".to_string());
7925
7926 let mut changed_max_attempts = retry_activity_options();
7927 let retry_policy = changed_max_attempts
7928 .retry_policy
7929 .as_mut()
7930 .expect("retry policy");
7931 retry_policy.max_attempts = Some(4);
7932
7933 let mut changed_backoff = retry_activity_options();
7934 let retry_policy = changed_backoff.retry_policy.as_mut().expect("retry policy");
7935 retry_policy.backoff = Some(ActivityBackoff::Explicit(vec![
7936 Duration::from_secs(3),
7937 Duration::from_secs(4),
7938 ]));
7939
7940 let mut changed_non_retryable_types = retry_activity_options();
7941 let retry_policy = changed_non_retryable_types
7942 .retry_policy
7943 .as_mut()
7944 .expect("retry policy");
7945 retry_policy.non_retryable_error_types = vec!["AnotherPermanentError".to_string()];
7946
7947 let mut changed_start_to_close = retry_activity_options();
7948 changed_start_to_close.start_to_close_timeout = Some(Duration::from_secs(31));
7949 let mut changed_schedule_to_start = retry_activity_options();
7950 changed_schedule_to_start.schedule_to_start_timeout = Some(Duration::from_secs(6));
7951 let mut changed_schedule_to_close = retry_activity_options();
7952 changed_schedule_to_close.schedule_to_close_timeout = Some(Duration::from_secs(91));
7953 let mut changed_heartbeat = retry_activity_options();
7954 changed_heartbeat.heartbeat_timeout = Some(Duration::from_secs(11));
7955
7956 let cases = [
7957 (changed_queue, "activity_task_queue_mismatch"),
7958 (changed_max_attempts, "activity_retry_policy_mismatch"),
7959 (changed_backoff, "activity_retry_policy_mismatch"),
7960 (
7961 changed_non_retryable_types,
7962 "activity_retry_policy_mismatch",
7963 ),
7964 (changed_start_to_close, "activity_retry_policy_mismatch"),
7965 (changed_schedule_to_start, "activity_retry_policy_mismatch"),
7966 (changed_schedule_to_close, "activity_retry_policy_mismatch"),
7967 (changed_heartbeat, "activity_retry_policy_mismatch"),
7968 ];
7969
7970 for (options, expected_reason) in cases {
7971 let ctx = workflow_context(completed_retry_activity_history());
7972 let mut call = Box::pin(ctx.activity_with_options("flaky", options, json!([])));
7973 let mut task_context = TaskContext::from_waker(noop_waker_ref());
7974 let Poll::Ready(Err(Error::NonDeterministicReplay(failure))) =
7975 call.as_mut().poll(&mut task_context)
7976 else {
7977 panic!("changed activity options must fail replay");
7978 };
7979 assert_eq!(failure.reason, expected_reason);
7980 assert_eq!(failure.sequence, Some(1));
7981 assert!(ctx.take_commands().expect("commands").is_empty());
7982 }
7983 }
7984
7985 #[test]
7986 fn replayed_activity_rejects_changed_execution_mode_and_snapshot_version() {
7987 let cases = [
7988 (
7989 "execution_mode",
7990 json!("local"),
7991 "activity_execution_mode_mismatch",
7992 ),
7993 (
7994 "snapshot_version",
7995 json!(2),
7996 "activity_retry_policy_mismatch",
7997 ),
7998 ];
7999
8000 for (field, value, expected_reason) in cases {
8001 let mut history = completed_retry_activity_history();
8002 let activity = history[0].payload["activity"]
8003 .as_object_mut()
8004 .expect("activity snapshot");
8005 if field == "execution_mode" {
8006 activity.insert(field.to_string(), value);
8007 } else {
8008 activity["retry_policy"]
8009 .as_object_mut()
8010 .expect("retry snapshot")
8011 .insert(field.to_string(), value);
8012 }
8013
8014 let ctx = workflow_context(history);
8015 let mut call =
8016 Box::pin(ctx.activity_with_options("flaky", retry_activity_options(), json!([])));
8017 let mut task_context = TaskContext::from_waker(noop_waker_ref());
8018 let Poll::Ready(Err(Error::NonDeterministicReplay(failure))) =
8019 call.as_mut().poll(&mut task_context)
8020 else {
8021 panic!("changed {field} must fail replay");
8022 };
8023 assert_eq!(failure.reason, expected_reason);
8024 assert_eq!(failure.sequence, Some(1));
8025 assert!(ctx.take_commands().expect("commands").is_empty());
8026 }
8027 }
8028
8029 #[test]
8030 fn replayed_legacy_activity_treats_missing_option_snapshot_as_unknown() {
8031 let mut history = completed_retry_activity_history();
8032 let activity = history[0].payload["activity"]
8033 .as_object_mut()
8034 .expect("activity snapshot");
8035 activity.remove("execution_mode");
8036 activity.remove("retry_policy");
8037
8038 let mut current = retry_activity_options();
8039 current.start_to_close_timeout = Some(Duration::from_secs(45));
8040 current.schedule_to_start_timeout = Some(Duration::from_secs(8));
8041 current.schedule_to_close_timeout = Some(Duration::from_secs(120));
8042 current.heartbeat_timeout = Some(Duration::from_secs(12));
8043
8044 let ctx = workflow_context(history);
8045 let mut call = Box::pin(ctx.activity_with_options("flaky", current, json!([])));
8046 let mut task_context = TaskContext::from_waker(noop_waker_ref());
8047 assert!(matches!(
8048 call.as_mut().poll(&mut task_context),
8049 Poll::Ready(Ok(result)) if result == json!({"status": "recovered"})
8050 ));
8051 assert!(ctx.take_commands().expect("commands").is_empty());
8052 ctx.ensure_history_consumed().expect("history consumed");
8053 }
8054
8055 #[test]
8056 fn terminal_activity_failed_after_start_returns_typed_failure() {
8057 let history = vec![
8058 history_event(
8059 "ActivityScheduled",
8060 json!({
8061 "sequence": 1,
8062 "activity_type": "flaky",
8063 "activity_execution_id": "act-terminal",
8064 "activity": {
8065 "id": "act-terminal",
8066 "sequence": 1,
8067 "type": "flaky",
8068 "queue": "critical-activities",
8069 "retry_policy": {
8070 "snapshot_version": 1,
8071 "max_attempts": 3,
8072 "backoff_seconds": [2, 4],
8073 "non_retryable_error_types": ["PermanentError"]
8074 }
8075 }
8076 }),
8077 ),
8078 history_event(
8079 "ActivityStarted",
8080 json!({
8081 "sequence": 1,
8082 "activity_type": "flaky",
8083 "activity_execution_id": "act-terminal",
8084 "activity_attempt_id": "attempt-1",
8085 "attempt_number": 1
8086 }),
8087 ),
8088 history_event(
8089 "ActivityFailed",
8090 json!({
8091 "sequence": 1,
8092 "activity_type": "flaky",
8093 "activity_execution_id": "act-terminal",
8094 "activity_attempt_id": "attempt-1",
8095 "attempt_number": 1,
8096 "failure_id": "failure-terminal",
8097 "failure_category": "activity",
8098 "exception_type": "PermanentError",
8099 "message": "cannot retry",
8100 "non_retryable": true
8101 }),
8102 ),
8103 ];
8104 let ctx = workflow_context(history);
8105 let mut call =
8106 Box::pin(ctx.activity_with_options("flaky", retry_activity_options(), json!([])));
8107 let mut task_context = TaskContext::from_waker(noop_waker_ref());
8108
8109 let Poll::Ready(Err(Error::ActivityFailed(failure))) =
8110 call.as_mut().poll(&mut task_context)
8111 else {
8112 panic!("terminal ActivityFailed must settle the activity future");
8113 };
8114 assert_eq!(failure.kind, ActivityFailureKind::Failed);
8115 assert_eq!(
8116 failure.activity_execution_id.as_deref(),
8117 Some("act-terminal")
8118 );
8119 assert_eq!(failure.exception_type.as_deref(), Some("PermanentError"));
8120 assert!(failure.non_retryable);
8121 assert!(ctx.take_commands().expect("commands").is_empty());
8122 ctx.ensure_history_consumed().expect("history consumed");
8123 }
8124
8125 #[test]
8126 fn activity_terminal_events_return_machine_readable_failures() {
8127 let cases = [
8128 (
8129 "ActivityFailed",
8130 json!({
8131 "sequence": 1,
8132 "activity_type": "charge-card",
8133 "activity_execution_id": "act-1",
8134 "activity_attempt_id": "attempt-2",
8135 "attempt_number": 2,
8136 "failure_id": "failure-1",
8137 "failure_category": "activity",
8138 "exception_type": "PaymentDeclined",
8139 "exception_class": "payments.PaymentDeclined",
8140 "message": "card declined",
8141 "non_retryable": true
8142 }),
8143 ActivityFailureKind::Failed,
8144 "activity",
8145 ),
8146 (
8147 "ActivityCancelled",
8148 json!({
8149 "sequence": 1,
8150 "activity_type": "charge-card",
8151 "activity_execution_id": "act-1",
8152 "activity_attempt_id": "attempt-1"
8153 }),
8154 ActivityFailureKind::Cancelled,
8155 "cancelled",
8156 ),
8157 ];
8158
8159 for (event_type, payload, expected_kind, expected_reason) in cases {
8160 let ctx = workflow_context(vec![history_event(event_type, payload)]);
8161 let mut call = Box::pin(ctx.activity("charge-card", json!([])));
8162 let mut task_context = TaskContext::from_waker(noop_waker_ref());
8163 let Poll::Ready(Err(Error::ActivityFailed(failure))) =
8164 call.as_mut().poll(&mut task_context)
8165 else {
8166 panic!("expected terminal activity failure");
8167 };
8168 assert_eq!(failure.kind, expected_kind);
8169 assert_eq!(failure.reason, expected_reason);
8170 assert_eq!(failure.activity_execution_id.as_deref(), Some("act-1"));
8171 assert_eq!(failure.activity_type.as_deref(), Some("charge-card"));
8172 }
8173 }
8174
8175 #[test]
8176 fn every_activity_timeout_class_is_typed() {
8177 for timeout_kind in [
8178 "start_to_close",
8179 "schedule_to_start",
8180 "schedule_to_close",
8181 "heartbeat",
8182 ] {
8183 let ctx = workflow_context(vec![history_event(
8184 "ActivityTimedOut",
8185 json!({
8186 "sequence": 1,
8187 "activity_type": "slow",
8188 "activity_execution_id": "act-timeout",
8189 "activity_attempt_id": "attempt-timeout",
8190 "failure_category": "timeout",
8191 "timeout_kind": timeout_kind,
8192 "message": "deadline expired"
8193 }),
8194 )]);
8195 let mut call = Box::pin(ctx.activity("slow", json!([])));
8196 let mut task_context = TaskContext::from_waker(noop_waker_ref());
8197 let Poll::Ready(Err(Error::ActivityFailed(failure))) =
8198 call.as_mut().poll(&mut task_context)
8199 else {
8200 panic!("expected timeout failure");
8201 };
8202 assert_eq!(failure.kind, ActivityFailureKind::TimedOut);
8203 assert_eq!(failure.reason, timeout_kind);
8204 assert_eq!(failure.timeout_kind.as_deref(), Some(timeout_kind));
8205 assert_eq!(failure.failure_category.as_deref(), Some("timeout"));
8206 }
8207 }
8208
8209 #[test]
8210 fn workflow_sleep_emits_one_durable_timer_and_rounds_up() {
8211 let ctx = workflow_context(Vec::new());
8212 let mut sleep = Box::pin(ctx.sleep(Duration::from_millis(1_001)));
8213 let mut task_context = TaskContext::from_waker(noop_waker_ref());
8214
8215 assert!(matches!(
8216 sleep.as_mut().poll(&mut task_context),
8217 Poll::Pending
8218 ));
8219 assert!(matches!(
8220 sleep.as_mut().poll(&mut task_context),
8221 Poll::Pending
8222 ));
8223
8224 let commands = ctx.take_commands().expect("timer command");
8225 assert_eq!(
8226 commands,
8227 vec![json!({
8228 "type": "start_timer",
8229 "delay_seconds": 2,
8230 })]
8231 );
8232 }
8233
8234 #[test]
8235 fn workflow_sleep_replays_matching_schedule_and_fire_without_a_command() {
8236 let history = vec![
8237 history_event(
8238 "TimerScheduled",
8239 json!({
8240 "sequence": 1,
8241 "timer_id": "timer-1",
8242 "delay_seconds": 5,
8243 "fire_at": "2026-07-11T12:00:05Z",
8244 }),
8245 ),
8246 history_event(
8247 "TimerFired",
8248 json!({
8249 "sequence": 1,
8250 "timer_id": "timer-1",
8251 "delay_seconds": 5,
8252 "fire_at": "2026-07-11T12:00:05Z",
8253 "fired_at": "2026-07-11T12:00:05Z",
8254 }),
8255 ),
8256 ];
8257
8258 for _restart in 0..2 {
8259 let ctx = workflow_context(history.clone());
8260 let mut sleep = Box::pin(ctx.sleep(Duration::from_secs(5)));
8261 let mut task_context = TaskContext::from_waker(noop_waker_ref());
8262 assert!(matches!(
8263 sleep.as_mut().poll(&mut task_context),
8264 Poll::Ready(Ok(()))
8265 ));
8266 assert!(ctx.take_commands().expect("commands").is_empty());
8267 ctx.ensure_history_consumed().expect("history consumed");
8268 }
8269 }
8270
8271 #[test]
8272 fn workflow_sleep_rejects_changed_delay_during_replay() {
8273 let ctx = workflow_context(vec![
8274 history_event(
8275 "TimerScheduled",
8276 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8277 ),
8278 history_event(
8279 "TimerFired",
8280 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8281 ),
8282 ]);
8283 let mut sleep = Box::pin(ctx.sleep(Duration::from_secs(500)));
8284 let mut task_context = TaskContext::from_waker(noop_waker_ref());
8285
8286 let Poll::Ready(Err(Error::NonDeterministicReplay(failure))) =
8287 sleep.as_mut().poll(&mut task_context)
8288 else {
8289 panic!("changed timer delay must be rejected");
8290 };
8291 assert_eq!(failure.reason, "timer_delay_mismatch");
8292 assert_eq!(failure.sequence, Some(1));
8293 }
8294
8295 #[test]
8296 fn workflow_history_rejects_unpaired_or_mismatched_timer_events() {
8297 let lone_fire = WorkflowState::new(
8298 vec![history_event(
8299 "TimerFired",
8300 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8301 )],
8302 "rust-workers".to_string(),
8303 JSON_CODEC.to_string(),
8304 None,
8305 )
8306 .expect_err("TimerFired requires TimerScheduled");
8307 assert!(matches!(
8308 lone_fire,
8309 Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
8310 if reason == "timer_schedule_missing_or_duplicate"
8311 ));
8312
8313 let wrong_identity = WorkflowState::new(
8314 vec![
8315 history_event(
8316 "TimerScheduled",
8317 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8318 ),
8319 history_event(
8320 "TimerFired",
8321 json!({"sequence": 1, "timer_id": "timer-2", "delay_seconds": 5}),
8322 ),
8323 ],
8324 "rust-workers".to_string(),
8325 JSON_CODEC.to_string(),
8326 None,
8327 )
8328 .expect_err("fire must match scheduled timer identity");
8329 assert!(matches!(
8330 wrong_identity,
8331 Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
8332 if reason == "timer_identity_mismatch"
8333 ));
8334
8335 let duplicate_fire = WorkflowState::new(
8336 vec![
8337 history_event(
8338 "TimerScheduled",
8339 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8340 ),
8341 history_event(
8342 "TimerFired",
8343 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8344 ),
8345 history_event(
8346 "TimerFired",
8347 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8348 ),
8349 ],
8350 "rust-workers".to_string(),
8351 JSON_CODEC.to_string(),
8352 None,
8353 )
8354 .expect_err("a durable timer cannot fire twice");
8355 assert!(matches!(
8356 duplicate_fire,
8357 Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
8358 if reason == "duplicate_timer_fire"
8359 ));
8360
8361 let wrong_fired_delay = WorkflowState::new(
8362 vec![
8363 history_event(
8364 "TimerScheduled",
8365 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8366 ),
8367 history_event(
8368 "TimerFired",
8369 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 6}),
8370 ),
8371 ],
8372 "rust-workers".to_string(),
8373 JSON_CODEC.to_string(),
8374 None,
8375 )
8376 .expect_err("timer schedule and fire delays must agree");
8377 assert!(matches!(
8378 wrong_fired_delay,
8379 Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
8380 if reason == "timer_history_delay_mismatch"
8381 ));
8382 }
8383
8384 #[test]
8385 fn replay_rejects_activity_moved_before_recorded_timer() {
8386 let ctx = workflow_context(vec![
8387 history_event(
8388 "TimerScheduled",
8389 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8390 ),
8391 history_event(
8392 "TimerFired",
8393 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8394 ),
8395 history_event(
8396 "ActivityCompleted",
8397 json!({
8398 "sequence": 2,
8399 "activity_type": "after-timer",
8400 "payload_codec": "json",
8401 "result": {"codec": "json", "blob": "\"done\""},
8402 }),
8403 ),
8404 ]);
8405 let mut activity = Box::pin(ctx.activity("after-timer", json!([])));
8406 let mut task_context = TaskContext::from_waker(noop_waker_ref());
8407
8408 let Poll::Ready(Err(Error::NonDeterministicReplay(failure))) =
8409 activity.as_mut().poll(&mut task_context)
8410 else {
8411 panic!("reordered durable command must be rejected");
8412 };
8413 assert_eq!(failure.reason, "recorded_command_mismatch");
8414 assert_eq!(failure.sequence, Some(1));
8415 assert_eq!(failure.expected.as_deref(), Some("timer"));
8416 assert_eq!(failure.actual.as_deref(), Some("activity:after-timer"));
8417 }
8418
8419 #[test]
8420 fn replay_orders_signal_waits_and_timers_in_one_command_stream() {
8421 let signal_then_timer = vec![
8422 history_event(
8423 "ConditionWaitOpened",
8424 json!({"sequence": 1, "condition_key": "signal:go"}),
8425 ),
8426 history_event(
8427 "SignalReceived",
8428 json!({
8429 "signal_name": "go",
8430 "arguments": ["now"],
8431 }),
8432 ),
8433 history_event(
8434 "TimerScheduled",
8435 json!({"sequence": 2, "timer_id": "timer-2", "delay_seconds": 5}),
8436 ),
8437 history_event(
8438 "TimerFired",
8439 json!({"sequence": 2, "timer_id": "timer-2", "delay_seconds": 5}),
8440 ),
8441 ];
8442
8443 let ctx = workflow_context(signal_then_timer.clone());
8444 let mut signal = Box::pin(ctx.wait_signal("go"));
8445 let mut task_context = TaskContext::from_waker(noop_waker_ref());
8446 assert!(matches!(
8447 signal.as_mut().poll(&mut task_context),
8448 Poll::Ready(Ok(arguments)) if arguments == vec![json!("now")]
8449 ));
8450 let mut timer = Box::pin(ctx.sleep(Duration::from_secs(5)));
8451 assert!(matches!(
8452 timer.as_mut().poll(&mut task_context),
8453 Poll::Ready(Ok(()))
8454 ));
8455 ctx.ensure_history_consumed()
8456 .expect("signal and timer history consumed in order");
8457
8458 let reordered = workflow_context(signal_then_timer);
8459 let mut timer_first = Box::pin(reordered.sleep(Duration::from_secs(5)));
8460 let Poll::Ready(Err(Error::NonDeterministicReplay(failure))) =
8461 timer_first.as_mut().poll(&mut task_context)
8462 else {
8463 panic!("timer cannot consume signal-wait-first history");
8464 };
8465 assert_eq!(failure.reason, "recorded_command_mismatch");
8466 assert_eq!(failure.sequence, Some(1));
8467 assert_eq!(failure.expected.as_deref(), Some("signal wait"));
8468
8469 let timer_then_signal = vec![
8470 history_event(
8471 "TimerScheduled",
8472 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8473 ),
8474 history_event(
8475 "TimerFired",
8476 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8477 ),
8478 history_event(
8479 "ConditionWaitOpened",
8480 json!({"sequence": 2, "condition_key": "signal:go"}),
8481 ),
8482 history_event(
8483 "SignalReceived",
8484 json!({"signal_name": "go", "arguments": []}),
8485 ),
8486 ];
8487 let reordered = workflow_context(timer_then_signal);
8488 let mut signal_first = Box::pin(reordered.wait_signal("go"));
8489 let Poll::Ready(Err(Error::NonDeterministicReplay(failure))) =
8490 signal_first.as_mut().poll(&mut task_context)
8491 else {
8492 panic!("signal wait cannot consume timer-first history");
8493 };
8494 assert_eq!(failure.reason, "recorded_command_mismatch");
8495 assert_eq!(failure.sequence, Some(1));
8496 assert_eq!(failure.expected.as_deref(), Some("timer"));
8497 }
8498
8499 #[test]
8500 fn workflow_history_rejects_duplicate_or_colliding_command_sequences() {
8501 let duplicate_timer = WorkflowState::new(
8502 vec![
8503 history_event(
8504 "TimerScheduled",
8505 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8506 ),
8507 history_event(
8508 "TimerScheduled",
8509 json!({"sequence": 1, "timer_id": "timer-2", "delay_seconds": 5}),
8510 ),
8511 ],
8512 "rust-workers".to_string(),
8513 JSON_CODEC.to_string(),
8514 None,
8515 )
8516 .expect_err("one workflow sequence cannot schedule two timers");
8517 assert!(matches!(
8518 duplicate_timer,
8519 Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
8520 if reason == "timer_schedule_missing_or_duplicate"
8521 ));
8522
8523 let colliding_kinds = WorkflowState::new(
8524 vec![
8525 history_event(
8526 "TimerScheduled",
8527 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8528 ),
8529 history_event(
8530 "ActivityCompleted",
8531 json!({"sequence": 1, "activity_type": "same-sequence"}),
8532 ),
8533 ],
8534 "rust-workers".to_string(),
8535 JSON_CODEC.to_string(),
8536 None,
8537 )
8538 .expect_err("one workflow sequence cannot identify two command kinds");
8539 assert!(matches!(
8540 colliding_kinds,
8541 Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
8542 if reason == "durable_command_sequence_collision"
8543 ));
8544
8545 let duplicate_signal_wait = WorkflowState::new(
8546 vec![
8547 history_event(
8548 "SignalWaitOpened",
8549 json!({"sequence": 1, "signal_name": "go"}),
8550 ),
8551 history_event(
8552 "SignalWaitOpened",
8553 json!({"sequence": 1, "signal_name": "go"}),
8554 ),
8555 ],
8556 "rust-workers".to_string(),
8557 JSON_CODEC.to_string(),
8558 None,
8559 )
8560 .expect_err("one workflow sequence cannot open two signal waits");
8561 assert!(matches!(
8562 duplicate_signal_wait,
8563 Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
8564 if reason == "duplicate_signal_wait_open"
8565 ));
8566 }
8567
8568 #[test]
8569 fn workflow_history_accepts_a_first_command_after_global_sequence_gaps() {
8570 let result = encode_value_envelope(&json!({"captured": true}), JSON_CODEC)
8571 .expect("side-effect result");
8572 let ctx = workflow_context(vec![history_event(
8573 "SideEffectRecorded",
8574 json!({"sequence": 99, "result": result}),
8575 )]);
8576
8577 let replayed: Value = ctx
8578 .side_effect(|| panic!("recorded side effect must not run"))
8579 .expect("positive global workflow sequence is valid");
8580 assert_eq!(replayed, json!({"captured": true}));
8581 ctx.ensure_history_consumed().expect("history consumed");
8582 }
8583
8584 #[test]
8585 fn workflow_history_rejects_zero_and_descending_command_sequences() {
8586 let result =
8587 encode_value_envelope(&json!("captured"), JSON_CODEC).expect("side-effect result");
8588 let zero = WorkflowState::new(
8589 vec![history_event(
8590 "SideEffectRecorded",
8591 json!({"sequence": 0, "result": result.clone()}),
8592 )],
8593 "rust-workers".to_string(),
8594 JSON_CODEC.to_string(),
8595 None,
8596 )
8597 .expect_err("durable command sequences must be positive");
8598 assert!(matches!(
8599 zero,
8600 Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
8601 if reason == "durable_command_sequence_invalid"
8602 ));
8603
8604 let descending = WorkflowState::new(
8605 vec![
8606 history_event(
8607 "SideEffectRecorded",
8608 json!({"sequence": 3, "result": result}),
8609 ),
8610 history_event(
8611 "VersionMarkerRecorded",
8612 json!({
8613 "sequence": 2,
8614 "change_id": "descending-marker",
8615 "version": 1,
8616 "min_supported": 1,
8617 "max_supported": 1,
8618 }),
8619 ),
8620 ],
8621 "rust-workers".to_string(),
8622 JSON_CODEC.to_string(),
8623 None,
8624 )
8625 .expect_err("new durable commands must remain strictly ordered");
8626 let Error::NonDeterministicReplay(failure) = descending else {
8627 panic!("expected typed replay failure");
8628 };
8629 assert_eq!(failure.reason, "durable_command_sequence_mismatch");
8630 assert_eq!(failure.sequence, Some(2));
8631 assert_eq!(
8632 failure.expected.as_deref(),
8633 Some("workflow sequence greater than 3")
8634 );
8635 assert_eq!(failure.actual.as_deref(), Some("2"));
8636 }
8637
8638 #[test]
8639 fn workflow_task_replay_completes_after_signals_create_sequence_gaps() {
8640 fn worker() -> Worker {
8641 let client = Client::new("http://127.0.0.1:8080").expect("client");
8642 let mut worker = Worker::new(client, "rust-workers");
8643 worker.register_workflow("rust.finish-after-gaps", |ctx, _input| async move {
8644 ctx.wait_signal("finish").await?;
8645 let marker: String =
8646 ctx.side_effect(|| panic!("recorded side effect must not run"))?;
8647 assert_eq!(marker, "after-finish");
8648 Ok(json!("finished"))
8649 });
8650 worker
8651 }
8652
8653 let marker =
8654 encode_value_envelope(&json!("after-finish"), JSON_CODEC).expect("side-effect result");
8655 let task = workflow_task(
8656 "rust.finish-after-gaps",
8657 vec![
8658 history_event(
8659 "ConditionWaitOpened",
8660 json!({"sequence": 1, "condition_key": "signal:finish"}),
8661 ),
8662 history_event(
8663 "SignalReceived",
8664 json!({
8665 "signal_id": "increment-3",
8666 "signal_name": "increment",
8667 "workflow_sequence": 2,
8668 "payload_codec": "json",
8669 "arguments": {"codec": "json", "blob": "[3]"},
8670 }),
8671 ),
8672 history_event(
8673 "SignalReceived",
8674 json!({
8675 "signal_id": "increment-5",
8676 "signal_name": "increment",
8677 "workflow_sequence": 3,
8678 "payload_codec": "json",
8679 "arguments": {"codec": "json", "blob": "[5]"},
8680 }),
8681 ),
8682 history_event(
8683 "SignalReceived",
8684 json!({
8685 "signal_id": "finish",
8686 "signal_name": "finish",
8687 "workflow_sequence": 4,
8688 "payload_codec": "json",
8689 "arguments": {"codec": "json", "blob": "[]"},
8690 }),
8691 ),
8692 history_event(
8693 "SideEffectRecorded",
8694 json!({"sequence": 5, "result": marker}),
8695 ),
8696 ],
8697 JSON_CODEC,
8698 );
8699
8700 for _original_or_cold_worker in 0..2 {
8701 let commands = worker()
8702 .execute_workflow_task(task.clone())
8703 .expect("signal gaps preserve deterministic replay");
8704 assert_eq!(commands.len(), 1, "replay emits only terminal completion");
8705 assert_eq!(commands[0]["type"], "complete_workflow");
8706 assert_eq!(
8707 decode_wire_value(&commands[0]["result"], JSON_CODEC).expect("workflow output"),
8708 json!("finished")
8709 );
8710 }
8711 }
8712
8713 #[test]
8714 fn workflow_sleep_rejects_unrepresentable_rounded_duration() {
8715 let ctx = workflow_context(Vec::new());
8716 let mut sleep = Box::pin(ctx.start_timer(Duration::new(u64::MAX, 1)));
8717 let mut task_context = TaskContext::from_waker(noop_waker_ref());
8718 assert!(matches!(
8719 sleep.as_mut().poll(&mut task_context),
8720 Poll::Ready(Err(Error::TimerDurationOverflow))
8721 ));
8722 assert!(ctx.take_commands().expect("commands").is_empty());
8723 }
8724
8725 #[test]
8726 fn workflow_task_replay_completes_without_rescheduling_recorded_commands() {
8727 let client = Client::new("http://127.0.0.1:8080").expect("client");
8728 let mut worker = Worker::new(client, "rust-workers");
8729 worker.register_workflow("rust.timer", |ctx, _input| async move {
8730 ctx.sleep(Duration::from_secs(5)).await?;
8731 ctx.activity("after-timer", json!([])).await
8732 });
8733
8734 let task = |history_events| WorkflowTask {
8735 task_id: "wft-rust-timer-1".to_string(),
8736 workflow_id: Some("wf-rust-timer".to_string()),
8737 run_id: Some("run-rust-timer".to_string()),
8738 workflow_type: "rust.timer".to_string(),
8739 payload_codec: JSON_CODEC.to_string(),
8740 arguments: Some(json!({"codec": "json", "blob": "[]"})),
8741 history_events,
8742 total_history_events: None,
8743 history_size_bytes: None,
8744 continue_as_new_recommended: None,
8745 history_budget_pressure: None,
8746 next_history_page_token: None,
8747 workflow_task_attempt: 1,
8748 workflow_signal_id: None,
8749 signal_name: None,
8750 signal_arguments: None,
8751 lease_owner: Some("rust-worker".to_string()),
8752 };
8753
8754 let initial = worker
8755 .execute_workflow_task(task(Vec::new()))
8756 .expect("initial timer task");
8757 assert_eq!(
8758 initial,
8759 vec![json!({"type": "start_timer", "delay_seconds": 5})]
8760 );
8761
8762 let replayed = worker
8763 .execute_workflow_task(task(vec![
8764 history_event(
8765 "TimerScheduled",
8766 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8767 ),
8768 history_event(
8769 "TimerFired",
8770 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
8771 ),
8772 history_event(
8773 "ActivityCompleted",
8774 json!({
8775 "sequence": 2,
8776 "activity_type": "after-timer",
8777 "payload_codec": "json",
8778 "result": {"codec": "json", "blob": "\"done\""},
8779 }),
8780 ),
8781 ]))
8782 .expect("replayed workflow task");
8783 assert_eq!(replayed.len(), 1);
8784 assert_eq!(replayed[0]["type"], "complete_workflow");
8785 assert_eq!(
8786 decode_wire_value(&replayed[0]["result"], JSON_CODEC).expect("result"),
8787 json!("done")
8788 );
8789 }
8790
8791 #[test]
8792 fn workflow_continue_as_new_emits_arguments_type_and_queue_once() {
8793 let client = Client::new("http://127.0.0.1:8080").expect("client");
8794 let mut worker = Worker::new(client, "rust-workers");
8795 worker.register_workflow("rust.continue", |ctx, _input| async move {
8796 ctx.continue_as_new_with_options(
8797 ContinueAsNewOptions::new()
8798 .workflow_type("rust.next")
8799 .task_queue("next-workers"),
8800 json!([2, {"cursor": "next"}]),
8801 )
8802 });
8803
8804 let commands = worker
8805 .execute_workflow_task(workflow_task("rust.continue", Vec::new(), DEFAULT_CODEC))
8806 .expect("continue-as-new command");
8807
8808 assert_eq!(commands.len(), 1);
8809 assert_eq!(commands[0]["type"], "continue_as_new");
8810 assert_eq!(commands[0]["workflow_type"], "rust.next");
8811 assert_eq!(commands[0]["queue"], "next-workers");
8812 assert_eq!(
8813 decode_wire_value(&commands[0]["arguments"], DEFAULT_CODEC)
8814 .expect("continue-as-new arguments"),
8815 json!([2, {"cursor": "next"}])
8816 );
8817 }
8818
8819 #[test]
8820 fn recorded_continue_as_new_is_consumed_without_duplicate_successor_command() {
8821 let client = Client::new("http://127.0.0.1:8080").expect("client");
8822 let mut worker = Worker::new(client, "rust-workers");
8823 worker.register_workflow("rust.continue", |ctx, _input| async move {
8824 ctx.continue_as_new(json!([2]))
8825 });
8826 let task = workflow_task(
8827 "rust.continue",
8828 vec![history_event(
8829 "WorkflowContinuedAsNew",
8830 json!({"sequence": 1, "continued_to_run_id": "run-next"}),
8831 )],
8832 JSON_CODEC,
8833 );
8834
8835 for _worker_restart_or_redelivery in 0..2 {
8836 let commands = worker
8837 .execute_workflow_task(task.clone())
8838 .expect("recorded transition replays");
8839 assert!(
8840 commands.is_empty(),
8841 "replay must not emit another successor"
8842 );
8843 }
8844 }
8845
8846 #[test]
8847 fn continue_as_new_rejects_invalid_overrides_before_emitting_a_command() {
8848 let ctx = workflow_context(Vec::new());
8849 let error = ctx
8850 .continue_as_new_with_options(ContinueAsNewOptions::new().task_queue(" "), json!([1]))
8851 .expect_err("blank queue must be rejected");
8852
8853 let Error::InvalidContinueAsNewOptions(error) = error else {
8854 panic!("expected typed continue-as-new validation error");
8855 };
8856 assert_eq!(error.field, "task_queue");
8857 assert!(ctx.take_commands().expect("commands").is_empty());
8858 }
8859
8860 #[test]
8861 fn workflow_context_exposes_server_history_budget() {
8862 let client = Client::new("http://127.0.0.1:8080").expect("client");
8863 let mut worker = Worker::new(client, "rust-workers");
8864 worker.register_workflow("rust.history-budget", |ctx, _input| async move {
8865 let budget = ctx.history_budget()?;
8866 Ok(json!({
8867 "events": budget.event_count,
8868 "bytes": budget.size_bytes,
8869 "recommended": budget.continue_as_new_recommended,
8870 "pressure": budget.pressure,
8871 }))
8872 });
8873 let task: WorkflowTask = serde_json::from_value(json!({
8874 "task_id": "task-history-budget",
8875 "workflow_type": "rust.history-budget",
8876 "payload_codec": JSON_CODEC,
8877 "history_events": [],
8878 "total_history_events": 480,
8879 "history_size_bytes": 1_048_576,
8880 "continue_as_new_recommended": true,
8881 "history_budget_pressure": "continue_as_new_recommended",
8882 }))
8883 .expect("published workflow task");
8884
8885 let commands = worker
8886 .execute_workflow_task(task)
8887 .expect("history-budget workflow");
8888 let result = decode_wire_value(&commands[0]["result"], JSON_CODEC).expect("result");
8889 assert_eq!(result["events"], 480);
8890 assert_eq!(result["bytes"], 1_048_576);
8891 assert_eq!(result["recommended"], true);
8892 assert_eq!(result["pressure"], "continue_as_new_recommended");
8893 }
8894
8895 #[test]
8896 fn uncaught_workflow_handler_error_emits_terminal_failure_command() {
8897 let client = Client::new("http://127.0.0.1:8080").expect("client");
8898 let mut worker = Worker::new(client, "rust-workers");
8899 worker.register_workflow("rust.failing", |_ctx, _input| async move {
8900 Err(Error::Codec("rust_conformance_failure".to_string()))
8901 });
8902 let task = WorkflowTask {
8903 task_id: "wft-rust-failing-1".to_string(),
8904 workflow_id: Some("wf-rust-failing".to_string()),
8905 run_id: Some("run-rust-failing".to_string()),
8906 workflow_type: "rust.failing".to_string(),
8907 payload_codec: JSON_CODEC.to_string(),
8908 arguments: Some(encode_value_envelope(&json!([]), JSON_CODEC).expect("input")),
8909 history_events: Vec::new(),
8910 total_history_events: Some(0),
8911 history_size_bytes: None,
8912 continue_as_new_recommended: None,
8913 history_budget_pressure: None,
8914 next_history_page_token: None,
8915 workflow_task_attempt: 1,
8916 workflow_signal_id: None,
8917 signal_name: None,
8918 signal_arguments: None,
8919 lease_owner: Some("rust-worker".to_string()),
8920 };
8921
8922 let commands = worker
8923 .execute_workflow_task(task)
8924 .expect("handler failure becomes a workflow command");
8925
8926 assert_eq!(commands.len(), 1);
8927 assert_eq!(commands[0]["type"], "fail_workflow");
8928 assert_eq!(commands[0]["exception_type"], "RustWorkflowError");
8929 assert_eq!(commands[0]["exception_class"], "durable_workflow::Error");
8930 assert_eq!(commands[0]["non_retryable"], false);
8931 assert_eq!(
8932 commands[0]["message"],
8933 "codec error: rust_conformance_failure"
8934 );
8935 assert_eq!(
8936 commands[0]["exception"]["message"],
8937 "codec error: rust_conformance_failure"
8938 );
8939 }
8940
8941 #[test]
8942 fn ordinary_handler_error_preserves_commands_queued_in_the_same_decision() {
8943 let client = Client::new("http://127.0.0.1:8080").expect("client");
8944 let mut worker = Worker::new(client, "rust-workers");
8945 worker.register_workflow("rust.failing-after-side-effect", |ctx, _input| async move {
8946 let _: String = ctx.side_effect(|| "captured".to_string())?;
8947 Err(Error::WorkerLoop("application failure".to_string()))
8948 });
8949
8950 let commands = worker
8951 .execute_workflow_task(workflow_task(
8952 "rust.failing-after-side-effect",
8953 Vec::new(),
8954 JSON_CODEC,
8955 ))
8956 .expect("ordinary failure remains a workflow decision");
8957
8958 assert_eq!(commands.len(), 2);
8959 assert_eq!(commands[0]["type"], "record_side_effect");
8960 assert_eq!(commands[1]["type"], "fail_workflow");
8961 }
8962
8963 #[test]
8964 fn handler_error_cannot_hide_an_unconsumed_committed_side_effect() {
8965 let client = Client::new("http://127.0.0.1:8080").expect("client");
8966 let mut worker = Worker::new(client, "rust-workers");
8967 worker.register_workflow("rust.removed-side-effect", |_ctx, _input| async move {
8968 Err(Error::WorkerLoop("application failure".to_string()))
8969 });
8970 let result =
8971 encode_value_envelope(&json!("committed"), JSON_CODEC).expect("side-effect result");
8972
8973 let error = worker
8974 .execute_workflow_task(workflow_task(
8975 "rust.removed-side-effect",
8976 vec![history_event(
8977 "SideEffectRecorded",
8978 json!({"sequence": 1, "result": result}),
8979 )],
8980 JSON_CODEC,
8981 ))
8982 .expect_err("removed committed history must not become fail_workflow");
8983
8984 let Error::NonDeterministicReplay(failure) = error else {
8985 panic!("expected typed replay failure");
8986 };
8987 assert_eq!(failure.reason, "recorded_commands_unconsumed");
8988 assert_eq!(failure.sequence, Some(1));
8989 assert_eq!(failure.expected.as_deref(), Some("side effect"));
8990 }
8991
8992 #[test]
8993 fn replay_error_discards_side_effect_queued_before_incompatible_marker_check() {
8994 let client = Client::new("http://127.0.0.1:8080").expect("client");
8995 let mut worker = Worker::new(client, "rust-workers");
8996 worker.register_workflow(
8997 "rust.side-effect-before-marker-error",
8998 |ctx, _input| async move {
8999 assert_eq!(ctx.get_version("restart-safe", 1, 1)?, 1);
9000 let _: String = ctx.side_effect(|| "must-not-commit".to_string())?;
9001 ctx.get_version("restart-safe", 2, 2)?;
9002 Ok(Value::Null)
9003 },
9004 );
9005
9006 let error = worker
9007 .execute_workflow_task(workflow_task(
9008 "rust.side-effect-before-marker-error",
9009 vec![history_event(
9010 "VersionMarkerRecorded",
9011 json!({
9012 "sequence": 1,
9013 "change_id": "restart-safe",
9014 "version": 1,
9015 "min_supported": 1,
9016 "max_supported": 1,
9017 }),
9018 )],
9019 JSON_CODEC,
9020 ))
9021 .expect_err("replay error must return no queued workflow commands");
9022
9023 let Error::NonDeterministicReplay(failure) = error else {
9024 panic!("expected typed replay failure");
9025 };
9026 assert_eq!(failure.reason, "version_marker_incompatible_range");
9027 assert_eq!(failure.sequence, Some(1));
9028 }
9029
9030 #[test]
9031 fn workflow_task_replay_keeps_recorded_unfired_timer_pending_without_rescheduling() {
9032 let client = Client::new("http://127.0.0.1:8080").expect("client");
9033 let mut worker = Worker::new(client, "rust-workers");
9034 worker.register_workflow("rust.timer.pending", |ctx, _input| async move {
9035 ctx.sleep(Duration::from_secs(5)).await?;
9036 Ok(json!({"status": "timer fired"}))
9037 });
9038
9039 let task = WorkflowTask {
9040 task_id: "wft-rust-timer-pending".to_string(),
9041 workflow_id: Some("wf-rust-timer".to_string()),
9042 run_id: Some("run-rust-timer".to_string()),
9043 workflow_type: "rust.timer.pending".to_string(),
9044 payload_codec: JSON_CODEC.to_string(),
9045 arguments: Some(json!({"codec": "json", "blob": "[]"})),
9046 history_events: vec![history_event(
9047 "TimerScheduled",
9048 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
9049 )],
9050 total_history_events: Some(1),
9051 history_size_bytes: None,
9052 continue_as_new_recommended: None,
9053 history_budget_pressure: None,
9054 next_history_page_token: None,
9055 workflow_task_attempt: 1,
9056 workflow_signal_id: None,
9057 signal_name: None,
9058 signal_arguments: None,
9059 lease_owner: Some("rust-worker".to_string()),
9060 };
9061
9062 for _redelivery_or_restart in 0..2 {
9063 let commands = worker
9064 .execute_workflow_task(task.clone())
9065 .expect("recorded timer remains pending");
9066 assert!(
9067 commands.is_empty(),
9068 "recorded timer must not be rescheduled"
9069 );
9070 }
9071 }
9072
9073 #[test]
9074 fn workflow_task_rejects_recorded_command_removed_from_workflow_code() {
9075 let client = Client::new("http://127.0.0.1:8080").expect("client");
9076 let mut worker = Worker::new(client, "rust-workers");
9077 worker.register_workflow("rust.timer.removed", |_ctx, _input| async move {
9078 Ok(json!({"status": "completed"}))
9079 });
9080 let task = WorkflowTask {
9081 task_id: "wft-rust-timer-removed".to_string(),
9082 workflow_id: Some("wf-rust-timer".to_string()),
9083 run_id: Some("run-rust-timer".to_string()),
9084 workflow_type: "rust.timer.removed".to_string(),
9085 payload_codec: JSON_CODEC.to_string(),
9086 arguments: Some(json!({"codec": "json", "blob": "[]"})),
9087 history_events: vec![
9088 history_event(
9089 "TimerScheduled",
9090 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
9091 ),
9092 history_event(
9093 "TimerFired",
9094 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
9095 ),
9096 ],
9097 total_history_events: Some(2),
9098 history_size_bytes: None,
9099 continue_as_new_recommended: None,
9100 history_budget_pressure: None,
9101 next_history_page_token: None,
9102 workflow_task_attempt: 1,
9103 workflow_signal_id: None,
9104 signal_name: None,
9105 signal_arguments: None,
9106 lease_owner: Some("rust-worker".to_string()),
9107 };
9108
9109 let Error::NonDeterministicReplay(failure) = worker
9110 .execute_workflow_task(task)
9111 .expect_err("removed timer must fail replay")
9112 else {
9113 panic!("expected typed replay failure");
9114 };
9115 assert_eq!(failure.reason, "recorded_commands_unconsumed");
9116 assert_eq!(failure.sequence, Some(1));
9117 }
9118
9119 #[test]
9120 fn workflow_context_emits_explicit_child_workflow_contract() {
9121 let ctx = WorkflowContext {
9122 state: Arc::new(Mutex::new(
9123 WorkflowState::new_with_identity(
9124 Vec::new(),
9125 Some("wf-parent".to_string()),
9126 Some("run-parent".to_string()),
9127 "parent-workers".to_string(),
9128 JSON_CODEC.to_string(),
9129 None,
9130 )
9131 .expect("workflow state"),
9132 )),
9133 };
9134 let options = ChildWorkflowOptions::new("python-workers")
9135 .parent_close_policy(ParentClosePolicy::RequestCancel)
9136 .retry_policy(ChildWorkflowRetryPolicy {
9137 max_attempts: Some(3),
9138 backoff_seconds: vec![1, 5],
9139 non_retryable_error_types: vec!["ValidationError".to_string()],
9140 })
9141 .execution_timeout_seconds(600)
9142 .run_timeout_seconds(120);
9143 let mut call = Box::pin(ctx.start_child_workflow(
9144 "python.fulfil-order",
9145 options,
9146 json!([{"order_id": "order-42"}]),
9147 ));
9148 let mut task_context = TaskContext::from_waker(noop_waker_ref());
9149
9150 assert!(matches!(
9151 call.as_mut().poll(&mut task_context),
9152 Poll::Pending
9153 ));
9154 let commands = ctx.take_commands().expect("commands");
9155 assert_eq!(commands.len(), 1);
9156 let command = &commands[0];
9157 assert_eq!(command["type"], "start_child_workflow");
9158 assert_eq!(command["workflow_type"], "python.fulfil-order");
9159 assert_eq!(command["queue"], "python-workers");
9160 assert_eq!(command["parent_close_policy"], "request_cancel");
9161 assert_eq!(command["retry_policy"]["max_attempts"], 3);
9162 assert_eq!(command["execution_timeout_seconds"], 600);
9163 assert_eq!(command["run_timeout_seconds"], 120);
9164 assert_eq!(
9165 decode_wire_value(&command["arguments"], JSON_CODEC).expect("child args"),
9166 json!([{"order_id": "order-42"}])
9167 );
9168 }
9169
9170 fn child_parent_worker() -> Worker {
9171 let client = Client::new("http://127.0.0.1:8080").expect("client");
9172 let mut worker = Worker::new(client, "rust-parent-workers");
9173 worker.register_workflow("rust.parent", |ctx, _input| async move {
9174 let child = ctx
9175 .start_child_workflow(
9176 "python.child",
9177 ChildWorkflowOptions::new("python-child-workers")
9178 .parent_close_policy(ParentClosePolicy::Terminate),
9179 json!([{"codec_probe": [1, true, "rust"]}]),
9180 )
9181 .await?;
9182 Ok(json!({
9183 "parent_workflow_id": child.parent.workflow_id,
9184 "parent_run_id": child.parent.run_id,
9185 "child_workflow_id": child.child.workflow_id,
9186 "child_run_id": child.child.run_id,
9187 "child_workflow_type": child.child_workflow_type,
9188 "result": child.result,
9189 }))
9190 });
9191 worker
9192 }
9193
9194 fn child_parent_task(event_type: &str, payload: Value) -> WorkflowTask {
9195 WorkflowTask {
9196 task_id: "wft-child-parent".to_string(),
9197 workflow_id: Some("wf-parent".to_string()),
9198 run_id: Some("run-parent".to_string()),
9199 workflow_type: "rust.parent".to_string(),
9200 payload_codec: JSON_CODEC.to_string(),
9201 arguments: Some(encode_value_envelope(&json!([]), JSON_CODEC).expect("input")),
9202 history_events: vec![
9203 HistoryEvent {
9204 event_type: "ChildWorkflowScheduled".to_string(),
9205 payload: json!({
9206 "sequence": 1,
9207 "child_call_id": "call-child",
9208 "child_workflow_instance_id": "wf-child",
9209 "child_workflow_run_id": "run-child",
9210 "child_workflow_type": "python.child",
9211 }),
9212 raw: HashMap::new(),
9213 },
9214 HistoryEvent {
9215 event_type: event_type.to_string(),
9216 payload,
9217 raw: HashMap::new(),
9218 },
9219 ],
9220 total_history_events: Some(2),
9221 history_size_bytes: None,
9222 continue_as_new_recommended: None,
9223 history_budget_pressure: None,
9224 next_history_page_token: None,
9225 workflow_task_attempt: 1,
9226 workflow_signal_id: None,
9227 signal_name: None,
9228 signal_arguments: None,
9229 lease_owner: Some("rust-worker".to_string()),
9230 }
9231 }
9232
9233 #[test]
9234 fn committed_child_result_replays_without_starting_a_duplicate() {
9235 let worker = child_parent_worker();
9236 let task = child_parent_task(
9237 "ChildRunCompleted",
9238 json!({
9239 "sequence": 1,
9240 "child_call_id": "call-child",
9241 "child_workflow_instance_id": "wf-child",
9242 "child_workflow_run_id": "run-child",
9243 "child_workflow_type": "python.child",
9244 "payload_codec": "json",
9245 "result": {"codec": "json", "blob": "{\"from\":\"python\",\"ok\":true}"},
9246 }),
9247 );
9248
9249 for _restart in 0..2 {
9250 let commands = worker
9251 .execute_workflow_task(task.clone())
9252 .expect("replayed parent task");
9253 assert_eq!(commands.len(), 1);
9254 assert_eq!(commands[0]["type"], "complete_workflow");
9255 assert!(!commands
9256 .iter()
9257 .any(|command| command["type"] == "start_child_workflow"));
9258 let output =
9259 decode_wire_value(&commands[0]["result"], JSON_CODEC).expect("parent output");
9260 assert_eq!(output["parent_workflow_id"], "wf-parent");
9261 assert_eq!(output["parent_run_id"], "run-parent");
9262 assert_eq!(output["child_workflow_id"], "wf-child");
9263 assert_eq!(output["child_run_id"], "run-child");
9264 assert_eq!(output["result"], json!({"from": "python", "ok": true}));
9265 }
9266 }
9267
9268 #[test]
9269 fn pending_child_replays_after_restart_without_starting_a_duplicate() {
9270 let worker = child_parent_worker();
9271 let mut task = child_parent_task("unused", Value::Null);
9272 task.history_events.truncate(1);
9273 task.total_history_events = Some(1);
9274
9275 for _redelivery_or_restart in 0..2 {
9276 let commands = worker
9277 .execute_workflow_task(task.clone())
9278 .expect("recorded child remains pending");
9279 assert!(
9280 commands.is_empty(),
9281 "recorded pending child must not be started again"
9282 );
9283 }
9284 }
9285
9286 #[test]
9287 fn child_cancellation_becomes_stable_parent_failure_command() {
9288 let worker = child_parent_worker();
9289 let task = child_parent_task(
9290 "ChildRunCancelled",
9291 json!({
9292 "sequence": 1,
9293 "child_workflow_instance_id": "wf-child",
9294 "child_workflow_run_id": "run-child",
9295 "child_workflow_type": "python.child",
9296 "failure_id": "failure-child",
9297 "failure_category": "cancelled",
9298 "message": "cancelled by parent-close policy",
9299 }),
9300 );
9301
9302 let commands = worker
9303 .execute_workflow_task(task)
9304 .expect("parent settlement");
9305 assert_eq!(commands.len(), 1);
9306 assert_eq!(commands[0]["type"], "fail_workflow");
9307 assert_eq!(commands[0]["exception_type"], "ChildWorkflowCancelled");
9308 assert_eq!(
9309 commands[0]["exception"]["properties"]["reason"],
9310 "cancelled"
9311 );
9312 assert_eq!(
9313 commands[0]["exception"]["properties"]["child_workflow_run_id"],
9314 "run-child"
9315 );
9316 }
9317
9318 #[test]
9319 fn workflow_can_handle_typed_child_failure() {
9320 let client = Client::new("http://127.0.0.1:8080").expect("client");
9321 let mut worker = Worker::new(client, "rust-parent-workers");
9322 worker.register_workflow("rust.handled-parent", |ctx, _input| async move {
9323 match ctx
9324 .start_child_workflow(
9325 "python.child",
9326 ChildWorkflowOptions::new("python-child-workers"),
9327 json!([]),
9328 )
9329 .await
9330 {
9331 Err(Error::ChildWorkflowFailed(failure)) => Ok(json!({
9332 "reason": failure.reason,
9333 "failure_id": failure.failure_id,
9334 "exception_class": failure.exception_class,
9335 "child_run_id": failure.child_workflow_run_id,
9336 })),
9337 Err(error) => Err(error),
9338 Ok(_) => Err(Error::WorkerLoop(
9339 "child unexpectedly succeeded".to_string(),
9340 )),
9341 }
9342 });
9343 let mut task = child_parent_task(
9344 "ChildRunFailed",
9345 json!({
9346 "sequence": 1,
9347 "child_workflow_instance_id": "wf-child",
9348 "child_workflow_run_id": "run-child",
9349 "child_workflow_type": "python.child",
9350 "failure_id": "failure-child",
9351 "failure_category": "child_workflow",
9352 "message": "payment rejected",
9353 "exception": {
9354 "type": "PaymentRejected",
9355 "class": "payments.PaymentRejected",
9356 "message": "payment rejected"
9357 }
9358 }),
9359 );
9360 task.workflow_type = "rust.handled-parent".to_string();
9361
9362 let commands = worker.execute_workflow_task(task).expect("handled failure");
9363 assert_eq!(commands[0]["type"], "complete_workflow");
9364 let output = decode_wire_value(&commands[0]["result"], JSON_CODEC).expect("parent output");
9365 assert_eq!(output["reason"], "child_workflow");
9366 assert_eq!(output["failure_id"], "failure-child");
9367 assert_eq!(output["exception_class"], "payments.PaymentRejected");
9368 assert_eq!(output["child_run_id"], "run-child");
9369 }
9370
9371 #[test]
9372 fn rust_hello_world_uses_signal_arguments_from_resume_payload() {
9373 let client = Client::new("http://127.0.0.1:8080").expect("client");
9374 let mut worker = Worker::new(client, "rust-workers");
9375
9376 worker.register_workflow("rust.hello_workflow", |ctx, _input| async move {
9377 let signal = ctx.wait_signal("start").await?;
9378 let name = signal
9379 .first()
9380 .and_then(|value| value.as_str())
9381 .unwrap_or("world");
9382 let greeting = ctx.activity("rust.hello_activity", json!([name])).await?;
9383 Ok(json!({
9384 "greeting": greeting,
9385 "language": "rust"
9386 }))
9387 });
9388
9389 let signal_arguments =
9390 encode_value_envelope(&json!(["Rust"]), DEFAULT_CODEC).expect("signal arguments");
9391 let task = WorkflowTask {
9392 task_id: "wft-rust-signal-1".to_string(),
9393 workflow_id: Some("wf-rust-hello".to_string()),
9394 run_id: Some("run-rust-hello".to_string()),
9395 workflow_type: "rust.hello_workflow".to_string(),
9396 payload_codec: DEFAULT_CODEC.to_string(),
9397 arguments: Some(encode_value_envelope(&json!([]), DEFAULT_CODEC).expect("input")),
9398 history_events: vec![HistoryEvent {
9399 event_type: "SignalReceived".to_string(),
9400 payload: json!({
9401 "signal_id": "sig-rust-1",
9402 "signal_name": "start"
9403 }),
9404 raw: HashMap::new(),
9405 }],
9406 total_history_events: Some(1),
9407 history_size_bytes: None,
9408 continue_as_new_recommended: None,
9409 history_budget_pressure: None,
9410 next_history_page_token: None,
9411 workflow_task_attempt: 1,
9412 workflow_signal_id: Some("sig-rust-1".to_string()),
9413 signal_name: Some("start".to_string()),
9414 signal_arguments: Some(signal_arguments),
9415 lease_owner: Some("rust-worker".to_string()),
9416 };
9417
9418 let commands = worker.execute_workflow_task(task).expect("workflow task");
9419
9420 assert_eq!(commands.len(), 1);
9421 assert_eq!(commands[0]["type"], "schedule_activity");
9422 assert_eq!(commands[0]["activity_type"], "rust.hello_activity");
9423 assert_eq!(
9424 decode_wire_value(&commands[0]["arguments"], DEFAULT_CODEC).expect("activity args"),
9425 json!(["Rust"])
9426 );
9427 }
9428
9429 #[test]
9430 fn workflow_task_appends_paginated_history_events() {
9431 let mut task = WorkflowTask {
9432 task_id: "wft-rust-pages-1".to_string(),
9433 workflow_id: Some("wf-rust-pages".to_string()),
9434 run_id: Some("run-rust-pages".to_string()),
9435 workflow_type: "rust.hello_workflow".to_string(),
9436 payload_codec: DEFAULT_CODEC.to_string(),
9437 arguments: Some(encode_value_envelope(&json!([]), DEFAULT_CODEC).expect("input")),
9438 history_events: vec![HistoryEvent {
9439 event_type: "WorkflowStarted".to_string(),
9440 payload: json!({}),
9441 raw: HashMap::new(),
9442 }],
9443 total_history_events: Some(3),
9444 history_size_bytes: None,
9445 continue_as_new_recommended: None,
9446 history_budget_pressure: None,
9447 next_history_page_token: Some("MQ==".to_string()),
9448 workflow_task_attempt: 1,
9449 workflow_signal_id: None,
9450 signal_name: None,
9451 signal_arguments: None,
9452 lease_owner: Some("rust-worker".to_string()),
9453 };
9454
9455 task.append_history_page(WorkflowTaskHistoryPage {
9456 history_events: vec![
9457 HistoryEvent {
9458 event_type: "SignalReceived".to_string(),
9459 payload: json!({
9460 "signal_id": "sig-rust-1",
9461 "signal_name": "start",
9462 "arguments": encode_value_envelope(&json!(["Rust"]), DEFAULT_CODEC)
9463 .expect("signal arguments")
9464 }),
9465 raw: HashMap::new(),
9466 },
9467 HistoryEvent {
9468 event_type: "MarkerRecorded".to_string(),
9469 payload: json!({"sequence": 3}),
9470 raw: HashMap::new(),
9471 },
9472 ],
9473 total_history_events: Some(3),
9474 next_history_page_token: None,
9475 });
9476
9477 assert_eq!(task.history_events.len(), 3);
9478 assert_eq!(task.total_history_events, Some(3));
9479 assert_eq!(task.next_history_page_token, None);
9480
9481 let signals =
9482 signal_values(&task.history_events, "start", DEFAULT_CODEC, None).expect("signals");
9483 assert_eq!(signals, vec![vec![json!("Rust")]]);
9484 }
9485
9486 #[tokio::test]
9487 async fn query_handler_reads_ordered_cross_codec_signals_without_commands() {
9488 let client = Client::new("http://127.0.0.1:8080").expect("client");
9489 let mut worker = Worker::new(client, "rust-workers");
9490 worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
9491 worker.register_query("counter", "current", |ctx, _args| async move {
9492 let mut count = 0_i64;
9493 for signal in ctx.signal_events() {
9494 let value = signal
9495 .arguments
9496 .first()
9497 .and_then(Value::as_i64)
9498 .unwrap_or_default();
9499 match signal.name.as_str() {
9500 "increment" => count += value,
9501 "set" => count = value,
9502 _ => {}
9503 }
9504 }
9505 Ok(json!(count))
9506 });
9507
9508 let task = QueryTask {
9509 query_task_id: "query-rust-counter".to_string(),
9510 query_task_attempt: 1,
9511 lease_owner: Some("rust-worker".to_string()),
9512 workflow_id: Some("counter-1".to_string()),
9513 run_id: Some("run-counter-1".to_string()),
9514 workflow_type: "counter".to_string(),
9515 query_name: "current".to_string(),
9516 payload_codec: DEFAULT_CODEC.to_string(),
9517 workflow_arguments: Some(
9518 encode_value_envelope(&json!([]), DEFAULT_CODEC).expect("workflow input"),
9519 ),
9520 query_arguments: Some(
9521 encode_value_envelope(&json!([]), DEFAULT_CODEC).expect("query arguments"),
9522 ),
9523 history_events: vec![
9524 HistoryEvent {
9525 event_type: "SignalReceived".to_string(),
9526 payload: json!({
9527 "signal_id": "php-signal-1",
9528 "signal_name": "increment",
9529 "workflow_sequence": 1,
9530 "payload_codec": DEFAULT_CODEC,
9531 "arguments": encode_value_envelope(&json!([3]), DEFAULT_CODEC).expect("php avro signal")
9532 }),
9533 raw: HashMap::new(),
9534 },
9535 HistoryEvent {
9536 event_type: "SignalReceived".to_string(),
9537 payload: json!({
9538 "signal_id": "python-signal-2",
9539 "signal_name": "increment",
9540 "workflow_sequence": 2,
9541 "payload_codec": JSON_CODEC,
9542 "arguments": encode_value_envelope(&json!([5]), JSON_CODEC).expect("python json signal")
9543 }),
9544 raw: HashMap::new(),
9545 },
9546 HistoryEvent {
9547 event_type: "SignalReceived".to_string(),
9548 payload: json!({
9549 "signal_id": "rust-signal-3",
9550 "signal_name": "set",
9551 "workflow_sequence": 3,
9552 "payload_codec": DEFAULT_CODEC,
9553 "arguments": encode_value_envelope(&json!([0]), DEFAULT_CODEC).expect("rust avro signal")
9554 }),
9555 raw: HashMap::new(),
9556 },
9557 ],
9558 history_export: None,
9559 run_status: Some("completed".to_string()),
9560 };
9561
9562 let result = worker.execute_query_task(task).await.expect("query result");
9563 assert_eq!(result, json!(0));
9564 }
9565
9566 #[tokio::test]
9567 async fn replayed_queries_read_running_completed_and_cold_restarted_instance_state() {
9568 let worker = replay_counter_worker();
9569 let running_history = json!([
9570 {
9571 "type": "ActivityCompleted",
9572 "payload": {
9573 "sequence": 1,
9574 "activity_type": "load-counter",
9575 "payload_codec": "json",
9576 "result": {"codec": "json", "blob": "\"loaded\""}
9577 }
9578 },
9579 {
9580 "type": "ConditionWaitOpened",
9581 "payload": {
9582 "sequence": 3,
9583 "condition_key": "signal:increment"
9584 }
9585 },
9586 {
9587 "type": "SignalReceived",
9588 "payload": {
9589 "signal_id": "signal-3",
9590 "signal_name": "increment",
9591 "workflow_sequence": 2,
9592 "payload_codec": "json",
9593 "arguments": {"codec": "json", "blob": "[3]"}
9594 }
9595 }
9596 ]);
9597
9598 let running = worker
9599 .execute_query_task(replay_counter_query(
9600 "current",
9601 running_history.clone(),
9602 "running",
9603 ))
9604 .await
9605 .expect("running replay query");
9606 assert_eq!(
9607 running,
9608 json!({"loaded": "loaded", "count": 3, "finished": false})
9609 );
9610
9611 let detached = worker
9612 .execute_query_task(replay_counter_query(
9613 "detached-mutation",
9614 running_history.clone(),
9615 "running",
9616 ))
9617 .await
9618 .expect("query mutates only its detached state clone");
9619 assert_eq!(detached, json!(999));
9620 let failed = worker
9621 .execute_query_task(replay_counter_query(
9622 "failed-mutation",
9623 running_history.clone(),
9624 "running",
9625 ))
9626 .await
9627 .expect_err("failed query");
9628 assert_eq!(failed.reason, "query_rejected");
9629 let unchanged = worker
9630 .execute_query_task(replay_counter_query("current", running_history, "running"))
9631 .await
9632 .expect("later query reconstructs unchanged state");
9633 assert_eq!(unchanged, running);
9634
9635 let restarted_worker = replay_counter_worker();
9636 let restarted_task: QueryTask = serde_json::from_value(json!({
9637 "query_task_id": "query-after-restart",
9638 "workflow_id": "counter-1",
9639 "run_id": "run-counter-1",
9640 "workflow_type": "replay-counter",
9641 "query_name": "current",
9642 "payload_codec": "json",
9643 "workflow_arguments": {"codec": "json", "blob": "[]"},
9644 "query_arguments": {"codec": "json", "blob": "[]"},
9645 "history_events": [],
9646 "history_export": {
9647 "payloads": {"codec": "json"},
9648 "history_events": [
9649 {
9650 "type": "ActivityCompleted",
9651 "payload": {
9652 "sequence": 1,
9653 "activity_type": "load-counter",
9654 "payload_codec": "json",
9655 "result": null
9656 }
9657 },
9658 {
9659 "type": "ConditionWaitOpened",
9660 "payload": {
9661 "sequence": 3,
9662 "condition_key": "signal:increment"
9663 }
9664 },
9665 {
9666 "type": "SignalReceived",
9667 "payload": {
9668 "signal_id": "signal-3",
9669 "signal_name": "increment",
9670 "workflow_sequence": 2
9671 }
9672 },
9673 {
9674 "type": "ConditionWaitOpened",
9675 "payload": {
9676 "sequence": 5,
9677 "condition_key": "signal:increment"
9678 }
9679 },
9680 {
9681 "type": "SignalReceived",
9682 "payload": {
9683 "signal_id": "signal-5",
9684 "signal_name": "increment",
9685 "workflow_sequence": 4
9686 }
9687 }
9688 ],
9689 "activities": [{
9690 "sequence": 1,
9691 "activity_type": "load-counter",
9692 "payload_codec": "json",
9693 "result": {"codec": "json", "blob": "\"loaded\""}
9694 }],
9695 "signals": [
9696 {
9697 "id": "signal-3",
9698 "name": "increment",
9699 "workflow_sequence": 2,
9700 "payload_codec": "json",
9701 "arguments": "[3]"
9702 },
9703 {
9704 "id": "signal-5",
9705 "name": "increment",
9706 "workflow_sequence": 4,
9707 "payload_codec": "json",
9708 "arguments": "[5]"
9709 }
9710 ]
9711 },
9712 "run_status": "completed"
9713 }))
9714 .expect("cold replay query task");
9715 let completed = restarted_worker
9716 .execute_query_task(restarted_task)
9717 .await
9718 .expect("completed cold replay query");
9719 assert_eq!(
9720 completed,
9721 json!({"loaded": "loaded", "count": 8, "finished": true})
9722 );
9723 }
9724
9725 #[tokio::test]
9726 async fn replayed_query_replay_failures_are_machine_readable() {
9727 let worker = replay_counter_worker();
9728 let task = replay_counter_query(
9729 "current",
9730 json!([{
9731 "type": "ActivityCompleted",
9732 "payload": {
9733 "sequence": 1,
9734 "payload_codec": "json",
9735 "result": {"codec": "json", "blob": "{"}
9736 }
9737 }]),
9738 "running",
9739 );
9740 let failure = worker
9741 .execute_query_task(task)
9742 .await
9743 .expect_err("invalid replay history payload");
9744 assert_eq!(failure.reason, "query_workflow_state_unavailable");
9745 assert_eq!(failure.failure_type, "QueryWorkflowStateUnavailable");
9746 }
9747
9748 #[tokio::test]
9749 async fn query_task_restores_compact_history_from_export() {
9750 let client = Client::new("http://127.0.0.1:8080").expect("client");
9751 let mut worker = Worker::new(client, "rust-workers");
9752 worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
9753 worker.register_query("counter", "current", |ctx, _args| async move {
9754 Ok(json!(ctx.signals("increment")[0][0]))
9755 });
9756 let task: QueryTask = serde_json::from_value(json!({
9757 "query_task_id": "query-export",
9758 "workflow_type": "counter",
9759 "query_name": "current",
9760 "payload_codec": "json",
9761 "workflow_arguments": {"codec": "json", "blob": "[]"},
9762 "query_arguments": {"codec": "json", "blob": "[]"},
9763 "history_events": [],
9764 "history_export": {
9765 "payloads": {"codec": "json"},
9766 "history_events": [{
9767 "type": "SignalReceived",
9768 "payload": {"signal_id": "signal-export", "signal_name": "increment"}
9769 }],
9770 "signals": [{
9771 "id": "signal-export",
9772 "name": "increment",
9773 "status": "applied",
9774 "workflow_sequence": 1,
9775 "payload_codec": "json",
9776 "arguments": "[9]"
9777 }]
9778 }
9779 }))
9780 .expect("query task");
9781
9782 let result = worker.execute_query_task(task).await.expect("query result");
9783 assert_eq!(result, json!(9));
9784 }
9785
9786 #[tokio::test]
9787 async fn query_task_failures_have_stable_reasons() {
9788 let client = Client::new("http://127.0.0.1:8080").expect("client");
9789 let mut worker = Worker::new(client, "rust-workers");
9790 worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
9791 worker.register_query(
9792 "counter",
9793 "current",
9794 |_ctx, _args| async move { Ok(json!(0)) },
9795 );
9796
9797 let base_task = QueryTask {
9798 query_task_id: "query-errors".to_string(),
9799 query_task_attempt: 1,
9800 lease_owner: None,
9801 workflow_id: Some("counter-errors".to_string()),
9802 run_id: Some("run-errors".to_string()),
9803 workflow_type: "counter".to_string(),
9804 query_name: "missing".to_string(),
9805 payload_codec: JSON_CODEC.to_string(),
9806 workflow_arguments: Some(json!({"codec": "json", "blob": "[]"})),
9807 query_arguments: Some(json!({"codec": "json", "blob": "[]"})),
9808 history_events: Vec::new(),
9809 history_export: None,
9810 run_status: Some("running".to_string()),
9811 };
9812
9813 let unknown = worker
9814 .execute_query_task(base_task.clone())
9815 .await
9816 .expect_err("unknown query");
9817 assert_eq!(unknown.reason, "rejected_unknown_query");
9818
9819 let mut malformed = base_task;
9820 malformed.query_name = "current".to_string();
9821 malformed.query_arguments = Some(json!({"codec": "json", "blob": "{"}));
9822 let malformed = worker
9823 .execute_query_task(malformed)
9824 .await
9825 .expect_err("malformed payload");
9826 assert_eq!(malformed.reason, "query_payload_decode_failed");
9827
9828 let client = Client::new("http://127.0.0.1:8080").expect("client");
9829 let mut unavailable_worker = Worker::new(client, "rust-workers");
9830 unavailable_worker
9831 .register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
9832 let unavailable_task: QueryTask = serde_json::from_value(json!({
9833 "query_task_id": "query-unavailable",
9834 "workflow_type": "counter",
9835 "query_name": "current",
9836 "payload_codec": "json",
9837 "workflow_arguments": {"codec": "json", "blob": "[]"},
9838 "query_arguments": {"codec": "json", "blob": "[]"}
9839 }))
9840 .expect("query task");
9841 let unavailable = unavailable_worker
9842 .execute_query_task(unavailable_task)
9843 .await
9844 .expect_err("query handler unavailable");
9845 assert_eq!(unavailable.reason, "query_handler_unavailable");
9846 }
9847
9848 #[tokio::test]
9849 async fn client_query_decodes_result_and_typed_failure() {
9850 let server = MockWorkerServer::start();
9851 let client = Client::builder(server.base_url())
9852 .timeout(Duration::from_secs(2))
9853 .build()
9854 .expect("client");
9855
9856 let result = client
9857 .query_workflow("counter-1", "current", json!([]))
9858 .await
9859 .expect("query result");
9860 assert_eq!(result, json!({"count": 8}));
9861
9862 let error = client
9863 .query_workflow("counter-1", "missing", json!([]))
9864 .await
9865 .expect_err("unknown query");
9866 let Error::QueryFailed(failure) = error else {
9867 panic!("expected typed query failure");
9868 };
9869 assert_eq!(failure.status, 404);
9870 assert_eq!(failure.reason, "rejected_unknown_query");
9871 }
9872
9873 #[tokio::test]
9874 async fn lifecycle_commands_support_instance_and_selected_run_targets() {
9875 let server = MockWorkerServer::start();
9876 let client = Client::builder(server.base_url())
9877 .timeout(Duration::from_secs(2))
9878 .build()
9879 .expect("client");
9880
9881 let options = WorkflowCommandOptions::new()
9882 .reason("cleanup requested")
9883 .request_id("cancel-17");
9884 let cancelled = client
9885 .cancel_workflow("wf-lifecycle", options)
9886 .await
9887 .expect("instance cancellation");
9888 assert_eq!(cancelled.command, WorkflowCommandKind::Cancel);
9889 assert_eq!(cancelled.run_id.as_deref(), Some("run-current"));
9890 assert_eq!(cancelled.outcome.as_deref(), Some("cancelled"));
9891 assert_eq!(
9892 server.request_body("/api/workflows/wf-lifecycle/cancel"),
9893 json!({"reason":"cleanup requested","request_id":"cancel-17"})
9894 );
9895
9896 let terminated = client
9897 .terminate_workflow(
9898 "wf-lifecycle",
9899 WorkflowCommandOptions::new().reason("forced stop"),
9900 )
9901 .await
9902 .expect("instance termination");
9903 assert_eq!(terminated.command, WorkflowCommandKind::Terminate);
9904 assert_eq!(terminated.outcome.as_deref(), Some("terminated"));
9905
9906 client
9907 .cancel_workflow_run(
9908 "wf-lifecycle",
9909 "run-current",
9910 WorkflowCommandOptions::default(),
9911 )
9912 .await
9913 .expect("selected run cancellation");
9914 client
9915 .terminate_workflow_run(
9916 "wf-lifecycle",
9917 "run-current",
9918 WorkflowCommandOptions::default(),
9919 )
9920 .await
9921 .expect("selected run termination");
9922
9923 for (command, error) in [
9924 (
9925 WorkflowCommandKind::Cancel,
9926 client
9927 .cancel_workflow_run(
9928 "wf-lifecycle",
9929 "run-stale",
9930 WorkflowCommandOptions::default(),
9931 )
9932 .await
9933 .expect_err("stale cancellation must be rejected"),
9934 ),
9935 (
9936 WorkflowCommandKind::Terminate,
9937 client
9938 .terminate_workflow_run(
9939 "wf-lifecycle",
9940 "run-stale",
9941 WorkflowCommandOptions::default(),
9942 )
9943 .await
9944 .expect_err("stale termination must be rejected"),
9945 ),
9946 ] {
9947 let Error::WorkflowCommandRejected(rejection) = error else {
9948 panic!("expected typed command rejection");
9949 };
9950 assert_eq!(rejection.command, command);
9951 assert_eq!(rejection.status, 409);
9952 assert_eq!(rejection.reason, "historical_run_command_rejected");
9953 assert_eq!(rejection.run_id.as_deref(), Some("run-stale"));
9954 assert_eq!(rejection.target_scope.as_deref(), Some("run"));
9955 }
9956 }
9957
9958 #[tokio::test]
9959 async fn workflow_start_options_send_server_enforced_deadlines() {
9960 let server = MockWorkerServer::start();
9961 let client = Client::builder(server.base_url())
9962 .timeout(Duration::from_secs(2))
9963 .build()
9964 .expect("client");
9965
9966 let handle = client
9967 .start_workflow_with_options(
9968 "rust.timeout",
9969 "rust-timeouts",
9970 "wf-start-options",
9971 WorkflowStartOptions::new()
9972 .execution_timeout_seconds(30)
9973 .run_timeout_seconds(1),
9974 json!([]),
9975 )
9976 .await
9977 .expect("workflow start");
9978
9979 assert_eq!(handle.run_id.as_deref(), Some("run-start-options"));
9980 let body = server.request_body("/api/workflows");
9981 assert_eq!(body["execution_timeout_seconds"], 30);
9982 assert_eq!(body["run_timeout_seconds"], 1);
9983
9984 let invalid = client
9985 .start_workflow_with_options(
9986 "rust.timeout",
9987 "rust-timeouts",
9988 "wf-invalid-options",
9989 WorkflowStartOptions::new()
9990 .execution_timeout_seconds(1)
9991 .run_timeout_seconds(2),
9992 json!([]),
9993 )
9994 .await
9995 .expect_err("invalid deadline ordering");
9996 assert!(invalid
9997 .to_string()
9998 .contains("run_timeout_seconds cannot exceed execution_timeout_seconds"));
9999 }
10000
10001 #[tokio::test]
10002 async fn workflow_result_returns_each_typed_terminal_outcome() {
10003 let server = MockWorkerServer::start();
10004 let client = Client::builder(server.base_url())
10005 .timeout(Duration::from_secs(2))
10006 .build()
10007 .expect("client");
10008 let options = WorkflowResultOptions {
10009 poll_interval: Duration::ZERO,
10010 timeout: Duration::from_secs(1),
10011 };
10012
10013 let failed = WorkflowHandle {
10014 client: client.clone(),
10015 workflow_id: "wf-failed".to_string(),
10016 run_id: Some("run-failed".to_string()),
10017 workflow_type: "failure".to_string(),
10018 }
10019 .result(options)
10020 .await
10021 .expect_err("failed outcome");
10022 let Error::WorkflowFailed(failure) = failed else {
10023 panic!("expected WorkflowFailed");
10024 };
10025 assert_eq!(failure.workflow_id, "wf-failed");
10026 assert_eq!(failure.run_id.as_deref(), Some("run-failed"));
10027 assert_eq!(failure.failure_id.as_deref(), Some("failure-17"));
10028 assert_eq!(failure.failure_category.as_deref(), Some("application"));
10029 assert_eq!(failure.exception_type.as_deref(), Some("PaymentError"));
10030 assert_eq!(
10031 failure.exception_class.as_deref(),
10032 Some("billing::PaymentError")
10033 );
10034 assert_eq!(failure.non_retryable, Some(true));
10035
10036 for (workflow_id, expected_kind, expected_reason) in [
10037 (
10038 "wf-cancelled",
10039 WorkflowTerminalKind::Cancelled,
10040 "cleanup requested",
10041 ),
10042 (
10043 "wf-terminated",
10044 WorkflowTerminalKind::Terminated,
10045 "forced stop",
10046 ),
10047 (
10048 "wf-timed-out",
10049 WorkflowTerminalKind::TimedOut,
10050 "run_timeout",
10051 ),
10052 ] {
10053 let error = WorkflowHandle {
10054 client: client.clone(),
10055 workflow_id: workflow_id.to_string(),
10056 run_id: None,
10057 workflow_type: "terminal".to_string(),
10058 }
10059 .result(options)
10060 .await
10061 .expect_err("typed terminal outcome");
10062 let outcome = match error {
10063 Error::WorkflowCancelled(outcome) => outcome,
10064 Error::WorkflowTerminated(outcome) => outcome,
10065 Error::WorkflowTimedOut(outcome) => outcome,
10066 other => panic!("unexpected terminal error: {other}"),
10067 };
10068 assert_eq!(outcome.kind, expected_kind);
10069 assert_eq!(outcome.workflow_id, workflow_id);
10070 assert_eq!(outcome.reason, expected_reason);
10071 }
10072
10073 let wait_timeout = WorkflowHandle {
10074 client,
10075 workflow_id: "wf-waiting".to_string(),
10076 run_id: Some("run-waiting".to_string()),
10077 workflow_type: "waiting".to_string(),
10078 }
10079 .result(WorkflowResultOptions {
10080 poll_interval: Duration::ZERO,
10081 timeout: Duration::ZERO,
10082 })
10083 .await
10084 .expect_err("client wait timeout");
10085 let Error::WorkflowTimedOut(timeout) = wait_timeout else {
10086 panic!("expected typed client timeout");
10087 };
10088 assert_eq!(timeout.reason, "result_wait_timeout");
10089 assert_eq!(timeout.failure_category.as_deref(), Some("client_timeout"));
10090 assert_eq!(timeout.run_id.as_deref(), Some("run-waiting"));
10091 }
10092
10093 #[tokio::test]
10094 async fn workflow_result_follows_chain_and_selected_result_preserves_history() {
10095 let server = MockWorkerServer::start();
10096 let client = Client::builder(server.base_url())
10097 .timeout(Duration::from_secs(2))
10098 .build()
10099 .expect("client");
10100
10101 let handle = WorkflowHandle {
10102 client,
10103 workflow_id: "wf-selected".to_string(),
10104 run_id: Some("run-selected".to_string()),
10105 workflow_type: "selected".to_string(),
10106 };
10107 let options = WorkflowResultOptions {
10108 poll_interval: Duration::ZERO,
10109 timeout: Duration::from_secs(1),
10110 };
10111
10112 let current = handle
10113 .result(options)
10114 .await
10115 .expect("instance result follows the current run");
10116 assert_eq!(current, json!("current run output"));
10117
10118 let error = handle
10119 .result_selected_run(options)
10120 .await
10121 .expect_err("the selected run is cancelled even though the current run completed");
10122
10123 let Error::WorkflowCancelled(outcome) = error else {
10124 panic!("expected selected run cancellation");
10125 };
10126 assert_eq!(outcome.run_id.as_deref(), Some("run-selected"));
10127 assert_eq!(outcome.reason, "selected run cancelled");
10128 assert_eq!(
10129 server.request_count("/api/workflows/wf-selected/runs/run-selected"),
10130 1
10131 );
10132 assert_eq!(server.request_count("/api/workflows/wf-selected"), 1);
10133 }
10134
10135 #[tokio::test]
10136 async fn poll_responses_decode_http_conflict_drain_as_a_stable_stop() {
10137 let server = MockWorkerServer::draining_polls();
10138 let client = Client::builder(server.base_url())
10139 .timeout(Duration::from_secs(2))
10140 .build()
10141 .expect("client");
10142
10143 let workflow = client
10144 .poll_workflow_task_response("draining-worker", "rust-workers", Duration::ZERO)
10145 .await
10146 .expect("workflow drain response");
10147 let activity = client
10148 .poll_activity_task_response("draining-worker", "rust-workers", Duration::ZERO)
10149 .await
10150 .expect("activity drain response");
10151 let query = client
10152 .poll_query_task_response("draining-worker", "rust-workers", Duration::ZERO)
10153 .await
10154 .expect("query drain response");
10155
10156 for outcome in [workflow.outcome(), activity.outcome(), query.outcome()] {
10157 assert_eq!(
10158 outcome,
10159 WorkerPollOutcome::Stop {
10160 poll_status: Some("draining".to_string()),
10161 reason: Some("worker_draining".to_string()),
10162 }
10163 );
10164 }
10165
10166 assert!(client
10167 .poll_workflow_task("draining-worker", "rust-workers", Duration::ZERO)
10168 .await
10169 .expect("compatibility poll")
10170 .is_none());
10171 }
10172
10173 #[tokio::test]
10174 async fn managed_worker_honors_drain_stop_for_every_task_family() {
10175 let server = MockWorkerServer::draining_polls();
10176 let client = Client::builder(server.base_url())
10177 .timeout(Duration::from_secs(2))
10178 .build()
10179 .expect("client");
10180
10181 let mut workflow_worker = Worker::new(client.clone(), "rust-workers")
10182 .worker_id("draining-workflow-worker")
10183 .poll_timeout(Duration::ZERO);
10184 workflow_worker.register_workflow("counter", |_ctx, _args| async { Ok(Value::Null) });
10185 workflow_worker
10186 .run()
10187 .await
10188 .expect("workflow drain is a clean stop");
10189
10190 let mut activity_worker = Worker::new(client.clone(), "rust-workers")
10191 .worker_id("draining-activity-worker")
10192 .poll_timeout(Duration::ZERO);
10193 activity_worker.register_activity("write", |_ctx, _args| async { Ok(Value::Null) });
10194 activity_worker
10195 .run()
10196 .await
10197 .expect("activity drain is a clean stop");
10198
10199 let mut query_worker = Worker::new(client, "rust-workers")
10200 .worker_id("draining-query-worker")
10201 .poll_timeout(Duration::ZERO);
10202 query_worker.register_query("counter", "current", |_ctx, _args| async {
10203 Ok(Value::Null)
10204 });
10205 query_worker
10206 .run()
10207 .await
10208 .expect("query drain is a clean stop");
10209 }
10210
10211 #[tokio::test]
10212 async fn activity_cancellation_and_late_completion_remain_machine_readable() {
10213 let server = MockWorkerServer::start();
10214 let client = Client::builder(server.base_url())
10215 .timeout(Duration::from_secs(2))
10216 .build()
10217 .expect("client");
10218
10219 let heartbeat = client
10220 .heartbeat_activity_task(
10221 "activity-cancel",
10222 "attempt-cancel",
10223 "rust-worker",
10224 json!({"stage":"cleanup"}),
10225 )
10226 .await
10227 .expect("cancellation heartbeat");
10228 assert!(heartbeat.cancel_requested);
10229 assert!(heartbeat.should_stop());
10230 assert_eq!(heartbeat.reason.as_deref(), Some("run_cancelled"));
10231 assert_eq!(heartbeat.run_closed_reason.as_deref(), Some("cancelled"));
10232
10233 let error = client
10234 .complete_activity_task(
10235 "activity-cancel",
10236 "attempt-cancel",
10237 "rust-worker",
10238 json!({"late":true}),
10239 JSON_CODEC,
10240 )
10241 .await
10242 .expect_err("late completion must be refused");
10243 assert!(activity_task_rejection_is_final(&error));
10244 let Error::ActivityTaskRejected(rejection) = error else {
10245 panic!("expected typed activity rejection");
10246 };
10247 assert_eq!(rejection.status, 409);
10248 assert_eq!(rejection.reason, "run_cancelled");
10249 assert!(rejection.cancel_requested);
10250 assert_eq!(rejection.can_continue, Some(false));
10251 }
10252
10253 #[tokio::test]
10254 async fn managed_worker_survives_late_completion_and_restart_during_cancellation() {
10255 let server = MockWorkerServer::cancelled_activity();
10256 let client = Client::builder(server.base_url())
10257 .timeout(Duration::from_secs(2))
10258 .build()
10259 .expect("client");
10260 let cancellation_observed = Arc::new(AtomicBool::new(false));
10261 let observed = Arc::clone(&cancellation_observed);
10262 let mut worker = Worker::new(client.clone(), "rust-workers")
10263 .worker_id("rust-cancel-worker")
10264 .poll_timeout(Duration::from_millis(10));
10265 worker.register_activity("cancel-aware", move |ctx, _args| {
10266 let observed = Arc::clone(&observed);
10267 async move {
10268 let heartbeat = ctx.heartbeat(json!({"stage":"running"})).await?;
10269 observed.store(heartbeat.should_stop(), Ordering::SeqCst);
10270 Ok(json!({"late":"completion"}))
10271 }
10272 });
10273
10274 assert_eq!(
10275 worker.run_once().await.expect("cancelled attempt handled"),
10276 1
10277 );
10278 assert!(cancellation_observed.load(Ordering::SeqCst));
10279 assert_eq!(
10280 server.request_count("/api/worker/activity-tasks/activity-cancel/complete"),
10281 1
10282 );
10283
10284 let mut restarted = Worker::new(client, "rust-workers")
10285 .worker_id("rust-cancel-worker-restarted")
10286 .poll_timeout(Duration::from_millis(10));
10287 restarted.register_activity("cancel-aware", |_ctx, _args| async move { Ok(Value::Null) });
10288 assert_eq!(
10289 restarted
10290 .run_once()
10291 .await
10292 .expect("replacement worker continues polling"),
10293 0
10294 );
10295 }
10296
10297 #[tokio::test]
10298 async fn managed_worker_absorbs_selected_run_terminal_timeout_completion_race() {
10299 let response = r#"{"task_id":"workflow-timeout-task","workflow_task_attempt":3,"outcome":"completed","recorded":false,"run_id":"run-selected-timeout","run_status":"failed","created_task_ids":[],"reason":"run_timed_out"}"#;
10300 let server = MockWorkerServer::workflow_completion("409 Conflict", response);
10301 let client = Client::builder(server.base_url())
10302 .timeout(Duration::from_secs(2))
10303 .build()
10304 .expect("client");
10305
10306 let direct_error = client
10307 .complete_workflow_task(
10308 "workflow-timeout-task",
10309 "timeout-worker",
10310 3,
10311 vec![json!({"type": "complete_workflow", "result": null})],
10312 )
10313 .await
10314 .expect_err("the low-level client preserves the completion rejection");
10315 let Error::Http { status, body } = direct_error else {
10316 panic!("expected the original HTTP completion rejection");
10317 };
10318 assert_eq!(status, reqwest::StatusCode::CONFLICT);
10319 assert_eq!(
10320 serde_json::from_str::<Value>(&body).expect("response body")["reason"],
10321 "run_timed_out"
10322 );
10323
10324 let mut worker = Worker::new(client, "rust-workers")
10325 .worker_id("timeout-worker")
10326 .poll_timeout(Duration::from_millis(10));
10327 worker.register_workflow("timeout.workflow", |_ctx, _input| async move {
10328 Ok(json!({"late": "result"}))
10329 });
10330
10331 assert_eq!(
10332 worker
10333 .run_once()
10334 .await
10335 .expect("authoritative selected-run timeout settles the tick"),
10336 1
10337 );
10338 assert_eq!(
10339 server.request_count("/api/worker/workflow-tasks/workflow-timeout-task/complete"),
10340 2,
10341 "both the direct client proof and managed worker must see the rejection"
10342 );
10343 }
10344
10345 #[tokio::test]
10346 async fn managed_worker_does_not_swallow_nearby_completion_errors() {
10347 for (name, status, response) in [
10348 ("bare conflict", "409 Conflict", r#"{"message":"conflict"}"#),
10349 (
10350 "command was recorded",
10351 "409 Conflict",
10352 r#"{"task_id":"workflow-timeout-task","workflow_task_attempt":3,"recorded":true,"run_id":"run-selected-timeout","run_status":"failed","reason":"run_timed_out"}"#,
10353 ),
10354 (
10355 "lease conflict",
10356 "409 Conflict",
10357 r#"{"task_id":"workflow-timeout-task","workflow_task_attempt":3,"recorded":false,"run_id":"run-selected-timeout","run_status":"failed","reason":"lease_expired"}"#,
10358 ),
10359 (
10360 "nonterminal run",
10361 "409 Conflict",
10362 r#"{"task_id":"workflow-timeout-task","workflow_task_attempt":3,"recorded":false,"run_id":"run-selected-timeout","run_status":"waiting","reason":"run_timed_out"}"#,
10363 ),
10364 (
10365 "different selected run",
10366 "409 Conflict",
10367 r#"{"task_id":"workflow-timeout-task","workflow_task_attempt":3,"recorded":false,"run_id":"run-reused-workflow-current","run_status":"failed","reason":"run_timed_out"}"#,
10368 ),
10369 (
10370 "different task attempt",
10371 "409 Conflict",
10372 r#"{"task_id":"workflow-timeout-task","workflow_task_attempt":4,"recorded":false,"run_id":"run-selected-timeout","run_status":"failed","reason":"run_timed_out"}"#,
10373 ),
10374 (
10375 "authentication failure",
10376 "401 Unauthorized",
10377 r#"{"task_id":"workflow-timeout-task","workflow_task_attempt":3,"recorded":false,"run_id":"run-selected-timeout","run_status":"failed","reason":"run_timed_out"}"#,
10378 ),
10379 (
10380 "authorization failure",
10381 "403 Forbidden",
10382 r#"{"task_id":"workflow-timeout-task","workflow_task_attempt":3,"recorded":false,"run_id":"run-selected-timeout","run_status":"failed","reason":"run_timed_out"}"#,
10383 ),
10384 (
10385 "protocol failure",
10386 "400 Bad Request",
10387 r#"{"reason":"unsupported_protocol_version","message":"unsupported worker protocol","supported_version":"1.2","requested_version":"1.3"}"#,
10388 ),
10389 (
10390 "malformed command",
10391 "422 Unprocessable Entity",
10392 r#"{"task_id":"workflow-timeout-task","workflow_task_attempt":3,"recorded":false,"run_id":"run-selected-timeout","run_status":"failed","reason":"run_timed_out"}"#,
10393 ),
10394 (
10395 "transient server failure",
10396 "503 Service Unavailable",
10397 r#"{"task_id":"workflow-timeout-task","workflow_task_attempt":3,"recorded":false,"run_id":"run-selected-timeout","run_status":"failed","reason":"run_timed_out"}"#,
10398 ),
10399 ] {
10400 let server = MockWorkerServer::workflow_completion(status, response);
10401 let client = Client::builder(server.base_url())
10402 .timeout(Duration::from_secs(2))
10403 .build()
10404 .expect("client");
10405 let mut worker = Worker::new(client, "rust-workers")
10406 .worker_id("timeout-worker")
10407 .poll_timeout(Duration::from_millis(10));
10408 worker.register_workflow("timeout.workflow", |_ctx, _input| async move {
10409 Ok(json!({"late": "result"}))
10410 });
10411
10412 let error = worker
10413 .run_once()
10414 .await
10415 .expect_err(&format!("{name} must remain an error"));
10416 assert!(
10417 matches!(error, Error::Http { .. } | Error::Protocol(_)),
10418 "{name} returned an unexpected error variant: {error}"
10419 );
10420 }
10421 }
10422
10423 #[tokio::test]
10424 async fn baseline_worker_endpoints_send_the_baseline_protocol() {
10425 let server = MockWorkerServer::start();
10426 let client = Client::builder(server.base_url())
10427 .timeout(Duration::from_secs(2))
10428 .build()
10429 .expect("client");
10430
10431 client
10432 .register_worker("capture-worker", "capture", vec![], vec![], 1, 1)
10433 .await
10434 .expect("register");
10435 client
10436 .heartbeat_worker("capture-worker", 1, 1)
10437 .await
10438 .expect("heartbeat");
10439 client
10440 .poll_workflow_task("capture-worker", "capture", Duration::from_millis(10))
10441 .await
10442 .expect("workflow poll");
10443 client
10444 .poll_activity_task("capture-worker", "capture", Duration::from_millis(10))
10445 .await
10446 .expect("activity poll");
10447
10448 for path in [
10449 "/api/worker/register",
10450 "/api/worker/heartbeat",
10451 "/api/worker/workflow-tasks/poll",
10452 "/api/worker/activity-tasks/poll",
10453 ] {
10454 assert_eq!(
10455 server.worker_protocol_for(path).as_deref(),
10456 Some(WORKER_PROTOCOL_VERSION),
10457 "unexpected protocol for {path}"
10458 );
10459 }
10460
10461 assert_eq!(
10462 server.request_body("/api/worker/workflow-tasks/poll")["timeout_seconds"],
10463 1
10464 );
10465 assert_eq!(
10466 server.request_body("/api/worker/activity-tasks/poll")["timeout_seconds"],
10467 1
10468 );
10469 }
10470
10471 #[tokio::test]
10472 async fn query_task_endpoints_send_the_query_feature_protocol() {
10473 let server = MockWorkerServer::start();
10474 let client = Client::builder(server.base_url())
10475 .timeout(Duration::from_secs(2))
10476 .build()
10477 .expect("client");
10478
10479 client
10480 .poll_query_task("capture-worker", "capture", Duration::from_millis(10))
10481 .await
10482 .expect("query poll");
10483 client
10484 .complete_query_task("query-capture", "capture-worker", 1, json!(8), JSON_CODEC)
10485 .await
10486 .expect("query complete");
10487 client
10488 .fail_query_task(
10489 "query-capture",
10490 "capture-worker",
10491 1,
10492 "failed",
10493 "query_rejected",
10494 "QueryFailed",
10495 )
10496 .await
10497 .expect("query fail");
10498
10499 for path in [
10500 "/api/worker/query-tasks/poll",
10501 "/api/worker/query-tasks/query-capture/complete",
10502 "/api/worker/query-tasks/query-capture/fail",
10503 ] {
10504 assert_eq!(
10505 server.worker_protocol_for(path).as_deref(),
10506 Some(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION),
10507 "unexpected protocol for {path}"
10508 );
10509 }
10510
10511 assert_eq!(
10512 server.request_body("/api/worker/query-tasks/poll")["timeout_seconds"],
10513 1
10514 );
10515 }
10516
10517 #[tokio::test]
10518 async fn query_protocol_rejection_from_older_server_is_typed() {
10519 let server = MockWorkerServer::reject_query_protocol();
10520 let client = Client::builder(server.base_url())
10521 .timeout(Duration::from_secs(2))
10522 .build()
10523 .expect("client");
10524
10525 let error = client
10526 .poll_query_task("capture-worker", "capture", Duration::from_millis(10))
10527 .await
10528 .expect_err("server below query protocol floor must reject");
10529 let Error::Protocol(failure) = error else {
10530 panic!("expected typed protocol failure");
10531 };
10532
10533 assert_eq!(failure.status, 400);
10534 assert_eq!(failure.reason, "unsupported_protocol_version");
10535 assert_eq!(failure.supported_version.as_deref(), Some("1.7"));
10536 assert_eq!(
10537 failure.requested_version.as_deref(),
10538 Some(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION)
10539 );
10540 assert_eq!(
10541 server
10542 .worker_protocol_for("/api/worker/query-tasks/poll")
10543 .as_deref(),
10544 Some(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION)
10545 );
10546 }
10547
10548 #[tokio::test]
10549 async fn run_once_without_query_handlers_keeps_pre_query_server_compatibility() {
10550 let server = MockWorkerServer::reject_query_protocol();
10551 let client = Client::builder(server.base_url())
10552 .timeout(Duration::from_secs(2))
10553 .build()
10554 .expect("client");
10555 let mut worker = Worker::new(client, "rust-workers")
10556 .worker_id("baseline-worker")
10557 .poll_timeout(Duration::from_millis(10));
10558
10559 worker.register_workflow("baseline.workflow", |_ctx, _input| async move {
10560 Ok(Value::Null)
10561 });
10562
10563 assert_eq!(worker.run_once().await.expect("baseline run once"), 0);
10564 assert_eq!(
10565 server
10566 .worker_protocol_for("/api/worker/workflow-tasks/poll")
10567 .as_deref(),
10568 Some(WORKER_PROTOCOL_VERSION)
10569 );
10570 assert_eq!(
10571 server.worker_protocol_for("/api/worker/query-tasks/poll"),
10572 None,
10573 "a worker without query handlers must not use the query-task endpoint"
10574 );
10575 }
10576
10577 #[tokio::test]
10578 async fn completion_time_query_rejection_is_typed_without_stopping_worker() {
10579 let server = MockWorkerServer::reject_query_completion();
10580 let client = Client::builder(server.base_url())
10581 .timeout(Duration::from_secs(2))
10582 .build()
10583 .expect("client");
10584
10585 let error = client
10586 .complete_query_task("query-late", "late-worker", 1, json!(8), JSON_CODEC)
10587 .await
10588 .expect_err("expired completion must be rejected");
10589 let Error::QueryFailed(failure) = error else {
10590 panic!("expected typed query failure");
10591 };
10592 assert_eq!(failure.status, 409);
10593 assert_eq!(failure.reason, "query_task_timed_out");
10594
10595 let mut worker = Worker::new(client, "rust-workers")
10596 .worker_id("late-worker")
10597 .poll_timeout(Duration::from_millis(10));
10598 worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
10599 worker.register_query(
10600 "counter",
10601 "current",
10602 |_ctx, _args| async move { Ok(json!(8)) },
10603 );
10604
10605 assert_eq!(worker.run_once().await.expect("late task is handled"), 1);
10606 assert_eq!(
10607 worker
10608 .run_once()
10609 .await
10610 .expect("worker continues after late completion"),
10611 0
10612 );
10613 assert_eq!(
10614 server.request_count("/api/worker/query-tasks/query-late/complete"),
10615 2
10616 );
10617 assert_eq!(
10618 server.request_count("/api/worker/query-tasks/query-late/fail"),
10619 0,
10620 "a server completion rejection must not be reported as an encoding failure"
10621 );
10622 }
10623
10624 #[tokio::test]
10625 async fn activity_only_worker_can_shutdown_without_workflow_poller() {
10626 let server = MockWorkerServer::start();
10627 let client = Client::builder(server.base_url())
10628 .timeout(Duration::from_secs(2))
10629 .build()
10630 .expect("client");
10631 let mut worker = Worker::new(client, "rust-workers")
10632 .worker_id("activity-only-worker")
10633 .poll_timeout(Duration::from_millis(10));
10634
10635 worker.register_activity(
10636 "activity.only",
10637 |_ctx, _args| async move { Ok(Value::Null) },
10638 );
10639
10640 worker.run_until(async {}).await.expect("run worker");
10641 }
10642
10643 #[tokio::test]
10644 async fn workflow_only_worker_can_shutdown_without_activity_poller() {
10645 let server = MockWorkerServer::start();
10646 let client = Client::builder(server.base_url())
10647 .timeout(Duration::from_secs(2))
10648 .build()
10649 .expect("client");
10650 let mut worker = Worker::new(client, "rust-workers")
10651 .worker_id("workflow-only-worker")
10652 .poll_timeout(Duration::from_millis(10));
10653
10654 worker.register_workflow(
10655 "workflow.only",
10656 |_ctx, _input| async move { Ok(Value::Null) },
10657 );
10658
10659 worker.run_until(async {}).await.expect("run worker");
10660 }
10661
10662 #[tokio::test]
10663 async fn worker_heartbeat_observer_receives_server_acknowledgements() {
10664 let server = MockWorkerServer::start();
10665 let client = Client::builder(server.base_url())
10666 .timeout(Duration::from_secs(2))
10667 .build()
10668 .expect("client");
10669 let observations = Arc::new(Mutex::new(Vec::new()));
10670 let observed = Arc::clone(&observations);
10671 let mut worker = Worker::new(client, "rust-workers")
10672 .worker_id("observed-heartbeat-worker")
10673 .poll_timeout(Duration::from_millis(10))
10674 .on_worker_heartbeat(move |observation| {
10675 observed
10676 .lock()
10677 .expect("heartbeat observations")
10678 .push(observation.clone());
10679 });
10680
10681 worker.register_workflow("workflow.observed", |_ctx, _input| async move {
10682 Ok(Value::Null)
10683 });
10684 worker
10685 .run_until(tokio::time::sleep(Duration::from_millis(20)))
10686 .await
10687 .expect("run worker");
10688
10689 let observations = observations.lock().expect("heartbeat observations");
10690 let first = observations.first().expect("heartbeat acknowledgement");
10691 assert_eq!(first.worker_id, "observed-heartbeat-worker");
10692 assert_eq!(first.task_queue, "rust-workers");
10693 assert!(first.acknowledged_at_unix_millis > 0);
10694 assert_eq!(first.acknowledgement, json!({}));
10695 }
10696
10697 #[tokio::test]
10698 async fn delayed_worker_heartbeat_keeps_cadence_and_pollers_live() {
10699 let server = MockWorkerServer::delayed_heartbeat_worker();
10700 let client = Client::builder(server.base_url())
10701 .timeout(Duration::from_secs(3))
10702 .build()
10703 .expect("client");
10704 let observations = Arc::new(Mutex::new(Vec::new()));
10705 let observed = Arc::clone(&observations);
10706 let mut worker = Worker::new(client, "rust-snapshot-workers")
10707 .worker_id("rust-snapshot-worker")
10708 .poll_timeout(Duration::from_millis(10))
10709 .on_worker_heartbeat(move |observation| {
10710 observed
10711 .lock()
10712 .expect("heartbeat observations")
10713 .push(observation.clone());
10714 });
10715
10716 worker.register_workflow("snapshot", |ctx, _input| async move {
10717 ctx.wait_signal("finish").await?;
10718 Ok(json!({"status": "finished"}))
10719 });
10720 worker.register_query("snapshot", "current", |ctx, _args| async move {
10721 Ok(json!(ctx
10722 .signals("increment")
10723 .iter()
10724 .filter_map(|arguments| arguments.first().and_then(Value::as_i64))
10725 .sum::<i64>()))
10726 });
10727 worker.register_activity("cancel-aware", |_ctx, _args| async move {
10728 Ok(json!({"late": "completion"}))
10729 });
10730
10731 worker
10732 .run_until(tokio::time::sleep(Duration::from_millis(3_800)))
10733 .await
10734 .expect("delayed heartbeat must allow a clean worker shutdown");
10735
10736 let observations = observations.lock().expect("heartbeat observations");
10737 assert!(
10738 observations.len() >= 3,
10739 "the immediate heartbeat, delayed acknowledgement, and next cadence heartbeat must complete"
10740 );
10741 assert!(
10742 observations.windows(2).all(|pair| {
10743 pair[1].acknowledged_at_unix_millis
10744 .saturating_sub(pair[0].acknowledged_at_unix_millis)
10745 >= 850
10746 }),
10747 "successful acknowledgements must not catch up faster than the advertised one-second cadence: {observations:?}"
10748 );
10749 drop(observations);
10750
10751 let heartbeat_times = server.request_times("/api/worker/heartbeat");
10752 let delayed_request_at = *heartbeat_times
10753 .get(1)
10754 .expect("intentionally delayed heartbeat request");
10755 let delay_window_start = delayed_request_at + Duration::from_millis(100);
10756 let delay_window_end = delayed_request_at + Duration::from_millis(1_400);
10757 for path in [
10758 "/api/worker/workflow-tasks/poll",
10759 "/api/worker/activity-tasks/poll",
10760 "/api/worker/query-tasks/poll",
10761 ] {
10762 assert!(
10763 server
10764 .request_times(path)
10765 .iter()
10766 .any(|received_at| *received_at >= delay_window_start
10767 && *received_at <= delay_window_end),
10768 "{path} must keep polling while a heartbeat acknowledgement is delayed"
10769 );
10770 }
10771 assert!(
10772 server.request_count("/api/worker/workflow-tasks/snapshot-wait-3/fail") >= 1,
10773 "workflow work must be settled"
10774 );
10775 assert!(
10776 server.request_count("/api/worker/activity-tasks/activity-cancel/complete") >= 1,
10777 "activity work must be settled"
10778 );
10779 assert!(
10780 server.request_count("/api/worker/query-tasks/snapshot-current/complete") >= 1,
10781 "query work must be settled"
10782 );
10783 }
10784
10785 #[tokio::test]
10786 async fn retried_worker_heartbeat_restarts_the_advertised_cadence() {
10787 let server = MockWorkerServer::heartbeat_retry_worker();
10788 let client = Client::builder(server.base_url())
10789 .timeout(Duration::from_secs(2))
10790 .build()
10791 .expect("client");
10792 let observations = Arc::new(Mutex::new(Vec::new()));
10793 let observed = Arc::clone(&observations);
10794 let worker = Worker::new(client, "rust-workers")
10795 .worker_id("heartbeat-retry-worker")
10796 .retry_policy(WorkerRetryPolicy {
10797 max_retries: 1,
10798 initial_backoff: Duration::from_millis(300),
10799 max_backoff: Duration::from_millis(300),
10800 })
10801 .on_worker_heartbeat(move |observation| {
10802 observed
10803 .lock()
10804 .expect("heartbeat observations")
10805 .push(observation.clone());
10806 });
10807
10808 worker
10809 .run_until(tokio::time::sleep(Duration::from_millis(2_700)))
10810 .await
10811 .expect("retryable heartbeat failure must remain bounded and recover");
10812
10813 let observations = observations.lock().expect("heartbeat observations");
10814 assert!(observations.len() >= 3, "heartbeat retry must recover");
10815 assert!(
10816 observations.windows(2).all(|pair| {
10817 pair[1]
10818 .acknowledged_at_unix_millis
10819 .saturating_sub(pair[0].acknowledged_at_unix_millis)
10820 >= 850
10821 }),
10822 "a successful retry must start a fresh advertised cadence: {observations:?}"
10823 );
10824 assert_eq!(
10825 server.request_count("/api/worker/heartbeat"),
10826 observations.len() + 1,
10827 "one retryable failure must add exactly one bounded request"
10828 );
10829 }
10830
10831 #[tokio::test]
10832 async fn query_enabled_worker_stays_live_when_signal_replay_emits_no_commands() {
10833 let server = MockWorkerServer::waiting_query_worker();
10834 let client = Client::builder(server.base_url())
10835 .timeout(Duration::from_secs(2))
10836 .build()
10837 .expect("client");
10838 let observations = Arc::new(Mutex::new(Vec::new()));
10839 let observed = Arc::clone(&observations);
10840 let mut worker = Worker::new(client, "rust-snapshot-workers")
10841 .worker_id("rust-snapshot-worker")
10842 .poll_timeout(Duration::from_millis(10))
10843 .on_worker_heartbeat(move |observation| {
10844 observed
10845 .lock()
10846 .expect("heartbeat observations")
10847 .push(observation.clone());
10848 });
10849
10850 worker.register_workflow("snapshot", |ctx, _input| async move {
10851 ctx.wait_signal("finish").await?;
10852 Ok(json!({"status": "finished"}))
10853 });
10854 worker.register_query("snapshot", "current", |ctx, _args| async move {
10855 let current = ctx
10856 .signals("increment")
10857 .iter()
10858 .filter_map(|arguments| arguments.first().and_then(Value::as_i64))
10859 .sum::<i64>();
10860 Ok(json!(current))
10861 });
10862
10863 worker
10864 .run_until(tokio::time::sleep(Duration::from_millis(3_200)))
10865 .await
10866 .expect("pending workflow and query poller must remain live until shutdown");
10867
10868 assert!(
10869 observations.lock().expect("heartbeat observations").len() >= 4,
10870 "the immediate heartbeat and at least three advertised one-second intervals must be acknowledged"
10871 );
10872 assert!(
10873 server.request_count("/api/worker/workflow-tasks/poll") >= 3,
10874 "workflow polling must continue after empty replay acknowledgements"
10875 );
10876 assert!(
10877 server.request_count("/api/worker/query-tasks/poll") >= 2,
10878 "query polling must continue after serving the current query"
10879 );
10880 assert_eq!(
10881 server.request_body("/api/worker/register")["capabilities"],
10882 json!([QUERY_TASKS_CAPABILITY])
10883 );
10884
10885 for task_id in ["snapshot-wait-3", "snapshot-wait-5"] {
10886 let fail_path = format!("/api/worker/workflow-tasks/{task_id}/fail");
10887 let completion_path = format!("/api/worker/workflow-tasks/{task_id}/complete");
10888 let failure = server.request_body(&fail_path);
10889 assert_eq!(
10890 failure["failure"]["type"],
10891 WORKFLOW_TASK_WAITING_FOR_HISTORY_TYPE
10892 );
10893 assert_eq!(server.request_count(&completion_path), 0);
10894 }
10895
10896 let query_completion =
10897 server.request_body("/api/worker/query-tasks/snapshot-current/complete");
10898 assert_eq!(query_completion["result"], json!(8));
10899 }
10900
10901 #[tokio::test]
10902 async fn worker_retries_poll_and_heartbeat_transport_failures_independently() {
10903 let server = MockWorkerServer::transient_worker_failures();
10904 let client = Client::builder(server.base_url())
10905 .timeout(Duration::from_secs(2))
10906 .build()
10907 .expect("client");
10908 let mut worker = Worker::new(client, "rust-workers")
10909 .worker_id("retry-worker")
10910 .poll_timeout(Duration::from_millis(10))
10911 .retry_policy(WorkerRetryPolicy {
10912 max_retries: 2,
10913 initial_backoff: Duration::from_millis(1),
10914 max_backoff: Duration::from_millis(1),
10915 });
10916 worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
10917 worker.register_activity(
10918 "counter.activity",
10919 |_ctx, _input| async move { Ok(Value::Null) },
10920 );
10921 worker.register_query(
10922 "counter",
10923 "current",
10924 |_ctx, _args| async move { Ok(json!(8)) },
10925 );
10926
10927 worker
10928 .run_until(tokio::time::sleep(Duration::from_millis(75)))
10929 .await
10930 .expect("transient failures must not stop the worker");
10931
10932 for path in [
10933 "/api/worker/heartbeat",
10934 "/api/worker/workflow-tasks/poll",
10935 "/api/worker/activity-tasks/poll",
10936 "/api/worker/query-tasks/poll",
10937 ] {
10938 assert!(
10939 server.request_count(path) >= 2,
10940 "{path} must continue after its transient failure"
10941 );
10942 }
10943 }
10944
10945 #[tokio::test]
10946 async fn worker_bounds_transport_retries() {
10947 let server = MockWorkerServer::unavailable_polls();
10948 let client = Client::builder(server.base_url())
10949 .timeout(Duration::from_secs(2))
10950 .build()
10951 .expect("client");
10952 let mut worker = Worker::new(client, "rust-workers")
10953 .worker_id("bounded-retry-worker")
10954 .poll_timeout(Duration::from_millis(10))
10955 .retry_policy(WorkerRetryPolicy {
10956 max_retries: 2,
10957 initial_backoff: Duration::from_millis(1),
10958 max_backoff: Duration::from_millis(1),
10959 });
10960 worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
10961
10962 let error = worker.run().await.expect_err("retry bound must terminate");
10963 assert!(matches!(error, Error::Transport(_)));
10964 assert_eq!(
10965 server.request_count("/api/worker/workflow-tasks/poll"),
10966 3,
10967 "one initial request plus two retries"
10968 );
10969 }
10970
10971 #[tokio::test]
10972 async fn worker_does_not_retry_authentication_failures() {
10973 let server = MockWorkerServer::unauthorized_polls();
10974 let client = Client::builder(server.base_url())
10975 .timeout(Duration::from_secs(2))
10976 .build()
10977 .expect("client");
10978 let mut worker = Worker::new(client, "rust-workers")
10979 .worker_id("unauthorized-worker")
10980 .poll_timeout(Duration::from_millis(10));
10981 worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
10982
10983 let error = worker
10984 .run()
10985 .await
10986 .expect_err("authentication must terminate");
10987 let Error::Http { status, body } = error else {
10988 panic!("expected stable HTTP authentication error");
10989 };
10990 assert_eq!(status, reqwest::StatusCode::UNAUTHORIZED);
10991 assert!(body.contains("authentication_failed"));
10992 assert_eq!(
10993 server.request_count("/api/worker/workflow-tasks/poll"),
10994 1,
10995 "authentication failures must not be retried"
10996 );
10997 }
10998
10999 #[derive(Clone, Debug)]
11000 struct CapturedRequest {
11001 path: String,
11002 worker_protocol: Option<String>,
11003 body: String,
11004 received_at: Instant,
11005 }
11006
11007 struct MockWorkerServer {
11008 addr: SocketAddr,
11009 stop: Arc<AtomicBool>,
11010 requests: Arc<Mutex<Vec<CapturedRequest>>>,
11011 thread: Option<thread::JoinHandle<()>>,
11012 }
11013
11014 #[derive(Clone, Copy, Default)]
11015 struct MockWorkerBehavior {
11016 reject_query_protocol: bool,
11017 reject_query_completion: bool,
11018 waiting_query_worker: bool,
11019 poll_failures_per_path: usize,
11020 heartbeat_failures: usize,
11021 heartbeat_failure_request: Option<usize>,
11022 delayed_heartbeat_request: Option<usize>,
11023 heartbeat_response_delay: Duration,
11024 concurrent_requests: bool,
11025 unauthorized_polls: bool,
11026 cancelled_activity: bool,
11027 draining_polls: bool,
11028 workflow_completion_status: Option<&'static str>,
11029 workflow_completion_body: Option<&'static str>,
11030 }
11031
11032 impl MockWorkerServer {
11033 fn start() -> Self {
11034 Self::start_with_behavior(MockWorkerBehavior::default())
11035 }
11036
11037 fn reject_query_protocol() -> Self {
11038 Self::start_with_behavior(MockWorkerBehavior {
11039 reject_query_protocol: true,
11040 ..MockWorkerBehavior::default()
11041 })
11042 }
11043
11044 fn reject_query_completion() -> Self {
11045 Self::start_with_behavior(MockWorkerBehavior {
11046 reject_query_completion: true,
11047 ..MockWorkerBehavior::default()
11048 })
11049 }
11050
11051 fn waiting_query_worker() -> Self {
11052 Self::start_with_behavior(MockWorkerBehavior {
11053 waiting_query_worker: true,
11054 ..MockWorkerBehavior::default()
11055 })
11056 }
11057
11058 fn transient_worker_failures() -> Self {
11059 Self::start_with_behavior(MockWorkerBehavior {
11060 poll_failures_per_path: 1,
11061 heartbeat_failures: 1,
11062 ..MockWorkerBehavior::default()
11063 })
11064 }
11065
11066 fn delayed_heartbeat_worker() -> Self {
11067 Self::start_with_behavior(MockWorkerBehavior {
11068 waiting_query_worker: true,
11069 delayed_heartbeat_request: Some(2),
11070 heartbeat_response_delay: Duration::from_millis(1_500),
11071 concurrent_requests: true,
11072 cancelled_activity: true,
11073 ..MockWorkerBehavior::default()
11074 })
11075 }
11076
11077 fn heartbeat_retry_worker() -> Self {
11078 Self::start_with_behavior(MockWorkerBehavior {
11079 waiting_query_worker: true,
11080 heartbeat_failure_request: Some(2),
11081 concurrent_requests: true,
11082 ..MockWorkerBehavior::default()
11083 })
11084 }
11085
11086 fn unavailable_polls() -> Self {
11087 Self::start_with_behavior(MockWorkerBehavior {
11088 poll_failures_per_path: usize::MAX,
11089 ..MockWorkerBehavior::default()
11090 })
11091 }
11092
11093 fn unauthorized_polls() -> Self {
11094 Self::start_with_behavior(MockWorkerBehavior {
11095 unauthorized_polls: true,
11096 ..MockWorkerBehavior::default()
11097 })
11098 }
11099
11100 fn cancelled_activity() -> Self {
11101 Self::start_with_behavior(MockWorkerBehavior {
11102 cancelled_activity: true,
11103 ..MockWorkerBehavior::default()
11104 })
11105 }
11106
11107 fn draining_polls() -> Self {
11108 Self::start_with_behavior(MockWorkerBehavior {
11109 draining_polls: true,
11110 ..MockWorkerBehavior::default()
11111 })
11112 }
11113
11114 fn workflow_completion(status: &'static str, body: &'static str) -> Self {
11115 Self::start_with_behavior(MockWorkerBehavior {
11116 workflow_completion_status: Some(status),
11117 workflow_completion_body: Some(body),
11118 ..MockWorkerBehavior::default()
11119 })
11120 }
11121
11122 fn start_with_behavior(behavior: MockWorkerBehavior) -> Self {
11123 let listener = TcpListener::bind("127.0.0.1:0").expect("bind mock server");
11124 listener
11125 .set_nonblocking(true)
11126 .expect("configure mock listener");
11127 let addr = listener.local_addr().expect("mock server address");
11128 let stop = Arc::new(AtomicBool::new(false));
11129 let server_stop = Arc::clone(&stop);
11130 let requests = Arc::new(Mutex::new(Vec::new()));
11131 let server_requests = Arc::clone(&requests);
11132 let thread = thread::spawn(move || {
11133 let mut request_threads = Vec::new();
11134 while !server_stop.load(Ordering::SeqCst) {
11135 match listener.accept() {
11136 Ok((mut stream, _)) => {
11137 if behavior.concurrent_requests {
11138 let requests = Arc::clone(&server_requests);
11139 request_threads.push(thread::spawn(move || {
11140 handle_mock_worker_request(&mut stream, &requests, behavior)
11141 }));
11142 } else {
11143 handle_mock_worker_request(&mut stream, &server_requests, behavior);
11144 }
11145 }
11146 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
11147 let mut index = 0;
11148 while index < request_threads.len() {
11149 if request_threads[index].is_finished() {
11150 request_threads
11151 .swap_remove(index)
11152 .join()
11153 .expect("join mock request");
11154 } else {
11155 index += 1;
11156 }
11157 }
11158 thread::sleep(Duration::from_millis(5));
11159 }
11160 Err(_) => break,
11161 }
11162 }
11163 for request_thread in request_threads {
11164 request_thread.join().expect("join mock request");
11165 }
11166 });
11167
11168 Self {
11169 addr,
11170 stop,
11171 requests,
11172 thread: Some(thread),
11173 }
11174 }
11175
11176 fn base_url(&self) -> String {
11177 format!("http://{}", self.addr)
11178 }
11179
11180 fn worker_protocol_for(&self, path: &str) -> Option<String> {
11181 self.requests
11182 .lock()
11183 .expect("captured requests")
11184 .iter()
11185 .find(|request| request.path == path)
11186 .and_then(|request| request.worker_protocol.clone())
11187 }
11188
11189 fn request_count(&self, path: &str) -> usize {
11190 self.requests
11191 .lock()
11192 .expect("captured requests")
11193 .iter()
11194 .filter(|request| request.path == path)
11195 .count()
11196 }
11197
11198 fn request_times(&self, path: &str) -> Vec<Instant> {
11199 self.requests
11200 .lock()
11201 .expect("captured requests")
11202 .iter()
11203 .filter(|request| request.path == path)
11204 .map(|request| request.received_at)
11205 .collect()
11206 }
11207
11208 fn request_body(&self, path: &str) -> Value {
11209 let requests = self.requests.lock().expect("captured requests");
11210 let body = &requests
11211 .iter()
11212 .find(|request| request.path == path)
11213 .unwrap_or_else(|| panic!("missing request for {path}"))
11214 .body;
11215 serde_json::from_str(body).unwrap_or_else(|error| {
11216 panic!("invalid JSON request body for {path}: {error}: {body:?}")
11217 })
11218 }
11219 }
11220
11221 impl Drop for MockWorkerServer {
11222 fn drop(&mut self) {
11223 self.stop.store(true, Ordering::SeqCst);
11224 let _ = TcpStream::connect(self.addr);
11225
11226 if let Some(thread) = self.thread.take() {
11227 thread.join().expect("join mock server");
11228 }
11229 }
11230 }
11231
11232 fn handle_mock_worker_request(
11233 stream: &mut TcpStream,
11234 requests: &Arc<Mutex<Vec<CapturedRequest>>>,
11235 behavior: MockWorkerBehavior,
11236 ) {
11237 let _ = stream.set_read_timeout(Some(Duration::from_millis(200)));
11238 let mut buffer = [0_u8; 8192];
11239 let mut request = Vec::new();
11240
11241 loop {
11242 match stream.read(&mut buffer) {
11243 Ok(0) => break,
11244 Ok(read) => {
11245 request.extend_from_slice(&buffer[..read]);
11246 if mock_request_is_complete(&request) {
11247 break;
11248 }
11249 }
11250 Err(error)
11251 if matches!(
11252 error.kind(),
11253 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
11254 ) =>
11255 {
11256 break;
11257 }
11258 Err(_) => return,
11259 }
11260 }
11261
11262 let request = String::from_utf8_lossy(&request);
11263 let body = request
11264 .split_once("\r\n\r\n")
11265 .map(|(_, body)| body)
11266 .unwrap_or_default();
11267 let path = request
11268 .lines()
11269 .next()
11270 .and_then(|line| line.split_whitespace().nth(1))
11271 .unwrap_or_default();
11272 let worker_protocol = request.lines().find_map(|line| {
11273 let (name, value) = line.split_once(':')?;
11274 name.eq_ignore_ascii_case("X-Durable-Workflow-Protocol-Version")
11275 .then(|| value.trim().to_string())
11276 });
11277 let request_number = {
11278 let mut requests = requests.lock().expect("captured requests");
11279 requests.push(CapturedRequest {
11280 path: path.to_string(),
11281 worker_protocol: worker_protocol.clone(),
11282 body: body.to_string(),
11283 received_at: Instant::now(),
11284 });
11285 requests
11286 .iter()
11287 .filter(|request| request.path == path)
11288 .count()
11289 };
11290
11291 let is_poll = matches!(
11292 path,
11293 "/api/worker/workflow-tasks/poll"
11294 | "/api/worker/activity-tasks/poll"
11295 | "/api/worker/query-tasks/poll"
11296 );
11297 if is_poll && request_number <= behavior.poll_failures_per_path {
11298 return;
11299 }
11300 if path == "/api/worker/heartbeat" && request_number <= behavior.heartbeat_failures {
11301 return;
11302 }
11303 if path == "/api/worker/heartbeat"
11304 && behavior.heartbeat_failure_request == Some(request_number)
11305 {
11306 return;
11307 }
11308 if path == "/api/worker/heartbeat"
11309 && behavior.delayed_heartbeat_request == Some(request_number)
11310 {
11311 thread::sleep(behavior.heartbeat_response_delay);
11312 }
11313 if behavior.unauthorized_polls && is_poll {
11314 write_mock_response(
11315 stream,
11316 "401 Unauthorized",
11317 r#"{"reason":"authentication_failed","message":"invalid worker token"}"#,
11318 );
11319 return;
11320 }
11321 if behavior.draining_polls && is_poll {
11322 write_mock_response(
11323 stream,
11324 "409 Conflict",
11325 r#"{"task":null,"poll_status":"draining","reason":"worker_draining","worker_status":"draining","drain_intent":"draining"}"#,
11326 );
11327 return;
11328 }
11329
11330 if behavior.reject_query_protocol && path.starts_with("/api/worker/query-tasks/") {
11331 let requested_version = worker_protocol.as_deref().unwrap_or("missing");
11332 let body = format!(
11333 r#"{{"reason":"unsupported_protocol_version","message":"Query tasks require worker protocol 1.8 or newer.","supported_version":"1.7","requested_version":"{requested_version}"}}"#
11334 );
11335 write_mock_response(stream, "400 Bad Request", &body);
11336 return;
11337 }
11338
11339 if behavior.reject_query_completion && path == "/api/worker/query-tasks/query-late/complete"
11340 {
11341 write_mock_response(
11342 stream,
11343 "409 Conflict",
11344 r#"{"reason":"query_task_timed_out","message":"query task timed out before completion"}"#,
11345 );
11346 return;
11347 }
11348
11349 if behavior.workflow_completion_status.is_some()
11350 && path == "/api/worker/workflow-tasks/poll"
11351 && request_number == 1
11352 {
11353 write_mock_response(
11354 stream,
11355 "200 OK",
11356 r#"{"task":{"task_id":"workflow-timeout-task","workflow_id":"reused-workflow-id","run_id":"run-selected-timeout","workflow_type":"timeout.workflow","payload_codec":"json","arguments":{"codec":"json","blob":"[]"},"history_events":[],"workflow_task_attempt":3,"lease_owner":"timeout-worker"}}"#,
11357 );
11358 return;
11359 }
11360
11361 if path == "/api/worker/workflow-tasks/workflow-timeout-task/complete" {
11362 if let (Some(status), Some(body)) = (
11363 behavior.workflow_completion_status,
11364 behavior.workflow_completion_body,
11365 ) {
11366 write_mock_response(stream, status, body);
11367 return;
11368 }
11369 }
11370
11371 if behavior.waiting_query_worker {
11372 if path == "/api/worker/workflow-tasks/poll" && request_number <= 2 {
11373 let amounts = if request_number == 1 {
11374 vec![3]
11375 } else {
11376 vec![3, 5]
11377 };
11378 let task_id = if request_number == 1 {
11379 "snapshot-wait-3"
11380 } else {
11381 "snapshot-wait-5"
11382 };
11383 let history_events = std::iter::once(json!({
11384 "event_type": "SignalWaitOpened",
11385 "payload": {"sequence": 1, "signal_name": "finish"}
11386 }))
11387 .chain(amounts.iter().enumerate().map(|(index, amount)| {
11388 json!({
11389 "event_type": "SignalReceived",
11390 "payload": {
11391 "signal_id": format!("increment-{amount}"),
11392 "signal_name": "increment",
11393 "workflow_sequence": index + 2,
11394 "payload_codec": DEFAULT_CODEC,
11395 "arguments": encode_value_envelope(&json!([amount]), DEFAULT_CODEC)
11396 .expect("Avro signal envelope")
11397 }
11398 })
11399 }))
11400 .collect::<Vec<_>>();
11401 let body = json!({
11402 "task": {
11403 "task_id": task_id,
11404 "workflow_id": "snapshot-1",
11405 "run_id": "snapshot-run-1",
11406 "workflow_type": "snapshot",
11407 "payload_codec": DEFAULT_CODEC,
11408 "arguments": encode_value_envelope(&json!([]), DEFAULT_CODEC)
11409 .expect("Avro workflow arguments"),
11410 "history_events": history_events,
11411 "workflow_task_attempt": 1,
11412 "workflow_signal_id": format!("increment-{}", amounts.last().expect("amount")),
11413 "signal_name": "increment",
11414 "signal_arguments": encode_value_envelope(
11415 &json!([amounts.last().expect("amount")]),
11416 DEFAULT_CODEC,
11417 )
11418 .expect("Avro resume signal"),
11419 "lease_owner": "rust-snapshot-worker"
11420 }
11421 })
11422 .to_string();
11423 write_mock_response(stream, "200 OK", &body);
11424 return;
11425 }
11426
11427 if path == "/api/worker/query-tasks/poll" && request_number == 1 {
11428 let history_events = [3, 5]
11429 .into_iter()
11430 .enumerate()
11431 .map(|(index, amount)| {
11432 json!({
11433 "event_type": "SignalReceived",
11434 "payload": {
11435 "signal_id": format!("increment-{amount}"),
11436 "signal_name": "increment",
11437 "workflow_sequence": index + 2,
11438 "payload_codec": DEFAULT_CODEC,
11439 "arguments": encode_value_envelope(&json!([amount]), DEFAULT_CODEC)
11440 .expect("Avro query signal envelope")
11441 }
11442 })
11443 })
11444 .collect::<Vec<_>>();
11445 let body = json!({
11446 "task": {
11447 "query_task_id": "snapshot-current",
11448 "query_task_attempt": 1,
11449 "lease_owner": "rust-snapshot-worker",
11450 "workflow_id": "snapshot-1",
11451 "run_id": "snapshot-run-1",
11452 "workflow_type": "snapshot",
11453 "query_name": "current",
11454 "payload_codec": DEFAULT_CODEC,
11455 "workflow_arguments": encode_value_envelope(&json!([]), DEFAULT_CODEC)
11456 .expect("Avro workflow arguments"),
11457 "query_arguments": encode_value_envelope(&json!([]), DEFAULT_CODEC)
11458 .expect("Avro query arguments"),
11459 "history_events": history_events,
11460 "run_status": "waiting"
11461 }
11462 })
11463 .to_string();
11464 write_mock_response(stream, "200 OK", &body);
11465 return;
11466 }
11467
11468 if path == "/api/worker/workflow-tasks/snapshot-wait-3/fail"
11469 || path == "/api/worker/workflow-tasks/snapshot-wait-5/fail"
11470 {
11471 write_mock_response(
11472 stream,
11473 "200 OK",
11474 r#"{"outcome":"waiting_for_history","recorded":true}"#,
11475 );
11476 return;
11477 }
11478
11479 if path == "/api/worker/query-tasks/snapshot-current/complete" {
11480 write_mock_response(stream, "200 OK", r#"{"outcome":"completed"}"#);
11481 return;
11482 }
11483 }
11484
11485 let (status, body) = match path {
11486 "/api/workflows" => (
11487 "201 Created",
11488 r#"{"workflow_id":"wf-start-options","run_id":"run-start-options","workflow_type":"rust.timeout"}"#,
11489 ),
11490 "/api/worker/register" if behavior.waiting_query_worker => (
11491 "200 OK",
11492 r#"{"worker_id":"rust-snapshot-worker","registered":true,"heartbeat_interval_seconds":1}"#,
11493 ),
11494 "/api/worker/register" => (
11495 "200 OK",
11496 r#"{"worker_id":"mock-worker","registered":true,"heartbeat_interval_seconds":3600}"#,
11497 ),
11498 "/api/worker/heartbeat" => ("200 OK", "{}"),
11499 "/api/worker/activity-tasks/poll"
11500 if behavior.cancelled_activity && request_number == 1 =>
11501 {
11502 (
11503 "200 OK",
11504 r#"{"task":{"task_id":"activity-cancel","activity_attempt_id":"attempt-cancel","activity_type":"cancel-aware","payload_codec":"json","arguments":{"codec":"json","blob":"[]"},"attempt_number":1,"lease_owner":"rust-cancel-worker"}}"#,
11505 )
11506 }
11507 "/api/worker/activity-tasks/poll" | "/api/worker/workflow-tasks/poll" => {
11508 ("200 OK", r#"{"task":null}"#)
11509 }
11510 "/api/worker/query-tasks/poll"
11511 if behavior.reject_query_completion && request_number == 1 =>
11512 {
11513 (
11514 "200 OK",
11515 r#"{"task":{"query_task_id":"query-late","query_task_attempt":1,"lease_owner":"late-worker","workflow_id":"counter-late","run_id":"run-late","workflow_type":"counter","query_name":"current","payload_codec":"json","workflow_arguments":{"codec":"json","blob":"[]"},"query_arguments":{"codec":"json","blob":"[]"},"history_events":[],"run_status":"running"}}"#,
11516 )
11517 }
11518 "/api/worker/query-tasks/poll" => ("200 OK", r#"{"task":null}"#),
11519 "/api/worker/query-tasks/query-capture/complete"
11520 | "/api/worker/query-tasks/query-capture/fail" => ("200 OK", "{}"),
11521 "/api/worker/activity-tasks/activity-cancel/heartbeat" => (
11522 "200 OK",
11523 r#"{"activity_attempt_id":"attempt-cancel","cancel_requested":true,"can_continue":false,"reason":"run_cancelled","run_closed_reason":"cancelled","heartbeat_recorded":false}"#,
11524 ),
11525 "/api/worker/activity-tasks/activity-cancel/complete" => (
11526 "409 Conflict",
11527 r#"{"task_id":"activity-cancel","activity_attempt_id":"attempt-cancel","reason":"run_cancelled","cancel_requested":true,"can_continue":false,"run_closed_reason":"cancelled"}"#,
11528 ),
11529 "/api/workflows/counter-1/query/current" => (
11530 "200 OK",
11531 r#"{"workflow_id":"counter-1","query_name":"current","result":{"count":8},"result_envelope":{"codec":"json","blob":"{\"count\":8}"}}"#,
11532 ),
11533 "/api/workflows/counter-1/query/missing" => (
11534 "404 Not Found",
11535 r#"{"workflow_id":"counter-1","query_name":"missing","reason":"rejected_unknown_query","message":"unknown query"}"#,
11536 ),
11537 "/api/workflows/wf-lifecycle/cancel" => (
11538 "200 OK",
11539 r#"{"workflow_id":"wf-lifecycle","run_id":"run-current","outcome":"cancelled","reason":"cleanup requested","command_status":"accepted"}"#,
11540 ),
11541 "/api/workflows/wf-lifecycle/terminate" => (
11542 "200 OK",
11543 r#"{"workflow_id":"wf-lifecycle","run_id":"run-current","outcome":"terminated","reason":"forced stop","command_status":"accepted"}"#,
11544 ),
11545 "/api/workflows/wf-lifecycle/runs/run-current/cancel" => (
11546 "200 OK",
11547 r#"{"workflow_id":"wf-lifecycle","run_id":"run-current","outcome":"cancelled","command_status":"accepted"}"#,
11548 ),
11549 "/api/workflows/wf-lifecycle/runs/run-current/terminate" => (
11550 "200 OK",
11551 r#"{"workflow_id":"wf-lifecycle","run_id":"run-current","outcome":"terminated","command_status":"accepted"}"#,
11552 ),
11553 "/api/workflows/wf-lifecycle/runs/run-stale/cancel"
11554 | "/api/workflows/wf-lifecycle/runs/run-stale/terminate" => (
11555 "409 Conflict",
11556 r#"{"workflow_id":"wf-lifecycle","run_id":"run-stale","reason":"historical_run_command_rejected","target_scope":"run","message":"Commands cannot target historical runs."}"#,
11557 ),
11558 "/api/workflows/wf-failed" | "/api/workflows/wf-failed/runs/run-failed" => (
11559 "200 OK",
11560 r#"{"workflow_id":"wf-failed","run_id":"run-failed","status":"failed","closed_reason":"failed","error":"payment failed","failure":{"message":"payment failed","failure_category":"application","exception_type":"PaymentError","exception_class":"billing::PaymentError","non_retryable":true,"exception":{"type":"PaymentError","class":"billing::PaymentError","message":"payment failed"},"failures":[{"id":"failure-17","failure_category":"application"}]}}"#,
11561 ),
11562 "/api/workflows/wf-cancelled" => (
11563 "200 OK",
11564 r#"{"workflow_id":"wf-cancelled","run_id":"run-cancelled","status":"cancelled","closed_reason":"cancelled","reason":"cleanup requested"}"#,
11565 ),
11566 "/api/workflows/wf-terminated" => (
11567 "200 OK",
11568 r#"{"workflow_id":"wf-terminated","run_id":"run-terminated","status":"terminated","closed_reason":"terminated","reason":"forced stop"}"#,
11569 ),
11570 "/api/workflows/wf-timed-out" => (
11571 "200 OK",
11572 r#"{"workflow_id":"wf-timed-out","run_id":"run-timed-out","status":"failed","closed_reason":"timed_out","reason":"run_timeout"}"#,
11573 ),
11574 "/api/workflows/wf-waiting" | "/api/workflows/wf-waiting/runs/run-waiting" => (
11575 "200 OK",
11576 r#"{"workflow_id":"wf-waiting","run_id":"run-waiting","status":"waiting"}"#,
11577 ),
11578 "/api/workflows/wf-selected" => (
11579 "200 OK",
11580 r#"{"workflow_id":"wf-selected","run_id":"run-current","status":"completed","output":"current run output"}"#,
11581 ),
11582 "/api/workflows/wf-selected/runs/run-selected" => (
11583 "200 OK",
11584 r#"{"workflow_id":"wf-selected","run_id":"run-selected","status":"cancelled","closed_reason":"cancelled","reason":"selected run cancelled"}"#,
11585 ),
11586 _ => ("404 Not Found", r#"{"message":"not found"}"#),
11587 };
11588 write_mock_response(stream, status, body);
11589 }
11590
11591 fn mock_request_is_complete(request: &[u8]) -> bool {
11592 let Some(header_end) = request
11593 .windows(4)
11594 .position(|window| window == b"\r\n\r\n")
11595 .map(|position| position + 4)
11596 else {
11597 return false;
11598 };
11599 let headers = String::from_utf8_lossy(&request[..header_end]);
11600 let content_length = headers.lines().find_map(|line| {
11601 let (name, value) = line.split_once(':')?;
11602 name.eq_ignore_ascii_case("content-length")
11603 .then(|| value.trim().parse::<usize>().ok())
11604 .flatten()
11605 });
11606
11607 request.len() >= header_end + content_length.unwrap_or(0)
11608 }
11609
11610 fn write_mock_response(stream: &mut TcpStream, status: &str, body: &str) {
11611 let response = format!(
11612 "HTTP/1.1 {status}\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}",
11613 body.len()
11614 );
11615
11616 let _ = stream.write_all(response.as_bytes());
11617 let _ = stream.flush();
11618 }
11619}