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