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