Skip to main content

durable_workflow/
lib.rs

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