Skip to main content

durable_workflow/
lib.rs

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