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