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, and durably
4//! sleep in workflows; register a Rust worker; poll workflow, activity, and
5//! read-only query tasks; reconstruct typed workflow-instance state through
6//! deterministic replay; heartbeat worker and activity liveness; and exchange
7//! JSON-native payloads through the same `avro` generic wrapper used by the
8//! 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;
41
42const QUERY_TASK_FINAL_REJECTION_REASONS: &[&str] = &[
43    "lease_expired",
44    "query_task_not_found",
45    "query_task_not_leased",
46    "query_task_timed_out",
47];
48
49const AVRO_PAYLOAD_SCHEMA_JSON: &str = r#"{"type":"record","name":"Payload","namespace":"durable_workflow","fields":[{"name":"json","type":"string"},{"name":"version","type":"int","default":1}]}"#;
50const AVRO_PAYLOAD_VERSION: i32 = 1;
51
52static AVRO_PAYLOAD_SCHEMA: OnceLock<std::result::Result<Schema, String>> = OnceLock::new();
53
54#[derive(Clone, Copy)]
55enum RequestProtocol {
56    ControlPlane,
57    Worker(&'static str),
58}
59
60impl RequestProtocol {
61    fn is_worker(self) -> bool {
62        matches!(self, Self::Worker(_))
63    }
64}
65
66pub type Result<T> = std::result::Result<T, Error>;
67
68#[derive(Debug, Error)]
69pub enum Error {
70    #[error("transport error: {0}")]
71    Transport(#[from] reqwest::Error),
72    #[error("json error: {0}")]
73    Json(#[from] serde_json::Error),
74    #[error("http {status}: {body}")]
75    Http {
76        status: reqwest::StatusCode,
77        body: String,
78    },
79    #[error("codec error: {0}")]
80    Codec(String),
81    #[error(transparent)]
82    QueryFailed(QueryFailure),
83    #[error(transparent)]
84    Protocol(ProtocolFailure),
85    #[error(transparent)]
86    NonDeterministicReplay(ReplayFailure),
87    #[error("workflow handler {0:?} is not registered")]
88    WorkflowNotRegistered(String),
89    #[error("activity handler {0:?} is not registered")]
90    ActivityNotRegistered(String),
91    #[error("workflow future yielded without emitting a durable command")]
92    WorkflowYieldedWithoutCommand,
93    #[error("workflow state lock is poisoned")]
94    WorkflowStatePoisoned,
95    #[error("timer duration is too large for the worker protocol")]
96    TimerDurationOverflow,
97    #[error("operation timed out")]
98    Timeout,
99    #[error("worker loop error: {0}")]
100    WorkerLoop(String),
101}
102
103/// A stable, machine-readable failure raised when workflow code no longer
104/// reconstructs the durable command stream recorded in history.
105#[derive(Clone, Debug, Error)]
106#[error("non-deterministic workflow replay ({reason}) at sequence {sequence:?}: {message}")]
107pub struct ReplayFailure {
108    pub reason: String,
109    pub sequence: Option<u64>,
110    pub expected: Option<String>,
111    pub actual: Option<String>,
112    pub message: String,
113}
114
115impl ReplayFailure {
116    fn new(
117        reason: impl Into<String>,
118        sequence: Option<u64>,
119        expected: Option<String>,
120        actual: Option<String>,
121        message: impl Into<String>,
122    ) -> Self {
123        Self {
124            reason: reason.into(),
125            sequence,
126            expected,
127            actual,
128            message: message.into(),
129        }
130    }
131}
132
133/// A stable, machine-readable workflow query or query-task settlement failure.
134#[derive(Clone, Debug, Error)]
135#[error("query failed ({reason}, HTTP {status}): {message}")]
136pub struct QueryFailure {
137    pub status: u16,
138    pub reason: String,
139    pub message: String,
140    pub body: Value,
141}
142
143/// A stable failure returned when a server rejects an SDK protocol version.
144#[derive(Clone, Debug, Error)]
145#[error("protocol rejected ({reason}, HTTP {status}): {message}")]
146pub struct ProtocolFailure {
147    pub status: u16,
148    pub reason: String,
149    pub message: String,
150    pub supported_version: Option<String>,
151    pub requested_version: Option<String>,
152    pub body: Value,
153}
154
155#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
156pub struct PayloadEnvelope {
157    pub codec: String,
158    pub blob: String,
159}
160
161impl PayloadEnvelope {
162    pub fn avro<T: Serialize>(value: &T) -> Result<Self> {
163        encode_payload(value, DEFAULT_CODEC)
164    }
165
166    pub fn json<T: Serialize>(value: &T) -> Result<Self> {
167        encode_payload(value, JSON_CODEC)
168    }
169}
170
171pub fn encode_payload<T: Serialize>(value: &T, codec: &str) -> Result<PayloadEnvelope> {
172    let value = serde_json::to_value(value)?;
173    let blob = encode_value_blob(&value, codec)?;
174
175    Ok(PayloadEnvelope {
176        codec: codec.to_string(),
177        blob,
178    })
179}
180
181pub fn decode_payload<T: DeserializeOwned>(envelope: &PayloadEnvelope) -> Result<T> {
182    let value = decode_blob(&envelope.blob, &envelope.codec)?;
183    Ok(serde_json::from_value(value)?)
184}
185
186fn encode_value_envelope(value: &Value, codec: &str) -> Result<Value> {
187    Ok(serde_json::to_value(encode_payload(value, codec)?)?)
188}
189
190fn encode_value_blob(value: &Value, codec: &str) -> Result<String> {
191    match codec {
192        JSON_CODEC => Ok(serde_json::to_string(value)?),
193        DEFAULT_CODEC => encode_avro_generic(value),
194        other => Err(Error::Codec(format!("unsupported payload codec {other:?}"))),
195    }
196}
197
198fn decode_wire_value(value: &Value, fallback_codec: &str) -> Result<Value> {
199    if value.is_null() {
200        return Ok(Value::Null);
201    }
202
203    if let Some(object) = value.as_object() {
204        if let (Some(codec), Some(blob)) = (
205            object.get("codec").and_then(Value::as_str),
206            object.get("blob").and_then(Value::as_str),
207        ) {
208            return decode_blob(blob, codec);
209        }
210    }
211
212    if let Some(blob) = value.as_str() {
213        return decode_blob(blob, fallback_codec);
214    }
215
216    Ok(value.clone())
217}
218
219fn decode_blob(blob: &str, codec: &str) -> Result<Value> {
220    match codec {
221        JSON_CODEC => Ok(serde_json::from_str(blob)?),
222        DEFAULT_CODEC => decode_avro_generic(blob),
223        other => Err(Error::Codec(format!("unsupported payload codec {other:?}"))),
224    }
225}
226
227fn encode_avro_generic(value: &Value) -> Result<String> {
228    let json = serde_json::to_string(value)?;
229    let datum = to_value(AvroPayload {
230        json,
231        version: AVRO_PAYLOAD_VERSION,
232    })
233    .map_err(|err| Error::Codec(format!("could not convert avro generic wrapper: {err}")))?;
234    let datum = to_avro_datum(avro_payload_schema()?, datum)
235        .map_err(|err| Error::Codec(format!("could not encode avro generic wrapper: {err}")))?;
236
237    let mut bytes = Vec::with_capacity(datum.len() + 1);
238    bytes.push(0x00);
239    bytes.extend_from_slice(&datum);
240    Ok(BASE64.encode(bytes))
241}
242
243fn decode_avro_generic(blob: &str) -> Result<Value> {
244    let bytes = BASE64
245        .decode(blob)
246        .map_err(|err| Error::Codec(format!("invalid avro base64 payload: {err}")))?;
247
248    if bytes.is_empty() {
249        return Err(Error::Codec("avro payload is empty".to_string()));
250    }
251
252    match bytes[0] {
253        0x00 => {}
254        0x01 => {
255            return Err(Error::Codec(
256                "typed avro payloads require a schema context; v1 supports the generic wrapper"
257                    .to_string(),
258            ));
259        }
260        other => {
261            return Err(Error::Codec(format!(
262                "unknown avro payload prefix 0x{other:02x}"
263            )));
264        }
265    }
266
267    let mut datum = &bytes[1..];
268    let datum = from_avro_datum(avro_payload_schema()?, &mut datum, None)
269        .map_err(|err| Error::Codec(format!("could not decode avro generic wrapper: {err}")))?;
270    let payload: AvroPayload = from_value(&datum)
271        .map_err(|err| Error::Codec(format!("invalid avro generic wrapper record: {err}")))?;
272
273    if payload.version != AVRO_PAYLOAD_VERSION {
274        return Err(Error::Codec(format!(
275            "unsupported avro generic wrapper version {}",
276            payload.version
277        )));
278    }
279
280    Ok(serde_json::from_str(&payload.json)?)
281}
282
283#[derive(Debug, Serialize, Deserialize)]
284struct AvroPayload {
285    json: String,
286    version: i32,
287}
288
289fn avro_payload_schema() -> Result<&'static Schema> {
290    match AVRO_PAYLOAD_SCHEMA.get_or_init(|| {
291        Schema::parse_str(AVRO_PAYLOAD_SCHEMA_JSON)
292            .map_err(|err| format!("could not parse avro payload schema: {err}"))
293    }) {
294        Ok(schema) => Ok(schema),
295        Err(message) => Err(Error::Codec(message.clone())),
296    }
297}
298
299#[derive(Clone, Debug)]
300pub struct Client {
301    http: reqwest::Client,
302    base_url: String,
303    token: Option<String>,
304    control_token: Option<String>,
305    worker_token: Option<String>,
306    namespace: String,
307}
308
309impl Client {
310    pub fn new(base_url: impl Into<String>) -> Result<Self> {
311        Self::builder(base_url).build()
312    }
313
314    pub fn builder(base_url: impl Into<String>) -> ClientBuilder {
315        ClientBuilder {
316            base_url: base_url.into(),
317            token: None,
318            control_token: None,
319            worker_token: None,
320            namespace: "default".to_string(),
321            timeout: Duration::from_secs(60),
322        }
323    }
324
325    pub async fn health(&self) -> Result<Value> {
326        self.request_json(
327            reqwest::Method::GET,
328            "/health",
329            RequestProtocol::ControlPlane,
330            Option::<&Value>::None,
331        )
332        .await
333    }
334
335    pub async fn cluster_info(&self) -> Result<Value> {
336        self.request_json(
337            reqwest::Method::GET,
338            "/cluster/info",
339            RequestProtocol::ControlPlane,
340            Option::<&Value>::None,
341        )
342        .await
343    }
344
345    pub async fn start_workflow<T: Serialize>(
346        &self,
347        workflow_type: &str,
348        task_queue: &str,
349        workflow_id: &str,
350        input: T,
351    ) -> Result<WorkflowHandle> {
352        let input = serde_json::to_value(input)?;
353        let input_envelope = encode_value_envelope(&normalize_arguments(input), DEFAULT_CODEC)?;
354        let body = json!({
355            "workflow_id": workflow_id,
356            "workflow_type": workflow_type,
357            "task_queue": task_queue,
358            "input": input_envelope,
359            "execution_timeout_seconds": 3600,
360            "run_timeout_seconds": 600
361        });
362
363        let data: Value = self
364            .request_json(
365                reqwest::Method::POST,
366                "/workflows",
367                RequestProtocol::ControlPlane,
368                Some(&body),
369            )
370            .await?;
371
372        Ok(WorkflowHandle {
373            client: self.clone(),
374            workflow_id: data
375                .get("workflow_id")
376                .and_then(Value::as_str)
377                .unwrap_or(workflow_id)
378                .to_string(),
379            run_id: data
380                .get("run_id")
381                .and_then(Value::as_str)
382                .map(str::to_string),
383            workflow_type: data
384                .get("workflow_type")
385                .and_then(Value::as_str)
386                .unwrap_or(workflow_type)
387                .to_string(),
388        })
389    }
390
391    pub async fn signal_workflow<T: Serialize>(
392        &self,
393        workflow_id: &str,
394        signal_name: &str,
395        input: T,
396    ) -> Result<Value> {
397        let input = serde_json::to_value(input)?;
398        let input_envelope = encode_value_envelope(&normalize_arguments(input), DEFAULT_CODEC)?;
399        let body = json!({
400            "input": input_envelope
401        });
402        let path = format!("/workflows/{workflow_id}/signal/{signal_name}");
403        self.request_json(
404            reqwest::Method::POST,
405            &path,
406            RequestProtocol::ControlPlane,
407            Some(&body),
408        )
409        .await
410    }
411
412    /// Execute a named, read-only query against a running or completed workflow.
413    ///
414    /// Arguments and results use the platform payload envelope. Server and
415    /// worker rejections are returned as [`Error::QueryFailed`] with a stable
416    /// reason, HTTP status, and original response body.
417    pub async fn query_workflow<T: Serialize>(
418        &self,
419        workflow_id: &str,
420        query_name: &str,
421        input: T,
422    ) -> Result<Value> {
423        let input = serde_json::to_value(input)?;
424        let input_envelope = encode_value_envelope(&normalize_arguments(input), DEFAULT_CODEC)?;
425        let body = json!({
426            "input": input_envelope
427        });
428        let path = format!("/workflows/{workflow_id}/query/{query_name}");
429        let response: Value = match self
430            .request_json(
431                reqwest::Method::POST,
432                &path,
433                RequestProtocol::ControlPlane,
434                Some(&body),
435            )
436            .await
437        {
438            Ok(response) => response,
439            Err(Error::Http { status, body }) => {
440                return Err(Error::QueryFailed(query_failure(status, body)));
441            }
442            Err(error) => return Err(error),
443        };
444
445        if let Some(envelope) = response
446            .get("result_envelope")
447            .filter(|envelope| !envelope.is_null())
448        {
449            return decode_wire_value(envelope, DEFAULT_CODEC);
450        }
451
452        Ok(response.get("result").cloned().unwrap_or(Value::Null))
453    }
454
455    pub async fn describe_workflow(&self, workflow_id: &str) -> Result<WorkflowDescription> {
456        let path = format!("/workflows/{workflow_id}");
457        let mut data: WorkflowDescription = self
458            .request_json(
459                reqwest::Method::GET,
460                &path,
461                RequestProtocol::ControlPlane,
462                Option::<&Value>::None,
463            )
464            .await?;
465        data.decode_payloads()?;
466        Ok(data)
467    }
468
469    pub async fn register_worker(
470        &self,
471        worker_id: &str,
472        task_queue: &str,
473        supported_workflow_types: Vec<String>,
474        supported_activity_types: Vec<String>,
475        max_concurrent_workflow_tasks: usize,
476        max_concurrent_activity_tasks: usize,
477    ) -> Result<RegisterWorkerResponse> {
478        self.register_worker_with_capabilities(
479            worker_id,
480            task_queue,
481            supported_workflow_types,
482            supported_activity_types,
483            max_concurrent_workflow_tasks,
484            max_concurrent_activity_tasks,
485            Vec::new(),
486        )
487        .await
488    }
489
490    /// Register a worker and explicitly advertise additive worker capabilities.
491    pub async fn register_worker_with_capabilities(
492        &self,
493        worker_id: &str,
494        task_queue: &str,
495        supported_workflow_types: Vec<String>,
496        supported_activity_types: Vec<String>,
497        max_concurrent_workflow_tasks: usize,
498        max_concurrent_activity_tasks: usize,
499        capabilities: Vec<String>,
500    ) -> Result<RegisterWorkerResponse> {
501        let body = json!({
502            "worker_id": worker_id,
503            "task_queue": task_queue,
504            "runtime": "rust",
505            "sdk_version": SDK_VERSION,
506            "supported_workflow_types": supported_workflow_types,
507            "supported_activity_types": supported_activity_types,
508            "capabilities": capabilities,
509            "max_concurrent_workflow_tasks": max_concurrent_workflow_tasks,
510            "max_concurrent_activity_tasks": max_concurrent_activity_tasks
511        });
512
513        self.request_json(
514            reqwest::Method::POST,
515            "/worker/register",
516            RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
517            Some(&body),
518        )
519        .await
520    }
521
522    /// Long-poll for an ephemeral, read-only workflow query task.
523    pub async fn poll_query_task(
524        &self,
525        worker_id: &str,
526        task_queue: &str,
527        timeout: Duration,
528    ) -> Result<Option<QueryTask>> {
529        let timeout_seconds = long_poll_timeout_seconds(timeout);
530        let body = json!({
531            "worker_id": worker_id,
532            "task_queue": task_queue,
533            "poll_request_id": unique_request_id("rust-query-poll"),
534            "timeout_seconds": timeout_seconds,
535        });
536        let data: PollQueryTaskResponse = self
537            .request_json_with_timeout(
538                reqwest::Method::POST,
539                "/worker/query-tasks/poll",
540                RequestProtocol::Worker(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION),
541                Some(&body),
542                timeout + Duration::from_secs(5),
543            )
544            .await?;
545        Ok(data.task)
546    }
547
548    /// Complete a query task without appending workflow history.
549    pub async fn complete_query_task(
550        &self,
551        query_task_id: &str,
552        lease_owner: &str,
553        query_task_attempt: u64,
554        result: Value,
555        codec: &str,
556    ) -> Result<Value> {
557        let result_envelope = encode_value_envelope(&result, codec)?;
558        self.complete_query_task_with_envelope(
559            query_task_id,
560            lease_owner,
561            query_task_attempt,
562            result,
563            result_envelope,
564        )
565        .await
566    }
567
568    async fn complete_query_task_with_envelope(
569        &self,
570        query_task_id: &str,
571        lease_owner: &str,
572        query_task_attempt: u64,
573        result: Value,
574        result_envelope: Value,
575    ) -> Result<Value> {
576        let body = json!({
577            "lease_owner": lease_owner,
578            "query_task_attempt": query_task_attempt,
579            "result": result,
580            "result_envelope": result_envelope,
581        });
582        let path = format!("/worker/query-tasks/{query_task_id}/complete");
583        let response = self
584            .request_json(
585                reqwest::Method::POST,
586                &path,
587                RequestProtocol::Worker(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION),
588                Some(&body),
589            )
590            .await;
591        query_task_response(response)
592    }
593
594    /// Report a stable machine-readable query-task failure.
595    pub async fn fail_query_task(
596        &self,
597        query_task_id: &str,
598        lease_owner: &str,
599        query_task_attempt: u64,
600        message: impl Into<String>,
601        reason: impl Into<String>,
602        failure_type: impl Into<String>,
603    ) -> Result<Value> {
604        let body = json!({
605            "lease_owner": lease_owner,
606            "query_task_attempt": query_task_attempt,
607            "failure": {
608                "message": message.into(),
609                "reason": reason.into(),
610                "type": failure_type.into(),
611            }
612        });
613        let path = format!("/worker/query-tasks/{query_task_id}/fail");
614        let response = self
615            .request_json(
616                reqwest::Method::POST,
617                &path,
618                RequestProtocol::Worker(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION),
619                Some(&body),
620            )
621            .await;
622        query_task_response(response)
623    }
624
625    pub async fn heartbeat_worker(
626        &self,
627        worker_id: &str,
628        workflow_available: usize,
629        activity_available: usize,
630    ) -> Result<Value> {
631        let body = json!({
632            "worker_id": worker_id,
633            "task_slots": {
634                "workflow_available": workflow_available,
635                "activity_available": activity_available
636            },
637            "process_metrics": {
638                "process_id": std::process::id(),
639                "process_uptime_seconds": 0
640            }
641        });
642
643        self.request_json(
644            reqwest::Method::POST,
645            "/worker/heartbeat",
646            RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
647            Some(&body),
648        )
649        .await
650    }
651
652    pub async fn poll_workflow_task(
653        &self,
654        worker_id: &str,
655        task_queue: &str,
656        timeout: Duration,
657    ) -> Result<Option<WorkflowTask>> {
658        Ok(self
659            .poll_workflow_task_response(worker_id, task_queue, timeout)
660            .await?
661            .task)
662    }
663
664    pub async fn poll_workflow_task_response(
665        &self,
666        worker_id: &str,
667        task_queue: &str,
668        timeout: Duration,
669    ) -> Result<PollWorkflowTaskResponse> {
670        let body = json!({
671            "worker_id": worker_id,
672            "task_queue": task_queue,
673            "timeout_seconds": long_poll_timeout_seconds(timeout),
674        });
675        let mut data: PollWorkflowTaskResponse = self
676            .request_json_with_timeout(
677                reqwest::Method::POST,
678                "/worker/workflow-tasks/poll",
679                RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
680                Some(&body),
681                timeout + Duration::from_secs(5),
682            )
683            .await?;
684
685        if let Some(task) = data.task.as_mut() {
686            self.fetch_remaining_workflow_history(worker_id, task)
687                .await?;
688        }
689
690        Ok(data)
691    }
692
693    async fn fetch_remaining_workflow_history(
694        &self,
695        worker_id: &str,
696        task: &mut WorkflowTask,
697    ) -> Result<()> {
698        let mut next_token = task.next_history_page_token.clone();
699
700        while let Some(token) = next_token.take().filter(|token| !token.is_empty()) {
701            let lease_owner = task
702                .lease_owner
703                .clone()
704                .unwrap_or_else(|| worker_id.to_string());
705            let page = self
706                .workflow_task_history_page(
707                    &task.task_id,
708                    &lease_owner,
709                    task.workflow_task_attempt,
710                    &token,
711                )
712                .await?;
713
714            task.append_history_page(page);
715
716            if task.next_history_page_token.as_deref() == Some(token.as_str()) {
717                return Err(Error::Codec(
718                    "workflow history pagination returned the same page token".to_string(),
719                ));
720            }
721
722            next_token = task.next_history_page_token.clone();
723        }
724
725        Ok(())
726    }
727
728    async fn workflow_task_history_page(
729        &self,
730        task_id: &str,
731        lease_owner: &str,
732        workflow_task_attempt: u64,
733        next_history_page_token: &str,
734    ) -> Result<WorkflowTaskHistoryPage> {
735        let body = json!({
736            "lease_owner": lease_owner,
737            "workflow_task_attempt": workflow_task_attempt,
738            "next_history_page_token": next_history_page_token
739        });
740        let path = format!("/worker/workflow-tasks/{task_id}/history");
741
742        self.request_json(
743            reqwest::Method::POST,
744            &path,
745            RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
746            Some(&body),
747        )
748        .await
749    }
750
751    pub async fn complete_workflow_task(
752        &self,
753        task_id: &str,
754        lease_owner: &str,
755        workflow_task_attempt: u64,
756        commands: Vec<Value>,
757    ) -> Result<Value> {
758        let body = json!({
759            "lease_owner": lease_owner,
760            "workflow_task_attempt": workflow_task_attempt,
761            "commands": commands
762        });
763        let path = format!("/worker/workflow-tasks/{task_id}/complete");
764        self.request_json(
765            reqwest::Method::POST,
766            &path,
767            RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
768            Some(&body),
769        )
770        .await
771    }
772
773    pub async fn fail_workflow_task(
774        &self,
775        task_id: &str,
776        lease_owner: &str,
777        workflow_task_attempt: u64,
778        message: impl Into<String>,
779    ) -> Result<Value> {
780        let body = json!({
781            "lease_owner": lease_owner,
782            "workflow_task_attempt": workflow_task_attempt,
783            "failure": {
784                "message": message.into(),
785                "type": "RustWorkflowTaskFailure"
786            }
787        });
788        let path = format!("/worker/workflow-tasks/{task_id}/fail");
789        self.request_json(
790            reqwest::Method::POST,
791            &path,
792            RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
793            Some(&body),
794        )
795        .await
796    }
797
798    pub async fn poll_activity_task(
799        &self,
800        worker_id: &str,
801        task_queue: &str,
802        timeout: Duration,
803    ) -> Result<Option<ActivityTask>> {
804        let body = json!({
805            "worker_id": worker_id,
806            "task_queue": task_queue,
807            "timeout_seconds": long_poll_timeout_seconds(timeout),
808        });
809        let data: PollActivityTaskResponse = self
810            .request_json_with_timeout(
811                reqwest::Method::POST,
812                "/worker/activity-tasks/poll",
813                RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
814                Some(&body),
815                timeout + Duration::from_secs(5),
816            )
817            .await?;
818        Ok(data.task)
819    }
820
821    pub async fn complete_activity_task(
822        &self,
823        task_id: &str,
824        activity_attempt_id: &str,
825        lease_owner: &str,
826        result: Value,
827        codec: &str,
828    ) -> Result<Value> {
829        let result = encode_value_envelope(&result, codec)?;
830        let body = json!({
831            "activity_attempt_id": activity_attempt_id,
832            "lease_owner": lease_owner,
833            "result": result
834        });
835        let path = format!("/worker/activity-tasks/{task_id}/complete");
836        self.request_json(
837            reqwest::Method::POST,
838            &path,
839            RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
840            Some(&body),
841        )
842        .await
843    }
844
845    pub async fn fail_activity_task(
846        &self,
847        task_id: &str,
848        activity_attempt_id: &str,
849        lease_owner: &str,
850        message: impl Into<String>,
851        non_retryable: bool,
852    ) -> Result<Value> {
853        let body = json!({
854            "activity_attempt_id": activity_attempt_id,
855            "lease_owner": lease_owner,
856            "failure": {
857                "message": message.into(),
858                "type": "RustActivityFailure",
859                "non_retryable": non_retryable
860            }
861        });
862        let path = format!("/worker/activity-tasks/{task_id}/fail");
863        self.request_json(
864            reqwest::Method::POST,
865            &path,
866            RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
867            Some(&body),
868        )
869        .await
870    }
871
872    pub async fn heartbeat_activity_task(
873        &self,
874        task_id: &str,
875        activity_attempt_id: &str,
876        lease_owner: &str,
877        details: Value,
878    ) -> Result<ActivityHeartbeatResponse> {
879        let body = json!({
880            "activity_attempt_id": activity_attempt_id,
881            "lease_owner": lease_owner,
882            "details": details
883        });
884        let path = format!("/worker/activity-tasks/{task_id}/heartbeat");
885        self.request_json(
886            reqwest::Method::POST,
887            &path,
888            RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
889            Some(&body),
890        )
891        .await
892    }
893
894    async fn request_json<T: DeserializeOwned, B: Serialize + ?Sized>(
895        &self,
896        method: reqwest::Method,
897        path: &str,
898        protocol: RequestProtocol,
899        body: Option<&B>,
900    ) -> Result<T> {
901        self.request_json_with_timeout(method, path, protocol, body, Duration::from_secs(60))
902            .await
903    }
904
905    async fn request_json_with_timeout<T: DeserializeOwned, B: Serialize + ?Sized>(
906        &self,
907        method: reqwest::Method,
908        path: &str,
909        protocol: RequestProtocol,
910        body: Option<&B>,
911        timeout: Duration,
912    ) -> Result<T> {
913        let mut request = self
914            .http
915            .request(method, format!("{}/api{}", self.base_url, path))
916            .timeout(timeout)
917            .header(reqwest::header::ACCEPT, "application/json")
918            .header(reqwest::header::CONTENT_TYPE, "application/json")
919            .header("X-Namespace", &self.namespace);
920
921        match protocol {
922            RequestProtocol::Worker(version) => {
923                request = request.header("X-Durable-Workflow-Protocol-Version", version);
924            }
925            RequestProtocol::ControlPlane => {
926                request = request.header(
927                    "X-Durable-Workflow-Control-Plane-Version",
928                    CONTROL_PLANE_VERSION,
929                );
930            }
931        }
932
933        if let Some(token) = self.auth_token(protocol.is_worker()) {
934            request = request.bearer_auth(token);
935        }
936
937        if let Some(body) = body {
938            request = request.json(body);
939        }
940
941        let response = request.send().await?;
942        let status = response.status();
943        let bytes = response.bytes().await?;
944
945        if !status.is_success() {
946            let body = String::from_utf8_lossy(&bytes).to_string();
947            if let Some(protocol) = protocol_failure(status, &body) {
948                return Err(Error::Protocol(protocol));
949            }
950            return Err(Error::Http { status, body });
951        }
952
953        if bytes.is_empty() {
954            return Ok(serde_json::from_value(Value::Null)?);
955        }
956
957        Ok(serde_json::from_slice(&bytes)?)
958    }
959
960    fn auth_token(&self, worker: bool) -> Option<&str> {
961        if worker {
962            self.worker_token
963                .as_deref()
964                .or(self.token.as_deref())
965                .or(self.control_token.as_deref())
966        } else {
967            self.control_token
968                .as_deref()
969                .or(self.token.as_deref())
970                .or(self.worker_token.as_deref())
971        }
972    }
973}
974
975fn query_failure(status: reqwest::StatusCode, raw_body: String) -> QueryFailure {
976    let body = serde_json::from_str(&raw_body).unwrap_or_else(|_| json!({"message": raw_body}));
977    let reason = body
978        .get("reason")
979        .and_then(Value::as_str)
980        .unwrap_or("query_rejected")
981        .to_string();
982    let message = body
983        .get("message")
984        .or_else(|| body.get("error"))
985        .and_then(Value::as_str)
986        .unwrap_or("workflow query was rejected")
987        .to_string();
988
989    QueryFailure {
990        status: status.as_u16(),
991        reason,
992        message,
993        body,
994    }
995}
996
997fn query_task_response(response: Result<Value>) -> Result<Value> {
998    match response {
999        Err(Error::Http { status, body }) => Err(Error::QueryFailed(query_failure(status, body))),
1000        response => response,
1001    }
1002}
1003
1004fn query_task_rejection_is_final(error: &Error) -> bool {
1005    matches!(
1006        error,
1007        Error::QueryFailed(failure)
1008            if QUERY_TASK_FINAL_REJECTION_REASONS.contains(&failure.reason.as_str())
1009    )
1010}
1011
1012fn protocol_failure(status: reqwest::StatusCode, raw_body: &str) -> Option<ProtocolFailure> {
1013    let body: Value = serde_json::from_str(raw_body).ok()?;
1014    let reason = body.get("reason")?.as_str()?;
1015    if !matches!(
1016        reason,
1017        "missing_protocol_version"
1018            | "unsupported_protocol_version"
1019            | "missing_control_plane_version"
1020            | "unsupported_control_plane_version"
1021    ) {
1022        return None;
1023    }
1024
1025    Some(ProtocolFailure {
1026        status: status.as_u16(),
1027        reason: reason.to_string(),
1028        message: body
1029            .get("message")
1030            .or_else(|| body.get("error"))
1031            .and_then(Value::as_str)
1032            .unwrap_or("protocol version rejected")
1033            .to_string(),
1034        supported_version: body
1035            .get("supported_version")
1036            .and_then(Value::as_str)
1037            .map(str::to_string),
1038        requested_version: body
1039            .get("requested_version")
1040            .and_then(Value::as_str)
1041            .map(str::to_string),
1042        body,
1043    })
1044}
1045
1046fn long_poll_timeout_seconds(timeout: Duration) -> u64 {
1047    timeout
1048        .as_secs()
1049        .saturating_add(u64::from(timeout.subsec_nanos() > 0))
1050        .min(MAX_LONG_POLL_TIMEOUT_SECONDS)
1051}
1052
1053fn worker_operation_is_retryable(error: &Error) -> bool {
1054    match error {
1055        Error::Transport(error) => {
1056            error.is_timeout() || error.is_connect() || error.is_request() || error.is_body()
1057        }
1058        Error::Http { status, .. } => {
1059            matches!(
1060                *status,
1061                reqwest::StatusCode::REQUEST_TIMEOUT | reqwest::StatusCode::TOO_MANY_REQUESTS
1062            ) || status.is_server_error()
1063        }
1064        _ => false,
1065    }
1066}
1067
1068fn worker_retry_delay(policy: WorkerRetryPolicy, retry: usize) -> Duration {
1069    let exponent = retry.saturating_sub(1).min(31) as u32;
1070    policy
1071        .initial_backoff
1072        .saturating_mul(1_u32 << exponent)
1073        .min(policy.max_backoff)
1074}
1075
1076#[derive(Debug)]
1077pub struct ClientBuilder {
1078    base_url: String,
1079    token: Option<String>,
1080    control_token: Option<String>,
1081    worker_token: Option<String>,
1082    namespace: String,
1083    timeout: Duration,
1084}
1085
1086impl ClientBuilder {
1087    pub fn token(mut self, token: Option<String>) -> Self {
1088        self.token = token;
1089        self
1090    }
1091
1092    pub fn control_token(mut self, token: Option<String>) -> Self {
1093        self.control_token = token;
1094        self
1095    }
1096
1097    pub fn worker_token(mut self, token: Option<String>) -> Self {
1098        self.worker_token = token;
1099        self
1100    }
1101
1102    pub fn namespace(mut self, namespace: impl Into<String>) -> Self {
1103        self.namespace = namespace.into();
1104        self
1105    }
1106
1107    pub fn timeout(mut self, timeout: Duration) -> Self {
1108        self.timeout = timeout;
1109        self
1110    }
1111
1112    pub fn build(self) -> Result<Client> {
1113        Ok(Client {
1114            http: reqwest::Client::builder().timeout(self.timeout).build()?,
1115            base_url: self.base_url.trim_end_matches('/').to_string(),
1116            token: self.token,
1117            control_token: self.control_token,
1118            worker_token: self.worker_token,
1119            namespace: self.namespace,
1120        })
1121    }
1122}
1123
1124#[derive(Clone, Debug)]
1125pub struct WorkflowHandle {
1126    client: Client,
1127    pub workflow_id: String,
1128    pub run_id: Option<String>,
1129    pub workflow_type: String,
1130}
1131
1132impl WorkflowHandle {
1133    pub async fn describe(&self) -> Result<WorkflowDescription> {
1134        self.client.describe_workflow(&self.workflow_id).await
1135    }
1136
1137    pub async fn signal<T: Serialize>(&self, signal_name: &str, input: T) -> Result<Value> {
1138        self.client
1139            .signal_workflow(&self.workflow_id, signal_name, input)
1140            .await
1141    }
1142
1143    /// Execute a named, read-only query against this workflow.
1144    pub async fn query<T: Serialize>(&self, query_name: &str, input: T) -> Result<Value> {
1145        self.client
1146            .query_workflow(&self.workflow_id, query_name, input)
1147            .await
1148    }
1149
1150    pub async fn result(&self, options: WorkflowResultOptions) -> Result<Value> {
1151        let started = Instant::now();
1152
1153        loop {
1154            let description = self.describe().await?;
1155            if description.is_completed() {
1156                return Ok(description.output.unwrap_or(Value::Null));
1157            }
1158
1159            if description.is_terminal() {
1160                return Err(Error::Codec(format!(
1161                    "workflow {} closed with status {:?}",
1162                    self.workflow_id, description.status
1163                )));
1164            }
1165
1166            if started.elapsed() >= options.timeout {
1167                return Err(Error::Timeout);
1168            }
1169
1170            tokio::time::sleep(options.poll_interval).await;
1171        }
1172    }
1173}
1174
1175#[derive(Clone, Copy, Debug)]
1176pub struct WorkflowResultOptions {
1177    pub poll_interval: Duration,
1178    pub timeout: Duration,
1179}
1180
1181impl Default for WorkflowResultOptions {
1182    fn default() -> Self {
1183        Self {
1184            poll_interval: Duration::from_millis(500),
1185            timeout: Duration::from_secs(30),
1186        }
1187    }
1188}
1189
1190#[derive(Clone, Debug, Deserialize)]
1191pub struct WorkflowDescription {
1192    pub workflow_id: Option<String>,
1193    pub run_id: Option<String>,
1194    pub workflow_type: Option<String>,
1195    pub status: Option<String>,
1196    #[serde(default)]
1197    pub output: Option<Value>,
1198    #[serde(default)]
1199    pub output_envelope: Option<Value>,
1200    #[serde(flatten)]
1201    pub raw: HashMap<String, Value>,
1202}
1203
1204impl WorkflowDescription {
1205    pub fn is_completed(&self) -> bool {
1206        matches!(self.status.as_deref(), Some("completed" | "Completed"))
1207    }
1208
1209    pub fn is_terminal(&self) -> bool {
1210        matches!(
1211            self.status.as_deref(),
1212            Some(
1213                "completed"
1214                    | "Completed"
1215                    | "failed"
1216                    | "Failed"
1217                    | "cancelled"
1218                    | "Cancelled"
1219                    | "terminated"
1220                    | "Terminated"
1221                    | "timed_out"
1222                    | "TimedOut",
1223            )
1224        )
1225    }
1226
1227    fn decode_payloads(&mut self) -> Result<()> {
1228        if let Some(envelope) = &self.output_envelope {
1229            self.output = Some(decode_wire_value(envelope, DEFAULT_CODEC)?);
1230        }
1231
1232        Ok(())
1233    }
1234}
1235
1236#[derive(Clone, Debug, Deserialize)]
1237pub struct RegisterWorkerResponse {
1238    pub worker_id: String,
1239    pub registered: bool,
1240    #[serde(default)]
1241    pub heartbeat_interval_seconds: Option<u64>,
1242    #[serde(default)]
1243    pub protocol_version: Option<String>,
1244    #[serde(default)]
1245    pub server_capabilities: Option<Value>,
1246}
1247
1248#[derive(Clone, Debug, Deserialize)]
1249pub struct PollWorkflowTaskResponse {
1250    #[serde(default)]
1251    pub task: Option<WorkflowTask>,
1252    #[serde(default)]
1253    pub poll_status: Option<String>,
1254    #[serde(default)]
1255    pub reason: Option<String>,
1256    #[serde(default)]
1257    pub protocol_version: Option<String>,
1258    #[serde(default)]
1259    pub server_capabilities: Option<Value>,
1260}
1261
1262#[derive(Clone, Debug, Deserialize)]
1263struct PollActivityTaskResponse {
1264    #[serde(default)]
1265    task: Option<ActivityTask>,
1266}
1267
1268#[derive(Clone, Debug, Deserialize)]
1269struct PollQueryTaskResponse {
1270    #[serde(default)]
1271    task: Option<QueryTask>,
1272}
1273
1274/// An ephemeral server-routed query task.
1275#[derive(Clone, Debug, Deserialize)]
1276pub struct QueryTask {
1277    pub query_task_id: String,
1278    #[serde(default = "default_workflow_task_attempt")]
1279    pub query_task_attempt: u64,
1280    #[serde(default)]
1281    pub lease_owner: Option<String>,
1282    #[serde(default)]
1283    pub workflow_id: Option<String>,
1284    #[serde(default)]
1285    pub run_id: Option<String>,
1286    pub workflow_type: String,
1287    pub query_name: String,
1288    #[serde(default = "default_payload_codec")]
1289    pub payload_codec: String,
1290    #[serde(default)]
1291    pub workflow_arguments: Option<Value>,
1292    #[serde(default)]
1293    pub query_arguments: Option<Value>,
1294    #[serde(default)]
1295    pub history_events: Vec<HistoryEvent>,
1296    #[serde(default)]
1297    pub history_export: Option<Value>,
1298    #[serde(default)]
1299    pub run_status: Option<String>,
1300}
1301
1302#[derive(Clone, Debug, Deserialize)]
1303pub struct WorkflowTask {
1304    pub task_id: String,
1305    #[serde(default)]
1306    pub workflow_id: Option<String>,
1307    #[serde(default)]
1308    pub run_id: Option<String>,
1309    pub workflow_type: String,
1310    #[serde(default = "default_payload_codec")]
1311    pub payload_codec: String,
1312    #[serde(default)]
1313    pub arguments: Option<Value>,
1314    #[serde(default)]
1315    pub history_events: Vec<HistoryEvent>,
1316    #[serde(default)]
1317    pub total_history_events: Option<u64>,
1318    #[serde(default)]
1319    pub next_history_page_token: Option<String>,
1320    #[serde(default = "default_workflow_task_attempt")]
1321    pub workflow_task_attempt: u64,
1322    #[serde(default)]
1323    pub workflow_signal_id: Option<String>,
1324    #[serde(default)]
1325    pub signal_name: Option<String>,
1326    #[serde(default)]
1327    pub signal_arguments: Option<Value>,
1328    #[serde(default)]
1329    pub lease_owner: Option<String>,
1330}
1331
1332impl WorkflowTask {
1333    fn append_history_page(&mut self, page: WorkflowTaskHistoryPage) {
1334        self.history_events.extend(page.history_events);
1335
1336        if page.total_history_events.is_some() {
1337            self.total_history_events = page.total_history_events;
1338        }
1339
1340        self.next_history_page_token = page
1341            .next_history_page_token
1342            .filter(|token| !token.is_empty());
1343    }
1344}
1345
1346#[derive(Clone, Debug, Deserialize)]
1347struct WorkflowTaskHistoryPage {
1348    #[serde(default)]
1349    history_events: Vec<HistoryEvent>,
1350    #[serde(default)]
1351    total_history_events: Option<u64>,
1352    #[serde(default)]
1353    next_history_page_token: Option<String>,
1354}
1355
1356#[derive(Clone, Debug, Deserialize)]
1357pub struct ActivityTask {
1358    pub task_id: String,
1359    #[serde(default)]
1360    pub activity_attempt_id: Option<String>,
1361    #[serde(default)]
1362    pub attempt_id: Option<String>,
1363    pub activity_type: String,
1364    #[serde(default = "default_payload_codec")]
1365    pub payload_codec: String,
1366    #[serde(default)]
1367    pub arguments: Option<Value>,
1368    #[serde(default = "default_attempt_number")]
1369    pub attempt_number: u64,
1370    #[serde(default)]
1371    pub lease_owner: Option<String>,
1372}
1373
1374#[derive(Clone, Debug, Deserialize)]
1375pub struct HistoryEvent {
1376    #[serde(alias = "type")]
1377    pub event_type: String,
1378    #[serde(default)]
1379    pub payload: Value,
1380    #[serde(flatten)]
1381    pub raw: HashMap<String, Value>,
1382}
1383
1384/// One decoded signal in the committed workflow-history snapshot.
1385#[derive(Clone, Debug, PartialEq)]
1386pub struct QuerySignal {
1387    pub id: Option<String>,
1388    pub name: String,
1389    pub arguments: Vec<Value>,
1390    pub workflow_sequence: Option<u64>,
1391}
1392
1393/// Immutable state supplied to a registered query handler.
1394///
1395/// This context intentionally exposes no activity, signal-wait, or command
1396/// APIs. Query handlers inspect committed history and return a value; query
1397/// completion does not append an event or advance deterministic execution.
1398#[derive(Clone, Debug)]
1399pub struct QueryContext {
1400    pub workflow_id: Option<String>,
1401    pub run_id: Option<String>,
1402    pub workflow_type: String,
1403    pub run_status: Option<String>,
1404    workflow_input: Value,
1405    history_events: Arc<Vec<HistoryEvent>>,
1406    signal_events: Arc<Vec<QuerySignal>>,
1407}
1408
1409impl QueryContext {
1410    /// The normalized argument list used to start the workflow.
1411    pub fn workflow_input(&self) -> &Value {
1412        &self.workflow_input
1413    }
1414
1415    /// The immutable committed history used for this query snapshot.
1416    pub fn history_events(&self) -> &[HistoryEvent] {
1417        self.history_events.as_slice()
1418    }
1419
1420    /// All decoded signals in committed workflow order.
1421    pub fn signal_events(&self) -> &[QuerySignal] {
1422        self.signal_events.as_slice()
1423    }
1424
1425    /// Decoded argument lists for each committed signal with `signal_name`.
1426    pub fn signals(&self, signal_name: &str) -> Vec<Vec<Value>> {
1427        self.signal_events
1428            .iter()
1429            .filter(|signal| signal.name == signal_name)
1430            .map(|signal| signal.arguments.clone())
1431            .collect()
1432    }
1433}
1434
1435#[derive(Clone, Debug, Deserialize)]
1436pub struct ActivityHeartbeatResponse {
1437    #[serde(default)]
1438    pub cancel_requested: bool,
1439    #[serde(default)]
1440    pub heartbeat_recorded: bool,
1441}
1442
1443fn default_payload_codec() -> String {
1444    DEFAULT_CODEC.to_string()
1445}
1446
1447fn default_workflow_task_attempt() -> u64 {
1448    1
1449}
1450
1451fn default_attempt_number() -> u64 {
1452    1
1453}
1454
1455type WorkflowFuture = Pin<Box<dyn Future<Output = Result<Value>> + Send + 'static>>;
1456type WorkflowHandler = Arc<dyn Fn(WorkflowContext, Value) -> WorkflowFuture + Send + Sync>;
1457type ErasedWorkflowState = Arc<dyn Any + Send + Sync>;
1458type WorkflowStateSnapshot = Arc<dyn Fn() -> Result<ErasedWorkflowState> + Send + Sync>;
1459type ReplayedWorkflowHandler =
1460    Arc<dyn Fn(WorkflowContext, Value) -> ReplayedWorkflowInvocation + Send + Sync>;
1461type ActivityFuture = Pin<Box<dyn Future<Output = Result<Value>> + Send + 'static>>;
1462type ActivityHandler = Arc<dyn Fn(ActivityContext, Value) -> ActivityFuture + Send + Sync>;
1463type QueryFuture = Pin<Box<dyn Future<Output = Result<Value>> + Send + 'static>>;
1464type QueryHandler = Arc<dyn Fn(QueryContext, Value) -> QueryFuture + Send + Sync>;
1465type ReplayedQueryHandler = Arc<
1466    dyn Fn(QueryContext, ErasedWorkflowState, Value) -> std::result::Result<QueryFuture, String>
1467        + Send
1468        + Sync,
1469>;
1470type WorkerHeartbeatObserver = Arc<dyn Fn(&WorkerHeartbeatObservation) + Send + Sync>;
1471
1472struct ReplayedWorkflowInvocation {
1473    future: WorkflowFuture,
1474    snapshot: WorkflowStateSnapshot,
1475}
1476
1477#[derive(Clone)]
1478struct RegisteredWorkflow {
1479    execute: WorkflowHandler,
1480    replay: Option<ReplayedWorkflowHandler>,
1481    state_type: Option<TypeId>,
1482}
1483
1484#[derive(Clone)]
1485enum RegisteredQuery {
1486    Snapshot(QueryHandler),
1487    Replayed {
1488        state_type: TypeId,
1489        handler: ReplayedQueryHandler,
1490    },
1491}
1492
1493#[derive(Clone, Debug)]
1494pub struct WorkerHeartbeatObservation {
1495    pub worker_id: String,
1496    pub task_queue: String,
1497    pub acknowledged_at_unix_millis: u64,
1498    pub acknowledgement: Value,
1499}
1500
1501/// Bounded retry policy for worker poll acquisition and worker heartbeats.
1502///
1503/// Expected empty long polls are normal successful responses. Transport
1504/// failures, HTTP 408/429 responses, and server errors are retried with capped
1505/// exponential backoff. Authentication, protocol, codec, and handler failures
1506/// are never retried by the worker.
1507#[derive(Clone, Copy, Debug)]
1508pub struct WorkerRetryPolicy {
1509    /// Number of retries after the initial request fails.
1510    pub max_retries: usize,
1511    /// Delay before the first retry.
1512    pub initial_backoff: Duration,
1513    /// Maximum delay between retries.
1514    pub max_backoff: Duration,
1515}
1516
1517impl Default for WorkerRetryPolicy {
1518    fn default() -> Self {
1519        Self {
1520            max_retries: 5,
1521            initial_backoff: Duration::from_millis(100),
1522            max_backoff: Duration::from_secs(5),
1523        }
1524    }
1525}
1526
1527#[derive(Clone)]
1528pub struct Worker {
1529    client: Client,
1530    worker_id: String,
1531    task_queue: String,
1532    workflows: HashMap<String, RegisteredWorkflow>,
1533    activities: HashMap<String, ActivityHandler>,
1534    queries: HashMap<String, HashMap<String, RegisteredQuery>>,
1535    max_concurrent_workflow_tasks: usize,
1536    max_concurrent_activity_tasks: usize,
1537    poll_timeout: Duration,
1538    heartbeat_interval: Duration,
1539    retry_policy: WorkerRetryPolicy,
1540    heartbeat_observer: Option<WorkerHeartbeatObserver>,
1541}
1542
1543impl Worker {
1544    pub fn new(client: Client, task_queue: impl Into<String>) -> Self {
1545        Self {
1546            client,
1547            worker_id: default_worker_id(),
1548            task_queue: task_queue.into(),
1549            workflows: HashMap::new(),
1550            activities: HashMap::new(),
1551            queries: HashMap::new(),
1552            max_concurrent_workflow_tasks: 10,
1553            max_concurrent_activity_tasks: 10,
1554            poll_timeout: Duration::from_secs(30),
1555            heartbeat_interval: Duration::from_secs(60),
1556            retry_policy: WorkerRetryPolicy::default(),
1557            heartbeat_observer: None,
1558        }
1559    }
1560
1561    pub fn worker_id(mut self, worker_id: impl Into<String>) -> Self {
1562        self.worker_id = worker_id.into();
1563        self
1564    }
1565
1566    pub fn poll_timeout(mut self, timeout: Duration) -> Self {
1567        self.poll_timeout = timeout;
1568        self
1569    }
1570
1571    pub fn heartbeat_interval(mut self, interval: Duration) -> Self {
1572        self.heartbeat_interval = interval;
1573        self
1574    }
1575
1576    /// Configure bounded retries for task-poll acquisition and worker heartbeats.
1577    pub fn retry_policy(mut self, policy: WorkerRetryPolicy) -> Self {
1578        self.retry_policy = policy;
1579        self
1580    }
1581
1582    pub fn on_worker_heartbeat<F>(mut self, observer: F) -> Self
1583    where
1584        F: Fn(&WorkerHeartbeatObservation) + Send + Sync + 'static,
1585    {
1586        self.heartbeat_observer = Some(Arc::new(observer));
1587        self
1588    }
1589
1590    pub fn max_concurrent_workflow_tasks(mut self, count: usize) -> Self {
1591        self.max_concurrent_workflow_tasks = count.max(1);
1592        self
1593    }
1594
1595    pub fn max_concurrent_activity_tasks(mut self, count: usize) -> Self {
1596        self.max_concurrent_activity_tasks = count.max(1);
1597        self
1598    }
1599
1600    pub fn register_workflow<F, Fut>(&mut self, workflow_type: impl Into<String>, handler: F)
1601    where
1602        F: Fn(WorkflowContext, Value) -> Fut + Send + Sync + 'static,
1603        Fut: Future<Output = Result<Value>> + Send + 'static,
1604    {
1605        self.workflows.insert(
1606            workflow_type.into(),
1607            RegisteredWorkflow {
1608                execute: Arc::new(move |ctx, input| Box::pin(handler(ctx, input))),
1609                replay: None,
1610                state_type: None,
1611            },
1612        );
1613    }
1614
1615    /// Register a workflow whose typed instance state can be reconstructed for queries.
1616    ///
1617    /// `state_factory` creates a fresh instance for every normal workflow task and
1618    /// query replay. The workflow handler is the single source of truth for state
1619    /// transitions: it updates [`WorkflowInstance`] after activities and signals
1620    /// resolve. Query replay runs this same handler over committed history and
1621    /// discards any commands it would emit.
1622    pub fn register_replayed_workflow<S, Factory, F, Fut>(
1623        &mut self,
1624        workflow_type: impl Into<String>,
1625        state_factory: Factory,
1626        handler: F,
1627    ) where
1628        S: Clone + Send + Sync + 'static,
1629        Factory: Fn() -> S + Send + Sync + 'static,
1630        F: Fn(WorkflowContext, Value, WorkflowInstance<S>) -> Fut + Send + Sync + 'static,
1631        Fut: Future<Output = Result<Value>> + Send + 'static,
1632    {
1633        let state_factory = Arc::new(state_factory);
1634        let handler = Arc::new(handler);
1635
1636        let execute_factory = Arc::clone(&state_factory);
1637        let execute_handler = Arc::clone(&handler);
1638        let execute = Arc::new(move |ctx: WorkflowContext, input: Value| {
1639            let state = WorkflowInstance::new(execute_factory());
1640            let future = execute_handler(ctx, input, state);
1641            Box::pin(future) as WorkflowFuture
1642        });
1643
1644        let replay = Arc::new(move |ctx: WorkflowContext, input: Value| {
1645            let state = WorkflowInstance::new(state_factory());
1646            let snapshot_state = state.clone();
1647            let snapshot: WorkflowStateSnapshot =
1648                Arc::new(move || Ok(Arc::new(snapshot_state.snapshot()?) as ErasedWorkflowState));
1649            let future = handler(ctx, input, state);
1650            ReplayedWorkflowInvocation {
1651                future: Box::pin(future),
1652                snapshot,
1653            }
1654        });
1655
1656        self.workflows.insert(
1657            workflow_type.into(),
1658            RegisteredWorkflow {
1659                execute,
1660                replay: Some(replay),
1661                state_type: Some(TypeId::of::<S>()),
1662            },
1663        );
1664    }
1665
1666    pub fn register_activity<F, Fut>(&mut self, activity_type: impl Into<String>, handler: F)
1667    where
1668        F: Fn(ActivityContext, Value) -> Fut + Send + Sync + 'static,
1669        Fut: Future<Output = Result<Value>> + Send + 'static,
1670    {
1671        self.activities.insert(
1672            activity_type.into(),
1673            Arc::new(move |ctx, args| Box::pin(handler(ctx, args))),
1674        );
1675    }
1676
1677    /// Register a named, read-only query handler for a workflow type.
1678    ///
1679    /// The workflow type must also be registered with [`Worker::register_workflow`]
1680    /// before the worker runs. The handler receives only an immutable committed
1681    /// state snapshot and normalized query arguments.
1682    pub fn register_query<F, Fut>(
1683        &mut self,
1684        workflow_type: impl Into<String>,
1685        query_name: impl Into<String>,
1686        handler: F,
1687    ) where
1688        F: Fn(QueryContext, Value) -> Fut + Send + Sync + 'static,
1689        Fut: Future<Output = Result<Value>> + Send + 'static,
1690    {
1691        self.queries
1692            .entry(workflow_type.into())
1693            .or_default()
1694            .insert(
1695                query_name.into(),
1696                RegisteredQuery::Snapshot(Arc::new(move |ctx, args| Box::pin(handler(ctx, args)))),
1697            );
1698    }
1699
1700    /// Register a named query against deterministically replayed instance state.
1701    ///
1702    /// The workflow type must use [`Worker::register_replayed_workflow`] with the
1703    /// same state type `S`. The handler receives an immutable, detached state
1704    /// clone, so successful and failed queries cannot affect workflow execution
1705    /// or the state reconstructed by a later query.
1706    pub fn register_replayed_query<S, F, Fut>(
1707        &mut self,
1708        workflow_type: impl Into<String>,
1709        query_name: impl Into<String>,
1710        handler: F,
1711    ) where
1712        S: Clone + Send + Sync + 'static,
1713        F: Fn(QueryContext, Arc<S>, Value) -> Fut + Send + Sync + 'static,
1714        Fut: Future<Output = Result<Value>> + Send + 'static,
1715    {
1716        let handler = Arc::new(handler);
1717        let erased_handler: ReplayedQueryHandler = Arc::new(move |ctx, state, args| {
1718            let state = state.downcast::<S>().map_err(|_| {
1719                "registered query state type does not match the replayed workflow state".to_string()
1720            })?;
1721            Ok(Box::pin(handler(ctx, state, args)))
1722        });
1723
1724        self.queries
1725            .entry(workflow_type.into())
1726            .or_default()
1727            .insert(
1728                query_name.into(),
1729                RegisteredQuery::Replayed {
1730                    state_type: TypeId::of::<S>(),
1731                    handler: erased_handler,
1732                },
1733            );
1734    }
1735
1736    pub async fn register(&self) -> Result<RegisterWorkerResponse> {
1737        self.client
1738            .register_worker_with_capabilities(
1739                &self.worker_id,
1740                &self.task_queue,
1741                self.workflows.keys().cloned().collect(),
1742                self.activities.keys().cloned().collect(),
1743                self.max_concurrent_workflow_tasks,
1744                self.max_concurrent_activity_tasks,
1745                (!self.queries.is_empty())
1746                    .then(|| QUERY_TASKS_CAPABILITY.to_string())
1747                    .into_iter()
1748                    .collect(),
1749            )
1750            .await
1751    }
1752
1753    /// Run until shutdown or a terminal worker error occurs.
1754    ///
1755    /// Empty long-poll expirations do not stop the worker. Retryable poll and
1756    /// heartbeat failures use [`WorkerRetryPolicy`] independently, while
1757    /// authentication, protocol, and other non-retryable failures are returned.
1758    pub async fn run(&self) -> Result<()> {
1759        self.run_until(std::future::pending::<()>()).await
1760    }
1761
1762    /// Run until `shutdown` resolves or a terminal worker error occurs.
1763    ///
1764    /// This has the same liveness and terminal-error contract as [`Worker::run`].
1765    pub async fn run_until<F>(&self, shutdown: F) -> Result<()>
1766    where
1767        F: Future<Output = ()>,
1768    {
1769        let registration = self.register().await?;
1770        let mut heartbeat = tokio::time::interval(Duration::from_secs(
1771            registration
1772                .heartbeat_interval_seconds
1773                .unwrap_or(self.heartbeat_interval.as_secs().max(1)),
1774        ));
1775        tokio::pin!(shutdown);
1776        let stop = Arc::new(AtomicBool::new(false));
1777        // Poll responses may already have leased server-side work by the time
1778        // they become ready, so each poller owns its responses through
1779        // completion or failure instead of racing raw polls in this select.
1780        let mut workflow_poller = (!self.workflows.is_empty()).then(|| {
1781            let worker = self.clone();
1782            let stop = Arc::clone(&stop);
1783            tokio::spawn(async move { worker.poll_workflows_until_stopped(stop).await })
1784        });
1785        let mut activity_poller = (!self.activities.is_empty()).then(|| {
1786            let worker = self.clone();
1787            let stop = Arc::clone(&stop);
1788            tokio::spawn(async move { worker.poll_activities_until_stopped(stop).await })
1789        });
1790        let mut query_poller = (!self.queries.is_empty()).then(|| {
1791            let worker = self.clone();
1792            let stop = Arc::clone(&stop);
1793            tokio::spawn(async move { worker.poll_queries_until_stopped(stop).await })
1794        });
1795
1796        loop {
1797            tokio::select! {
1798                _ = &mut shutdown => {
1799                    stop.store(true, Ordering::SeqCst);
1800                    break;
1801                }
1802                _ = heartbeat.tick() => {
1803                    match self.retry_worker_operation(|| {
1804                        self.client.heartbeat_worker(
1805                            &self.worker_id,
1806                            self.max_concurrent_workflow_tasks,
1807                            self.max_concurrent_activity_tasks,
1808                        )
1809                    }).await
1810                    {
1811                        Ok(acknowledgement) => {
1812                            if let Some(observer) = &self.heartbeat_observer {
1813                                observer(&WorkerHeartbeatObservation {
1814                                    worker_id: self.worker_id.clone(),
1815                                    task_queue: self.task_queue.clone(),
1816                                    acknowledged_at_unix_millis: SystemTime::now()
1817                                        .duration_since(UNIX_EPOCH)
1818                                        .unwrap_or_default()
1819                                        .as_millis()
1820                                        .min(u64::MAX as u128)
1821                                        as u64,
1822                                    acknowledgement,
1823                                });
1824                            }
1825                        }
1826                        Err(error) => {
1827                            stop.store(true, Ordering::SeqCst);
1828                            join_pollers(workflow_poller.take(), activity_poller.take(), query_poller.take()).await?;
1829                            return Err(error);
1830                        }
1831                    }
1832                }
1833                result = OptionFuture::from(workflow_poller.as_mut()), if workflow_poller.is_some() => {
1834                    workflow_poller = None;
1835                    stop.store(true, Ordering::SeqCst);
1836                    let poller_result = optional_poller_result("workflow", result);
1837                    let join_result =
1838                        join_pollers(workflow_poller.take(), activity_poller.take(), query_poller.take()).await;
1839                    poller_result?;
1840                    join_result?;
1841                    return Err(Error::WorkerLoop(
1842                        "workflow poller stopped unexpectedly".to_string(),
1843                    ));
1844                }
1845                result = OptionFuture::from(activity_poller.as_mut()), if activity_poller.is_some() => {
1846                    activity_poller = None;
1847                    stop.store(true, Ordering::SeqCst);
1848                    let poller_result = optional_poller_result("activity", result);
1849                    let join_result =
1850                        join_pollers(workflow_poller.take(), activity_poller.take(), query_poller.take()).await;
1851                    poller_result?;
1852                    join_result?;
1853                    return Err(Error::WorkerLoop(
1854                        "activity poller stopped unexpectedly".to_string(),
1855                    ));
1856                }
1857                result = OptionFuture::from(query_poller.as_mut()), if query_poller.is_some() => {
1858                    query_poller = None;
1859                    stop.store(true, Ordering::SeqCst);
1860                    let poller_result = optional_poller_result("query", result);
1861                    let join_result =
1862                        join_pollers(workflow_poller.take(), activity_poller.take(), query_poller.take()).await;
1863                    poller_result?;
1864                    join_result?;
1865                    return Err(Error::WorkerLoop(
1866                        "query poller stopped unexpectedly".to_string(),
1867                    ));
1868                }
1869            }
1870        }
1871
1872        join_pollers(
1873            workflow_poller.take(),
1874            activity_poller.take(),
1875            query_poller.take(),
1876        )
1877        .await
1878    }
1879
1880    pub async fn run_once(&self) -> Result<usize> {
1881        let mut handled = 0;
1882        if self.poll_workflow_once().await? {
1883            handled += 1;
1884        }
1885        if self.poll_activity_once().await? {
1886            handled += 1;
1887        }
1888        if !self.queries.is_empty() && self.poll_query_once().await? {
1889            handled += 1;
1890        }
1891        Ok(handled)
1892    }
1893
1894    async fn poll_workflow_once(&self) -> Result<bool> {
1895        let Some(task) = self
1896            .retry_worker_operation(|| {
1897                self.client
1898                    .poll_workflow_task(&self.worker_id, &self.task_queue, self.poll_timeout)
1899            })
1900            .await?
1901        else {
1902            return Ok(false);
1903        };
1904
1905        let task_id = task.task_id.clone();
1906        let attempt = task.workflow_task_attempt;
1907        let lease_owner = task
1908            .lease_owner
1909            .clone()
1910            .unwrap_or_else(|| self.worker_id.clone());
1911
1912        match self.execute_workflow_task(task) {
1913            Ok(commands) => {
1914                self.client
1915                    .complete_workflow_task(&task_id, &lease_owner, attempt, commands)
1916                    .await?;
1917            }
1918            Err(error) => {
1919                self.client
1920                    .fail_workflow_task(&task_id, &lease_owner, attempt, error.to_string())
1921                    .await?;
1922            }
1923        }
1924
1925        Ok(true)
1926    }
1927
1928    async fn poll_workflows_until_stopped(self, stop: Arc<AtomicBool>) -> Result<()> {
1929        while !stop.load(Ordering::SeqCst) {
1930            self.poll_workflow_once().await?;
1931        }
1932
1933        Ok(())
1934    }
1935
1936    async fn poll_activity_once(&self) -> Result<bool> {
1937        let Some(task) = self
1938            .retry_worker_operation(|| {
1939                self.client
1940                    .poll_activity_task(&self.worker_id, &self.task_queue, self.poll_timeout)
1941            })
1942            .await?
1943        else {
1944            return Ok(false);
1945        };
1946
1947        let task_id = task.task_id.clone();
1948        let attempt_id = task
1949            .activity_attempt_id
1950            .clone()
1951            .or(task.attempt_id.clone())
1952            .unwrap_or_default();
1953        let lease_owner = task
1954            .lease_owner
1955            .clone()
1956            .unwrap_or_else(|| self.worker_id.clone());
1957        let codec = task.payload_codec.clone();
1958        let result = self.execute_activity_task(task).await;
1959        match result {
1960            Ok(value) => {
1961                self.client
1962                    .complete_activity_task(&task_id, &attempt_id, &lease_owner, value, &codec)
1963                    .await?;
1964            }
1965            Err(error) => {
1966                self.client
1967                    .fail_activity_task(
1968                        &task_id,
1969                        &attempt_id,
1970                        &lease_owner,
1971                        error.to_string(),
1972                        false,
1973                    )
1974                    .await?;
1975            }
1976        }
1977
1978        Ok(true)
1979    }
1980
1981    async fn poll_activities_until_stopped(self, stop: Arc<AtomicBool>) -> Result<()> {
1982        while !stop.load(Ordering::SeqCst) {
1983            self.poll_activity_once().await?;
1984        }
1985
1986        Ok(())
1987    }
1988
1989    async fn poll_query_once(&self) -> Result<bool> {
1990        let Some(task) = self
1991            .retry_worker_operation(|| {
1992                self.client
1993                    .poll_query_task(&self.worker_id, &self.task_queue, self.poll_timeout)
1994            })
1995            .await?
1996        else {
1997            return Ok(false);
1998        };
1999
2000        let query_task_id = task.query_task_id.clone();
2001        let attempt = task.query_task_attempt;
2002        let lease_owner = task
2003            .lease_owner
2004            .clone()
2005            .unwrap_or_else(|| self.worker_id.clone());
2006        let codec = task.payload_codec.clone();
2007
2008        match self.execute_query_task(task).await {
2009            Ok(value) => {
2010                let result_envelope = match encode_value_envelope(&value, &codec) {
2011                    Ok(result_envelope) => result_envelope,
2012                    Err(error) => {
2013                        let failure = self
2014                            .client
2015                            .fail_query_task(
2016                                &query_task_id,
2017                                &lease_owner,
2018                                attempt,
2019                                error.to_string(),
2020                                "query_result_encode_failed",
2021                                "QueryResultEncodeFailed",
2022                            )
2023                            .await;
2024                        if let Err(error) = failure {
2025                            if !query_task_rejection_is_final(&error) {
2026                                return Err(error);
2027                            }
2028                        }
2029                        return Ok(true);
2030                    }
2031                };
2032
2033                if let Err(error) = self
2034                    .client
2035                    .complete_query_task_with_envelope(
2036                        &query_task_id,
2037                        &lease_owner,
2038                        attempt,
2039                        value,
2040                        result_envelope,
2041                    )
2042                    .await
2043                {
2044                    if !query_task_rejection_is_final(&error) {
2045                        return Err(error);
2046                    }
2047                }
2048            }
2049            Err(failure) => {
2050                let result = self
2051                    .client
2052                    .fail_query_task(
2053                        &query_task_id,
2054                        &lease_owner,
2055                        attempt,
2056                        failure.message,
2057                        failure.reason,
2058                        failure.failure_type,
2059                    )
2060                    .await;
2061                if let Err(error) = result {
2062                    if !query_task_rejection_is_final(&error) {
2063                        return Err(error);
2064                    }
2065                }
2066            }
2067        }
2068
2069        Ok(true)
2070    }
2071
2072    async fn poll_queries_until_stopped(self, stop: Arc<AtomicBool>) -> Result<()> {
2073        while !stop.load(Ordering::SeqCst) {
2074            self.poll_query_once().await?;
2075        }
2076
2077        Ok(())
2078    }
2079
2080    async fn retry_worker_operation<T, F, Fut>(&self, mut operation: F) -> Result<T>
2081    where
2082        F: FnMut() -> Fut,
2083        Fut: Future<Output = Result<T>>,
2084    {
2085        let mut retries = 0;
2086
2087        loop {
2088            match operation().await {
2089                Err(error)
2090                    if worker_operation_is_retryable(&error)
2091                        && retries < self.retry_policy.max_retries =>
2092                {
2093                    retries += 1;
2094                    tokio::time::sleep(worker_retry_delay(self.retry_policy, retries)).await;
2095                }
2096                result => return result,
2097            }
2098        }
2099    }
2100
2101    async fn execute_query_task(
2102        &self,
2103        mut task: QueryTask,
2104    ) -> std::result::Result<Value, QueryTaskExecutionFailure> {
2105        if !matches!(task.payload_codec.as_str(), DEFAULT_CODEC | JSON_CODEC) {
2106            return Err(QueryTaskExecutionFailure::new(
2107                "query_payload_decode_failed",
2108                format!(
2109                    "cannot decode query payload with unsupported codec {:?}",
2110                    task.payload_codec
2111                ),
2112                "QueryPayloadDecodeFailed",
2113            ));
2114        }
2115
2116        if !self.workflows.contains_key(&task.workflow_type) {
2117            return Err(QueryTaskExecutionFailure::new(
2118                "query_workflow_type_not_registered",
2119                format!("no workflow registered for type {:?}", task.workflow_type),
2120                "WorkflowTypeNotRegistered",
2121            ));
2122        }
2123
2124        let Some(handlers) = self.queries.get(&task.workflow_type) else {
2125            return Err(QueryTaskExecutionFailure::new(
2126                "query_handler_unavailable",
2127                format!(
2128                    "query handlers are unavailable for workflow type {:?}",
2129                    task.workflow_type
2130                ),
2131                "QueryHandlerUnavailable",
2132            ));
2133        };
2134        let Some(query) = handlers.get(&task.query_name) else {
2135            return Err(QueryTaskExecutionFailure::new(
2136                "rejected_unknown_query",
2137                format!("unknown query {:?}", task.query_name),
2138                "QueryFailed",
2139            ));
2140        };
2141
2142        let args = decode_task_arguments(task.query_arguments.as_ref(), &task.payload_codec)
2143            .map_err(|error| {
2144                QueryTaskExecutionFailure::new(
2145                    "query_payload_decode_failed",
2146                    format!("cannot decode query arguments: {error}"),
2147                    "QueryPayloadDecodeFailed",
2148                )
2149            })?;
2150        let workflow_input =
2151            decode_task_arguments(task.workflow_arguments.as_ref(), &task.payload_codec).map_err(
2152                |error| {
2153                    QueryTaskExecutionFailure::new(
2154                        "query_workflow_state_unavailable",
2155                        format!("cannot decode workflow start input: {error}"),
2156                        "QueryWorkflowStateUnavailable",
2157                    )
2158                },
2159            )?;
2160        hydrate_query_history_from_export(&mut task).map_err(|error| {
2161            QueryTaskExecutionFailure::new(
2162                "query_workflow_state_unavailable",
2163                format!("cannot restore query history snapshot: {error}"),
2164                "QueryWorkflowStateUnavailable",
2165            )
2166        })?;
2167        enrich_query_history_from_export(&mut task).map_err(|error| {
2168            QueryTaskExecutionFailure::new(
2169                "query_workflow_state_unavailable",
2170                format!("cannot restore compact query history payloads: {error}"),
2171                "QueryWorkflowStateUnavailable",
2172            )
2173        })?;
2174        let signal_events = query_signal_events(&task).map_err(|error| {
2175            QueryTaskExecutionFailure::new(
2176                "query_workflow_state_unavailable",
2177                format!("cannot decode committed workflow signals: {error}"),
2178                "QueryWorkflowStateUnavailable",
2179            )
2180        })?;
2181        let history_events = Arc::new(std::mem::take(&mut task.history_events));
2182        let context = QueryContext {
2183            workflow_id: task.workflow_id,
2184            run_id: task.run_id,
2185            workflow_type: task.workflow_type.clone(),
2186            run_status: task.run_status,
2187            workflow_input,
2188            history_events: Arc::clone(&history_events),
2189            signal_events: Arc::new(signal_events),
2190        };
2191
2192        let future = match query {
2193            RegisteredQuery::Snapshot(handler) => handler(context, args),
2194            RegisteredQuery::Replayed {
2195                state_type,
2196                handler,
2197            } => {
2198                let workflow = self
2199                    .workflows
2200                    .get(&task.workflow_type)
2201                    .expect("workflow registration was checked above");
2202                if workflow.state_type != Some(*state_type) {
2203                    return Err(QueryTaskExecutionFailure::new(
2204                        "query_workflow_state_unavailable",
2205                        "replayed query state type does not match its workflow registration",
2206                        "QueryWorkflowStateUnavailable",
2207                    ));
2208                }
2209                let replay = workflow.replay.as_ref().ok_or_else(|| {
2210                    QueryTaskExecutionFailure::new(
2211                        "query_workflow_state_unavailable",
2212                        format!(
2213                            "workflow type {:?} is not registered for instance-state replay",
2214                            task.workflow_type
2215                        ),
2216                        "QueryWorkflowStateUnavailable",
2217                    )
2218                })?;
2219                let workflow_state = Arc::new(Mutex::new(
2220                    WorkflowState::new(
2221                        history_events.as_ref().clone(),
2222                        self.task_queue.clone(),
2223                        task.payload_codec,
2224                        None,
2225                    )
2226                    .map_err(|error| {
2227                        QueryTaskExecutionFailure::new(
2228                            "query_workflow_state_unavailable",
2229                            format!("workflow replay failed before query: {error}"),
2230                            "QueryWorkflowStateUnavailable",
2231                        )
2232                    })?,
2233                ));
2234                let workflow_context = WorkflowContext {
2235                    state: workflow_state,
2236                };
2237                let mut invocation =
2238                    replay(workflow_context.clone(), context.workflow_input.clone());
2239                let mut cx = TaskContext::from_waker(noop_waker_ref());
2240                match invocation.future.as_mut().poll(&mut cx) {
2241                    Poll::Ready(Ok(_)) => {
2242                        workflow_context
2243                            .ensure_history_consumed()
2244                            .map_err(|error| {
2245                                QueryTaskExecutionFailure::new(
2246                                    "query_workflow_state_unavailable",
2247                                    format!("workflow replay failed before query: {error}"),
2248                                    "QueryWorkflowStateUnavailable",
2249                                )
2250                            })?;
2251                    }
2252                    Poll::Ready(Err(error)) => {
2253                        return Err(QueryTaskExecutionFailure::new(
2254                            "query_workflow_state_unavailable",
2255                            format!("workflow replay failed before query: {error}"),
2256                            "QueryWorkflowStateUnavailable",
2257                        ));
2258                    }
2259                    Poll::Pending => {
2260                        let commands = workflow_context.take_commands().map_err(|error| {
2261                            QueryTaskExecutionFailure::new(
2262                                "query_workflow_state_unavailable",
2263                                format!("workflow replay failed before query: {error}"),
2264                                "QueryWorkflowStateUnavailable",
2265                            )
2266                        })?;
2267                        if commands.is_empty()
2268                            && !workflow_context
2269                                .matched_recorded_pending()
2270                                .map_err(|error| {
2271                                    QueryTaskExecutionFailure::new(
2272                                        "query_workflow_state_unavailable",
2273                                        format!("workflow replay failed before query: {error}"),
2274                                        "QueryWorkflowStateUnavailable",
2275                                    )
2276                                })?
2277                        {
2278                            return Err(QueryTaskExecutionFailure::new(
2279                                "query_workflow_state_unavailable",
2280                                "workflow replay yielded without a durable command",
2281                                "QueryWorkflowStateUnavailable",
2282                            ));
2283                        }
2284                    }
2285                }
2286                let state = (invocation.snapshot)().map_err(|error| {
2287                    QueryTaskExecutionFailure::new(
2288                        "query_workflow_state_unavailable",
2289                        format!("cannot snapshot replayed workflow state: {error}"),
2290                        "QueryWorkflowStateUnavailable",
2291                    )
2292                })?;
2293                handler(context, state, args).map_err(|message| {
2294                    QueryTaskExecutionFailure::new(
2295                        "query_workflow_state_unavailable",
2296                        message,
2297                        "QueryWorkflowStateUnavailable",
2298                    )
2299                })?
2300            }
2301        };
2302
2303        future.await.map_err(|error| {
2304            QueryTaskExecutionFailure::new("query_rejected", error.to_string(), "QueryFailed")
2305        })
2306    }
2307
2308    fn execute_workflow_task(&self, task: WorkflowTask) -> Result<Vec<Value>> {
2309        let workflow = self
2310            .workflows
2311            .get(&task.workflow_type)
2312            .ok_or_else(|| Error::WorkflowNotRegistered(task.workflow_type.clone()))?;
2313        let input = decode_task_arguments(task.arguments.as_ref(), &task.payload_codec)?;
2314        let resume_signal = decode_resume_signal(&task)?;
2315        let state = Arc::new(Mutex::new(WorkflowState::new(
2316            task.history_events,
2317            self.task_queue.clone(),
2318            task.payload_codec.clone(),
2319            resume_signal,
2320        )?));
2321        let ctx = WorkflowContext { state };
2322        let mut future = (workflow.execute)(ctx.clone(), input);
2323        let mut cx = TaskContext::from_waker(noop_waker_ref());
2324
2325        match future.as_mut().poll(&mut cx) {
2326            Poll::Ready(Ok(result)) => {
2327                ctx.ensure_history_consumed()?;
2328                let result = encode_value_envelope(&result, &task.payload_codec)?;
2329                Ok(vec![json!({
2330                    "type": "complete_workflow",
2331                    "result": result
2332                })])
2333            }
2334            Poll::Ready(Err(error)) => Err(error),
2335            Poll::Pending => {
2336                let commands = ctx.take_commands()?;
2337                if commands.is_empty() && !ctx.matched_recorded_pending()? {
2338                    Err(Error::WorkflowYieldedWithoutCommand)
2339                } else {
2340                    Ok(commands)
2341                }
2342            }
2343        }
2344    }
2345
2346    async fn execute_activity_task(&self, task: ActivityTask) -> Result<Value> {
2347        let handler = self
2348            .activities
2349            .get(&task.activity_type)
2350            .ok_or_else(|| Error::ActivityNotRegistered(task.activity_type.clone()))?;
2351        let args = decode_task_arguments(task.arguments.as_ref(), &task.payload_codec)?;
2352        let attempt_id = task
2353            .activity_attempt_id
2354            .clone()
2355            .or(task.attempt_id.clone())
2356            .unwrap_or_default();
2357        let lease_owner = task
2358            .lease_owner
2359            .clone()
2360            .unwrap_or_else(|| self.worker_id.clone());
2361        let ctx = ActivityContext {
2362            client: self.client.clone(),
2363            task_id: task.task_id,
2364            activity_attempt_id: attempt_id,
2365            lease_owner,
2366            activity_type: task.activity_type,
2367            attempt_number: task.attempt_number,
2368            task_queue: self.task_queue.clone(),
2369            worker_id: self.worker_id.clone(),
2370        };
2371
2372        handler(ctx, args).await
2373    }
2374}
2375
2376fn poller_result(
2377    kind: &str,
2378    result: std::result::Result<Result<()>, tokio::task::JoinError>,
2379) -> Result<()> {
2380    match result {
2381        Ok(result) => result,
2382        Err(error) => Err(Error::WorkerLoop(format!(
2383            "{kind} poller join error: {error}"
2384        ))),
2385    }
2386}
2387
2388fn optional_poller_result(
2389    kind: &str,
2390    result: Option<std::result::Result<Result<()>, tokio::task::JoinError>>,
2391) -> Result<()> {
2392    match result {
2393        Some(result) => poller_result(kind, result),
2394        None => Ok(()),
2395    }
2396}
2397
2398async fn join_pollers(
2399    workflow_poller: Option<tokio::task::JoinHandle<Result<()>>>,
2400    activity_poller: Option<tokio::task::JoinHandle<Result<()>>>,
2401    query_poller: Option<tokio::task::JoinHandle<Result<()>>>,
2402) -> Result<()> {
2403    let mut first_error = None;
2404
2405    if let Some(handle) = workflow_poller {
2406        if let Err(error) = poller_result("workflow", handle.await) {
2407            first_error.get_or_insert(error);
2408        }
2409    }
2410
2411    if let Some(handle) = activity_poller {
2412        if let Err(error) = poller_result("activity", handle.await) {
2413            first_error.get_or_insert(error);
2414        }
2415    }
2416
2417    if let Some(handle) = query_poller {
2418        if let Err(error) = poller_result("query", handle.await) {
2419            first_error.get_or_insert(error);
2420        }
2421    }
2422
2423    if let Some(error) = first_error {
2424        Err(error)
2425    } else {
2426        Ok(())
2427    }
2428}
2429
2430fn default_worker_id() -> String {
2431    let millis = SystemTime::now()
2432        .duration_since(UNIX_EPOCH)
2433        .unwrap_or_default()
2434        .as_millis();
2435    format!("rust-worker-{}-{millis}", std::process::id())
2436}
2437
2438fn unique_request_id(prefix: &str) -> String {
2439    let nanos = SystemTime::now()
2440        .duration_since(UNIX_EPOCH)
2441        .unwrap_or_default()
2442        .as_nanos();
2443    format!("{prefix}-{}-{nanos}", std::process::id())
2444}
2445
2446#[derive(Debug)]
2447struct QueryTaskExecutionFailure {
2448    reason: String,
2449    message: String,
2450    failure_type: String,
2451}
2452
2453impl QueryTaskExecutionFailure {
2454    fn new(
2455        reason: impl Into<String>,
2456        message: impl Into<String>,
2457        failure_type: impl Into<String>,
2458    ) -> Self {
2459        Self {
2460            reason: reason.into(),
2461            message: message.into(),
2462            failure_type: failure_type.into(),
2463        }
2464    }
2465}
2466
2467/// Typed local state owned by one deterministic workflow invocation.
2468///
2469/// Use [`WorkflowInstance::update`] for the same state transitions during
2470/// ordinary execution and replay. A replayed query receives a detached
2471/// immutable `Arc<S>` rather than this mutation-capable handle.
2472#[derive(Clone, Debug)]
2473pub struct WorkflowInstance<S> {
2474    state: Arc<Mutex<S>>,
2475}
2476
2477impl<S> WorkflowInstance<S> {
2478    fn new(state: S) -> Self {
2479        Self {
2480            state: Arc::new(Mutex::new(state)),
2481        }
2482    }
2483
2484    /// Read the current workflow-instance state without changing it.
2485    pub fn read<R>(&self, reader: impl FnOnce(&S) -> R) -> Result<R> {
2486        let state = self
2487            .state
2488            .lock()
2489            .map_err(|_| Error::WorkflowStatePoisoned)?;
2490        Ok(reader(&state))
2491    }
2492
2493    /// Apply one deterministic workflow-instance state transition.
2494    pub fn update<R>(&self, transition: impl FnOnce(&mut S) -> R) -> Result<R> {
2495        let mut state = self
2496            .state
2497            .lock()
2498            .map_err(|_| Error::WorkflowStatePoisoned)?;
2499        Ok(transition(&mut state))
2500    }
2501}
2502
2503impl<S: Clone> WorkflowInstance<S> {
2504    fn snapshot(&self) -> Result<S> {
2505        self.read(Clone::clone)
2506    }
2507}
2508
2509#[derive(Clone, Debug)]
2510pub struct WorkflowContext {
2511    state: Arc<Mutex<WorkflowState>>,
2512}
2513
2514impl WorkflowContext {
2515    pub fn activity<T: Serialize>(
2516        &self,
2517        activity_type: impl Into<String>,
2518        args: T,
2519    ) -> ActivityCall {
2520        self.activity_on_queue(activity_type, None::<String>, args)
2521    }
2522
2523    pub fn activity_on_queue<T, Q>(
2524        &self,
2525        activity_type: impl Into<String>,
2526        task_queue: Option<Q>,
2527        args: T,
2528    ) -> ActivityCall
2529    where
2530        T: Serialize,
2531        Q: Into<String>,
2532    {
2533        ActivityCall {
2534            ctx: self.clone(),
2535            activity_type: activity_type.into(),
2536            task_queue: task_queue.map(Into::into),
2537            args: Some(serde_json::to_value(args).map_err(Error::from)),
2538            scheduled: false,
2539        }
2540    }
2541
2542    pub fn wait_signal(&self, signal_name: impl Into<String>) -> SignalCall {
2543        SignalCall {
2544            ctx: self.clone(),
2545            signal_name: signal_name.into(),
2546            opened_wait: false,
2547            matched_pending: false,
2548        }
2549    }
2550
2551    /// Wait for server-backed durable time without blocking the worker executor.
2552    ///
2553    /// Polling this future emits one `start_timer` command and yields. The
2554    /// server records the deadline, so neither worker nor server restarts reset
2555    /// the wait. Replay resolves the future only from a `TimerScheduled` and
2556    /// `TimerFired` pair at the same position in the shared durable-command
2557    /// stream, with matching sequence, timer identity, and delay. Sub-second
2558    /// durations round up because protocol deadlines use whole seconds.
2559    ///
2560    /// ```no_run
2561    /// # use durable_workflow::{json, Client, Worker};
2562    /// # use std::time::Duration;
2563    /// # fn configure(client: Client) {
2564    /// let mut worker = Worker::new(client, "rust-workers");
2565    /// worker.register_workflow("delayed-greeting", |ctx, _input| async move {
2566    ///     ctx.sleep(Duration::from_secs(5)).await?;
2567    ///     Ok(json!({"status": "timer fired"}))
2568    /// });
2569    /// # }
2570    /// ```
2571    pub fn sleep(&self, duration: Duration) -> TimerCall {
2572        let delay_seconds = duration
2573            .as_secs()
2574            .checked_add(u64::from(duration.subsec_nanos() > 0));
2575        TimerCall {
2576            ctx: self.clone(),
2577            delay_seconds,
2578            scheduled: false,
2579            matched_pending: false,
2580        }
2581    }
2582
2583    /// Alias for [`WorkflowContext::sleep`] for timer-oriented workflow code.
2584    pub fn start_timer(&self, duration: Duration) -> TimerCall {
2585        self.sleep(duration)
2586    }
2587
2588    fn take_commands(&self) -> Result<Vec<Value>> {
2589        let mut state = self
2590            .state
2591            .lock()
2592            .map_err(|_| Error::WorkflowStatePoisoned)?;
2593        Ok(std::mem::take(&mut state.commands))
2594    }
2595
2596    fn matched_recorded_pending(&self) -> Result<bool> {
2597        let state = self
2598            .state
2599            .lock()
2600            .map_err(|_| Error::WorkflowStatePoisoned)?;
2601        Ok(state.matched_recorded_pending)
2602    }
2603
2604    fn ensure_history_consumed(&self) -> Result<()> {
2605        let state = self
2606            .state
2607            .lock()
2608            .map_err(|_| Error::WorkflowStatePoisoned)?;
2609        if let Some(command) = state.recorded_commands.get(state.command_cursor) {
2610            return Err(Error::NonDeterministicReplay(ReplayFailure::new(
2611                "recorded_commands_unconsumed",
2612                Some(command.sequence()),
2613                Some(command.shape().to_string()),
2614                Some("workflow completion".to_string()),
2615                "workflow completed before consuming all recorded durable commands",
2616            )));
2617        }
2618        Ok(())
2619    }
2620}
2621
2622#[derive(Debug)]
2623struct WorkflowState {
2624    history: Vec<HistoryEvent>,
2625    task_queue: String,
2626    payload_codec: String,
2627    resume_signal: Option<ResumeSignal>,
2628    recorded_commands: Vec<RecordedCommand>,
2629    command_cursor: usize,
2630    matched_recorded_pending: bool,
2631    signal_cursors: HashMap<String, usize>,
2632    commands: Vec<Value>,
2633}
2634
2635impl WorkflowState {
2636    fn new(
2637        history: Vec<HistoryEvent>,
2638        task_queue: String,
2639        payload_codec: String,
2640        resume_signal: Option<ResumeSignal>,
2641    ) -> Result<Self> {
2642        let recorded_commands = recorded_commands(&history, &payload_codec)?;
2643        Ok(Self {
2644            history,
2645            task_queue,
2646            payload_codec,
2647            resume_signal,
2648            recorded_commands,
2649            command_cursor: 0,
2650            matched_recorded_pending: false,
2651            signal_cursors: HashMap::new(),
2652            commands: Vec::new(),
2653        })
2654    }
2655}
2656
2657#[derive(Clone, Debug)]
2658enum RecordedCommand {
2659    Activity {
2660        sequence: u64,
2661        activity_type: Option<String>,
2662        result: Option<Value>,
2663    },
2664    Timer {
2665        sequence: u64,
2666        delay_seconds: u64,
2667        fired: bool,
2668    },
2669    SignalWait {
2670        sequence: u64,
2671        signal_name: Option<String>,
2672    },
2673}
2674
2675impl RecordedCommand {
2676    fn sequence(&self) -> u64 {
2677        match self {
2678            Self::Activity { sequence, .. }
2679            | Self::Timer { sequence, .. }
2680            | Self::SignalWait { sequence, .. } => *sequence,
2681        }
2682    }
2683
2684    fn shape(&self) -> &'static str {
2685        match self {
2686            Self::Activity { .. } => "activity",
2687            Self::Timer { .. } => "timer",
2688            Self::SignalWait { .. } => "signal wait",
2689        }
2690    }
2691}
2692
2693#[derive(Clone, Debug)]
2694struct ResumeSignal {
2695    signal_id: Option<String>,
2696    signal_name: String,
2697    arguments: Vec<Value>,
2698}
2699
2700pub struct ActivityCall {
2701    ctx: WorkflowContext,
2702    activity_type: String,
2703    task_queue: Option<String>,
2704    args: Option<Result<Value>>,
2705    scheduled: bool,
2706}
2707
2708impl Future for ActivityCall {
2709    type Output = Result<Value>;
2710
2711    fn poll(mut self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
2712        let ctx = self.ctx.clone();
2713        let mut state = match ctx.state.lock() {
2714            Ok(state) => state,
2715            Err(_) => return Poll::Ready(Err(Error::WorkflowStatePoisoned)),
2716        };
2717
2718        if self.scheduled {
2719            return Poll::Pending;
2720        }
2721
2722        if let Some(recorded) = state.recorded_commands.get(state.command_cursor).cloned() {
2723            let sequence = recorded.sequence();
2724            match recorded {
2725                RecordedCommand::Activity {
2726                    activity_type,
2727                    result,
2728                    ..
2729                } => {
2730                    if let Some(recorded_type) = activity_type {
2731                        if recorded_type != self.activity_type {
2732                            return Poll::Ready(Err(Error::NonDeterministicReplay(
2733                                ReplayFailure::new(
2734                                    "recorded_command_detail_mismatch",
2735                                    Some(sequence),
2736                                    Some(format!("activity:{recorded_type}")),
2737                                    Some(format!("activity:{}", self.activity_type)),
2738                                    "recorded activity type differs from the current workflow command",
2739                                ),
2740                            )));
2741                        }
2742                    }
2743                    state.command_cursor += 1;
2744                    if let Some(value) = result {
2745                        return Poll::Ready(Ok(value));
2746                    }
2747                    state.matched_recorded_pending = true;
2748                    self.scheduled = true;
2749                    return Poll::Pending;
2750                }
2751                other => {
2752                    return Poll::Ready(Err(command_mismatch(
2753                        &other,
2754                        format!("activity:{}", self.activity_type),
2755                    )));
2756                }
2757            }
2758        }
2759
2760        if !self.scheduled {
2761            let task_queue = self
2762                .task_queue
2763                .clone()
2764                .unwrap_or_else(|| state.task_queue.clone());
2765            let args = match self.args.take().unwrap_or(Ok(Value::Null)) {
2766                Ok(args) => args,
2767                Err(error) => return Poll::Ready(Err(error)),
2768            };
2769            let arguments = normalize_arguments(args);
2770            let envelope = match encode_value_envelope(&arguments, &state.payload_codec) {
2771                Ok(envelope) => envelope,
2772                Err(error) => return Poll::Ready(Err(error)),
2773            };
2774
2775            state.commands.push(json!({
2776                "type": "schedule_activity",
2777                "activity_type": self.activity_type.clone(),
2778                "queue": task_queue,
2779                "arguments": envelope
2780            }));
2781            self.scheduled = true;
2782        }
2783
2784        Poll::Pending
2785    }
2786}
2787
2788/// Future returned by [`WorkflowContext::sleep`].
2789pub struct TimerCall {
2790    ctx: WorkflowContext,
2791    delay_seconds: Option<u64>,
2792    scheduled: bool,
2793    matched_pending: bool,
2794}
2795
2796impl Future for TimerCall {
2797    type Output = Result<()>;
2798
2799    fn poll(mut self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
2800        if self.matched_pending {
2801            return Poll::Pending;
2802        }
2803
2804        let ctx = self.ctx.clone();
2805        let Some(requested_delay) = self.delay_seconds else {
2806            return Poll::Ready(Err(Error::TimerDurationOverflow));
2807        };
2808        let mut state = match ctx.state.lock() {
2809            Ok(state) => state,
2810            Err(_) => return Poll::Ready(Err(Error::WorkflowStatePoisoned)),
2811        };
2812
2813        if let Some(recorded) = state.recorded_commands.get(state.command_cursor).cloned() {
2814            match recorded {
2815                RecordedCommand::Timer {
2816                    sequence,
2817                    delay_seconds,
2818                    fired,
2819                    ..
2820                } => {
2821                    if delay_seconds != requested_delay {
2822                        return Poll::Ready(Err(Error::NonDeterministicReplay(
2823                            ReplayFailure::new(
2824                                "timer_delay_mismatch",
2825                                Some(sequence),
2826                                Some(format!("timer:{delay_seconds}s")),
2827                                Some(format!("timer:{requested_delay}s")),
2828                                "recorded timer delay differs from the current workflow command",
2829                            ),
2830                        )));
2831                    }
2832                    state.command_cursor += 1;
2833                    if fired {
2834                        return Poll::Ready(Ok(()));
2835                    }
2836                    state.matched_recorded_pending = true;
2837                    self.scheduled = true;
2838                    self.matched_pending = true;
2839                    return Poll::Pending;
2840                }
2841                other => return Poll::Ready(Err(command_mismatch(&other, "timer"))),
2842            }
2843        }
2844
2845        if !self.scheduled {
2846            state.commands.push(json!({
2847                "type": "start_timer",
2848                "delay_seconds": requested_delay,
2849            }));
2850            self.scheduled = true;
2851        }
2852
2853        Poll::Pending
2854    }
2855}
2856
2857fn command_mismatch(recorded: &RecordedCommand, actual: impl Into<String>) -> Error {
2858    Error::NonDeterministicReplay(ReplayFailure::new(
2859        "recorded_command_mismatch",
2860        Some(recorded.sequence()),
2861        Some(recorded.shape().to_string()),
2862        Some(actual.into()),
2863        "current workflow command does not match the recorded durable command sequence",
2864    ))
2865}
2866
2867pub struct SignalCall {
2868    ctx: WorkflowContext,
2869    signal_name: String,
2870    opened_wait: bool,
2871    matched_pending: bool,
2872}
2873
2874impl Future for SignalCall {
2875    type Output = Result<Vec<Value>>;
2876
2877    fn poll(mut self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
2878        if self.matched_pending {
2879            return Poll::Pending;
2880        }
2881
2882        let ctx = self.ctx.clone();
2883        let mut state = match ctx.state.lock() {
2884            Ok(state) => state,
2885            Err(_) => return Poll::Ready(Err(Error::WorkflowStatePoisoned)),
2886        };
2887
2888        let signals = match signal_values(
2889            &state.history,
2890            &self.signal_name,
2891            &state.payload_codec,
2892            state.resume_signal.as_ref(),
2893        ) {
2894            Ok(signals) => signals,
2895            Err(error) => return Poll::Ready(Err(error)),
2896        };
2897        let cursor = *state.signal_cursors.get(&self.signal_name).unwrap_or(&0);
2898
2899        if let Some(recorded) = state.recorded_commands.get(state.command_cursor).cloned() {
2900            match recorded {
2901                RecordedCommand::SignalWait {
2902                    sequence,
2903                    signal_name,
2904                } => {
2905                    if let Some(recorded_name) = signal_name {
2906                        if recorded_name != self.signal_name {
2907                            return Poll::Ready(Err(Error::NonDeterministicReplay(
2908                                ReplayFailure::new(
2909                                    "recorded_command_detail_mismatch",
2910                                    Some(sequence),
2911                                    Some(format!("signal wait:{recorded_name}")),
2912                                    Some(format!("signal wait:{}", self.signal_name)),
2913                                    "recorded signal name differs from the current workflow command",
2914                                ),
2915                            )));
2916                        }
2917                    }
2918
2919                    state.command_cursor += 1;
2920                    if cursor < signals.len() {
2921                        state
2922                            .signal_cursors
2923                            .insert(self.signal_name.clone(), cursor + 1);
2924                        return Poll::Ready(Ok(signals[cursor].clone()));
2925                    }
2926
2927                    state.matched_recorded_pending = true;
2928                    self.opened_wait = true;
2929                    self.matched_pending = true;
2930                    return Poll::Pending;
2931                }
2932                other => {
2933                    return Poll::Ready(Err(command_mismatch(
2934                        &other,
2935                        format!("signal wait:{}", self.signal_name),
2936                    )));
2937                }
2938            }
2939        }
2940
2941        if cursor < signals.len() {
2942            state
2943                .signal_cursors
2944                .insert(self.signal_name.clone(), cursor + 1);
2945            return Poll::Ready(Ok(signals[cursor].clone()));
2946        }
2947
2948        if !self.opened_wait {
2949            state.commands.push(json!({
2950                "type": "open_condition_wait",
2951                "condition_key": format!("signal:{}", self.signal_name)
2952            }));
2953            self.opened_wait = true;
2954        }
2955
2956        Poll::Pending
2957    }
2958}
2959
2960#[derive(Clone, Debug)]
2961pub struct ActivityContext {
2962    client: Client,
2963    pub task_id: String,
2964    pub activity_attempt_id: String,
2965    pub lease_owner: String,
2966    pub activity_type: String,
2967    pub attempt_number: u64,
2968    pub task_queue: String,
2969    pub worker_id: String,
2970}
2971
2972impl ActivityContext {
2973    pub async fn heartbeat<T: Serialize>(&self, details: T) -> Result<ActivityHeartbeatResponse> {
2974        self.client
2975            .heartbeat_activity_task(
2976                &self.task_id,
2977                &self.activity_attempt_id,
2978                &self.lease_owner,
2979                serde_json::to_value(details)?,
2980            )
2981            .await
2982    }
2983}
2984
2985fn decode_task_arguments(value: Option<&Value>, codec: &str) -> Result<Value> {
2986    match value {
2987        Some(value) => Ok(normalize_arguments(decode_wire_value(value, codec)?)),
2988        None => Ok(Value::Array(Vec::new())),
2989    }
2990}
2991
2992fn decode_resume_signal(task: &WorkflowTask) -> Result<Option<ResumeSignal>> {
2993    let Some(signal_name) = task
2994        .signal_name
2995        .as_deref()
2996        .filter(|value| !value.is_empty())
2997    else {
2998        return Ok(None);
2999    };
3000    let Some(arguments) = task.signal_arguments.as_ref() else {
3001        return Ok(None);
3002    };
3003
3004    let decoded = normalize_arguments(decode_wire_value(arguments, &task.payload_codec)?);
3005    let Value::Array(arguments) = decoded else {
3006        unreachable!("normalize_arguments always returns an array");
3007    };
3008
3009    Ok(Some(ResumeSignal {
3010        signal_id: task.workflow_signal_id.clone(),
3011        signal_name: signal_name.to_string(),
3012        arguments,
3013    }))
3014}
3015
3016fn normalize_arguments(value: Value) -> Value {
3017    match value {
3018        Value::Null => Value::Array(Vec::new()),
3019        Value::Array(_) => value,
3020        other => Value::Array(vec![other]),
3021    }
3022}
3023
3024fn recorded_commands(
3025    events: &[HistoryEvent],
3026    fallback_codec: &str,
3027) -> Result<Vec<RecordedCommand>> {
3028    let mut events_by_sequence: BTreeMap<u64, Vec<&HistoryEvent>> = BTreeMap::new();
3029
3030    for event in events {
3031        let is_activity = matches!(
3032            event.event_type.as_str(),
3033            "ActivityScheduled"
3034                | "ActivityStarted"
3035                | "ActivityHeartbeatRecorded"
3036                | "ActivityRetryScheduled"
3037                | "ActivityCompleted"
3038                | "ActivityFailed"
3039                | "ActivityCancelled"
3040                | "ActivityTimedOut"
3041        );
3042        let is_workflow_timer = matches!(
3043            event.event_type.as_str(),
3044            "TimerScheduled" | "TimerCancelled" | "TimerFired"
3045        ) && !is_internal_timer_event(event);
3046        let is_signal_wait = is_recorded_signal_wait_event(event);
3047        if !is_activity && !is_workflow_timer && !is_signal_wait {
3048            continue;
3049        }
3050
3051        let sequence = durable_event_sequence(event).ok_or_else(|| {
3052            Error::NonDeterministicReplay(ReplayFailure::new(
3053                "durable_command_sequence_missing",
3054                None,
3055                Some("positive workflow sequence".to_string()),
3056                Some(event.event_type.clone()),
3057                "durable command history event has no workflow sequence",
3058            ))
3059        })?;
3060        if sequence == 0 {
3061            return Err(Error::NonDeterministicReplay(ReplayFailure::new(
3062                "durable_command_sequence_invalid",
3063                Some(sequence),
3064                Some("positive workflow sequence".to_string()),
3065                Some(sequence.to_string()),
3066                "durable command history uses an invalid workflow sequence",
3067            )));
3068        }
3069        events_by_sequence.entry(sequence).or_default().push(event);
3070    }
3071
3072    events_by_sequence
3073        .into_iter()
3074        .map(|(sequence, sequence_events)| {
3075            let activity_events: Vec<_> = sequence_events
3076                .iter()
3077                .copied()
3078                .filter(|event| event.event_type.starts_with("Activity"))
3079                .collect();
3080            let timer_events: Vec<_> = sequence_events
3081                .iter()
3082                .copied()
3083                .filter(|event| event.event_type.starts_with("Timer"))
3084                .collect();
3085            let signal_wait_events: Vec<_> = sequence_events
3086                .iter()
3087                .copied()
3088                .filter(|event| is_recorded_signal_wait_event(event))
3089                .collect();
3090
3091            let command_kind_count = usize::from(!activity_events.is_empty())
3092                + usize::from(!timer_events.is_empty())
3093                + usize::from(!signal_wait_events.is_empty());
3094            if command_kind_count > 1 {
3095                let actual = [
3096                    (!activity_events.is_empty()).then_some("activity"),
3097                    (!timer_events.is_empty()).then_some("timer"),
3098                    (!signal_wait_events.is_empty()).then_some("signal wait"),
3099                ]
3100                .into_iter()
3101                .flatten()
3102                .collect::<Vec<_>>()
3103                .join(" and ");
3104                return Err(invalid_recorded_history(
3105                    "durable_command_sequence_collision",
3106                    sequence,
3107                    "one durable command kind",
3108                    &actual,
3109                    "one workflow sequence records more than one durable command kind",
3110                ));
3111            }
3112
3113            if !activity_events.is_empty() {
3114                let scheduled_count = activity_events
3115                    .iter()
3116                    .filter(|event| event.event_type == "ActivityScheduled")
3117                    .count();
3118                if scheduled_count > 1 {
3119                    return Err(invalid_recorded_history(
3120                        "duplicate_activity_schedule",
3121                        sequence,
3122                        "at most one ActivityScheduled event",
3123                        "multiple ActivityScheduled events",
3124                        "activity history schedules more than one command at one workflow sequence",
3125                    ));
3126                }
3127                let activity_type = activity_events.iter().find_map(|event| {
3128                    event
3129                        .payload
3130                        .get("activity_type")
3131                        .or_else(|| event.payload.get("activity_name"))
3132                        .and_then(Value::as_str)
3133                        .map(str::to_string)
3134                });
3135                if activity_events.iter().filter_map(|event| {
3136                    event
3137                        .payload
3138                        .get("activity_type")
3139                        .or_else(|| event.payload.get("activity_name"))
3140                        .and_then(Value::as_str)
3141                }).any(|candidate| Some(candidate) != activity_type.as_deref()) {
3142                    return Err(invalid_recorded_history(
3143                        "activity_identity_mismatch",
3144                        sequence,
3145                        activity_type.as_deref().unwrap_or("one activity identity"),
3146                        "conflicting activity identities",
3147                        "activity lifecycle events at one workflow sequence disagree on identity",
3148                    ));
3149                }
3150                let completed: Vec<_> = activity_events
3151                    .iter()
3152                    .copied()
3153                    .filter(|event| event.event_type == "ActivityCompleted")
3154                    .collect();
3155                if completed.len() > 1 {
3156                    return Err(invalid_recorded_history(
3157                        "duplicate_activity_completion",
3158                        sequence,
3159                        "one ActivityCompleted event",
3160                        "multiple ActivityCompleted events",
3161                        "activity history contains more than one completion for a workflow sequence",
3162                    ));
3163                }
3164                let result = completed
3165                    .first()
3166                    .map(|event| {
3167                        let codec = event
3168                            .payload
3169                            .get("payload_codec")
3170                            .and_then(Value::as_str)
3171                            .unwrap_or(fallback_codec);
3172                        decode_wire_value(
3173                            event.payload.get("result").unwrap_or(&Value::Null),
3174                            codec,
3175                        )
3176                    })
3177                    .transpose()?;
3178                return Ok(RecordedCommand::Activity {
3179                    sequence,
3180                    activity_type,
3181                    result,
3182                });
3183            }
3184
3185            if !signal_wait_events.is_empty() {
3186                let opened_count = signal_wait_events
3187                    .iter()
3188                    .filter(|event| {
3189                        matches!(
3190                            event.event_type.as_str(),
3191                            "SignalWaitOpened" | "ConditionWaitOpened"
3192                        )
3193                    })
3194                    .count();
3195                if opened_count > 1 {
3196                    return Err(invalid_recorded_history(
3197                        "duplicate_signal_wait_open",
3198                        sequence,
3199                        "at most one signal wait open event",
3200                        "multiple signal wait open events",
3201                        "signal history opens more than one durable wait at one workflow sequence",
3202                    ));
3203                }
3204
3205                let signal_name = signal_wait_events
3206                    .iter()
3207                    .find_map(|event| recorded_signal_wait_name(event));
3208                if signal_wait_events
3209                    .iter()
3210                    .filter_map(|event| recorded_signal_wait_name(event))
3211                    .any(|candidate| Some(candidate.as_str()) != signal_name.as_deref())
3212                {
3213                    return Err(invalid_recorded_history(
3214                        "signal_wait_identity_mismatch",
3215                        sequence,
3216                        signal_name.as_deref().unwrap_or("one signal name"),
3217                        "conflicting signal names",
3218                        "signal wait lifecycle events at one workflow sequence disagree on identity",
3219                    ));
3220                }
3221                return Ok(RecordedCommand::SignalWait {
3222                    sequence,
3223                    signal_name,
3224                });
3225            }
3226
3227            let scheduled: Vec<_> = timer_events
3228                .iter()
3229                .copied()
3230                .filter(|event| event.event_type == "TimerScheduled")
3231                .collect();
3232            let fired: Vec<_> = timer_events
3233                .iter()
3234                .copied()
3235                .filter(|event| event.event_type == "TimerFired")
3236                .collect();
3237            if scheduled.len() != 1 {
3238                return Err(invalid_recorded_history(
3239                    "timer_schedule_missing_or_duplicate",
3240                    sequence,
3241                    "one TimerScheduled event",
3242                    &format!("{} TimerScheduled events", scheduled.len()),
3243                    "timer replay requires exactly one recorded schedule event",
3244                ));
3245            }
3246            if fired.len() > 1 {
3247                return Err(invalid_recorded_history(
3248                    "duplicate_timer_fire",
3249                    sequence,
3250                    "at most one TimerFired event",
3251                    "multiple TimerFired events",
3252                    "timer history contains more than one fire event for a workflow sequence",
3253                ));
3254            }
3255
3256            let scheduled = scheduled[0];
3257            let timer_id = required_history_string(scheduled, "timer_id", sequence)?;
3258            let delay_seconds = required_history_u64(scheduled, "delay_seconds", sequence)?;
3259            if let Some(fired) = fired.first() {
3260                let fired_timer_id = required_history_string(fired, "timer_id", sequence)?;
3261                if fired_timer_id != timer_id {
3262                    return Err(invalid_recorded_history(
3263                        "timer_identity_mismatch",
3264                        sequence,
3265                        &timer_id,
3266                        &fired_timer_id,
3267                        "TimerFired does not correspond to the recorded TimerScheduled event",
3268                    ));
3269                }
3270                let fired_delay = required_history_u64(fired, "delay_seconds", sequence)?;
3271                if fired_delay != delay_seconds {
3272                    return Err(invalid_recorded_history(
3273                        "timer_history_delay_mismatch",
3274                        sequence,
3275                        &delay_seconds.to_string(),
3276                        &fired_delay.to_string(),
3277                        "TimerScheduled and TimerFired record different delays",
3278                    ));
3279                }
3280            }
3281
3282            Ok(RecordedCommand::Timer {
3283                sequence,
3284                delay_seconds,
3285                fired: !fired.is_empty(),
3286            })
3287        })
3288        .collect()
3289}
3290
3291fn durable_event_sequence(event: &HistoryEvent) -> Option<u64> {
3292    event
3293        .payload
3294        .get("sequence")
3295        .or_else(|| event.payload.get("workflow_sequence"))
3296        .or_else(|| event.raw.get("sequence"))
3297        .or_else(|| event.raw.get("workflow_sequence"))
3298        .and_then(value_as_u64)
3299}
3300
3301fn is_internal_timer_event(event: &HistoryEvent) -> bool {
3302    matches!(
3303        event
3304            .payload
3305            .get("timer_kind")
3306            .or_else(|| event.raw.get("timer_kind"))
3307            .and_then(Value::as_str),
3308        Some("condition_timeout" | "signal_timeout")
3309    )
3310}
3311
3312fn recorded_signal_wait_name(event: &HistoryEvent) -> Option<String> {
3313    match event.event_type.as_str() {
3314        "SignalWaitOpened" | "SignalApplied" => event
3315            .payload
3316            .get("signal_name")
3317            .or_else(|| event.raw.get("signal_name"))
3318            .and_then(Value::as_str)
3319            .filter(|value| !value.is_empty())
3320            .map(str::to_string),
3321        "ConditionWaitOpened" | "ConditionWaitSatisfied" | "ConditionWaitTimedOut" => event
3322            .payload
3323            .get("condition_key")
3324            .or_else(|| event.raw.get("condition_key"))
3325            .and_then(Value::as_str)
3326            .and_then(|key| key.strip_prefix("signal:"))
3327            .filter(|value| !value.is_empty())
3328            .map(str::to_string),
3329        _ => None,
3330    }
3331}
3332
3333fn is_recorded_signal_wait_event(event: &HistoryEvent) -> bool {
3334    match event.event_type.as_str() {
3335        "SignalWaitOpened" | "SignalApplied" => true,
3336        "ConditionWaitOpened" | "ConditionWaitSatisfied" | "ConditionWaitTimedOut" => event
3337            .payload
3338            .get("condition_key")
3339            .or_else(|| event.raw.get("condition_key"))
3340            .and_then(Value::as_str)
3341            .is_some_and(|key| key.starts_with("signal:")),
3342        _ => false,
3343    }
3344}
3345
3346fn required_history_string(event: &HistoryEvent, field: &str, sequence: u64) -> Result<String> {
3347    event
3348        .payload
3349        .get(field)
3350        .and_then(Value::as_str)
3351        .filter(|value| !value.is_empty())
3352        .map(str::to_string)
3353        .ok_or_else(|| {
3354            invalid_recorded_history(
3355                "timer_history_field_missing",
3356                sequence,
3357                field,
3358                &event.event_type,
3359                "timer history is missing a required identity field",
3360            )
3361        })
3362}
3363
3364fn required_history_u64(event: &HistoryEvent, field: &str, sequence: u64) -> Result<u64> {
3365    event
3366        .payload
3367        .get(field)
3368        .and_then(value_as_u64)
3369        .ok_or_else(|| {
3370            invalid_recorded_history(
3371                "timer_history_field_missing",
3372                sequence,
3373                field,
3374                &event.event_type,
3375                "timer history is missing a required numeric field",
3376            )
3377        })
3378}
3379
3380fn invalid_recorded_history(
3381    reason: &str,
3382    sequence: u64,
3383    expected: &str,
3384    actual: &str,
3385    message: &str,
3386) -> Error {
3387    Error::NonDeterministicReplay(ReplayFailure::new(
3388        reason,
3389        Some(sequence),
3390        Some(expected.to_string()),
3391        Some(actual.to_string()),
3392        message,
3393    ))
3394}
3395
3396fn signal_values(
3397    events: &[HistoryEvent],
3398    signal_name: &str,
3399    fallback_codec: &str,
3400    resume_signal: Option<&ResumeSignal>,
3401) -> Result<Vec<Vec<Value>>> {
3402    let mut signals = Vec::new();
3403
3404    for event in events {
3405        if event.event_type != "SignalApplied" && event.event_type != "SignalReceived" {
3406            continue;
3407        }
3408
3409        if event.payload.get("signal_name").and_then(Value::as_str) != Some(signal_name) {
3410            continue;
3411        }
3412
3413        let codec = event
3414            .payload
3415            .get("payload_codec")
3416            .and_then(Value::as_str)
3417            .unwrap_or(fallback_codec);
3418        let raw = event
3419            .payload
3420            .get("value")
3421            .or_else(|| event.payload.get("input"))
3422            .or_else(|| event.payload.get("arguments"));
3423        let decoded = match raw.filter(|value| !value.is_null()) {
3424            Some(value) => decode_wire_value(value, codec)?,
3425            None => resume_signal
3426                .filter(|signal| resume_signal_matches_event(signal, event, signal_name))
3427                .map(|signal| Value::Array(signal.arguments.clone()))
3428                .unwrap_or_else(|| Value::Array(Vec::new())),
3429        };
3430        let args = match normalize_arguments(decoded) {
3431            Value::Array(values) => values,
3432            _ => unreachable!("normalize_arguments always returns an array"),
3433        };
3434        signals.push(args);
3435    }
3436
3437    Ok(signals)
3438}
3439
3440fn hydrate_query_history_from_export(task: &mut QueryTask) -> Result<()> {
3441    let Some(export_events) = task
3442        .history_export
3443        .as_ref()
3444        .and_then(|export| export.get("history_events"))
3445        .and_then(Value::as_array)
3446    else {
3447        return Ok(());
3448    };
3449
3450    if export_events.len() > task.history_events.len() {
3451        task.history_events = serde_json::from_value(Value::Array(export_events.clone()))?;
3452    }
3453
3454    Ok(())
3455}
3456
3457fn enrich_query_history_from_export(task: &mut QueryTask) -> Result<()> {
3458    let Some(export) = task.history_export.as_ref() else {
3459        return Ok(());
3460    };
3461    let signals = export
3462        .get("signals")
3463        .and_then(Value::as_array)
3464        .cloned()
3465        .unwrap_or_default();
3466    let activities = export
3467        .get("activities")
3468        .and_then(Value::as_array)
3469        .cloned()
3470        .unwrap_or_default();
3471    let export_codec = export
3472        .get("payloads")
3473        .and_then(|payloads| payloads.get("codec"))
3474        .and_then(Value::as_str)
3475        .unwrap_or(&task.payload_codec)
3476        .to_string();
3477    let mut signal_name_offsets: HashMap<String, usize> = HashMap::new();
3478
3479    for event in &mut task.history_events {
3480        if event.event_type == "ActivityCompleted" {
3481            let sequence = event
3482                .payload
3483                .get("sequence")
3484                .or_else(|| event.payload.get("workflow_sequence"))
3485                .and_then(value_as_u64);
3486            let Some(activity) = sequence.and_then(|sequence| {
3487                activities.iter().find(|activity| {
3488                    activity.get("sequence").and_then(value_as_u64) == Some(sequence)
3489                })
3490            }) else {
3491                continue;
3492            };
3493            let Some(payload) = event.payload.as_object_mut() else {
3494                continue;
3495            };
3496            if missing_payload(payload.get("result")) {
3497                if let Some(result) = activity
3498                    .get("result")
3499                    .filter(|value| !missing_payload(Some(value)))
3500                {
3501                    payload.insert("result".to_string(), result.clone());
3502                }
3503            }
3504            for field in ["payload_codec", "activity_type"] {
3505                if payload
3506                    .get(field)
3507                    .and_then(Value::as_str)
3508                    .unwrap_or_default()
3509                    .is_empty()
3510                {
3511                    if let Some(value) = activity.get(field) {
3512                        payload.insert(field.to_string(), value.clone());
3513                    }
3514                }
3515            }
3516            continue;
3517        }
3518
3519        if event.event_type != "SignalReceived" && event.event_type != "SignalApplied" {
3520            continue;
3521        }
3522        let signal_id = event.payload.get("signal_id").and_then(Value::as_str);
3523        let command_id = event
3524            .payload
3525            .get("workflow_command_id")
3526            .or_else(|| event.raw.get("workflow_command_id"))
3527            .and_then(Value::as_str);
3528        let signal_name = event
3529            .payload
3530            .get("signal_name")
3531            .and_then(Value::as_str)
3532            .unwrap_or_default()
3533            .to_string();
3534        let matched = signals
3535            .iter()
3536            .find(|signal| {
3537                signal_id.is_some() && signal.get("id").and_then(Value::as_str) == signal_id
3538            })
3539            .or_else(|| {
3540                signals.iter().find(|signal| {
3541                    command_id.is_some()
3542                        && signal.get("command_id").and_then(Value::as_str) == command_id
3543                })
3544            })
3545            .or_else(|| {
3546                let offset = signal_name_offsets.entry(signal_name.clone()).or_default();
3547                let signal = signals
3548                    .iter()
3549                    .filter(|signal| {
3550                        signal.get("name").and_then(Value::as_str) == Some(signal_name.as_str())
3551                    })
3552                    .nth(*offset);
3553                if signal.is_some() {
3554                    *offset += 1;
3555                }
3556                signal
3557            });
3558        let Some(signal) = matched else {
3559            continue;
3560        };
3561        let signal_codec = signal
3562            .get("payload_codec")
3563            .and_then(Value::as_str)
3564            .unwrap_or(&export_codec);
3565        let Some(payload) = event.payload.as_object_mut() else {
3566            continue;
3567        };
3568        if missing_payload(payload.get("arguments")) {
3569            if let Some(arguments) = signal
3570                .get("arguments")
3571                .filter(|value| !missing_payload(Some(value)))
3572            {
3573                let envelope = match arguments {
3574                    Value::String(blob) => json!({"codec": signal_codec, "blob": blob}),
3575                    other => other.clone(),
3576                };
3577                payload.insert("arguments".to_string(), envelope);
3578            }
3579        }
3580        if payload
3581            .get("payload_codec")
3582            .and_then(Value::as_str)
3583            .unwrap_or_default()
3584            .is_empty()
3585        {
3586            payload.insert("payload_codec".to_string(), json!(signal_codec));
3587        }
3588    }
3589
3590    Ok(())
3591}
3592
3593fn missing_payload(value: Option<&Value>) -> bool {
3594    match value {
3595        None | Some(Value::Null) => true,
3596        Some(Value::String(value)) => value.is_empty(),
3597        Some(_) => false,
3598    }
3599}
3600
3601fn query_signal_events(task: &QueryTask) -> Result<Vec<QuerySignal>> {
3602    let export_signals = task
3603        .history_export
3604        .as_ref()
3605        .and_then(|export| export.get("signals"))
3606        .and_then(Value::as_array)
3607        .cloned()
3608        .unwrap_or_default();
3609    let export_codec = task
3610        .history_export
3611        .as_ref()
3612        .and_then(|export| export.get("payloads"))
3613        .and_then(|payloads| payloads.get("codec"))
3614        .and_then(Value::as_str)
3615        .unwrap_or(&task.payload_codec);
3616    let mut name_offsets: HashMap<String, usize> = HashMap::new();
3617    let mut signals = Vec::new();
3618
3619    for event in &task.history_events {
3620        if event.event_type != "SignalApplied" && event.event_type != "SignalReceived" {
3621            continue;
3622        }
3623
3624        let name = event
3625            .payload
3626            .get("signal_name")
3627            .and_then(Value::as_str)
3628            .unwrap_or_default();
3629        if name.is_empty() {
3630            continue;
3631        }
3632        let signal_id = event.payload.get("signal_id").and_then(Value::as_str);
3633        let command_id = event
3634            .payload
3635            .get("workflow_command_id")
3636            .or_else(|| event.raw.get("workflow_command_id"))
3637            .and_then(Value::as_str);
3638        let matched_export = export_signals
3639            .iter()
3640            .find(|candidate| {
3641                signal_id.is_some() && candidate.get("id").and_then(Value::as_str) == signal_id
3642            })
3643            .or_else(|| {
3644                export_signals.iter().find(|candidate| {
3645                    command_id.is_some()
3646                        && candidate.get("command_id").and_then(Value::as_str) == command_id
3647                })
3648            })
3649            .or_else(|| {
3650                let offset = name_offsets.entry(name.to_string()).or_default();
3651                let candidate = export_signals
3652                    .iter()
3653                    .filter(|candidate| candidate.get("name").and_then(Value::as_str) == Some(name))
3654                    .nth(*offset);
3655                if candidate.is_some() {
3656                    *offset += 1;
3657                }
3658                candidate
3659            });
3660        let codec = event
3661            .payload
3662            .get("payload_codec")
3663            .and_then(Value::as_str)
3664            .or_else(|| {
3665                matched_export
3666                    .and_then(|signal| signal.get("payload_codec"))
3667                    .and_then(Value::as_str)
3668            })
3669            .unwrap_or(export_codec);
3670        let raw_arguments = event
3671            .payload
3672            .get("value")
3673            .or_else(|| event.payload.get("input"))
3674            .or_else(|| event.payload.get("arguments"))
3675            .filter(|value| !value.is_null())
3676            .or_else(|| matched_export.and_then(|signal| signal.get("arguments")));
3677        let arguments = decode_query_signal_arguments(raw_arguments, codec)?;
3678        let workflow_sequence = event
3679            .payload
3680            .get("workflow_sequence")
3681            .and_then(value_as_u64)
3682            .or_else(|| {
3683                matched_export
3684                    .and_then(|signal| signal.get("workflow_sequence"))
3685                    .and_then(value_as_u64)
3686            });
3687
3688        signals.push(QuerySignal {
3689            id: signal_id.map(str::to_string).or_else(|| {
3690                matched_export
3691                    .and_then(|signal| signal.get("id"))
3692                    .and_then(Value::as_str)
3693                    .map(str::to_string)
3694            }),
3695            name: name.to_string(),
3696            arguments,
3697            workflow_sequence,
3698        });
3699    }
3700
3701    if signals.is_empty() {
3702        for signal in export_signals {
3703            if signal.get("status").and_then(Value::as_str) == Some("rejected") {
3704                continue;
3705            }
3706            let Some(name) = signal.get("name").and_then(Value::as_str) else {
3707                continue;
3708            };
3709            let codec = signal
3710                .get("payload_codec")
3711                .and_then(Value::as_str)
3712                .unwrap_or(export_codec);
3713            let arguments = decode_query_signal_arguments(signal.get("arguments"), codec)?;
3714            signals.push(QuerySignal {
3715                id: signal.get("id").and_then(Value::as_str).map(str::to_string),
3716                name: name.to_string(),
3717                arguments,
3718                workflow_sequence: signal.get("workflow_sequence").and_then(value_as_u64),
3719            });
3720        }
3721        signals.sort_by_key(|signal| signal.workflow_sequence.unwrap_or(u64::MAX));
3722    }
3723
3724    Ok(signals)
3725}
3726
3727fn decode_query_signal_arguments(raw: Option<&Value>, codec: &str) -> Result<Vec<Value>> {
3728    let decoded = match raw.filter(|value| !value.is_null()) {
3729        Some(value) => decode_wire_value(value, codec)?,
3730        None => Value::Array(Vec::new()),
3731    };
3732    let Value::Array(arguments) = normalize_arguments(decoded) else {
3733        unreachable!("normalize_arguments always returns an array");
3734    };
3735    Ok(arguments)
3736}
3737
3738fn value_as_u64(value: &Value) -> Option<u64> {
3739    value
3740        .as_u64()
3741        .or_else(|| value.as_str().and_then(|value| value.parse().ok()))
3742}
3743
3744fn resume_signal_matches_event(
3745    resume_signal: &ResumeSignal,
3746    event: &HistoryEvent,
3747    signal_name: &str,
3748) -> bool {
3749    if resume_signal.signal_name != signal_name {
3750        return false;
3751    }
3752
3753    match (
3754        resume_signal.signal_id.as_deref(),
3755        event.payload.get("signal_id").and_then(Value::as_str),
3756    ) {
3757        (Some(resume_id), Some(event_id)) => resume_id == event_id,
3758        _ => true,
3759    }
3760}
3761
3762#[cfg(test)]
3763mod tests {
3764    use super::*;
3765    use std::{
3766        io::{Read, Write},
3767        net::{SocketAddr, TcpListener, TcpStream},
3768        thread,
3769    };
3770
3771    #[derive(Clone, Debug, Default, PartialEq)]
3772    struct ReplayCounterState {
3773        loaded: Option<String>,
3774        count: i64,
3775        finished: bool,
3776    }
3777
3778    fn replay_counter_worker() -> Worker {
3779        let client = Client::new("http://127.0.0.1:8080").expect("client");
3780        let mut worker = Worker::new(client, "rust-workers");
3781        worker.register_replayed_workflow(
3782            "replay-counter",
3783            ReplayCounterState::default,
3784            |ctx, _input, state| async move {
3785                let loaded = ctx.activity("load-counter", json!([])).await?;
3786                state.update(|current| {
3787                    current.loaded = loaded.as_str().map(str::to_string);
3788                })?;
3789                for _ in 0..2 {
3790                    let signal = ctx.wait_signal("increment").await?;
3791                    let amount = signal.first().and_then(Value::as_i64).unwrap_or_default();
3792                    state.update(|current| current.count += amount)?;
3793                }
3794                state.update(|current| current.finished = true)?;
3795                state.read(|current| Ok(json!(current.count)))?
3796            },
3797        );
3798        worker.register_replayed_query::<ReplayCounterState, _, _>(
3799            "replay-counter",
3800            "current",
3801            |_ctx, state, _args| async move {
3802                Ok(json!({
3803                    "loaded": state.loaded,
3804                    "count": state.count,
3805                    "finished": state.finished,
3806                }))
3807            },
3808        );
3809        worker.register_replayed_query::<ReplayCounterState, _, _>(
3810            "replay-counter",
3811            "detached-mutation",
3812            |_ctx, state, _args| async move {
3813                let mut detached = (*state).clone();
3814                detached.count = 999;
3815                Ok(json!(detached.count))
3816            },
3817        );
3818        worker.register_replayed_query::<ReplayCounterState, _, _>(
3819            "replay-counter",
3820            "failed-mutation",
3821            |_ctx, state, _args| async move {
3822                let mut detached = (*state).clone();
3823                detached.count = 999;
3824                Err(Error::WorkerLoop("query refused".to_string()))
3825            },
3826        );
3827        worker
3828    }
3829
3830    fn replay_counter_query(
3831        query_name: &str,
3832        history_events: Value,
3833        run_status: &str,
3834    ) -> QueryTask {
3835        serde_json::from_value(json!({
3836            "query_task_id": format!("query-{query_name}"),
3837            "workflow_type": "replay-counter",
3838            "query_name": query_name,
3839            "payload_codec": "json",
3840            "workflow_arguments": {"codec": "json", "blob": "[]"},
3841            "query_arguments": {"codec": "json", "blob": "[]"},
3842            "history_events": history_events,
3843            "run_status": run_status,
3844        }))
3845        .expect("query task")
3846    }
3847
3848    fn workflow_context(history: Vec<HistoryEvent>) -> WorkflowContext {
3849        WorkflowContext {
3850            state: Arc::new(Mutex::new(
3851                WorkflowState::new(
3852                    history,
3853                    "rust-workers".to_string(),
3854                    JSON_CODEC.to_string(),
3855                    None,
3856                )
3857                .expect("valid workflow history"),
3858            )),
3859        }
3860    }
3861
3862    fn history_event(event_type: &str, payload: Value) -> HistoryEvent {
3863        HistoryEvent {
3864            event_type: event_type.to_string(),
3865            payload,
3866            raw: HashMap::new(),
3867        }
3868    }
3869
3870    #[test]
3871    fn avro_generic_wrapper_round_trips_json_values() {
3872        let value = json!({"greeting": "hello", "count": 3, "ok": true});
3873        let envelope = PayloadEnvelope::avro(&value).expect("encode");
3874        assert_eq!(envelope.codec, DEFAULT_CODEC);
3875        assert_eq!(decode_payload::<Value>(&envelope).expect("decode"), value);
3876    }
3877
3878    #[test]
3879    fn json_codec_remains_plain_json() {
3880        let value = json!({"greeting": "hello", "count": 3, "ok": true});
3881        let envelope = PayloadEnvelope::json(&value).expect("encode");
3882
3883        assert_eq!(envelope.codec, JSON_CODEC);
3884        assert_eq!(envelope.blob, serde_json::to_string(&value).expect("json"));
3885        assert_eq!(decode_payload::<Value>(&envelope).expect("decode"), value);
3886    }
3887
3888    #[test]
3889    fn typed_avro_payload_without_schema_context_keeps_diagnostic() {
3890        let envelope = PayloadEnvelope {
3891            codec: DEFAULT_CODEC.to_string(),
3892            blob: BASE64.encode([0x01]),
3893        };
3894
3895        let error = decode_payload::<Value>(&envelope).expect_err("typed payload must fail");
3896        assert_eq!(
3897            error.to_string(),
3898            "codec error: typed avro payloads require a schema context; v1 supports the generic wrapper"
3899        );
3900    }
3901
3902    #[test]
3903    fn workflow_context_schedules_activity_until_completion_is_in_history() {
3904        let ctx = WorkflowContext {
3905            state: Arc::new(Mutex::new(
3906                WorkflowState::new(
3907                    Vec::new(),
3908                    "rust-workers".to_string(),
3909                    DEFAULT_CODEC.to_string(),
3910                    None,
3911                )
3912                .expect("workflow state"),
3913            )),
3914        };
3915
3916        let mut call = Box::pin(ctx.activity("hello.activity", json!(["Ada"])));
3917        let mut task_context = TaskContext::from_waker(noop_waker_ref());
3918        assert!(matches!(
3919            call.as_mut().poll(&mut task_context),
3920            Poll::Pending
3921        ));
3922
3923        let commands = ctx.take_commands().expect("commands");
3924        assert_eq!(commands[0]["type"], "schedule_activity");
3925        assert_eq!(commands[0]["activity_type"], "hello.activity");
3926    }
3927
3928    #[test]
3929    fn workflow_sleep_emits_one_durable_timer_and_rounds_up() {
3930        let ctx = workflow_context(Vec::new());
3931        let mut sleep = Box::pin(ctx.sleep(Duration::from_millis(1_001)));
3932        let mut task_context = TaskContext::from_waker(noop_waker_ref());
3933
3934        assert!(matches!(
3935            sleep.as_mut().poll(&mut task_context),
3936            Poll::Pending
3937        ));
3938        assert!(matches!(
3939            sleep.as_mut().poll(&mut task_context),
3940            Poll::Pending
3941        ));
3942
3943        let commands = ctx.take_commands().expect("timer command");
3944        assert_eq!(
3945            commands,
3946            vec![json!({
3947                "type": "start_timer",
3948                "delay_seconds": 2,
3949            })]
3950        );
3951    }
3952
3953    #[test]
3954    fn workflow_sleep_replays_matching_schedule_and_fire_without_a_command() {
3955        let history = vec![
3956            history_event(
3957                "TimerScheduled",
3958                json!({
3959                    "sequence": 1,
3960                    "timer_id": "timer-1",
3961                    "delay_seconds": 5,
3962                    "fire_at": "2026-07-11T12:00:05Z",
3963                }),
3964            ),
3965            history_event(
3966                "TimerFired",
3967                json!({
3968                    "sequence": 1,
3969                    "timer_id": "timer-1",
3970                    "delay_seconds": 5,
3971                    "fire_at": "2026-07-11T12:00:05Z",
3972                    "fired_at": "2026-07-11T12:00:05Z",
3973                }),
3974            ),
3975        ];
3976
3977        for _restart in 0..2 {
3978            let ctx = workflow_context(history.clone());
3979            let mut sleep = Box::pin(ctx.sleep(Duration::from_secs(5)));
3980            let mut task_context = TaskContext::from_waker(noop_waker_ref());
3981            assert!(matches!(
3982                sleep.as_mut().poll(&mut task_context),
3983                Poll::Ready(Ok(()))
3984            ));
3985            assert!(ctx.take_commands().expect("commands").is_empty());
3986            ctx.ensure_history_consumed().expect("history consumed");
3987        }
3988    }
3989
3990    #[test]
3991    fn workflow_sleep_rejects_changed_delay_during_replay() {
3992        let ctx = workflow_context(vec![
3993            history_event(
3994                "TimerScheduled",
3995                json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
3996            ),
3997            history_event(
3998                "TimerFired",
3999                json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4000            ),
4001        ]);
4002        let mut sleep = Box::pin(ctx.sleep(Duration::from_secs(500)));
4003        let mut task_context = TaskContext::from_waker(noop_waker_ref());
4004
4005        let Poll::Ready(Err(Error::NonDeterministicReplay(failure))) =
4006            sleep.as_mut().poll(&mut task_context)
4007        else {
4008            panic!("changed timer delay must be rejected");
4009        };
4010        assert_eq!(failure.reason, "timer_delay_mismatch");
4011        assert_eq!(failure.sequence, Some(1));
4012    }
4013
4014    #[test]
4015    fn workflow_history_rejects_unpaired_or_mismatched_timer_events() {
4016        let lone_fire = WorkflowState::new(
4017            vec![history_event(
4018                "TimerFired",
4019                json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4020            )],
4021            "rust-workers".to_string(),
4022            JSON_CODEC.to_string(),
4023            None,
4024        )
4025        .expect_err("TimerFired requires TimerScheduled");
4026        assert!(matches!(
4027            lone_fire,
4028            Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
4029                if reason == "timer_schedule_missing_or_duplicate"
4030        ));
4031
4032        let wrong_identity = WorkflowState::new(
4033            vec![
4034                history_event(
4035                    "TimerScheduled",
4036                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4037                ),
4038                history_event(
4039                    "TimerFired",
4040                    json!({"sequence": 1, "timer_id": "timer-2", "delay_seconds": 5}),
4041                ),
4042            ],
4043            "rust-workers".to_string(),
4044            JSON_CODEC.to_string(),
4045            None,
4046        )
4047        .expect_err("fire must match scheduled timer identity");
4048        assert!(matches!(
4049            wrong_identity,
4050            Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
4051                if reason == "timer_identity_mismatch"
4052        ));
4053
4054        let duplicate_fire = WorkflowState::new(
4055            vec![
4056                history_event(
4057                    "TimerScheduled",
4058                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4059                ),
4060                history_event(
4061                    "TimerFired",
4062                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4063                ),
4064                history_event(
4065                    "TimerFired",
4066                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4067                ),
4068            ],
4069            "rust-workers".to_string(),
4070            JSON_CODEC.to_string(),
4071            None,
4072        )
4073        .expect_err("a durable timer cannot fire twice");
4074        assert!(matches!(
4075            duplicate_fire,
4076            Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
4077                if reason == "duplicate_timer_fire"
4078        ));
4079
4080        let wrong_fired_delay = WorkflowState::new(
4081            vec![
4082                history_event(
4083                    "TimerScheduled",
4084                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4085                ),
4086                history_event(
4087                    "TimerFired",
4088                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 6}),
4089                ),
4090            ],
4091            "rust-workers".to_string(),
4092            JSON_CODEC.to_string(),
4093            None,
4094        )
4095        .expect_err("timer schedule and fire delays must agree");
4096        assert!(matches!(
4097            wrong_fired_delay,
4098            Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
4099                if reason == "timer_history_delay_mismatch"
4100        ));
4101    }
4102
4103    #[test]
4104    fn replay_rejects_activity_moved_before_recorded_timer() {
4105        let ctx = workflow_context(vec![
4106            history_event(
4107                "TimerScheduled",
4108                json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4109            ),
4110            history_event(
4111                "TimerFired",
4112                json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4113            ),
4114            history_event(
4115                "ActivityCompleted",
4116                json!({
4117                    "sequence": 2,
4118                    "activity_type": "after-timer",
4119                    "payload_codec": "json",
4120                    "result": {"codec": "json", "blob": "\"done\""},
4121                }),
4122            ),
4123        ]);
4124        let mut activity = Box::pin(ctx.activity("after-timer", json!([])));
4125        let mut task_context = TaskContext::from_waker(noop_waker_ref());
4126
4127        let Poll::Ready(Err(Error::NonDeterministicReplay(failure))) =
4128            activity.as_mut().poll(&mut task_context)
4129        else {
4130            panic!("reordered durable command must be rejected");
4131        };
4132        assert_eq!(failure.reason, "recorded_command_mismatch");
4133        assert_eq!(failure.sequence, Some(1));
4134        assert_eq!(failure.expected.as_deref(), Some("timer"));
4135        assert_eq!(failure.actual.as_deref(), Some("activity:after-timer"));
4136    }
4137
4138    #[test]
4139    fn replay_orders_signal_waits_and_timers_in_one_command_stream() {
4140        let signal_then_timer = vec![
4141            history_event(
4142                "ConditionWaitOpened",
4143                json!({"sequence": 1, "condition_key": "signal:go"}),
4144            ),
4145            history_event(
4146                "SignalReceived",
4147                json!({
4148                    "signal_name": "go",
4149                    "arguments": ["now"],
4150                }),
4151            ),
4152            history_event(
4153                "TimerScheduled",
4154                json!({"sequence": 2, "timer_id": "timer-2", "delay_seconds": 5}),
4155            ),
4156            history_event(
4157                "TimerFired",
4158                json!({"sequence": 2, "timer_id": "timer-2", "delay_seconds": 5}),
4159            ),
4160        ];
4161
4162        let ctx = workflow_context(signal_then_timer.clone());
4163        let mut signal = Box::pin(ctx.wait_signal("go"));
4164        let mut task_context = TaskContext::from_waker(noop_waker_ref());
4165        assert!(matches!(
4166            signal.as_mut().poll(&mut task_context),
4167            Poll::Ready(Ok(arguments)) if arguments == vec![json!("now")]
4168        ));
4169        let mut timer = Box::pin(ctx.sleep(Duration::from_secs(5)));
4170        assert!(matches!(
4171            timer.as_mut().poll(&mut task_context),
4172            Poll::Ready(Ok(()))
4173        ));
4174        ctx.ensure_history_consumed()
4175            .expect("signal and timer history consumed in order");
4176
4177        let reordered = workflow_context(signal_then_timer);
4178        let mut timer_first = Box::pin(reordered.sleep(Duration::from_secs(5)));
4179        let Poll::Ready(Err(Error::NonDeterministicReplay(failure))) =
4180            timer_first.as_mut().poll(&mut task_context)
4181        else {
4182            panic!("timer cannot consume signal-wait-first history");
4183        };
4184        assert_eq!(failure.reason, "recorded_command_mismatch");
4185        assert_eq!(failure.sequence, Some(1));
4186        assert_eq!(failure.expected.as_deref(), Some("signal wait"));
4187
4188        let timer_then_signal = vec![
4189            history_event(
4190                "TimerScheduled",
4191                json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4192            ),
4193            history_event(
4194                "TimerFired",
4195                json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4196            ),
4197            history_event(
4198                "ConditionWaitOpened",
4199                json!({"sequence": 2, "condition_key": "signal:go"}),
4200            ),
4201            history_event(
4202                "SignalReceived",
4203                json!({"signal_name": "go", "arguments": []}),
4204            ),
4205        ];
4206        let reordered = workflow_context(timer_then_signal);
4207        let mut signal_first = Box::pin(reordered.wait_signal("go"));
4208        let Poll::Ready(Err(Error::NonDeterministicReplay(failure))) =
4209            signal_first.as_mut().poll(&mut task_context)
4210        else {
4211            panic!("signal wait cannot consume timer-first history");
4212        };
4213        assert_eq!(failure.reason, "recorded_command_mismatch");
4214        assert_eq!(failure.sequence, Some(1));
4215        assert_eq!(failure.expected.as_deref(), Some("timer"));
4216    }
4217
4218    #[test]
4219    fn workflow_history_rejects_duplicate_or_colliding_command_sequences() {
4220        let duplicate_timer = WorkflowState::new(
4221            vec![
4222                history_event(
4223                    "TimerScheduled",
4224                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4225                ),
4226                history_event(
4227                    "TimerScheduled",
4228                    json!({"sequence": 1, "timer_id": "timer-2", "delay_seconds": 5}),
4229                ),
4230            ],
4231            "rust-workers".to_string(),
4232            JSON_CODEC.to_string(),
4233            None,
4234        )
4235        .expect_err("one workflow sequence cannot schedule two timers");
4236        assert!(matches!(
4237            duplicate_timer,
4238            Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
4239                if reason == "timer_schedule_missing_or_duplicate"
4240        ));
4241
4242        let colliding_kinds = WorkflowState::new(
4243            vec![
4244                history_event(
4245                    "TimerScheduled",
4246                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4247                ),
4248                history_event(
4249                    "ActivityCompleted",
4250                    json!({"sequence": 1, "activity_type": "same-sequence"}),
4251                ),
4252            ],
4253            "rust-workers".to_string(),
4254            JSON_CODEC.to_string(),
4255            None,
4256        )
4257        .expect_err("one workflow sequence cannot identify two command kinds");
4258        assert!(matches!(
4259            colliding_kinds,
4260            Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
4261                if reason == "durable_command_sequence_collision"
4262        ));
4263
4264        let duplicate_signal_wait = WorkflowState::new(
4265            vec![
4266                history_event(
4267                    "SignalWaitOpened",
4268                    json!({"sequence": 1, "signal_name": "go"}),
4269                ),
4270                history_event(
4271                    "SignalWaitOpened",
4272                    json!({"sequence": 1, "signal_name": "go"}),
4273                ),
4274            ],
4275            "rust-workers".to_string(),
4276            JSON_CODEC.to_string(),
4277            None,
4278        )
4279        .expect_err("one workflow sequence cannot open two signal waits");
4280        assert!(matches!(
4281            duplicate_signal_wait,
4282            Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
4283                if reason == "duplicate_signal_wait_open"
4284        ));
4285    }
4286
4287    #[test]
4288    fn workflow_sleep_rejects_unrepresentable_rounded_duration() {
4289        let ctx = workflow_context(Vec::new());
4290        let mut sleep = Box::pin(ctx.start_timer(Duration::new(u64::MAX, 1)));
4291        let mut task_context = TaskContext::from_waker(noop_waker_ref());
4292        assert!(matches!(
4293            sleep.as_mut().poll(&mut task_context),
4294            Poll::Ready(Err(Error::TimerDurationOverflow))
4295        ));
4296        assert!(ctx.take_commands().expect("commands").is_empty());
4297    }
4298
4299    #[test]
4300    fn workflow_task_replay_completes_without_rescheduling_recorded_commands() {
4301        let client = Client::new("http://127.0.0.1:8080").expect("client");
4302        let mut worker = Worker::new(client, "rust-workers");
4303        worker.register_workflow("rust.timer", |ctx, _input| async move {
4304            ctx.sleep(Duration::from_secs(5)).await?;
4305            ctx.activity("after-timer", json!([])).await
4306        });
4307
4308        let task = |history_events| WorkflowTask {
4309            task_id: "wft-rust-timer-1".to_string(),
4310            workflow_id: Some("wf-rust-timer".to_string()),
4311            run_id: Some("run-rust-timer".to_string()),
4312            workflow_type: "rust.timer".to_string(),
4313            payload_codec: JSON_CODEC.to_string(),
4314            arguments: Some(json!({"codec": "json", "blob": "[]"})),
4315            history_events,
4316            total_history_events: None,
4317            next_history_page_token: None,
4318            workflow_task_attempt: 1,
4319            workflow_signal_id: None,
4320            signal_name: None,
4321            signal_arguments: None,
4322            lease_owner: Some("rust-worker".to_string()),
4323        };
4324
4325        let initial = worker
4326            .execute_workflow_task(task(Vec::new()))
4327            .expect("initial timer task");
4328        assert_eq!(
4329            initial,
4330            vec![json!({"type": "start_timer", "delay_seconds": 5})]
4331        );
4332
4333        let replayed = worker
4334            .execute_workflow_task(task(vec![
4335                history_event(
4336                    "TimerScheduled",
4337                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4338                ),
4339                history_event(
4340                    "TimerFired",
4341                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4342                ),
4343                history_event(
4344                    "ActivityCompleted",
4345                    json!({
4346                        "sequence": 2,
4347                        "activity_type": "after-timer",
4348                        "payload_codec": "json",
4349                        "result": {"codec": "json", "blob": "\"done\""},
4350                    }),
4351                ),
4352            ]))
4353            .expect("replayed workflow task");
4354        assert_eq!(replayed.len(), 1);
4355        assert_eq!(replayed[0]["type"], "complete_workflow");
4356        assert_eq!(
4357            decode_wire_value(&replayed[0]["result"], JSON_CODEC).expect("result"),
4358            json!("done")
4359        );
4360    }
4361
4362    #[test]
4363    fn workflow_task_replay_keeps_recorded_unfired_timer_pending_without_rescheduling() {
4364        let client = Client::new("http://127.0.0.1:8080").expect("client");
4365        let mut worker = Worker::new(client, "rust-workers");
4366        worker.register_workflow("rust.timer.pending", |ctx, _input| async move {
4367            ctx.sleep(Duration::from_secs(5)).await?;
4368            Ok(json!({"status": "timer fired"}))
4369        });
4370
4371        let task = WorkflowTask {
4372            task_id: "wft-rust-timer-pending".to_string(),
4373            workflow_id: Some("wf-rust-timer".to_string()),
4374            run_id: Some("run-rust-timer".to_string()),
4375            workflow_type: "rust.timer.pending".to_string(),
4376            payload_codec: JSON_CODEC.to_string(),
4377            arguments: Some(json!({"codec": "json", "blob": "[]"})),
4378            history_events: vec![history_event(
4379                "TimerScheduled",
4380                json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4381            )],
4382            total_history_events: Some(1),
4383            next_history_page_token: None,
4384            workflow_task_attempt: 1,
4385            workflow_signal_id: None,
4386            signal_name: None,
4387            signal_arguments: None,
4388            lease_owner: Some("rust-worker".to_string()),
4389        };
4390
4391        for _redelivery_or_restart in 0..2 {
4392            let commands = worker
4393                .execute_workflow_task(task.clone())
4394                .expect("recorded timer remains pending");
4395            assert!(
4396                commands.is_empty(),
4397                "recorded timer must not be rescheduled"
4398            );
4399        }
4400    }
4401
4402    #[test]
4403    fn workflow_task_rejects_recorded_command_removed_from_workflow_code() {
4404        let client = Client::new("http://127.0.0.1:8080").expect("client");
4405        let mut worker = Worker::new(client, "rust-workers");
4406        worker.register_workflow("rust.timer.removed", |_ctx, _input| async move {
4407            Ok(json!({"status": "completed"}))
4408        });
4409        let task = WorkflowTask {
4410            task_id: "wft-rust-timer-removed".to_string(),
4411            workflow_id: Some("wf-rust-timer".to_string()),
4412            run_id: Some("run-rust-timer".to_string()),
4413            workflow_type: "rust.timer.removed".to_string(),
4414            payload_codec: JSON_CODEC.to_string(),
4415            arguments: Some(json!({"codec": "json", "blob": "[]"})),
4416            history_events: vec![
4417                history_event(
4418                    "TimerScheduled",
4419                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4420                ),
4421                history_event(
4422                    "TimerFired",
4423                    json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4424                ),
4425            ],
4426            total_history_events: Some(2),
4427            next_history_page_token: None,
4428            workflow_task_attempt: 1,
4429            workflow_signal_id: None,
4430            signal_name: None,
4431            signal_arguments: None,
4432            lease_owner: Some("rust-worker".to_string()),
4433        };
4434
4435        let Error::NonDeterministicReplay(failure) = worker
4436            .execute_workflow_task(task)
4437            .expect_err("removed timer must fail replay")
4438        else {
4439            panic!("expected typed replay failure");
4440        };
4441        assert_eq!(failure.reason, "recorded_commands_unconsumed");
4442        assert_eq!(failure.sequence, Some(1));
4443    }
4444
4445    #[test]
4446    fn rust_hello_world_uses_signal_arguments_from_resume_payload() {
4447        let client = Client::new("http://127.0.0.1:8080").expect("client");
4448        let mut worker = Worker::new(client, "rust-workers");
4449
4450        worker.register_workflow("rust.hello_workflow", |ctx, _input| async move {
4451            let signal = ctx.wait_signal("start").await?;
4452            let name = signal
4453                .first()
4454                .and_then(|value| value.as_str())
4455                .unwrap_or("world");
4456            let greeting = ctx.activity("rust.hello_activity", json!([name])).await?;
4457            Ok(json!({
4458                "greeting": greeting,
4459                "language": "rust"
4460            }))
4461        });
4462
4463        let signal_arguments =
4464            encode_value_envelope(&json!(["Rust"]), DEFAULT_CODEC).expect("signal arguments");
4465        let task = WorkflowTask {
4466            task_id: "wft-rust-signal-1".to_string(),
4467            workflow_id: Some("wf-rust-hello".to_string()),
4468            run_id: Some("run-rust-hello".to_string()),
4469            workflow_type: "rust.hello_workflow".to_string(),
4470            payload_codec: DEFAULT_CODEC.to_string(),
4471            arguments: Some(encode_value_envelope(&json!([]), DEFAULT_CODEC).expect("input")),
4472            history_events: vec![HistoryEvent {
4473                event_type: "SignalReceived".to_string(),
4474                payload: json!({
4475                    "signal_id": "sig-rust-1",
4476                    "signal_name": "start"
4477                }),
4478                raw: HashMap::new(),
4479            }],
4480            total_history_events: Some(1),
4481            next_history_page_token: None,
4482            workflow_task_attempt: 1,
4483            workflow_signal_id: Some("sig-rust-1".to_string()),
4484            signal_name: Some("start".to_string()),
4485            signal_arguments: Some(signal_arguments),
4486            lease_owner: Some("rust-worker".to_string()),
4487        };
4488
4489        let commands = worker.execute_workflow_task(task).expect("workflow task");
4490
4491        assert_eq!(commands.len(), 1);
4492        assert_eq!(commands[0]["type"], "schedule_activity");
4493        assert_eq!(commands[0]["activity_type"], "rust.hello_activity");
4494        assert_eq!(
4495            decode_wire_value(&commands[0]["arguments"], DEFAULT_CODEC).expect("activity args"),
4496            json!(["Rust"])
4497        );
4498    }
4499
4500    #[test]
4501    fn workflow_task_appends_paginated_history_events() {
4502        let mut task = WorkflowTask {
4503            task_id: "wft-rust-pages-1".to_string(),
4504            workflow_id: Some("wf-rust-pages".to_string()),
4505            run_id: Some("run-rust-pages".to_string()),
4506            workflow_type: "rust.hello_workflow".to_string(),
4507            payload_codec: DEFAULT_CODEC.to_string(),
4508            arguments: Some(encode_value_envelope(&json!([]), DEFAULT_CODEC).expect("input")),
4509            history_events: vec![HistoryEvent {
4510                event_type: "WorkflowStarted".to_string(),
4511                payload: json!({}),
4512                raw: HashMap::new(),
4513            }],
4514            total_history_events: Some(3),
4515            next_history_page_token: Some("MQ==".to_string()),
4516            workflow_task_attempt: 1,
4517            workflow_signal_id: None,
4518            signal_name: None,
4519            signal_arguments: None,
4520            lease_owner: Some("rust-worker".to_string()),
4521        };
4522
4523        task.append_history_page(WorkflowTaskHistoryPage {
4524            history_events: vec![
4525                HistoryEvent {
4526                    event_type: "SignalReceived".to_string(),
4527                    payload: json!({
4528                        "signal_id": "sig-rust-1",
4529                        "signal_name": "start",
4530                        "arguments": encode_value_envelope(&json!(["Rust"]), DEFAULT_CODEC)
4531                            .expect("signal arguments")
4532                    }),
4533                    raw: HashMap::new(),
4534                },
4535                HistoryEvent {
4536                    event_type: "MarkerRecorded".to_string(),
4537                    payload: json!({"sequence": 3}),
4538                    raw: HashMap::new(),
4539                },
4540            ],
4541            total_history_events: Some(3),
4542            next_history_page_token: None,
4543        });
4544
4545        assert_eq!(task.history_events.len(), 3);
4546        assert_eq!(task.total_history_events, Some(3));
4547        assert_eq!(task.next_history_page_token, None);
4548
4549        let signals =
4550            signal_values(&task.history_events, "start", DEFAULT_CODEC, None).expect("signals");
4551        assert_eq!(signals, vec![vec![json!("Rust")]]);
4552    }
4553
4554    #[tokio::test]
4555    async fn query_handler_reads_ordered_cross_codec_signals_without_commands() {
4556        let client = Client::new("http://127.0.0.1:8080").expect("client");
4557        let mut worker = Worker::new(client, "rust-workers");
4558        worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
4559        worker.register_query("counter", "current", |ctx, _args| async move {
4560            let mut count = 0_i64;
4561            for signal in ctx.signal_events() {
4562                let value = signal
4563                    .arguments
4564                    .first()
4565                    .and_then(Value::as_i64)
4566                    .unwrap_or_default();
4567                match signal.name.as_str() {
4568                    "increment" => count += value,
4569                    "set" => count = value,
4570                    _ => {}
4571                }
4572            }
4573            Ok(json!(count))
4574        });
4575
4576        let task = QueryTask {
4577            query_task_id: "query-rust-counter".to_string(),
4578            query_task_attempt: 1,
4579            lease_owner: Some("rust-worker".to_string()),
4580            workflow_id: Some("counter-1".to_string()),
4581            run_id: Some("run-counter-1".to_string()),
4582            workflow_type: "counter".to_string(),
4583            query_name: "current".to_string(),
4584            payload_codec: DEFAULT_CODEC.to_string(),
4585            workflow_arguments: Some(
4586                encode_value_envelope(&json!([]), DEFAULT_CODEC).expect("workflow input"),
4587            ),
4588            query_arguments: Some(
4589                encode_value_envelope(&json!([]), DEFAULT_CODEC).expect("query arguments"),
4590            ),
4591            history_events: vec![
4592                HistoryEvent {
4593                    event_type: "SignalReceived".to_string(),
4594                    payload: json!({
4595                        "signal_id": "php-signal-1",
4596                        "signal_name": "increment",
4597                        "workflow_sequence": 1,
4598                        "payload_codec": DEFAULT_CODEC,
4599                        "arguments": encode_value_envelope(&json!([3]), DEFAULT_CODEC).expect("php avro signal")
4600                    }),
4601                    raw: HashMap::new(),
4602                },
4603                HistoryEvent {
4604                    event_type: "SignalReceived".to_string(),
4605                    payload: json!({
4606                        "signal_id": "python-signal-2",
4607                        "signal_name": "increment",
4608                        "workflow_sequence": 2,
4609                        "payload_codec": JSON_CODEC,
4610                        "arguments": encode_value_envelope(&json!([5]), JSON_CODEC).expect("python json signal")
4611                    }),
4612                    raw: HashMap::new(),
4613                },
4614                HistoryEvent {
4615                    event_type: "SignalReceived".to_string(),
4616                    payload: json!({
4617                        "signal_id": "rust-signal-3",
4618                        "signal_name": "set",
4619                        "workflow_sequence": 3,
4620                        "payload_codec": DEFAULT_CODEC,
4621                        "arguments": encode_value_envelope(&json!([0]), DEFAULT_CODEC).expect("rust avro signal")
4622                    }),
4623                    raw: HashMap::new(),
4624                },
4625            ],
4626            history_export: None,
4627            run_status: Some("completed".to_string()),
4628        };
4629
4630        let result = worker.execute_query_task(task).await.expect("query result");
4631        assert_eq!(result, json!(0));
4632    }
4633
4634    #[tokio::test]
4635    async fn replayed_queries_read_running_completed_and_cold_restarted_instance_state() {
4636        let worker = replay_counter_worker();
4637        let running_history = json!([
4638            {
4639                "type": "ActivityCompleted",
4640                "payload": {
4641                    "sequence": 1,
4642                    "activity_type": "load-counter",
4643                    "payload_codec": "json",
4644                    "result": {"codec": "json", "blob": "\"loaded\""}
4645                }
4646            },
4647            {
4648                "type": "SignalReceived",
4649                "payload": {
4650                    "signal_id": "signal-3",
4651                    "signal_name": "increment",
4652                    "payload_codec": "json",
4653                    "arguments": {"codec": "json", "blob": "[3]"}
4654                }
4655            }
4656        ]);
4657
4658        let running = worker
4659            .execute_query_task(replay_counter_query(
4660                "current",
4661                running_history.clone(),
4662                "running",
4663            ))
4664            .await
4665            .expect("running replay query");
4666        assert_eq!(
4667            running,
4668            json!({"loaded": "loaded", "count": 3, "finished": false})
4669        );
4670
4671        let detached = worker
4672            .execute_query_task(replay_counter_query(
4673                "detached-mutation",
4674                running_history.clone(),
4675                "running",
4676            ))
4677            .await
4678            .expect("query mutates only its detached state clone");
4679        assert_eq!(detached, json!(999));
4680        let failed = worker
4681            .execute_query_task(replay_counter_query(
4682                "failed-mutation",
4683                running_history.clone(),
4684                "running",
4685            ))
4686            .await
4687            .expect_err("failed query");
4688        assert_eq!(failed.reason, "query_rejected");
4689        let unchanged = worker
4690            .execute_query_task(replay_counter_query("current", running_history, "running"))
4691            .await
4692            .expect("later query reconstructs unchanged state");
4693        assert_eq!(unchanged, running);
4694
4695        let restarted_worker = replay_counter_worker();
4696        let restarted_task: QueryTask = serde_json::from_value(json!({
4697            "query_task_id": "query-after-restart",
4698            "workflow_id": "counter-1",
4699            "run_id": "run-counter-1",
4700            "workflow_type": "replay-counter",
4701            "query_name": "current",
4702            "payload_codec": "json",
4703            "workflow_arguments": {"codec": "json", "blob": "[]"},
4704            "query_arguments": {"codec": "json", "blob": "[]"},
4705            "history_events": [],
4706            "history_export": {
4707                "payloads": {"codec": "json"},
4708                "history_events": [
4709                    {
4710                        "type": "ActivityCompleted",
4711                        "payload": {
4712                            "sequence": 1,
4713                            "activity_type": "load-counter",
4714                            "payload_codec": "json",
4715                            "result": null
4716                        }
4717                    },
4718                    {
4719                        "type": "SignalReceived",
4720                        "payload": {"signal_id": "signal-3", "signal_name": "increment"}
4721                    },
4722                    {
4723                        "type": "SignalReceived",
4724                        "payload": {"signal_id": "signal-5", "signal_name": "increment"}
4725                    }
4726                ],
4727                "activities": [{
4728                    "sequence": 1,
4729                    "activity_type": "load-counter",
4730                    "payload_codec": "json",
4731                    "result": {"codec": "json", "blob": "\"loaded\""}
4732                }],
4733                "signals": [
4734                    {
4735                        "id": "signal-3",
4736                        "name": "increment",
4737                        "payload_codec": "json",
4738                        "arguments": "[3]"
4739                    },
4740                    {
4741                        "id": "signal-5",
4742                        "name": "increment",
4743                        "payload_codec": "json",
4744                        "arguments": "[5]"
4745                    }
4746                ]
4747            },
4748            "run_status": "completed"
4749        }))
4750        .expect("cold replay query task");
4751        let completed = restarted_worker
4752            .execute_query_task(restarted_task)
4753            .await
4754            .expect("completed cold replay query");
4755        assert_eq!(
4756            completed,
4757            json!({"loaded": "loaded", "count": 8, "finished": true})
4758        );
4759    }
4760
4761    #[tokio::test]
4762    async fn replayed_query_replay_failures_are_machine_readable() {
4763        let worker = replay_counter_worker();
4764        let task = replay_counter_query(
4765            "current",
4766            json!([{
4767                "type": "ActivityCompleted",
4768                "payload": {
4769                    "sequence": 1,
4770                    "payload_codec": "json",
4771                    "result": {"codec": "json", "blob": "{"}
4772                }
4773            }]),
4774            "running",
4775        );
4776        let failure = worker
4777            .execute_query_task(task)
4778            .await
4779            .expect_err("invalid replay history payload");
4780        assert_eq!(failure.reason, "query_workflow_state_unavailable");
4781        assert_eq!(failure.failure_type, "QueryWorkflowStateUnavailable");
4782    }
4783
4784    #[tokio::test]
4785    async fn query_task_restores_compact_history_from_export() {
4786        let client = Client::new("http://127.0.0.1:8080").expect("client");
4787        let mut worker = Worker::new(client, "rust-workers");
4788        worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
4789        worker.register_query("counter", "current", |ctx, _args| async move {
4790            Ok(json!(ctx.signals("increment")[0][0]))
4791        });
4792        let task: QueryTask = serde_json::from_value(json!({
4793            "query_task_id": "query-export",
4794            "workflow_type": "counter",
4795            "query_name": "current",
4796            "payload_codec": "json",
4797            "workflow_arguments": {"codec": "json", "blob": "[]"},
4798            "query_arguments": {"codec": "json", "blob": "[]"},
4799            "history_events": [],
4800            "history_export": {
4801                "payloads": {"codec": "json"},
4802                "history_events": [{
4803                    "type": "SignalReceived",
4804                    "payload": {"signal_id": "signal-export", "signal_name": "increment"}
4805                }],
4806                "signals": [{
4807                    "id": "signal-export",
4808                    "name": "increment",
4809                    "status": "applied",
4810                    "workflow_sequence": 1,
4811                    "payload_codec": "json",
4812                    "arguments": "[9]"
4813                }]
4814            }
4815        }))
4816        .expect("query task");
4817
4818        let result = worker.execute_query_task(task).await.expect("query result");
4819        assert_eq!(result, json!(9));
4820    }
4821
4822    #[tokio::test]
4823    async fn query_task_failures_have_stable_reasons() {
4824        let client = Client::new("http://127.0.0.1:8080").expect("client");
4825        let mut worker = Worker::new(client, "rust-workers");
4826        worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
4827        worker.register_query(
4828            "counter",
4829            "current",
4830            |_ctx, _args| async move { Ok(json!(0)) },
4831        );
4832
4833        let base_task = QueryTask {
4834            query_task_id: "query-errors".to_string(),
4835            query_task_attempt: 1,
4836            lease_owner: None,
4837            workflow_id: Some("counter-errors".to_string()),
4838            run_id: Some("run-errors".to_string()),
4839            workflow_type: "counter".to_string(),
4840            query_name: "missing".to_string(),
4841            payload_codec: JSON_CODEC.to_string(),
4842            workflow_arguments: Some(json!({"codec": "json", "blob": "[]"})),
4843            query_arguments: Some(json!({"codec": "json", "blob": "[]"})),
4844            history_events: Vec::new(),
4845            history_export: None,
4846            run_status: Some("running".to_string()),
4847        };
4848
4849        let unknown = worker
4850            .execute_query_task(base_task.clone())
4851            .await
4852            .expect_err("unknown query");
4853        assert_eq!(unknown.reason, "rejected_unknown_query");
4854
4855        let mut malformed = base_task;
4856        malformed.query_name = "current".to_string();
4857        malformed.query_arguments = Some(json!({"codec": "json", "blob": "{"}));
4858        let malformed = worker
4859            .execute_query_task(malformed)
4860            .await
4861            .expect_err("malformed payload");
4862        assert_eq!(malformed.reason, "query_payload_decode_failed");
4863
4864        let client = Client::new("http://127.0.0.1:8080").expect("client");
4865        let mut unavailable_worker = Worker::new(client, "rust-workers");
4866        unavailable_worker
4867            .register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
4868        let unavailable_task: QueryTask = serde_json::from_value(json!({
4869            "query_task_id": "query-unavailable",
4870            "workflow_type": "counter",
4871            "query_name": "current",
4872            "payload_codec": "json",
4873            "workflow_arguments": {"codec": "json", "blob": "[]"},
4874            "query_arguments": {"codec": "json", "blob": "[]"}
4875        }))
4876        .expect("query task");
4877        let unavailable = unavailable_worker
4878            .execute_query_task(unavailable_task)
4879            .await
4880            .expect_err("query handler unavailable");
4881        assert_eq!(unavailable.reason, "query_handler_unavailable");
4882    }
4883
4884    #[tokio::test]
4885    async fn client_query_decodes_result_and_typed_failure() {
4886        let server = MockWorkerServer::start();
4887        let client = Client::builder(server.base_url())
4888            .timeout(Duration::from_secs(2))
4889            .build()
4890            .expect("client");
4891
4892        let result = client
4893            .query_workflow("counter-1", "current", json!([]))
4894            .await
4895            .expect("query result");
4896        assert_eq!(result, json!({"count": 8}));
4897
4898        let error = client
4899            .query_workflow("counter-1", "missing", json!([]))
4900            .await
4901            .expect_err("unknown query");
4902        let Error::QueryFailed(failure) = error else {
4903            panic!("expected typed query failure");
4904        };
4905        assert_eq!(failure.status, 404);
4906        assert_eq!(failure.reason, "rejected_unknown_query");
4907    }
4908
4909    #[tokio::test]
4910    async fn baseline_worker_endpoints_send_the_baseline_protocol() {
4911        let server = MockWorkerServer::start();
4912        let client = Client::builder(server.base_url())
4913            .timeout(Duration::from_secs(2))
4914            .build()
4915            .expect("client");
4916
4917        client
4918            .register_worker("capture-worker", "capture", vec![], vec![], 1, 1)
4919            .await
4920            .expect("register");
4921        client
4922            .heartbeat_worker("capture-worker", 1, 1)
4923            .await
4924            .expect("heartbeat");
4925        client
4926            .poll_workflow_task("capture-worker", "capture", Duration::from_millis(10))
4927            .await
4928            .expect("workflow poll");
4929        client
4930            .poll_activity_task("capture-worker", "capture", Duration::from_millis(10))
4931            .await
4932            .expect("activity poll");
4933
4934        for path in [
4935            "/api/worker/register",
4936            "/api/worker/heartbeat",
4937            "/api/worker/workflow-tasks/poll",
4938            "/api/worker/activity-tasks/poll",
4939        ] {
4940            assert_eq!(
4941                server.worker_protocol_for(path).as_deref(),
4942                Some(WORKER_PROTOCOL_VERSION),
4943                "unexpected protocol for {path}"
4944            );
4945        }
4946
4947        assert_eq!(
4948            server.request_body("/api/worker/workflow-tasks/poll")["timeout_seconds"],
4949            1
4950        );
4951        assert_eq!(
4952            server.request_body("/api/worker/activity-tasks/poll")["timeout_seconds"],
4953            1
4954        );
4955    }
4956
4957    #[tokio::test]
4958    async fn query_task_endpoints_send_the_query_feature_protocol() {
4959        let server = MockWorkerServer::start();
4960        let client = Client::builder(server.base_url())
4961            .timeout(Duration::from_secs(2))
4962            .build()
4963            .expect("client");
4964
4965        client
4966            .poll_query_task("capture-worker", "capture", Duration::from_millis(10))
4967            .await
4968            .expect("query poll");
4969        client
4970            .complete_query_task("query-capture", "capture-worker", 1, json!(8), JSON_CODEC)
4971            .await
4972            .expect("query complete");
4973        client
4974            .fail_query_task(
4975                "query-capture",
4976                "capture-worker",
4977                1,
4978                "failed",
4979                "query_rejected",
4980                "QueryFailed",
4981            )
4982            .await
4983            .expect("query fail");
4984
4985        for path in [
4986            "/api/worker/query-tasks/poll",
4987            "/api/worker/query-tasks/query-capture/complete",
4988            "/api/worker/query-tasks/query-capture/fail",
4989        ] {
4990            assert_eq!(
4991                server.worker_protocol_for(path).as_deref(),
4992                Some(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION),
4993                "unexpected protocol for {path}"
4994            );
4995        }
4996
4997        assert_eq!(
4998            server.request_body("/api/worker/query-tasks/poll")["timeout_seconds"],
4999            1
5000        );
5001    }
5002
5003    #[tokio::test]
5004    async fn query_protocol_rejection_from_older_server_is_typed() {
5005        let server = MockWorkerServer::reject_query_protocol();
5006        let client = Client::builder(server.base_url())
5007            .timeout(Duration::from_secs(2))
5008            .build()
5009            .expect("client");
5010
5011        let error = client
5012            .poll_query_task("capture-worker", "capture", Duration::from_millis(10))
5013            .await
5014            .expect_err("server below query protocol floor must reject");
5015        let Error::Protocol(failure) = error else {
5016            panic!("expected typed protocol failure");
5017        };
5018
5019        assert_eq!(failure.status, 400);
5020        assert_eq!(failure.reason, "unsupported_protocol_version");
5021        assert_eq!(failure.supported_version.as_deref(), Some("1.7"));
5022        assert_eq!(
5023            failure.requested_version.as_deref(),
5024            Some(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION)
5025        );
5026        assert_eq!(
5027            server
5028                .worker_protocol_for("/api/worker/query-tasks/poll")
5029                .as_deref(),
5030            Some(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION)
5031        );
5032    }
5033
5034    #[tokio::test]
5035    async fn run_once_without_query_handlers_keeps_pre_query_server_compatibility() {
5036        let server = MockWorkerServer::reject_query_protocol();
5037        let client = Client::builder(server.base_url())
5038            .timeout(Duration::from_secs(2))
5039            .build()
5040            .expect("client");
5041        let mut worker = Worker::new(client, "rust-workers")
5042            .worker_id("baseline-worker")
5043            .poll_timeout(Duration::from_millis(10));
5044
5045        worker.register_workflow("baseline.workflow", |_ctx, _input| async move {
5046            Ok(Value::Null)
5047        });
5048
5049        assert_eq!(worker.run_once().await.expect("baseline run once"), 0);
5050        assert_eq!(
5051            server
5052                .worker_protocol_for("/api/worker/workflow-tasks/poll")
5053                .as_deref(),
5054            Some(WORKER_PROTOCOL_VERSION)
5055        );
5056        assert_eq!(
5057            server.worker_protocol_for("/api/worker/query-tasks/poll"),
5058            None,
5059            "a worker without query handlers must not use the query-task endpoint"
5060        );
5061    }
5062
5063    #[tokio::test]
5064    async fn completion_time_query_rejection_is_typed_without_stopping_worker() {
5065        let server = MockWorkerServer::reject_query_completion();
5066        let client = Client::builder(server.base_url())
5067            .timeout(Duration::from_secs(2))
5068            .build()
5069            .expect("client");
5070
5071        let error = client
5072            .complete_query_task("query-late", "late-worker", 1, json!(8), JSON_CODEC)
5073            .await
5074            .expect_err("expired completion must be rejected");
5075        let Error::QueryFailed(failure) = error else {
5076            panic!("expected typed query failure");
5077        };
5078        assert_eq!(failure.status, 409);
5079        assert_eq!(failure.reason, "query_task_timed_out");
5080
5081        let mut worker = Worker::new(client, "rust-workers")
5082            .worker_id("late-worker")
5083            .poll_timeout(Duration::from_millis(10));
5084        worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
5085        worker.register_query(
5086            "counter",
5087            "current",
5088            |_ctx, _args| async move { Ok(json!(8)) },
5089        );
5090
5091        assert_eq!(worker.run_once().await.expect("late task is handled"), 1);
5092        assert_eq!(
5093            worker
5094                .run_once()
5095                .await
5096                .expect("worker continues after late completion"),
5097            0
5098        );
5099        assert_eq!(
5100            server.request_count("/api/worker/query-tasks/query-late/complete"),
5101            2
5102        );
5103        assert_eq!(
5104            server.request_count("/api/worker/query-tasks/query-late/fail"),
5105            0,
5106            "a server completion rejection must not be reported as an encoding failure"
5107        );
5108    }
5109
5110    #[tokio::test]
5111    async fn activity_only_worker_can_shutdown_without_workflow_poller() {
5112        let server = MockWorkerServer::start();
5113        let client = Client::builder(server.base_url())
5114            .timeout(Duration::from_secs(2))
5115            .build()
5116            .expect("client");
5117        let mut worker = Worker::new(client, "rust-workers")
5118            .worker_id("activity-only-worker")
5119            .poll_timeout(Duration::from_millis(10));
5120
5121        worker.register_activity(
5122            "activity.only",
5123            |_ctx, _args| async move { Ok(Value::Null) },
5124        );
5125
5126        worker.run_until(async {}).await.expect("run worker");
5127    }
5128
5129    #[tokio::test]
5130    async fn workflow_only_worker_can_shutdown_without_activity_poller() {
5131        let server = MockWorkerServer::start();
5132        let client = Client::builder(server.base_url())
5133            .timeout(Duration::from_secs(2))
5134            .build()
5135            .expect("client");
5136        let mut worker = Worker::new(client, "rust-workers")
5137            .worker_id("workflow-only-worker")
5138            .poll_timeout(Duration::from_millis(10));
5139
5140        worker.register_workflow(
5141            "workflow.only",
5142            |_ctx, _input| async move { Ok(Value::Null) },
5143        );
5144
5145        worker.run_until(async {}).await.expect("run worker");
5146    }
5147
5148    #[tokio::test]
5149    async fn worker_heartbeat_observer_receives_server_acknowledgements() {
5150        let server = MockWorkerServer::start();
5151        let client = Client::builder(server.base_url())
5152            .timeout(Duration::from_secs(2))
5153            .build()
5154            .expect("client");
5155        let observations = Arc::new(Mutex::new(Vec::new()));
5156        let observed = Arc::clone(&observations);
5157        let mut worker = Worker::new(client, "rust-workers")
5158            .worker_id("observed-heartbeat-worker")
5159            .poll_timeout(Duration::from_millis(10))
5160            .on_worker_heartbeat(move |observation| {
5161                observed
5162                    .lock()
5163                    .expect("heartbeat observations")
5164                    .push(observation.clone());
5165            });
5166
5167        worker.register_workflow("workflow.observed", |_ctx, _input| async move {
5168            Ok(Value::Null)
5169        });
5170        worker
5171            .run_until(tokio::time::sleep(Duration::from_millis(20)))
5172            .await
5173            .expect("run worker");
5174
5175        let observations = observations.lock().expect("heartbeat observations");
5176        let first = observations.first().expect("heartbeat acknowledgement");
5177        assert_eq!(first.worker_id, "observed-heartbeat-worker");
5178        assert_eq!(first.task_queue, "rust-workers");
5179        assert!(first.acknowledged_at_unix_millis > 0);
5180        assert_eq!(first.acknowledgement, json!({}));
5181    }
5182
5183    #[tokio::test]
5184    async fn worker_retries_poll_and_heartbeat_transport_failures_independently() {
5185        let server = MockWorkerServer::transient_worker_failures();
5186        let client = Client::builder(server.base_url())
5187            .timeout(Duration::from_secs(2))
5188            .build()
5189            .expect("client");
5190        let mut worker = Worker::new(client, "rust-workers")
5191            .worker_id("retry-worker")
5192            .poll_timeout(Duration::from_millis(10))
5193            .retry_policy(WorkerRetryPolicy {
5194                max_retries: 2,
5195                initial_backoff: Duration::from_millis(1),
5196                max_backoff: Duration::from_millis(1),
5197            });
5198        worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
5199        worker.register_activity(
5200            "counter.activity",
5201            |_ctx, _input| async move { Ok(Value::Null) },
5202        );
5203        worker.register_query(
5204            "counter",
5205            "current",
5206            |_ctx, _args| async move { Ok(json!(8)) },
5207        );
5208
5209        worker
5210            .run_until(tokio::time::sleep(Duration::from_millis(75)))
5211            .await
5212            .expect("transient failures must not stop the worker");
5213
5214        for path in [
5215            "/api/worker/heartbeat",
5216            "/api/worker/workflow-tasks/poll",
5217            "/api/worker/activity-tasks/poll",
5218            "/api/worker/query-tasks/poll",
5219        ] {
5220            assert!(
5221                server.request_count(path) >= 2,
5222                "{path} must continue after its transient failure"
5223            );
5224        }
5225    }
5226
5227    #[tokio::test]
5228    async fn worker_bounds_transport_retries() {
5229        let server = MockWorkerServer::unavailable_polls();
5230        let client = Client::builder(server.base_url())
5231            .timeout(Duration::from_secs(2))
5232            .build()
5233            .expect("client");
5234        let mut worker = Worker::new(client, "rust-workers")
5235            .worker_id("bounded-retry-worker")
5236            .poll_timeout(Duration::from_millis(10))
5237            .retry_policy(WorkerRetryPolicy {
5238                max_retries: 2,
5239                initial_backoff: Duration::from_millis(1),
5240                max_backoff: Duration::from_millis(1),
5241            });
5242        worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
5243
5244        let error = worker.run().await.expect_err("retry bound must terminate");
5245        assert!(matches!(error, Error::Transport(_)));
5246        assert_eq!(
5247            server.request_count("/api/worker/workflow-tasks/poll"),
5248            3,
5249            "one initial request plus two retries"
5250        );
5251    }
5252
5253    #[tokio::test]
5254    async fn worker_does_not_retry_authentication_failures() {
5255        let server = MockWorkerServer::unauthorized_polls();
5256        let client = Client::builder(server.base_url())
5257            .timeout(Duration::from_secs(2))
5258            .build()
5259            .expect("client");
5260        let mut worker = Worker::new(client, "rust-workers")
5261            .worker_id("unauthorized-worker")
5262            .poll_timeout(Duration::from_millis(10));
5263        worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
5264
5265        let error = worker
5266            .run()
5267            .await
5268            .expect_err("authentication must terminate");
5269        let Error::Http { status, body } = error else {
5270            panic!("expected stable HTTP authentication error");
5271        };
5272        assert_eq!(status, reqwest::StatusCode::UNAUTHORIZED);
5273        assert!(body.contains("authentication_failed"));
5274        assert_eq!(
5275            server.request_count("/api/worker/workflow-tasks/poll"),
5276            1,
5277            "authentication failures must not be retried"
5278        );
5279    }
5280
5281    #[derive(Clone, Debug)]
5282    struct CapturedRequest {
5283        path: String,
5284        worker_protocol: Option<String>,
5285        body: String,
5286    }
5287
5288    struct MockWorkerServer {
5289        addr: SocketAddr,
5290        stop: Arc<AtomicBool>,
5291        requests: Arc<Mutex<Vec<CapturedRequest>>>,
5292        thread: Option<thread::JoinHandle<()>>,
5293    }
5294
5295    #[derive(Clone, Copy, Default)]
5296    struct MockWorkerBehavior {
5297        reject_query_protocol: bool,
5298        reject_query_completion: bool,
5299        poll_failures_per_path: usize,
5300        heartbeat_failures: usize,
5301        unauthorized_polls: bool,
5302    }
5303
5304    impl MockWorkerServer {
5305        fn start() -> Self {
5306            Self::start_with_behavior(MockWorkerBehavior::default())
5307        }
5308
5309        fn reject_query_protocol() -> Self {
5310            Self::start_with_behavior(MockWorkerBehavior {
5311                reject_query_protocol: true,
5312                ..MockWorkerBehavior::default()
5313            })
5314        }
5315
5316        fn reject_query_completion() -> Self {
5317            Self::start_with_behavior(MockWorkerBehavior {
5318                reject_query_completion: true,
5319                ..MockWorkerBehavior::default()
5320            })
5321        }
5322
5323        fn transient_worker_failures() -> Self {
5324            Self::start_with_behavior(MockWorkerBehavior {
5325                poll_failures_per_path: 1,
5326                heartbeat_failures: 1,
5327                ..MockWorkerBehavior::default()
5328            })
5329        }
5330
5331        fn unavailable_polls() -> Self {
5332            Self::start_with_behavior(MockWorkerBehavior {
5333                poll_failures_per_path: usize::MAX,
5334                ..MockWorkerBehavior::default()
5335            })
5336        }
5337
5338        fn unauthorized_polls() -> Self {
5339            Self::start_with_behavior(MockWorkerBehavior {
5340                unauthorized_polls: true,
5341                ..MockWorkerBehavior::default()
5342            })
5343        }
5344
5345        fn start_with_behavior(behavior: MockWorkerBehavior) -> Self {
5346            let listener = TcpListener::bind("127.0.0.1:0").expect("bind mock server");
5347            listener
5348                .set_nonblocking(true)
5349                .expect("configure mock listener");
5350            let addr = listener.local_addr().expect("mock server address");
5351            let stop = Arc::new(AtomicBool::new(false));
5352            let server_stop = Arc::clone(&stop);
5353            let requests = Arc::new(Mutex::new(Vec::new()));
5354            let server_requests = Arc::clone(&requests);
5355            let thread = thread::spawn(move || {
5356                while !server_stop.load(Ordering::SeqCst) {
5357                    match listener.accept() {
5358                        Ok((mut stream, _)) => {
5359                            handle_mock_worker_request(&mut stream, &server_requests, behavior)
5360                        }
5361                        Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
5362                            thread::sleep(Duration::from_millis(5));
5363                        }
5364                        Err(_) => break,
5365                    }
5366                }
5367            });
5368
5369            Self {
5370                addr,
5371                stop,
5372                requests,
5373                thread: Some(thread),
5374            }
5375        }
5376
5377        fn base_url(&self) -> String {
5378            format!("http://{}", self.addr)
5379        }
5380
5381        fn worker_protocol_for(&self, path: &str) -> Option<String> {
5382            self.requests
5383                .lock()
5384                .expect("captured requests")
5385                .iter()
5386                .find(|request| request.path == path)
5387                .and_then(|request| request.worker_protocol.clone())
5388        }
5389
5390        fn request_count(&self, path: &str) -> usize {
5391            self.requests
5392                .lock()
5393                .expect("captured requests")
5394                .iter()
5395                .filter(|request| request.path == path)
5396                .count()
5397        }
5398
5399        fn request_body(&self, path: &str) -> Value {
5400            let requests = self.requests.lock().expect("captured requests");
5401            let body = &requests
5402                .iter()
5403                .find(|request| request.path == path)
5404                .unwrap_or_else(|| panic!("missing request for {path}"))
5405                .body;
5406            serde_json::from_str(body).unwrap_or_else(|error| {
5407                panic!("invalid JSON request body for {path}: {error}: {body:?}")
5408            })
5409        }
5410    }
5411
5412    impl Drop for MockWorkerServer {
5413        fn drop(&mut self) {
5414            self.stop.store(true, Ordering::SeqCst);
5415            let _ = TcpStream::connect(self.addr);
5416
5417            if let Some(thread) = self.thread.take() {
5418                thread.join().expect("join mock server");
5419            }
5420        }
5421    }
5422
5423    fn handle_mock_worker_request(
5424        stream: &mut TcpStream,
5425        requests: &Arc<Mutex<Vec<CapturedRequest>>>,
5426        behavior: MockWorkerBehavior,
5427    ) {
5428        let _ = stream.set_read_timeout(Some(Duration::from_millis(200)));
5429        let mut buffer = [0_u8; 8192];
5430        let mut request = Vec::new();
5431
5432        loop {
5433            match stream.read(&mut buffer) {
5434                Ok(0) => break,
5435                Ok(read) => {
5436                    request.extend_from_slice(&buffer[..read]);
5437                    if mock_request_is_complete(&request) {
5438                        break;
5439                    }
5440                }
5441                Err(error)
5442                    if matches!(
5443                        error.kind(),
5444                        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
5445                    ) =>
5446                {
5447                    break;
5448                }
5449                Err(_) => return,
5450            }
5451        }
5452
5453        let request = String::from_utf8_lossy(&request);
5454        let body = request
5455            .split_once("\r\n\r\n")
5456            .map(|(_, body)| body)
5457            .unwrap_or_default();
5458        let path = request
5459            .lines()
5460            .next()
5461            .and_then(|line| line.split_whitespace().nth(1))
5462            .unwrap_or_default();
5463        let worker_protocol = request.lines().find_map(|line| {
5464            let (name, value) = line.split_once(':')?;
5465            name.eq_ignore_ascii_case("X-Durable-Workflow-Protocol-Version")
5466                .then(|| value.trim().to_string())
5467        });
5468        let request_number = {
5469            let mut requests = requests.lock().expect("captured requests");
5470            requests.push(CapturedRequest {
5471                path: path.to_string(),
5472                worker_protocol: worker_protocol.clone(),
5473                body: body.to_string(),
5474            });
5475            requests
5476                .iter()
5477                .filter(|request| request.path == path)
5478                .count()
5479        };
5480
5481        let is_poll = matches!(
5482            path,
5483            "/api/worker/workflow-tasks/poll"
5484                | "/api/worker/activity-tasks/poll"
5485                | "/api/worker/query-tasks/poll"
5486        );
5487        if is_poll && request_number <= behavior.poll_failures_per_path {
5488            return;
5489        }
5490        if path == "/api/worker/heartbeat" && request_number <= behavior.heartbeat_failures {
5491            return;
5492        }
5493        if behavior.unauthorized_polls && is_poll {
5494            write_mock_response(
5495                stream,
5496                "401 Unauthorized",
5497                r#"{"reason":"authentication_failed","message":"invalid worker token"}"#,
5498            );
5499            return;
5500        }
5501
5502        if behavior.reject_query_protocol && path.starts_with("/api/worker/query-tasks/") {
5503            let requested_version = worker_protocol.as_deref().unwrap_or("missing");
5504            let body = format!(
5505                r#"{{"reason":"unsupported_protocol_version","message":"Query tasks require worker protocol 1.8 or newer.","supported_version":"1.7","requested_version":"{requested_version}"}}"#
5506            );
5507            write_mock_response(stream, "400 Bad Request", &body);
5508            return;
5509        }
5510
5511        if behavior.reject_query_completion && path == "/api/worker/query-tasks/query-late/complete"
5512        {
5513            write_mock_response(
5514                stream,
5515                "409 Conflict",
5516                r#"{"reason":"query_task_timed_out","message":"query task timed out before completion"}"#,
5517            );
5518            return;
5519        }
5520
5521        let (status, body) = match path {
5522            "/api/worker/register" => (
5523                "200 OK",
5524                r#"{"worker_id":"mock-worker","registered":true,"heartbeat_interval_seconds":3600}"#,
5525            ),
5526            "/api/worker/heartbeat" => ("200 OK", "{}"),
5527            "/api/worker/activity-tasks/poll" | "/api/worker/workflow-tasks/poll" => {
5528                ("200 OK", r#"{"task":null}"#)
5529            }
5530            "/api/worker/query-tasks/poll"
5531                if behavior.reject_query_completion && request_number == 1 =>
5532            {
5533                (
5534                    "200 OK",
5535                    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"}}"#,
5536                )
5537            }
5538            "/api/worker/query-tasks/poll" => ("200 OK", r#"{"task":null}"#),
5539            "/api/worker/query-tasks/query-capture/complete"
5540            | "/api/worker/query-tasks/query-capture/fail" => ("200 OK", "{}"),
5541            "/api/workflows/counter-1/query/current" => (
5542                "200 OK",
5543                r#"{"workflow_id":"counter-1","query_name":"current","result":{"count":8},"result_envelope":{"codec":"json","blob":"{\"count\":8}"}}"#,
5544            ),
5545            "/api/workflows/counter-1/query/missing" => (
5546                "404 Not Found",
5547                r#"{"workflow_id":"counter-1","query_name":"missing","reason":"rejected_unknown_query","message":"unknown query"}"#,
5548            ),
5549            _ => ("404 Not Found", r#"{"message":"not found"}"#),
5550        };
5551        write_mock_response(stream, status, body);
5552    }
5553
5554    fn mock_request_is_complete(request: &[u8]) -> bool {
5555        let Some(header_end) = request
5556            .windows(4)
5557            .position(|window| window == b"\r\n\r\n")
5558            .map(|position| position + 4)
5559        else {
5560            return false;
5561        };
5562        let headers = String::from_utf8_lossy(&request[..header_end]);
5563        let content_length = headers.lines().find_map(|line| {
5564            let (name, value) = line.split_once(':')?;
5565            name.eq_ignore_ascii_case("content-length")
5566                .then(|| value.trim().parse::<usize>().ok())
5567                .flatten()
5568        });
5569
5570        request.len() >= header_end + content_length.unwrap_or(0)
5571    }
5572
5573    fn write_mock_response(stream: &mut TcpStream, status: &str, body: &str) {
5574        let response = format!(
5575            "HTTP/1.1 {status}\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}",
5576            body.len()
5577        );
5578
5579        let _ = stream.write_all(response.as_bytes());
5580        let _ = stream.flush();
5581    }
5582}