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