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