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