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