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