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, and query workflows,
4//! register a Rust worker, poll workflow, activity, and read-only query tasks,
5//! reconstruct typed workflow-instance state through deterministic replay,
6//! heartbeat worker and activity liveness, and exchange JSON-native payloads
7//! through the same `avro` generic wrapper used by the existing first-party
8//! SDKs.
9
10use std::{
11    any::{Any, TypeId},
12    collections::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("workflow handler {0:?} is not registered")]
86    WorkflowNotRegistered(String),
87    #[error("activity handler {0:?} is not registered")]
88    ActivityNotRegistered(String),
89    #[error("workflow future yielded without emitting a durable command")]
90    WorkflowYieldedWithoutCommand,
91    #[error("workflow state lock is poisoned")]
92    WorkflowStatePoisoned,
93    #[error("operation timed out")]
94    Timeout,
95    #[error("worker loop error: {0}")]
96    WorkerLoop(String),
97}
98
99/// A stable, machine-readable workflow query or query-task settlement failure.
100#[derive(Clone, Debug, Error)]
101#[error("query failed ({reason}, HTTP {status}): {message}")]
102pub struct QueryFailure {
103    pub status: u16,
104    pub reason: String,
105    pub message: String,
106    pub body: Value,
107}
108
109/// A stable failure returned when a server rejects an SDK protocol version.
110#[derive(Clone, Debug, Error)]
111#[error("protocol rejected ({reason}, HTTP {status}): {message}")]
112pub struct ProtocolFailure {
113    pub status: u16,
114    pub reason: String,
115    pub message: String,
116    pub supported_version: Option<String>,
117    pub requested_version: Option<String>,
118    pub body: Value,
119}
120
121#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
122pub struct PayloadEnvelope {
123    pub codec: String,
124    pub blob: String,
125}
126
127impl PayloadEnvelope {
128    pub fn avro<T: Serialize>(value: &T) -> Result<Self> {
129        encode_payload(value, DEFAULT_CODEC)
130    }
131
132    pub fn json<T: Serialize>(value: &T) -> Result<Self> {
133        encode_payload(value, JSON_CODEC)
134    }
135}
136
137pub fn encode_payload<T: Serialize>(value: &T, codec: &str) -> Result<PayloadEnvelope> {
138    let value = serde_json::to_value(value)?;
139    let blob = encode_value_blob(&value, codec)?;
140
141    Ok(PayloadEnvelope {
142        codec: codec.to_string(),
143        blob,
144    })
145}
146
147pub fn decode_payload<T: DeserializeOwned>(envelope: &PayloadEnvelope) -> Result<T> {
148    let value = decode_blob(&envelope.blob, &envelope.codec)?;
149    Ok(serde_json::from_value(value)?)
150}
151
152fn encode_value_envelope(value: &Value, codec: &str) -> Result<Value> {
153    Ok(serde_json::to_value(encode_payload(value, codec)?)?)
154}
155
156fn encode_value_blob(value: &Value, codec: &str) -> Result<String> {
157    match codec {
158        JSON_CODEC => Ok(serde_json::to_string(value)?),
159        DEFAULT_CODEC => encode_avro_generic(value),
160        other => Err(Error::Codec(format!("unsupported payload codec {other:?}"))),
161    }
162}
163
164fn decode_wire_value(value: &Value, fallback_codec: &str) -> Result<Value> {
165    if value.is_null() {
166        return Ok(Value::Null);
167    }
168
169    if let Some(object) = value.as_object() {
170        if let (Some(codec), Some(blob)) = (
171            object.get("codec").and_then(Value::as_str),
172            object.get("blob").and_then(Value::as_str),
173        ) {
174            return decode_blob(blob, codec);
175        }
176    }
177
178    if let Some(blob) = value.as_str() {
179        return decode_blob(blob, fallback_codec);
180    }
181
182    Ok(value.clone())
183}
184
185fn decode_blob(blob: &str, codec: &str) -> Result<Value> {
186    match codec {
187        JSON_CODEC => Ok(serde_json::from_str(blob)?),
188        DEFAULT_CODEC => decode_avro_generic(blob),
189        other => Err(Error::Codec(format!("unsupported payload codec {other:?}"))),
190    }
191}
192
193fn encode_avro_generic(value: &Value) -> Result<String> {
194    let json = serde_json::to_string(value)?;
195    let datum = to_value(AvroPayload {
196        json,
197        version: AVRO_PAYLOAD_VERSION,
198    })
199    .map_err(|err| Error::Codec(format!("could not convert avro generic wrapper: {err}")))?;
200    let datum = to_avro_datum(avro_payload_schema()?, datum)
201        .map_err(|err| Error::Codec(format!("could not encode avro generic wrapper: {err}")))?;
202
203    let mut bytes = Vec::with_capacity(datum.len() + 1);
204    bytes.push(0x00);
205    bytes.extend_from_slice(&datum);
206    Ok(BASE64.encode(bytes))
207}
208
209fn decode_avro_generic(blob: &str) -> Result<Value> {
210    let bytes = BASE64
211        .decode(blob)
212        .map_err(|err| Error::Codec(format!("invalid avro base64 payload: {err}")))?;
213
214    if bytes.is_empty() {
215        return Err(Error::Codec("avro payload is empty".to_string()));
216    }
217
218    match bytes[0] {
219        0x00 => {}
220        0x01 => {
221            return Err(Error::Codec(
222                "typed avro payloads require a schema context; v1 supports the generic wrapper"
223                    .to_string(),
224            ));
225        }
226        other => {
227            return Err(Error::Codec(format!(
228                "unknown avro payload prefix 0x{other:02x}"
229            )));
230        }
231    }
232
233    let mut datum = &bytes[1..];
234    let datum = from_avro_datum(avro_payload_schema()?, &mut datum, None)
235        .map_err(|err| Error::Codec(format!("could not decode avro generic wrapper: {err}")))?;
236    let payload: AvroPayload = from_value(&datum)
237        .map_err(|err| Error::Codec(format!("invalid avro generic wrapper record: {err}")))?;
238
239    if payload.version != AVRO_PAYLOAD_VERSION {
240        return Err(Error::Codec(format!(
241            "unsupported avro generic wrapper version {}",
242            payload.version
243        )));
244    }
245
246    Ok(serde_json::from_str(&payload.json)?)
247}
248
249#[derive(Debug, Serialize, Deserialize)]
250struct AvroPayload {
251    json: String,
252    version: i32,
253}
254
255fn avro_payload_schema() -> Result<&'static Schema> {
256    match AVRO_PAYLOAD_SCHEMA.get_or_init(|| {
257        Schema::parse_str(AVRO_PAYLOAD_SCHEMA_JSON)
258            .map_err(|err| format!("could not parse avro payload schema: {err}"))
259    }) {
260        Ok(schema) => Ok(schema),
261        Err(message) => Err(Error::Codec(message.clone())),
262    }
263}
264
265#[derive(Clone, Debug)]
266pub struct Client {
267    http: reqwest::Client,
268    base_url: String,
269    token: Option<String>,
270    control_token: Option<String>,
271    worker_token: Option<String>,
272    namespace: String,
273}
274
275impl Client {
276    pub fn new(base_url: impl Into<String>) -> Result<Self> {
277        Self::builder(base_url).build()
278    }
279
280    pub fn builder(base_url: impl Into<String>) -> ClientBuilder {
281        ClientBuilder {
282            base_url: base_url.into(),
283            token: None,
284            control_token: None,
285            worker_token: None,
286            namespace: "default".to_string(),
287            timeout: Duration::from_secs(60),
288        }
289    }
290
291    pub async fn health(&self) -> Result<Value> {
292        self.request_json(
293            reqwest::Method::GET,
294            "/health",
295            RequestProtocol::ControlPlane,
296            Option::<&Value>::None,
297        )
298        .await
299    }
300
301    pub async fn cluster_info(&self) -> Result<Value> {
302        self.request_json(
303            reqwest::Method::GET,
304            "/cluster/info",
305            RequestProtocol::ControlPlane,
306            Option::<&Value>::None,
307        )
308        .await
309    }
310
311    pub async fn start_workflow<T: Serialize>(
312        &self,
313        workflow_type: &str,
314        task_queue: &str,
315        workflow_id: &str,
316        input: T,
317    ) -> Result<WorkflowHandle> {
318        let input = serde_json::to_value(input)?;
319        let input_envelope = encode_value_envelope(&normalize_arguments(input), DEFAULT_CODEC)?;
320        let body = json!({
321            "workflow_id": workflow_id,
322            "workflow_type": workflow_type,
323            "task_queue": task_queue,
324            "input": input_envelope,
325            "execution_timeout_seconds": 3600,
326            "run_timeout_seconds": 600
327        });
328
329        let data: Value = self
330            .request_json(
331                reqwest::Method::POST,
332                "/workflows",
333                RequestProtocol::ControlPlane,
334                Some(&body),
335            )
336            .await?;
337
338        Ok(WorkflowHandle {
339            client: self.clone(),
340            workflow_id: data
341                .get("workflow_id")
342                .and_then(Value::as_str)
343                .unwrap_or(workflow_id)
344                .to_string(),
345            run_id: data
346                .get("run_id")
347                .and_then(Value::as_str)
348                .map(str::to_string),
349            workflow_type: data
350                .get("workflow_type")
351                .and_then(Value::as_str)
352                .unwrap_or(workflow_type)
353                .to_string(),
354        })
355    }
356
357    pub async fn signal_workflow<T: Serialize>(
358        &self,
359        workflow_id: &str,
360        signal_name: &str,
361        input: T,
362    ) -> Result<Value> {
363        let input = serde_json::to_value(input)?;
364        let input_envelope = encode_value_envelope(&normalize_arguments(input), DEFAULT_CODEC)?;
365        let body = json!({
366            "input": input_envelope
367        });
368        let path = format!("/workflows/{workflow_id}/signal/{signal_name}");
369        self.request_json(
370            reqwest::Method::POST,
371            &path,
372            RequestProtocol::ControlPlane,
373            Some(&body),
374        )
375        .await
376    }
377
378    /// Execute a named, read-only query against a running or completed workflow.
379    ///
380    /// Arguments and results use the platform payload envelope. Server and
381    /// worker rejections are returned as [`Error::QueryFailed`] with a stable
382    /// reason, HTTP status, and original response body.
383    pub async fn query_workflow<T: Serialize>(
384        &self,
385        workflow_id: &str,
386        query_name: &str,
387        input: T,
388    ) -> Result<Value> {
389        let input = serde_json::to_value(input)?;
390        let input_envelope = encode_value_envelope(&normalize_arguments(input), DEFAULT_CODEC)?;
391        let body = json!({
392            "input": input_envelope
393        });
394        let path = format!("/workflows/{workflow_id}/query/{query_name}");
395        let response: Value = match self
396            .request_json(
397                reqwest::Method::POST,
398                &path,
399                RequestProtocol::ControlPlane,
400                Some(&body),
401            )
402            .await
403        {
404            Ok(response) => response,
405            Err(Error::Http { status, body }) => {
406                return Err(Error::QueryFailed(query_failure(status, body)));
407            }
408            Err(error) => return Err(error),
409        };
410
411        if let Some(envelope) = response
412            .get("result_envelope")
413            .filter(|envelope| !envelope.is_null())
414        {
415            return decode_wire_value(envelope, DEFAULT_CODEC);
416        }
417
418        Ok(response.get("result").cloned().unwrap_or(Value::Null))
419    }
420
421    pub async fn describe_workflow(&self, workflow_id: &str) -> Result<WorkflowDescription> {
422        let path = format!("/workflows/{workflow_id}");
423        let mut data: WorkflowDescription = self
424            .request_json(
425                reqwest::Method::GET,
426                &path,
427                RequestProtocol::ControlPlane,
428                Option::<&Value>::None,
429            )
430            .await?;
431        data.decode_payloads()?;
432        Ok(data)
433    }
434
435    pub async fn register_worker(
436        &self,
437        worker_id: &str,
438        task_queue: &str,
439        supported_workflow_types: Vec<String>,
440        supported_activity_types: Vec<String>,
441        max_concurrent_workflow_tasks: usize,
442        max_concurrent_activity_tasks: usize,
443    ) -> Result<RegisterWorkerResponse> {
444        self.register_worker_with_capabilities(
445            worker_id,
446            task_queue,
447            supported_workflow_types,
448            supported_activity_types,
449            max_concurrent_workflow_tasks,
450            max_concurrent_activity_tasks,
451            Vec::new(),
452        )
453        .await
454    }
455
456    /// Register a worker and explicitly advertise additive worker capabilities.
457    pub async fn register_worker_with_capabilities(
458        &self,
459        worker_id: &str,
460        task_queue: &str,
461        supported_workflow_types: Vec<String>,
462        supported_activity_types: Vec<String>,
463        max_concurrent_workflow_tasks: usize,
464        max_concurrent_activity_tasks: usize,
465        capabilities: Vec<String>,
466    ) -> Result<RegisterWorkerResponse> {
467        let body = json!({
468            "worker_id": worker_id,
469            "task_queue": task_queue,
470            "runtime": "rust",
471            "sdk_version": SDK_VERSION,
472            "supported_workflow_types": supported_workflow_types,
473            "supported_activity_types": supported_activity_types,
474            "capabilities": capabilities,
475            "max_concurrent_workflow_tasks": max_concurrent_workflow_tasks,
476            "max_concurrent_activity_tasks": max_concurrent_activity_tasks
477        });
478
479        self.request_json(
480            reqwest::Method::POST,
481            "/worker/register",
482            RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
483            Some(&body),
484        )
485        .await
486    }
487
488    /// Long-poll for an ephemeral, read-only workflow query task.
489    pub async fn poll_query_task(
490        &self,
491        worker_id: &str,
492        task_queue: &str,
493        timeout: Duration,
494    ) -> Result<Option<QueryTask>> {
495        let timeout_seconds = long_poll_timeout_seconds(timeout);
496        let body = json!({
497            "worker_id": worker_id,
498            "task_queue": task_queue,
499            "poll_request_id": unique_request_id("rust-query-poll"),
500            "timeout_seconds": timeout_seconds,
501        });
502        let data: PollQueryTaskResponse = self
503            .request_json_with_timeout(
504                reqwest::Method::POST,
505                "/worker/query-tasks/poll",
506                RequestProtocol::Worker(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION),
507                Some(&body),
508                timeout + Duration::from_secs(5),
509            )
510            .await?;
511        Ok(data.task)
512    }
513
514    /// Complete a query task without appending workflow history.
515    pub async fn complete_query_task(
516        &self,
517        query_task_id: &str,
518        lease_owner: &str,
519        query_task_attempt: u64,
520        result: Value,
521        codec: &str,
522    ) -> Result<Value> {
523        let result_envelope = encode_value_envelope(&result, codec)?;
524        self.complete_query_task_with_envelope(
525            query_task_id,
526            lease_owner,
527            query_task_attempt,
528            result,
529            result_envelope,
530        )
531        .await
532    }
533
534    async fn complete_query_task_with_envelope(
535        &self,
536        query_task_id: &str,
537        lease_owner: &str,
538        query_task_attempt: u64,
539        result: Value,
540        result_envelope: Value,
541    ) -> Result<Value> {
542        let body = json!({
543            "lease_owner": lease_owner,
544            "query_task_attempt": query_task_attempt,
545            "result": result,
546            "result_envelope": result_envelope,
547        });
548        let path = format!("/worker/query-tasks/{query_task_id}/complete");
549        let response = self
550            .request_json(
551                reqwest::Method::POST,
552                &path,
553                RequestProtocol::Worker(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION),
554                Some(&body),
555            )
556            .await;
557        query_task_response(response)
558    }
559
560    /// Report a stable machine-readable query-task failure.
561    pub async fn fail_query_task(
562        &self,
563        query_task_id: &str,
564        lease_owner: &str,
565        query_task_attempt: u64,
566        message: impl Into<String>,
567        reason: impl Into<String>,
568        failure_type: impl Into<String>,
569    ) -> Result<Value> {
570        let body = json!({
571            "lease_owner": lease_owner,
572            "query_task_attempt": query_task_attempt,
573            "failure": {
574                "message": message.into(),
575                "reason": reason.into(),
576                "type": failure_type.into(),
577            }
578        });
579        let path = format!("/worker/query-tasks/{query_task_id}/fail");
580        let response = self
581            .request_json(
582                reqwest::Method::POST,
583                &path,
584                RequestProtocol::Worker(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION),
585                Some(&body),
586            )
587            .await;
588        query_task_response(response)
589    }
590
591    pub async fn heartbeat_worker(
592        &self,
593        worker_id: &str,
594        workflow_available: usize,
595        activity_available: usize,
596    ) -> Result<Value> {
597        let body = json!({
598            "worker_id": worker_id,
599            "task_slots": {
600                "workflow_available": workflow_available,
601                "activity_available": activity_available
602            },
603            "process_metrics": {
604                "process_id": std::process::id(),
605                "process_uptime_seconds": 0
606            }
607        });
608
609        self.request_json(
610            reqwest::Method::POST,
611            "/worker/heartbeat",
612            RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
613            Some(&body),
614        )
615        .await
616    }
617
618    pub async fn poll_workflow_task(
619        &self,
620        worker_id: &str,
621        task_queue: &str,
622        timeout: Duration,
623    ) -> Result<Option<WorkflowTask>> {
624        Ok(self
625            .poll_workflow_task_response(worker_id, task_queue, timeout)
626            .await?
627            .task)
628    }
629
630    pub async fn poll_workflow_task_response(
631        &self,
632        worker_id: &str,
633        task_queue: &str,
634        timeout: Duration,
635    ) -> Result<PollWorkflowTaskResponse> {
636        let body = json!({
637            "worker_id": worker_id,
638            "task_queue": task_queue,
639            "timeout_seconds": long_poll_timeout_seconds(timeout),
640        });
641        let mut data: PollWorkflowTaskResponse = self
642            .request_json_with_timeout(
643                reqwest::Method::POST,
644                "/worker/workflow-tasks/poll",
645                RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
646                Some(&body),
647                timeout + Duration::from_secs(5),
648            )
649            .await?;
650
651        if let Some(task) = data.task.as_mut() {
652            self.fetch_remaining_workflow_history(worker_id, task)
653                .await?;
654        }
655
656        Ok(data)
657    }
658
659    async fn fetch_remaining_workflow_history(
660        &self,
661        worker_id: &str,
662        task: &mut WorkflowTask,
663    ) -> Result<()> {
664        let mut next_token = task.next_history_page_token.clone();
665
666        while let Some(token) = next_token.take().filter(|token| !token.is_empty()) {
667            let lease_owner = task
668                .lease_owner
669                .clone()
670                .unwrap_or_else(|| worker_id.to_string());
671            let page = self
672                .workflow_task_history_page(
673                    &task.task_id,
674                    &lease_owner,
675                    task.workflow_task_attempt,
676                    &token,
677                )
678                .await?;
679
680            task.append_history_page(page);
681
682            if task.next_history_page_token.as_deref() == Some(token.as_str()) {
683                return Err(Error::Codec(
684                    "workflow history pagination returned the same page token".to_string(),
685                ));
686            }
687
688            next_token = task.next_history_page_token.clone();
689        }
690
691        Ok(())
692    }
693
694    async fn workflow_task_history_page(
695        &self,
696        task_id: &str,
697        lease_owner: &str,
698        workflow_task_attempt: u64,
699        next_history_page_token: &str,
700    ) -> Result<WorkflowTaskHistoryPage> {
701        let body = json!({
702            "lease_owner": lease_owner,
703            "workflow_task_attempt": workflow_task_attempt,
704            "next_history_page_token": next_history_page_token
705        });
706        let path = format!("/worker/workflow-tasks/{task_id}/history");
707
708        self.request_json(
709            reqwest::Method::POST,
710            &path,
711            RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
712            Some(&body),
713        )
714        .await
715    }
716
717    pub async fn complete_workflow_task(
718        &self,
719        task_id: &str,
720        lease_owner: &str,
721        workflow_task_attempt: u64,
722        commands: Vec<Value>,
723    ) -> Result<Value> {
724        let body = json!({
725            "lease_owner": lease_owner,
726            "workflow_task_attempt": workflow_task_attempt,
727            "commands": commands
728        });
729        let path = format!("/worker/workflow-tasks/{task_id}/complete");
730        self.request_json(
731            reqwest::Method::POST,
732            &path,
733            RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
734            Some(&body),
735        )
736        .await
737    }
738
739    pub async fn fail_workflow_task(
740        &self,
741        task_id: &str,
742        lease_owner: &str,
743        workflow_task_attempt: u64,
744        message: impl Into<String>,
745    ) -> Result<Value> {
746        let body = json!({
747            "lease_owner": lease_owner,
748            "workflow_task_attempt": workflow_task_attempt,
749            "failure": {
750                "message": message.into(),
751                "type": "RustWorkflowTaskFailure"
752            }
753        });
754        let path = format!("/worker/workflow-tasks/{task_id}/fail");
755        self.request_json(
756            reqwest::Method::POST,
757            &path,
758            RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
759            Some(&body),
760        )
761        .await
762    }
763
764    pub async fn poll_activity_task(
765        &self,
766        worker_id: &str,
767        task_queue: &str,
768        timeout: Duration,
769    ) -> Result<Option<ActivityTask>> {
770        let body = json!({
771            "worker_id": worker_id,
772            "task_queue": task_queue,
773            "timeout_seconds": long_poll_timeout_seconds(timeout),
774        });
775        let data: PollActivityTaskResponse = self
776            .request_json_with_timeout(
777                reqwest::Method::POST,
778                "/worker/activity-tasks/poll",
779                RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
780                Some(&body),
781                timeout + Duration::from_secs(5),
782            )
783            .await?;
784        Ok(data.task)
785    }
786
787    pub async fn complete_activity_task(
788        &self,
789        task_id: &str,
790        activity_attempt_id: &str,
791        lease_owner: &str,
792        result: Value,
793        codec: &str,
794    ) -> Result<Value> {
795        let result = encode_value_envelope(&result, codec)?;
796        let body = json!({
797            "activity_attempt_id": activity_attempt_id,
798            "lease_owner": lease_owner,
799            "result": result
800        });
801        let path = format!("/worker/activity-tasks/{task_id}/complete");
802        self.request_json(
803            reqwest::Method::POST,
804            &path,
805            RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
806            Some(&body),
807        )
808        .await
809    }
810
811    pub async fn fail_activity_task(
812        &self,
813        task_id: &str,
814        activity_attempt_id: &str,
815        lease_owner: &str,
816        message: impl Into<String>,
817        non_retryable: bool,
818    ) -> Result<Value> {
819        let body = json!({
820            "activity_attempt_id": activity_attempt_id,
821            "lease_owner": lease_owner,
822            "failure": {
823                "message": message.into(),
824                "type": "RustActivityFailure",
825                "non_retryable": non_retryable
826            }
827        });
828        let path = format!("/worker/activity-tasks/{task_id}/fail");
829        self.request_json(
830            reqwest::Method::POST,
831            &path,
832            RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
833            Some(&body),
834        )
835        .await
836    }
837
838    pub async fn heartbeat_activity_task(
839        &self,
840        task_id: &str,
841        activity_attempt_id: &str,
842        lease_owner: &str,
843        details: Value,
844    ) -> Result<ActivityHeartbeatResponse> {
845        let body = json!({
846            "activity_attempt_id": activity_attempt_id,
847            "lease_owner": lease_owner,
848            "details": details
849        });
850        let path = format!("/worker/activity-tasks/{task_id}/heartbeat");
851        self.request_json(
852            reqwest::Method::POST,
853            &path,
854            RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
855            Some(&body),
856        )
857        .await
858    }
859
860    async fn request_json<T: DeserializeOwned, B: Serialize + ?Sized>(
861        &self,
862        method: reqwest::Method,
863        path: &str,
864        protocol: RequestProtocol,
865        body: Option<&B>,
866    ) -> Result<T> {
867        self.request_json_with_timeout(method, path, protocol, body, Duration::from_secs(60))
868            .await
869    }
870
871    async fn request_json_with_timeout<T: DeserializeOwned, B: Serialize + ?Sized>(
872        &self,
873        method: reqwest::Method,
874        path: &str,
875        protocol: RequestProtocol,
876        body: Option<&B>,
877        timeout: Duration,
878    ) -> Result<T> {
879        let mut request = self
880            .http
881            .request(method, format!("{}/api{}", self.base_url, path))
882            .timeout(timeout)
883            .header(reqwest::header::ACCEPT, "application/json")
884            .header(reqwest::header::CONTENT_TYPE, "application/json")
885            .header("X-Namespace", &self.namespace);
886
887        match protocol {
888            RequestProtocol::Worker(version) => {
889                request = request.header("X-Durable-Workflow-Protocol-Version", version);
890            }
891            RequestProtocol::ControlPlane => {
892                request = request.header(
893                    "X-Durable-Workflow-Control-Plane-Version",
894                    CONTROL_PLANE_VERSION,
895                );
896            }
897        }
898
899        if let Some(token) = self.auth_token(protocol.is_worker()) {
900            request = request.bearer_auth(token);
901        }
902
903        if let Some(body) = body {
904            request = request.json(body);
905        }
906
907        let response = request.send().await?;
908        let status = response.status();
909        let bytes = response.bytes().await?;
910
911        if !status.is_success() {
912            let body = String::from_utf8_lossy(&bytes).to_string();
913            if let Some(protocol) = protocol_failure(status, &body) {
914                return Err(Error::Protocol(protocol));
915            }
916            return Err(Error::Http { status, body });
917        }
918
919        if bytes.is_empty() {
920            return Ok(serde_json::from_value(Value::Null)?);
921        }
922
923        Ok(serde_json::from_slice(&bytes)?)
924    }
925
926    fn auth_token(&self, worker: bool) -> Option<&str> {
927        if worker {
928            self.worker_token
929                .as_deref()
930                .or(self.token.as_deref())
931                .or(self.control_token.as_deref())
932        } else {
933            self.control_token
934                .as_deref()
935                .or(self.token.as_deref())
936                .or(self.worker_token.as_deref())
937        }
938    }
939}
940
941fn query_failure(status: reqwest::StatusCode, raw_body: String) -> QueryFailure {
942    let body = serde_json::from_str(&raw_body).unwrap_or_else(|_| json!({"message": raw_body}));
943    let reason = body
944        .get("reason")
945        .and_then(Value::as_str)
946        .unwrap_or("query_rejected")
947        .to_string();
948    let message = body
949        .get("message")
950        .or_else(|| body.get("error"))
951        .and_then(Value::as_str)
952        .unwrap_or("workflow query was rejected")
953        .to_string();
954
955    QueryFailure {
956        status: status.as_u16(),
957        reason,
958        message,
959        body,
960    }
961}
962
963fn query_task_response(response: Result<Value>) -> Result<Value> {
964    match response {
965        Err(Error::Http { status, body }) => Err(Error::QueryFailed(query_failure(status, body))),
966        response => response,
967    }
968}
969
970fn query_task_rejection_is_final(error: &Error) -> bool {
971    matches!(
972        error,
973        Error::QueryFailed(failure)
974            if QUERY_TASK_FINAL_REJECTION_REASONS.contains(&failure.reason.as_str())
975    )
976}
977
978fn protocol_failure(status: reqwest::StatusCode, raw_body: &str) -> Option<ProtocolFailure> {
979    let body: Value = serde_json::from_str(raw_body).ok()?;
980    let reason = body.get("reason")?.as_str()?;
981    if !matches!(
982        reason,
983        "missing_protocol_version"
984            | "unsupported_protocol_version"
985            | "missing_control_plane_version"
986            | "unsupported_control_plane_version"
987    ) {
988        return None;
989    }
990
991    Some(ProtocolFailure {
992        status: status.as_u16(),
993        reason: reason.to_string(),
994        message: body
995            .get("message")
996            .or_else(|| body.get("error"))
997            .and_then(Value::as_str)
998            .unwrap_or("protocol version rejected")
999            .to_string(),
1000        supported_version: body
1001            .get("supported_version")
1002            .and_then(Value::as_str)
1003            .map(str::to_string),
1004        requested_version: body
1005            .get("requested_version")
1006            .and_then(Value::as_str)
1007            .map(str::to_string),
1008        body,
1009    })
1010}
1011
1012fn long_poll_timeout_seconds(timeout: Duration) -> u64 {
1013    timeout
1014        .as_secs()
1015        .saturating_add(u64::from(timeout.subsec_nanos() > 0))
1016        .min(MAX_LONG_POLL_TIMEOUT_SECONDS)
1017}
1018
1019fn worker_operation_is_retryable(error: &Error) -> bool {
1020    match error {
1021        Error::Transport(error) => {
1022            error.is_timeout() || error.is_connect() || error.is_request() || error.is_body()
1023        }
1024        Error::Http { status, .. } => {
1025            matches!(
1026                *status,
1027                reqwest::StatusCode::REQUEST_TIMEOUT | reqwest::StatusCode::TOO_MANY_REQUESTS
1028            ) || status.is_server_error()
1029        }
1030        _ => false,
1031    }
1032}
1033
1034fn worker_retry_delay(policy: WorkerRetryPolicy, retry: usize) -> Duration {
1035    let exponent = retry.saturating_sub(1).min(31) as u32;
1036    policy
1037        .initial_backoff
1038        .saturating_mul(1_u32 << exponent)
1039        .min(policy.max_backoff)
1040}
1041
1042#[derive(Debug)]
1043pub struct ClientBuilder {
1044    base_url: String,
1045    token: Option<String>,
1046    control_token: Option<String>,
1047    worker_token: Option<String>,
1048    namespace: String,
1049    timeout: Duration,
1050}
1051
1052impl ClientBuilder {
1053    pub fn token(mut self, token: Option<String>) -> Self {
1054        self.token = token;
1055        self
1056    }
1057
1058    pub fn control_token(mut self, token: Option<String>) -> Self {
1059        self.control_token = token;
1060        self
1061    }
1062
1063    pub fn worker_token(mut self, token: Option<String>) -> Self {
1064        self.worker_token = token;
1065        self
1066    }
1067
1068    pub fn namespace(mut self, namespace: impl Into<String>) -> Self {
1069        self.namespace = namespace.into();
1070        self
1071    }
1072
1073    pub fn timeout(mut self, timeout: Duration) -> Self {
1074        self.timeout = timeout;
1075        self
1076    }
1077
1078    pub fn build(self) -> Result<Client> {
1079        Ok(Client {
1080            http: reqwest::Client::builder().timeout(self.timeout).build()?,
1081            base_url: self.base_url.trim_end_matches('/').to_string(),
1082            token: self.token,
1083            control_token: self.control_token,
1084            worker_token: self.worker_token,
1085            namespace: self.namespace,
1086        })
1087    }
1088}
1089
1090#[derive(Clone, Debug)]
1091pub struct WorkflowHandle {
1092    client: Client,
1093    pub workflow_id: String,
1094    pub run_id: Option<String>,
1095    pub workflow_type: String,
1096}
1097
1098impl WorkflowHandle {
1099    pub async fn describe(&self) -> Result<WorkflowDescription> {
1100        self.client.describe_workflow(&self.workflow_id).await
1101    }
1102
1103    pub async fn signal<T: Serialize>(&self, signal_name: &str, input: T) -> Result<Value> {
1104        self.client
1105            .signal_workflow(&self.workflow_id, signal_name, input)
1106            .await
1107    }
1108
1109    /// Execute a named, read-only query against this workflow.
1110    pub async fn query<T: Serialize>(&self, query_name: &str, input: T) -> Result<Value> {
1111        self.client
1112            .query_workflow(&self.workflow_id, query_name, input)
1113            .await
1114    }
1115
1116    pub async fn result(&self, options: WorkflowResultOptions) -> Result<Value> {
1117        let started = Instant::now();
1118
1119        loop {
1120            let description = self.describe().await?;
1121            if description.is_completed() {
1122                return Ok(description.output.unwrap_or(Value::Null));
1123            }
1124
1125            if description.is_terminal() {
1126                return Err(Error::Codec(format!(
1127                    "workflow {} closed with status {:?}",
1128                    self.workflow_id, description.status
1129                )));
1130            }
1131
1132            if started.elapsed() >= options.timeout {
1133                return Err(Error::Timeout);
1134            }
1135
1136            tokio::time::sleep(options.poll_interval).await;
1137        }
1138    }
1139}
1140
1141#[derive(Clone, Copy, Debug)]
1142pub struct WorkflowResultOptions {
1143    pub poll_interval: Duration,
1144    pub timeout: Duration,
1145}
1146
1147impl Default for WorkflowResultOptions {
1148    fn default() -> Self {
1149        Self {
1150            poll_interval: Duration::from_millis(500),
1151            timeout: Duration::from_secs(30),
1152        }
1153    }
1154}
1155
1156#[derive(Clone, Debug, Deserialize)]
1157pub struct WorkflowDescription {
1158    pub workflow_id: Option<String>,
1159    pub run_id: Option<String>,
1160    pub workflow_type: Option<String>,
1161    pub status: Option<String>,
1162    #[serde(default)]
1163    pub output: Option<Value>,
1164    #[serde(default)]
1165    pub output_envelope: Option<Value>,
1166    #[serde(flatten)]
1167    pub raw: HashMap<String, Value>,
1168}
1169
1170impl WorkflowDescription {
1171    pub fn is_completed(&self) -> bool {
1172        matches!(self.status.as_deref(), Some("completed" | "Completed"))
1173    }
1174
1175    pub fn is_terminal(&self) -> bool {
1176        matches!(
1177            self.status.as_deref(),
1178            Some(
1179                "completed"
1180                    | "Completed"
1181                    | "failed"
1182                    | "Failed"
1183                    | "cancelled"
1184                    | "Cancelled"
1185                    | "terminated"
1186                    | "Terminated"
1187                    | "timed_out"
1188                    | "TimedOut",
1189            )
1190        )
1191    }
1192
1193    fn decode_payloads(&mut self) -> Result<()> {
1194        if let Some(envelope) = &self.output_envelope {
1195            self.output = Some(decode_wire_value(envelope, DEFAULT_CODEC)?);
1196        }
1197
1198        Ok(())
1199    }
1200}
1201
1202#[derive(Clone, Debug, Deserialize)]
1203pub struct RegisterWorkerResponse {
1204    pub worker_id: String,
1205    pub registered: bool,
1206    #[serde(default)]
1207    pub heartbeat_interval_seconds: Option<u64>,
1208    #[serde(default)]
1209    pub protocol_version: Option<String>,
1210    #[serde(default)]
1211    pub server_capabilities: Option<Value>,
1212}
1213
1214#[derive(Clone, Debug, Deserialize)]
1215pub struct PollWorkflowTaskResponse {
1216    #[serde(default)]
1217    pub task: Option<WorkflowTask>,
1218    #[serde(default)]
1219    pub poll_status: Option<String>,
1220    #[serde(default)]
1221    pub reason: Option<String>,
1222    #[serde(default)]
1223    pub protocol_version: Option<String>,
1224    #[serde(default)]
1225    pub server_capabilities: Option<Value>,
1226}
1227
1228#[derive(Clone, Debug, Deserialize)]
1229struct PollActivityTaskResponse {
1230    #[serde(default)]
1231    task: Option<ActivityTask>,
1232}
1233
1234#[derive(Clone, Debug, Deserialize)]
1235struct PollQueryTaskResponse {
1236    #[serde(default)]
1237    task: Option<QueryTask>,
1238}
1239
1240/// An ephemeral server-routed query task.
1241#[derive(Clone, Debug, Deserialize)]
1242pub struct QueryTask {
1243    pub query_task_id: String,
1244    #[serde(default = "default_workflow_task_attempt")]
1245    pub query_task_attempt: u64,
1246    #[serde(default)]
1247    pub lease_owner: Option<String>,
1248    #[serde(default)]
1249    pub workflow_id: Option<String>,
1250    #[serde(default)]
1251    pub run_id: Option<String>,
1252    pub workflow_type: String,
1253    pub query_name: String,
1254    #[serde(default = "default_payload_codec")]
1255    pub payload_codec: String,
1256    #[serde(default)]
1257    pub workflow_arguments: Option<Value>,
1258    #[serde(default)]
1259    pub query_arguments: Option<Value>,
1260    #[serde(default)]
1261    pub history_events: Vec<HistoryEvent>,
1262    #[serde(default)]
1263    pub history_export: Option<Value>,
1264    #[serde(default)]
1265    pub run_status: Option<String>,
1266}
1267
1268#[derive(Clone, Debug, Deserialize)]
1269pub struct WorkflowTask {
1270    pub task_id: String,
1271    #[serde(default)]
1272    pub workflow_id: Option<String>,
1273    #[serde(default)]
1274    pub run_id: Option<String>,
1275    pub workflow_type: String,
1276    #[serde(default = "default_payload_codec")]
1277    pub payload_codec: String,
1278    #[serde(default)]
1279    pub arguments: Option<Value>,
1280    #[serde(default)]
1281    pub history_events: Vec<HistoryEvent>,
1282    #[serde(default)]
1283    pub total_history_events: Option<u64>,
1284    #[serde(default)]
1285    pub next_history_page_token: Option<String>,
1286    #[serde(default = "default_workflow_task_attempt")]
1287    pub workflow_task_attempt: u64,
1288    #[serde(default)]
1289    pub workflow_signal_id: Option<String>,
1290    #[serde(default)]
1291    pub signal_name: Option<String>,
1292    #[serde(default)]
1293    pub signal_arguments: Option<Value>,
1294    #[serde(default)]
1295    pub lease_owner: Option<String>,
1296}
1297
1298impl WorkflowTask {
1299    fn append_history_page(&mut self, page: WorkflowTaskHistoryPage) {
1300        self.history_events.extend(page.history_events);
1301
1302        if page.total_history_events.is_some() {
1303            self.total_history_events = page.total_history_events;
1304        }
1305
1306        self.next_history_page_token = page
1307            .next_history_page_token
1308            .filter(|token| !token.is_empty());
1309    }
1310}
1311
1312#[derive(Clone, Debug, Deserialize)]
1313struct WorkflowTaskHistoryPage {
1314    #[serde(default)]
1315    history_events: Vec<HistoryEvent>,
1316    #[serde(default)]
1317    total_history_events: Option<u64>,
1318    #[serde(default)]
1319    next_history_page_token: Option<String>,
1320}
1321
1322#[derive(Clone, Debug, Deserialize)]
1323pub struct ActivityTask {
1324    pub task_id: String,
1325    #[serde(default)]
1326    pub activity_attempt_id: Option<String>,
1327    #[serde(default)]
1328    pub attempt_id: Option<String>,
1329    pub activity_type: String,
1330    #[serde(default = "default_payload_codec")]
1331    pub payload_codec: String,
1332    #[serde(default)]
1333    pub arguments: Option<Value>,
1334    #[serde(default = "default_attempt_number")]
1335    pub attempt_number: u64,
1336    #[serde(default)]
1337    pub lease_owner: Option<String>,
1338}
1339
1340#[derive(Clone, Debug, Deserialize)]
1341pub struct HistoryEvent {
1342    #[serde(alias = "type")]
1343    pub event_type: String,
1344    #[serde(default)]
1345    pub payload: Value,
1346    #[serde(flatten)]
1347    pub raw: HashMap<String, Value>,
1348}
1349
1350/// One decoded signal in the committed workflow-history snapshot.
1351#[derive(Clone, Debug, PartialEq)]
1352pub struct QuerySignal {
1353    pub id: Option<String>,
1354    pub name: String,
1355    pub arguments: Vec<Value>,
1356    pub workflow_sequence: Option<u64>,
1357}
1358
1359/// Immutable state supplied to a registered query handler.
1360///
1361/// This context intentionally exposes no activity, signal-wait, or command
1362/// APIs. Query handlers inspect committed history and return a value; query
1363/// completion does not append an event or advance deterministic execution.
1364#[derive(Clone, Debug)]
1365pub struct QueryContext {
1366    pub workflow_id: Option<String>,
1367    pub run_id: Option<String>,
1368    pub workflow_type: String,
1369    pub run_status: Option<String>,
1370    workflow_input: Value,
1371    history_events: Arc<Vec<HistoryEvent>>,
1372    signal_events: Arc<Vec<QuerySignal>>,
1373}
1374
1375impl QueryContext {
1376    /// The normalized argument list used to start the workflow.
1377    pub fn workflow_input(&self) -> &Value {
1378        &self.workflow_input
1379    }
1380
1381    /// The immutable committed history used for this query snapshot.
1382    pub fn history_events(&self) -> &[HistoryEvent] {
1383        self.history_events.as_slice()
1384    }
1385
1386    /// All decoded signals in committed workflow order.
1387    pub fn signal_events(&self) -> &[QuerySignal] {
1388        self.signal_events.as_slice()
1389    }
1390
1391    /// Decoded argument lists for each committed signal with `signal_name`.
1392    pub fn signals(&self, signal_name: &str) -> Vec<Vec<Value>> {
1393        self.signal_events
1394            .iter()
1395            .filter(|signal| signal.name == signal_name)
1396            .map(|signal| signal.arguments.clone())
1397            .collect()
1398    }
1399}
1400
1401#[derive(Clone, Debug, Deserialize)]
1402pub struct ActivityHeartbeatResponse {
1403    #[serde(default)]
1404    pub cancel_requested: bool,
1405    #[serde(default)]
1406    pub heartbeat_recorded: bool,
1407}
1408
1409fn default_payload_codec() -> String {
1410    DEFAULT_CODEC.to_string()
1411}
1412
1413fn default_workflow_task_attempt() -> u64 {
1414    1
1415}
1416
1417fn default_attempt_number() -> u64 {
1418    1
1419}
1420
1421type WorkflowFuture = Pin<Box<dyn Future<Output = Result<Value>> + Send + 'static>>;
1422type WorkflowHandler = Arc<dyn Fn(WorkflowContext, Value) -> WorkflowFuture + Send + Sync>;
1423type ErasedWorkflowState = Arc<dyn Any + Send + Sync>;
1424type WorkflowStateSnapshot = Arc<dyn Fn() -> Result<ErasedWorkflowState> + Send + Sync>;
1425type ReplayedWorkflowHandler =
1426    Arc<dyn Fn(WorkflowContext, Value) -> ReplayedWorkflowInvocation + Send + Sync>;
1427type ActivityFuture = Pin<Box<dyn Future<Output = Result<Value>> + Send + 'static>>;
1428type ActivityHandler = Arc<dyn Fn(ActivityContext, Value) -> ActivityFuture + Send + Sync>;
1429type QueryFuture = Pin<Box<dyn Future<Output = Result<Value>> + Send + 'static>>;
1430type QueryHandler = Arc<dyn Fn(QueryContext, Value) -> QueryFuture + Send + Sync>;
1431type ReplayedQueryHandler = Arc<
1432    dyn Fn(QueryContext, ErasedWorkflowState, Value) -> std::result::Result<QueryFuture, String>
1433        + Send
1434        + Sync,
1435>;
1436type WorkerHeartbeatObserver = Arc<dyn Fn(&WorkerHeartbeatObservation) + Send + Sync>;
1437
1438struct ReplayedWorkflowInvocation {
1439    future: WorkflowFuture,
1440    snapshot: WorkflowStateSnapshot,
1441}
1442
1443#[derive(Clone)]
1444struct RegisteredWorkflow {
1445    execute: WorkflowHandler,
1446    replay: Option<ReplayedWorkflowHandler>,
1447    state_type: Option<TypeId>,
1448}
1449
1450#[derive(Clone)]
1451enum RegisteredQuery {
1452    Snapshot(QueryHandler),
1453    Replayed {
1454        state_type: TypeId,
1455        handler: ReplayedQueryHandler,
1456    },
1457}
1458
1459#[derive(Clone, Debug)]
1460pub struct WorkerHeartbeatObservation {
1461    pub worker_id: String,
1462    pub task_queue: String,
1463    pub acknowledged_at_unix_millis: u64,
1464    pub acknowledgement: Value,
1465}
1466
1467/// Bounded retry policy for worker poll acquisition and worker heartbeats.
1468///
1469/// Expected empty long polls are normal successful responses. Transport
1470/// failures, HTTP 408/429 responses, and server errors are retried with capped
1471/// exponential backoff. Authentication, protocol, codec, and handler failures
1472/// are never retried by the worker.
1473#[derive(Clone, Copy, Debug)]
1474pub struct WorkerRetryPolicy {
1475    /// Number of retries after the initial request fails.
1476    pub max_retries: usize,
1477    /// Delay before the first retry.
1478    pub initial_backoff: Duration,
1479    /// Maximum delay between retries.
1480    pub max_backoff: Duration,
1481}
1482
1483impl Default for WorkerRetryPolicy {
1484    fn default() -> Self {
1485        Self {
1486            max_retries: 5,
1487            initial_backoff: Duration::from_millis(100),
1488            max_backoff: Duration::from_secs(5),
1489        }
1490    }
1491}
1492
1493#[derive(Clone)]
1494pub struct Worker {
1495    client: Client,
1496    worker_id: String,
1497    task_queue: String,
1498    workflows: HashMap<String, RegisteredWorkflow>,
1499    activities: HashMap<String, ActivityHandler>,
1500    queries: HashMap<String, HashMap<String, RegisteredQuery>>,
1501    max_concurrent_workflow_tasks: usize,
1502    max_concurrent_activity_tasks: usize,
1503    poll_timeout: Duration,
1504    heartbeat_interval: Duration,
1505    retry_policy: WorkerRetryPolicy,
1506    heartbeat_observer: Option<WorkerHeartbeatObserver>,
1507}
1508
1509impl Worker {
1510    pub fn new(client: Client, task_queue: impl Into<String>) -> Self {
1511        Self {
1512            client,
1513            worker_id: default_worker_id(),
1514            task_queue: task_queue.into(),
1515            workflows: HashMap::new(),
1516            activities: HashMap::new(),
1517            queries: HashMap::new(),
1518            max_concurrent_workflow_tasks: 10,
1519            max_concurrent_activity_tasks: 10,
1520            poll_timeout: Duration::from_secs(30),
1521            heartbeat_interval: Duration::from_secs(60),
1522            retry_policy: WorkerRetryPolicy::default(),
1523            heartbeat_observer: None,
1524        }
1525    }
1526
1527    pub fn worker_id(mut self, worker_id: impl Into<String>) -> Self {
1528        self.worker_id = worker_id.into();
1529        self
1530    }
1531
1532    pub fn poll_timeout(mut self, timeout: Duration) -> Self {
1533        self.poll_timeout = timeout;
1534        self
1535    }
1536
1537    pub fn heartbeat_interval(mut self, interval: Duration) -> Self {
1538        self.heartbeat_interval = interval;
1539        self
1540    }
1541
1542    /// Configure bounded retries for task-poll acquisition and worker heartbeats.
1543    pub fn retry_policy(mut self, policy: WorkerRetryPolicy) -> Self {
1544        self.retry_policy = policy;
1545        self
1546    }
1547
1548    pub fn on_worker_heartbeat<F>(mut self, observer: F) -> Self
1549    where
1550        F: Fn(&WorkerHeartbeatObservation) + Send + Sync + 'static,
1551    {
1552        self.heartbeat_observer = Some(Arc::new(observer));
1553        self
1554    }
1555
1556    pub fn max_concurrent_workflow_tasks(mut self, count: usize) -> Self {
1557        self.max_concurrent_workflow_tasks = count.max(1);
1558        self
1559    }
1560
1561    pub fn max_concurrent_activity_tasks(mut self, count: usize) -> Self {
1562        self.max_concurrent_activity_tasks = count.max(1);
1563        self
1564    }
1565
1566    pub fn register_workflow<F, Fut>(&mut self, workflow_type: impl Into<String>, handler: F)
1567    where
1568        F: Fn(WorkflowContext, Value) -> Fut + Send + Sync + 'static,
1569        Fut: Future<Output = Result<Value>> + Send + 'static,
1570    {
1571        self.workflows.insert(
1572            workflow_type.into(),
1573            RegisteredWorkflow {
1574                execute: Arc::new(move |ctx, input| Box::pin(handler(ctx, input))),
1575                replay: None,
1576                state_type: None,
1577            },
1578        );
1579    }
1580
1581    /// Register a workflow whose typed instance state can be reconstructed for queries.
1582    ///
1583    /// `state_factory` creates a fresh instance for every normal workflow task and
1584    /// query replay. The workflow handler is the single source of truth for state
1585    /// transitions: it updates [`WorkflowInstance`] after activities and signals
1586    /// resolve. Query replay runs this same handler over committed history and
1587    /// discards any commands it would emit.
1588    pub fn register_replayed_workflow<S, Factory, F, Fut>(
1589        &mut self,
1590        workflow_type: impl Into<String>,
1591        state_factory: Factory,
1592        handler: F,
1593    ) where
1594        S: Clone + Send + Sync + 'static,
1595        Factory: Fn() -> S + Send + Sync + 'static,
1596        F: Fn(WorkflowContext, Value, WorkflowInstance<S>) -> Fut + Send + Sync + 'static,
1597        Fut: Future<Output = Result<Value>> + Send + 'static,
1598    {
1599        let state_factory = Arc::new(state_factory);
1600        let handler = Arc::new(handler);
1601
1602        let execute_factory = Arc::clone(&state_factory);
1603        let execute_handler = Arc::clone(&handler);
1604        let execute = Arc::new(move |ctx: WorkflowContext, input: Value| {
1605            let state = WorkflowInstance::new(execute_factory());
1606            let future = execute_handler(ctx, input, state);
1607            Box::pin(future) as WorkflowFuture
1608        });
1609
1610        let replay = Arc::new(move |ctx: WorkflowContext, input: Value| {
1611            let state = WorkflowInstance::new(state_factory());
1612            let snapshot_state = state.clone();
1613            let snapshot: WorkflowStateSnapshot =
1614                Arc::new(move || Ok(Arc::new(snapshot_state.snapshot()?) as ErasedWorkflowState));
1615            let future = handler(ctx, input, state);
1616            ReplayedWorkflowInvocation {
1617                future: Box::pin(future),
1618                snapshot,
1619            }
1620        });
1621
1622        self.workflows.insert(
1623            workflow_type.into(),
1624            RegisteredWorkflow {
1625                execute,
1626                replay: Some(replay),
1627                state_type: Some(TypeId::of::<S>()),
1628            },
1629        );
1630    }
1631
1632    pub fn register_activity<F, Fut>(&mut self, activity_type: impl Into<String>, handler: F)
1633    where
1634        F: Fn(ActivityContext, Value) -> Fut + Send + Sync + 'static,
1635        Fut: Future<Output = Result<Value>> + Send + 'static,
1636    {
1637        self.activities.insert(
1638            activity_type.into(),
1639            Arc::new(move |ctx, args| Box::pin(handler(ctx, args))),
1640        );
1641    }
1642
1643    /// Register a named, read-only query handler for a workflow type.
1644    ///
1645    /// The workflow type must also be registered with [`Worker::register_workflow`]
1646    /// before the worker runs. The handler receives only an immutable committed
1647    /// state snapshot and normalized query arguments.
1648    pub fn register_query<F, Fut>(
1649        &mut self,
1650        workflow_type: impl Into<String>,
1651        query_name: impl Into<String>,
1652        handler: F,
1653    ) where
1654        F: Fn(QueryContext, Value) -> Fut + Send + Sync + 'static,
1655        Fut: Future<Output = Result<Value>> + Send + 'static,
1656    {
1657        self.queries
1658            .entry(workflow_type.into())
1659            .or_default()
1660            .insert(
1661                query_name.into(),
1662                RegisteredQuery::Snapshot(Arc::new(move |ctx, args| Box::pin(handler(ctx, args)))),
1663            );
1664    }
1665
1666    /// Register a named query against deterministically replayed instance state.
1667    ///
1668    /// The workflow type must use [`Worker::register_replayed_workflow`] with the
1669    /// same state type `S`. The handler receives an immutable, detached state
1670    /// clone, so successful and failed queries cannot affect workflow execution
1671    /// or the state reconstructed by a later query.
1672    pub fn register_replayed_query<S, F, Fut>(
1673        &mut self,
1674        workflow_type: impl Into<String>,
1675        query_name: impl Into<String>,
1676        handler: F,
1677    ) where
1678        S: Clone + Send + Sync + 'static,
1679        F: Fn(QueryContext, Arc<S>, Value) -> Fut + Send + Sync + 'static,
1680        Fut: Future<Output = Result<Value>> + Send + 'static,
1681    {
1682        let handler = Arc::new(handler);
1683        let erased_handler: ReplayedQueryHandler = Arc::new(move |ctx, state, args| {
1684            let state = state.downcast::<S>().map_err(|_| {
1685                "registered query state type does not match the replayed workflow state".to_string()
1686            })?;
1687            Ok(Box::pin(handler(ctx, state, args)))
1688        });
1689
1690        self.queries
1691            .entry(workflow_type.into())
1692            .or_default()
1693            .insert(
1694                query_name.into(),
1695                RegisteredQuery::Replayed {
1696                    state_type: TypeId::of::<S>(),
1697                    handler: erased_handler,
1698                },
1699            );
1700    }
1701
1702    pub async fn register(&self) -> Result<RegisterWorkerResponse> {
1703        self.client
1704            .register_worker_with_capabilities(
1705                &self.worker_id,
1706                &self.task_queue,
1707                self.workflows.keys().cloned().collect(),
1708                self.activities.keys().cloned().collect(),
1709                self.max_concurrent_workflow_tasks,
1710                self.max_concurrent_activity_tasks,
1711                (!self.queries.is_empty())
1712                    .then(|| QUERY_TASKS_CAPABILITY.to_string())
1713                    .into_iter()
1714                    .collect(),
1715            )
1716            .await
1717    }
1718
1719    /// Run until shutdown or a terminal worker error occurs.
1720    ///
1721    /// Empty long-poll expirations do not stop the worker. Retryable poll and
1722    /// heartbeat failures use [`WorkerRetryPolicy`] independently, while
1723    /// authentication, protocol, and other non-retryable failures are returned.
1724    pub async fn run(&self) -> Result<()> {
1725        self.run_until(std::future::pending::<()>()).await
1726    }
1727
1728    /// Run until `shutdown` resolves or a terminal worker error occurs.
1729    ///
1730    /// This has the same liveness and terminal-error contract as [`Worker::run`].
1731    pub async fn run_until<F>(&self, shutdown: F) -> Result<()>
1732    where
1733        F: Future<Output = ()>,
1734    {
1735        let registration = self.register().await?;
1736        let mut heartbeat = tokio::time::interval(Duration::from_secs(
1737            registration
1738                .heartbeat_interval_seconds
1739                .unwrap_or(self.heartbeat_interval.as_secs().max(1)),
1740        ));
1741        tokio::pin!(shutdown);
1742        let stop = Arc::new(AtomicBool::new(false));
1743        // Poll responses may already have leased server-side work by the time
1744        // they become ready, so each poller owns its responses through
1745        // completion or failure instead of racing raw polls in this select.
1746        let mut workflow_poller = (!self.workflows.is_empty()).then(|| {
1747            let worker = self.clone();
1748            let stop = Arc::clone(&stop);
1749            tokio::spawn(async move { worker.poll_workflows_until_stopped(stop).await })
1750        });
1751        let mut activity_poller = (!self.activities.is_empty()).then(|| {
1752            let worker = self.clone();
1753            let stop = Arc::clone(&stop);
1754            tokio::spawn(async move { worker.poll_activities_until_stopped(stop).await })
1755        });
1756        let mut query_poller = (!self.queries.is_empty()).then(|| {
1757            let worker = self.clone();
1758            let stop = Arc::clone(&stop);
1759            tokio::spawn(async move { worker.poll_queries_until_stopped(stop).await })
1760        });
1761
1762        loop {
1763            tokio::select! {
1764                _ = &mut shutdown => {
1765                    stop.store(true, Ordering::SeqCst);
1766                    break;
1767                }
1768                _ = heartbeat.tick() => {
1769                    match self.retry_worker_operation(|| {
1770                        self.client.heartbeat_worker(
1771                            &self.worker_id,
1772                            self.max_concurrent_workflow_tasks,
1773                            self.max_concurrent_activity_tasks,
1774                        )
1775                    }).await
1776                    {
1777                        Ok(acknowledgement) => {
1778                            if let Some(observer) = &self.heartbeat_observer {
1779                                observer(&WorkerHeartbeatObservation {
1780                                    worker_id: self.worker_id.clone(),
1781                                    task_queue: self.task_queue.clone(),
1782                                    acknowledged_at_unix_millis: SystemTime::now()
1783                                        .duration_since(UNIX_EPOCH)
1784                                        .unwrap_or_default()
1785                                        .as_millis()
1786                                        .min(u64::MAX as u128)
1787                                        as u64,
1788                                    acknowledgement,
1789                                });
1790                            }
1791                        }
1792                        Err(error) => {
1793                            stop.store(true, Ordering::SeqCst);
1794                            join_pollers(workflow_poller.take(), activity_poller.take(), query_poller.take()).await?;
1795                            return Err(error);
1796                        }
1797                    }
1798                }
1799                result = OptionFuture::from(workflow_poller.as_mut()), if workflow_poller.is_some() => {
1800                    workflow_poller = None;
1801                    stop.store(true, Ordering::SeqCst);
1802                    let poller_result = optional_poller_result("workflow", result);
1803                    let join_result =
1804                        join_pollers(workflow_poller.take(), activity_poller.take(), query_poller.take()).await;
1805                    poller_result?;
1806                    join_result?;
1807                    return Err(Error::WorkerLoop(
1808                        "workflow poller stopped unexpectedly".to_string(),
1809                    ));
1810                }
1811                result = OptionFuture::from(activity_poller.as_mut()), if activity_poller.is_some() => {
1812                    activity_poller = None;
1813                    stop.store(true, Ordering::SeqCst);
1814                    let poller_result = optional_poller_result("activity", result);
1815                    let join_result =
1816                        join_pollers(workflow_poller.take(), activity_poller.take(), query_poller.take()).await;
1817                    poller_result?;
1818                    join_result?;
1819                    return Err(Error::WorkerLoop(
1820                        "activity poller stopped unexpectedly".to_string(),
1821                    ));
1822                }
1823                result = OptionFuture::from(query_poller.as_mut()), if query_poller.is_some() => {
1824                    query_poller = None;
1825                    stop.store(true, Ordering::SeqCst);
1826                    let poller_result = optional_poller_result("query", result);
1827                    let join_result =
1828                        join_pollers(workflow_poller.take(), activity_poller.take(), query_poller.take()).await;
1829                    poller_result?;
1830                    join_result?;
1831                    return Err(Error::WorkerLoop(
1832                        "query poller stopped unexpectedly".to_string(),
1833                    ));
1834                }
1835            }
1836        }
1837
1838        join_pollers(
1839            workflow_poller.take(),
1840            activity_poller.take(),
1841            query_poller.take(),
1842        )
1843        .await
1844    }
1845
1846    pub async fn run_once(&self) -> Result<usize> {
1847        let mut handled = 0;
1848        if self.poll_workflow_once().await? {
1849            handled += 1;
1850        }
1851        if self.poll_activity_once().await? {
1852            handled += 1;
1853        }
1854        if !self.queries.is_empty() && self.poll_query_once().await? {
1855            handled += 1;
1856        }
1857        Ok(handled)
1858    }
1859
1860    async fn poll_workflow_once(&self) -> Result<bool> {
1861        let Some(task) = self
1862            .retry_worker_operation(|| {
1863                self.client
1864                    .poll_workflow_task(&self.worker_id, &self.task_queue, self.poll_timeout)
1865            })
1866            .await?
1867        else {
1868            return Ok(false);
1869        };
1870
1871        let task_id = task.task_id.clone();
1872        let attempt = task.workflow_task_attempt;
1873        let lease_owner = task
1874            .lease_owner
1875            .clone()
1876            .unwrap_or_else(|| self.worker_id.clone());
1877
1878        match self.execute_workflow_task(task) {
1879            Ok(commands) => {
1880                self.client
1881                    .complete_workflow_task(&task_id, &lease_owner, attempt, commands)
1882                    .await?;
1883            }
1884            Err(error) => {
1885                self.client
1886                    .fail_workflow_task(&task_id, &lease_owner, attempt, error.to_string())
1887                    .await?;
1888            }
1889        }
1890
1891        Ok(true)
1892    }
1893
1894    async fn poll_workflows_until_stopped(self, stop: Arc<AtomicBool>) -> Result<()> {
1895        while !stop.load(Ordering::SeqCst) {
1896            self.poll_workflow_once().await?;
1897        }
1898
1899        Ok(())
1900    }
1901
1902    async fn poll_activity_once(&self) -> Result<bool> {
1903        let Some(task) = self
1904            .retry_worker_operation(|| {
1905                self.client
1906                    .poll_activity_task(&self.worker_id, &self.task_queue, self.poll_timeout)
1907            })
1908            .await?
1909        else {
1910            return Ok(false);
1911        };
1912
1913        let task_id = task.task_id.clone();
1914        let attempt_id = task
1915            .activity_attempt_id
1916            .clone()
1917            .or(task.attempt_id.clone())
1918            .unwrap_or_default();
1919        let lease_owner = task
1920            .lease_owner
1921            .clone()
1922            .unwrap_or_else(|| self.worker_id.clone());
1923        let codec = task.payload_codec.clone();
1924        let result = self.execute_activity_task(task).await;
1925        match result {
1926            Ok(value) => {
1927                self.client
1928                    .complete_activity_task(&task_id, &attempt_id, &lease_owner, value, &codec)
1929                    .await?;
1930            }
1931            Err(error) => {
1932                self.client
1933                    .fail_activity_task(
1934                        &task_id,
1935                        &attempt_id,
1936                        &lease_owner,
1937                        error.to_string(),
1938                        false,
1939                    )
1940                    .await?;
1941            }
1942        }
1943
1944        Ok(true)
1945    }
1946
1947    async fn poll_activities_until_stopped(self, stop: Arc<AtomicBool>) -> Result<()> {
1948        while !stop.load(Ordering::SeqCst) {
1949            self.poll_activity_once().await?;
1950        }
1951
1952        Ok(())
1953    }
1954
1955    async fn poll_query_once(&self) -> Result<bool> {
1956        let Some(task) = self
1957            .retry_worker_operation(|| {
1958                self.client
1959                    .poll_query_task(&self.worker_id, &self.task_queue, self.poll_timeout)
1960            })
1961            .await?
1962        else {
1963            return Ok(false);
1964        };
1965
1966        let query_task_id = task.query_task_id.clone();
1967        let attempt = task.query_task_attempt;
1968        let lease_owner = task
1969            .lease_owner
1970            .clone()
1971            .unwrap_or_else(|| self.worker_id.clone());
1972        let codec = task.payload_codec.clone();
1973
1974        match self.execute_query_task(task).await {
1975            Ok(value) => {
1976                let result_envelope = match encode_value_envelope(&value, &codec) {
1977                    Ok(result_envelope) => result_envelope,
1978                    Err(error) => {
1979                        let failure = self
1980                            .client
1981                            .fail_query_task(
1982                                &query_task_id,
1983                                &lease_owner,
1984                                attempt,
1985                                error.to_string(),
1986                                "query_result_encode_failed",
1987                                "QueryResultEncodeFailed",
1988                            )
1989                            .await;
1990                        if let Err(error) = failure {
1991                            if !query_task_rejection_is_final(&error) {
1992                                return Err(error);
1993                            }
1994                        }
1995                        return Ok(true);
1996                    }
1997                };
1998
1999                if let Err(error) = self
2000                    .client
2001                    .complete_query_task_with_envelope(
2002                        &query_task_id,
2003                        &lease_owner,
2004                        attempt,
2005                        value,
2006                        result_envelope,
2007                    )
2008                    .await
2009                {
2010                    if !query_task_rejection_is_final(&error) {
2011                        return Err(error);
2012                    }
2013                }
2014            }
2015            Err(failure) => {
2016                let result = self
2017                    .client
2018                    .fail_query_task(
2019                        &query_task_id,
2020                        &lease_owner,
2021                        attempt,
2022                        failure.message,
2023                        failure.reason,
2024                        failure.failure_type,
2025                    )
2026                    .await;
2027                if let Err(error) = result {
2028                    if !query_task_rejection_is_final(&error) {
2029                        return Err(error);
2030                    }
2031                }
2032            }
2033        }
2034
2035        Ok(true)
2036    }
2037
2038    async fn poll_queries_until_stopped(self, stop: Arc<AtomicBool>) -> Result<()> {
2039        while !stop.load(Ordering::SeqCst) {
2040            self.poll_query_once().await?;
2041        }
2042
2043        Ok(())
2044    }
2045
2046    async fn retry_worker_operation<T, F, Fut>(&self, mut operation: F) -> Result<T>
2047    where
2048        F: FnMut() -> Fut,
2049        Fut: Future<Output = Result<T>>,
2050    {
2051        let mut retries = 0;
2052
2053        loop {
2054            match operation().await {
2055                Err(error)
2056                    if worker_operation_is_retryable(&error)
2057                        && retries < self.retry_policy.max_retries =>
2058                {
2059                    retries += 1;
2060                    tokio::time::sleep(worker_retry_delay(self.retry_policy, retries)).await;
2061                }
2062                result => return result,
2063            }
2064        }
2065    }
2066
2067    async fn execute_query_task(
2068        &self,
2069        mut task: QueryTask,
2070    ) -> std::result::Result<Value, QueryTaskExecutionFailure> {
2071        if !matches!(task.payload_codec.as_str(), DEFAULT_CODEC | JSON_CODEC) {
2072            return Err(QueryTaskExecutionFailure::new(
2073                "query_payload_decode_failed",
2074                format!(
2075                    "cannot decode query payload with unsupported codec {:?}",
2076                    task.payload_codec
2077                ),
2078                "QueryPayloadDecodeFailed",
2079            ));
2080        }
2081
2082        if !self.workflows.contains_key(&task.workflow_type) {
2083            return Err(QueryTaskExecutionFailure::new(
2084                "query_workflow_type_not_registered",
2085                format!("no workflow registered for type {:?}", task.workflow_type),
2086                "WorkflowTypeNotRegistered",
2087            ));
2088        }
2089
2090        let Some(handlers) = self.queries.get(&task.workflow_type) else {
2091            return Err(QueryTaskExecutionFailure::new(
2092                "query_handler_unavailable",
2093                format!(
2094                    "query handlers are unavailable for workflow type {:?}",
2095                    task.workflow_type
2096                ),
2097                "QueryHandlerUnavailable",
2098            ));
2099        };
2100        let Some(query) = handlers.get(&task.query_name) else {
2101            return Err(QueryTaskExecutionFailure::new(
2102                "rejected_unknown_query",
2103                format!("unknown query {:?}", task.query_name),
2104                "QueryFailed",
2105            ));
2106        };
2107
2108        let args = decode_task_arguments(task.query_arguments.as_ref(), &task.payload_codec)
2109            .map_err(|error| {
2110                QueryTaskExecutionFailure::new(
2111                    "query_payload_decode_failed",
2112                    format!("cannot decode query arguments: {error}"),
2113                    "QueryPayloadDecodeFailed",
2114                )
2115            })?;
2116        let workflow_input =
2117            decode_task_arguments(task.workflow_arguments.as_ref(), &task.payload_codec).map_err(
2118                |error| {
2119                    QueryTaskExecutionFailure::new(
2120                        "query_workflow_state_unavailable",
2121                        format!("cannot decode workflow start input: {error}"),
2122                        "QueryWorkflowStateUnavailable",
2123                    )
2124                },
2125            )?;
2126        hydrate_query_history_from_export(&mut task).map_err(|error| {
2127            QueryTaskExecutionFailure::new(
2128                "query_workflow_state_unavailable",
2129                format!("cannot restore query history snapshot: {error}"),
2130                "QueryWorkflowStateUnavailable",
2131            )
2132        })?;
2133        enrich_query_history_from_export(&mut task).map_err(|error| {
2134            QueryTaskExecutionFailure::new(
2135                "query_workflow_state_unavailable",
2136                format!("cannot restore compact query history payloads: {error}"),
2137                "QueryWorkflowStateUnavailable",
2138            )
2139        })?;
2140        let signal_events = query_signal_events(&task).map_err(|error| {
2141            QueryTaskExecutionFailure::new(
2142                "query_workflow_state_unavailable",
2143                format!("cannot decode committed workflow signals: {error}"),
2144                "QueryWorkflowStateUnavailable",
2145            )
2146        })?;
2147        let history_events = Arc::new(std::mem::take(&mut task.history_events));
2148        let context = QueryContext {
2149            workflow_id: task.workflow_id,
2150            run_id: task.run_id,
2151            workflow_type: task.workflow_type.clone(),
2152            run_status: task.run_status,
2153            workflow_input,
2154            history_events: Arc::clone(&history_events),
2155            signal_events: Arc::new(signal_events),
2156        };
2157
2158        let future = match query {
2159            RegisteredQuery::Snapshot(handler) => handler(context, args),
2160            RegisteredQuery::Replayed {
2161                state_type,
2162                handler,
2163            } => {
2164                let workflow = self
2165                    .workflows
2166                    .get(&task.workflow_type)
2167                    .expect("workflow registration was checked above");
2168                if workflow.state_type != Some(*state_type) {
2169                    return Err(QueryTaskExecutionFailure::new(
2170                        "query_workflow_state_unavailable",
2171                        "replayed query state type does not match its workflow registration",
2172                        "QueryWorkflowStateUnavailable",
2173                    ));
2174                }
2175                let replay = workflow.replay.as_ref().ok_or_else(|| {
2176                    QueryTaskExecutionFailure::new(
2177                        "query_workflow_state_unavailable",
2178                        format!(
2179                            "workflow type {:?} is not registered for instance-state replay",
2180                            task.workflow_type
2181                        ),
2182                        "QueryWorkflowStateUnavailable",
2183                    )
2184                })?;
2185                let workflow_state = Arc::new(Mutex::new(WorkflowState {
2186                    history: history_events.as_ref().clone(),
2187                    task_queue: self.task_queue.clone(),
2188                    payload_codec: task.payload_codec,
2189                    resume_signal: None,
2190                    activity_cursor: 0,
2191                    signal_cursors: HashMap::new(),
2192                    commands: Vec::new(),
2193                }));
2194                let workflow_context = WorkflowContext {
2195                    state: workflow_state,
2196                };
2197                let mut invocation =
2198                    replay(workflow_context.clone(), context.workflow_input.clone());
2199                let mut cx = TaskContext::from_waker(noop_waker_ref());
2200                match invocation.future.as_mut().poll(&mut cx) {
2201                    Poll::Ready(Ok(_)) => {}
2202                    Poll::Ready(Err(error)) => {
2203                        return Err(QueryTaskExecutionFailure::new(
2204                            "query_workflow_state_unavailable",
2205                            format!("workflow replay failed before query: {error}"),
2206                            "QueryWorkflowStateUnavailable",
2207                        ));
2208                    }
2209                    Poll::Pending => {
2210                        let commands = workflow_context.take_commands().map_err(|error| {
2211                            QueryTaskExecutionFailure::new(
2212                                "query_workflow_state_unavailable",
2213                                format!("workflow replay failed before query: {error}"),
2214                                "QueryWorkflowStateUnavailable",
2215                            )
2216                        })?;
2217                        if commands.is_empty() {
2218                            return Err(QueryTaskExecutionFailure::new(
2219                                "query_workflow_state_unavailable",
2220                                "workflow replay yielded without a durable command",
2221                                "QueryWorkflowStateUnavailable",
2222                            ));
2223                        }
2224                    }
2225                }
2226                let state = (invocation.snapshot)().map_err(|error| {
2227                    QueryTaskExecutionFailure::new(
2228                        "query_workflow_state_unavailable",
2229                        format!("cannot snapshot replayed workflow state: {error}"),
2230                        "QueryWorkflowStateUnavailable",
2231                    )
2232                })?;
2233                handler(context, state, args).map_err(|message| {
2234                    QueryTaskExecutionFailure::new(
2235                        "query_workflow_state_unavailable",
2236                        message,
2237                        "QueryWorkflowStateUnavailable",
2238                    )
2239                })?
2240            }
2241        };
2242
2243        future.await.map_err(|error| {
2244            QueryTaskExecutionFailure::new("query_rejected", error.to_string(), "QueryFailed")
2245        })
2246    }
2247
2248    fn execute_workflow_task(&self, task: WorkflowTask) -> Result<Vec<Value>> {
2249        let workflow = self
2250            .workflows
2251            .get(&task.workflow_type)
2252            .ok_or_else(|| Error::WorkflowNotRegistered(task.workflow_type.clone()))?;
2253        let input = decode_task_arguments(task.arguments.as_ref(), &task.payload_codec)?;
2254        let resume_signal = decode_resume_signal(&task)?;
2255        let state = Arc::new(Mutex::new(WorkflowState {
2256            history: task.history_events,
2257            task_queue: self.task_queue.clone(),
2258            payload_codec: task.payload_codec.clone(),
2259            resume_signal,
2260            activity_cursor: 0,
2261            signal_cursors: HashMap::new(),
2262            commands: Vec::new(),
2263        }));
2264        let ctx = WorkflowContext { state };
2265        let mut future = (workflow.execute)(ctx.clone(), input);
2266        let mut cx = TaskContext::from_waker(noop_waker_ref());
2267
2268        match future.as_mut().poll(&mut cx) {
2269            Poll::Ready(Ok(result)) => {
2270                let result = encode_value_envelope(&result, &task.payload_codec)?;
2271                Ok(vec![json!({
2272                    "type": "complete_workflow",
2273                    "result": result
2274                })])
2275            }
2276            Poll::Ready(Err(error)) => Err(error),
2277            Poll::Pending => {
2278                let commands = ctx.take_commands()?;
2279                if commands.is_empty() {
2280                    Err(Error::WorkflowYieldedWithoutCommand)
2281                } else {
2282                    Ok(commands)
2283                }
2284            }
2285        }
2286    }
2287
2288    async fn execute_activity_task(&self, task: ActivityTask) -> Result<Value> {
2289        let handler = self
2290            .activities
2291            .get(&task.activity_type)
2292            .ok_or_else(|| Error::ActivityNotRegistered(task.activity_type.clone()))?;
2293        let args = decode_task_arguments(task.arguments.as_ref(), &task.payload_codec)?;
2294        let attempt_id = task
2295            .activity_attempt_id
2296            .clone()
2297            .or(task.attempt_id.clone())
2298            .unwrap_or_default();
2299        let lease_owner = task
2300            .lease_owner
2301            .clone()
2302            .unwrap_or_else(|| self.worker_id.clone());
2303        let ctx = ActivityContext {
2304            client: self.client.clone(),
2305            task_id: task.task_id,
2306            activity_attempt_id: attempt_id,
2307            lease_owner,
2308            activity_type: task.activity_type,
2309            attempt_number: task.attempt_number,
2310            task_queue: self.task_queue.clone(),
2311            worker_id: self.worker_id.clone(),
2312        };
2313
2314        handler(ctx, args).await
2315    }
2316}
2317
2318fn poller_result(
2319    kind: &str,
2320    result: std::result::Result<Result<()>, tokio::task::JoinError>,
2321) -> Result<()> {
2322    match result {
2323        Ok(result) => result,
2324        Err(error) => Err(Error::WorkerLoop(format!(
2325            "{kind} poller join error: {error}"
2326        ))),
2327    }
2328}
2329
2330fn optional_poller_result(
2331    kind: &str,
2332    result: Option<std::result::Result<Result<()>, tokio::task::JoinError>>,
2333) -> Result<()> {
2334    match result {
2335        Some(result) => poller_result(kind, result),
2336        None => Ok(()),
2337    }
2338}
2339
2340async fn join_pollers(
2341    workflow_poller: Option<tokio::task::JoinHandle<Result<()>>>,
2342    activity_poller: Option<tokio::task::JoinHandle<Result<()>>>,
2343    query_poller: Option<tokio::task::JoinHandle<Result<()>>>,
2344) -> Result<()> {
2345    let mut first_error = None;
2346
2347    if let Some(handle) = workflow_poller {
2348        if let Err(error) = poller_result("workflow", handle.await) {
2349            first_error.get_or_insert(error);
2350        }
2351    }
2352
2353    if let Some(handle) = activity_poller {
2354        if let Err(error) = poller_result("activity", handle.await) {
2355            first_error.get_or_insert(error);
2356        }
2357    }
2358
2359    if let Some(handle) = query_poller {
2360        if let Err(error) = poller_result("query", handle.await) {
2361            first_error.get_or_insert(error);
2362        }
2363    }
2364
2365    if let Some(error) = first_error {
2366        Err(error)
2367    } else {
2368        Ok(())
2369    }
2370}
2371
2372fn default_worker_id() -> String {
2373    let millis = SystemTime::now()
2374        .duration_since(UNIX_EPOCH)
2375        .unwrap_or_default()
2376        .as_millis();
2377    format!("rust-worker-{}-{millis}", std::process::id())
2378}
2379
2380fn unique_request_id(prefix: &str) -> String {
2381    let nanos = SystemTime::now()
2382        .duration_since(UNIX_EPOCH)
2383        .unwrap_or_default()
2384        .as_nanos();
2385    format!("{prefix}-{}-{nanos}", std::process::id())
2386}
2387
2388#[derive(Debug)]
2389struct QueryTaskExecutionFailure {
2390    reason: String,
2391    message: String,
2392    failure_type: String,
2393}
2394
2395impl QueryTaskExecutionFailure {
2396    fn new(
2397        reason: impl Into<String>,
2398        message: impl Into<String>,
2399        failure_type: impl Into<String>,
2400    ) -> Self {
2401        Self {
2402            reason: reason.into(),
2403            message: message.into(),
2404            failure_type: failure_type.into(),
2405        }
2406    }
2407}
2408
2409/// Typed local state owned by one deterministic workflow invocation.
2410///
2411/// Use [`WorkflowInstance::update`] for the same state transitions during
2412/// ordinary execution and replay. A replayed query receives a detached
2413/// immutable `Arc<S>` rather than this mutation-capable handle.
2414#[derive(Clone, Debug)]
2415pub struct WorkflowInstance<S> {
2416    state: Arc<Mutex<S>>,
2417}
2418
2419impl<S> WorkflowInstance<S> {
2420    fn new(state: S) -> Self {
2421        Self {
2422            state: Arc::new(Mutex::new(state)),
2423        }
2424    }
2425
2426    /// Read the current workflow-instance state without changing it.
2427    pub fn read<R>(&self, reader: impl FnOnce(&S) -> R) -> Result<R> {
2428        let state = self
2429            .state
2430            .lock()
2431            .map_err(|_| Error::WorkflowStatePoisoned)?;
2432        Ok(reader(&state))
2433    }
2434
2435    /// Apply one deterministic workflow-instance state transition.
2436    pub fn update<R>(&self, transition: impl FnOnce(&mut S) -> R) -> Result<R> {
2437        let mut state = self
2438            .state
2439            .lock()
2440            .map_err(|_| Error::WorkflowStatePoisoned)?;
2441        Ok(transition(&mut state))
2442    }
2443}
2444
2445impl<S: Clone> WorkflowInstance<S> {
2446    fn snapshot(&self) -> Result<S> {
2447        self.read(Clone::clone)
2448    }
2449}
2450
2451#[derive(Clone, Debug)]
2452pub struct WorkflowContext {
2453    state: Arc<Mutex<WorkflowState>>,
2454}
2455
2456impl WorkflowContext {
2457    pub fn activity<T: Serialize>(
2458        &self,
2459        activity_type: impl Into<String>,
2460        args: T,
2461    ) -> ActivityCall {
2462        self.activity_on_queue(activity_type, None::<String>, args)
2463    }
2464
2465    pub fn activity_on_queue<T, Q>(
2466        &self,
2467        activity_type: impl Into<String>,
2468        task_queue: Option<Q>,
2469        args: T,
2470    ) -> ActivityCall
2471    where
2472        T: Serialize,
2473        Q: Into<String>,
2474    {
2475        ActivityCall {
2476            ctx: self.clone(),
2477            activity_type: activity_type.into(),
2478            task_queue: task_queue.map(Into::into),
2479            args: Some(serde_json::to_value(args).map_err(Error::from)),
2480            scheduled: false,
2481        }
2482    }
2483
2484    pub fn wait_signal(&self, signal_name: impl Into<String>) -> SignalCall {
2485        SignalCall {
2486            ctx: self.clone(),
2487            signal_name: signal_name.into(),
2488            opened_wait: false,
2489        }
2490    }
2491
2492    fn take_commands(&self) -> Result<Vec<Value>> {
2493        let mut state = self
2494            .state
2495            .lock()
2496            .map_err(|_| Error::WorkflowStatePoisoned)?;
2497        Ok(std::mem::take(&mut state.commands))
2498    }
2499}
2500
2501#[derive(Debug)]
2502struct WorkflowState {
2503    history: Vec<HistoryEvent>,
2504    task_queue: String,
2505    payload_codec: String,
2506    resume_signal: Option<ResumeSignal>,
2507    activity_cursor: usize,
2508    signal_cursors: HashMap<String, usize>,
2509    commands: Vec<Value>,
2510}
2511
2512#[derive(Clone, Debug)]
2513struct ResumeSignal {
2514    signal_id: Option<String>,
2515    signal_name: String,
2516    arguments: Vec<Value>,
2517}
2518
2519pub struct ActivityCall {
2520    ctx: WorkflowContext,
2521    activity_type: String,
2522    task_queue: Option<String>,
2523    args: Option<Result<Value>>,
2524    scheduled: bool,
2525}
2526
2527impl Future for ActivityCall {
2528    type Output = Result<Value>;
2529
2530    fn poll(mut self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
2531        let ctx = self.ctx.clone();
2532        let mut state = match ctx.state.lock() {
2533            Ok(state) => state,
2534            Err(_) => return Poll::Ready(Err(Error::WorkflowStatePoisoned)),
2535        };
2536
2537        let completed = match completed_activity_results(&state.history, &state.payload_codec) {
2538            Ok(completed) => completed,
2539            Err(error) => return Poll::Ready(Err(error)),
2540        };
2541
2542        if state.activity_cursor < completed.len() {
2543            let value = completed[state.activity_cursor].clone();
2544            state.activity_cursor += 1;
2545            return Poll::Ready(Ok(value));
2546        }
2547
2548        if !self.scheduled {
2549            let task_queue = self
2550                .task_queue
2551                .clone()
2552                .unwrap_or_else(|| state.task_queue.clone());
2553            let args = match self.args.take().unwrap_or(Ok(Value::Null)) {
2554                Ok(args) => args,
2555                Err(error) => return Poll::Ready(Err(error)),
2556            };
2557            let arguments = normalize_arguments(args);
2558            let envelope = match encode_value_envelope(&arguments, &state.payload_codec) {
2559                Ok(envelope) => envelope,
2560                Err(error) => return Poll::Ready(Err(error)),
2561            };
2562
2563            state.commands.push(json!({
2564                "type": "schedule_activity",
2565                "activity_type": self.activity_type.clone(),
2566                "queue": task_queue,
2567                "arguments": envelope
2568            }));
2569            self.scheduled = true;
2570        }
2571
2572        Poll::Pending
2573    }
2574}
2575
2576pub struct SignalCall {
2577    ctx: WorkflowContext,
2578    signal_name: String,
2579    opened_wait: bool,
2580}
2581
2582impl Future for SignalCall {
2583    type Output = Result<Vec<Value>>;
2584
2585    fn poll(mut self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
2586        let ctx = self.ctx.clone();
2587        let mut state = match ctx.state.lock() {
2588            Ok(state) => state,
2589            Err(_) => return Poll::Ready(Err(Error::WorkflowStatePoisoned)),
2590        };
2591
2592        let signals = match signal_values(
2593            &state.history,
2594            &self.signal_name,
2595            &state.payload_codec,
2596            state.resume_signal.as_ref(),
2597        ) {
2598            Ok(signals) => signals,
2599            Err(error) => return Poll::Ready(Err(error)),
2600        };
2601        let cursor = *state.signal_cursors.get(&self.signal_name).unwrap_or(&0);
2602
2603        if cursor < signals.len() {
2604            state
2605                .signal_cursors
2606                .insert(self.signal_name.clone(), cursor + 1);
2607            return Poll::Ready(Ok(signals[cursor].clone()));
2608        }
2609
2610        if !self.opened_wait {
2611            state.commands.push(json!({
2612                "type": "open_condition_wait",
2613                "condition_key": format!("signal:{}", self.signal_name)
2614            }));
2615            self.opened_wait = true;
2616        }
2617
2618        Poll::Pending
2619    }
2620}
2621
2622#[derive(Clone, Debug)]
2623pub struct ActivityContext {
2624    client: Client,
2625    pub task_id: String,
2626    pub activity_attempt_id: String,
2627    pub lease_owner: String,
2628    pub activity_type: String,
2629    pub attempt_number: u64,
2630    pub task_queue: String,
2631    pub worker_id: String,
2632}
2633
2634impl ActivityContext {
2635    pub async fn heartbeat<T: Serialize>(&self, details: T) -> Result<ActivityHeartbeatResponse> {
2636        self.client
2637            .heartbeat_activity_task(
2638                &self.task_id,
2639                &self.activity_attempt_id,
2640                &self.lease_owner,
2641                serde_json::to_value(details)?,
2642            )
2643            .await
2644    }
2645}
2646
2647fn decode_task_arguments(value: Option<&Value>, codec: &str) -> Result<Value> {
2648    match value {
2649        Some(value) => Ok(normalize_arguments(decode_wire_value(value, codec)?)),
2650        None => Ok(Value::Array(Vec::new())),
2651    }
2652}
2653
2654fn decode_resume_signal(task: &WorkflowTask) -> Result<Option<ResumeSignal>> {
2655    let Some(signal_name) = task
2656        .signal_name
2657        .as_deref()
2658        .filter(|value| !value.is_empty())
2659    else {
2660        return Ok(None);
2661    };
2662    let Some(arguments) = task.signal_arguments.as_ref() else {
2663        return Ok(None);
2664    };
2665
2666    let decoded = normalize_arguments(decode_wire_value(arguments, &task.payload_codec)?);
2667    let Value::Array(arguments) = decoded else {
2668        unreachable!("normalize_arguments always returns an array");
2669    };
2670
2671    Ok(Some(ResumeSignal {
2672        signal_id: task.workflow_signal_id.clone(),
2673        signal_name: signal_name.to_string(),
2674        arguments,
2675    }))
2676}
2677
2678fn normalize_arguments(value: Value) -> Value {
2679    match value {
2680        Value::Null => Value::Array(Vec::new()),
2681        Value::Array(_) => value,
2682        other => Value::Array(vec![other]),
2683    }
2684}
2685
2686fn completed_activity_results(events: &[HistoryEvent], fallback_codec: &str) -> Result<Vec<Value>> {
2687    let mut results = Vec::new();
2688
2689    for event in events {
2690        if event.event_type != "ActivityCompleted" {
2691            continue;
2692        }
2693
2694        let codec = event
2695            .payload
2696            .get("payload_codec")
2697            .and_then(Value::as_str)
2698            .unwrap_or(fallback_codec);
2699        let result = event.payload.get("result").unwrap_or(&Value::Null);
2700        results.push(decode_wire_value(result, codec)?);
2701    }
2702
2703    Ok(results)
2704}
2705
2706fn signal_values(
2707    events: &[HistoryEvent],
2708    signal_name: &str,
2709    fallback_codec: &str,
2710    resume_signal: Option<&ResumeSignal>,
2711) -> Result<Vec<Vec<Value>>> {
2712    let mut signals = Vec::new();
2713
2714    for event in events {
2715        if event.event_type != "SignalApplied" && event.event_type != "SignalReceived" {
2716            continue;
2717        }
2718
2719        if event.payload.get("signal_name").and_then(Value::as_str) != Some(signal_name) {
2720            continue;
2721        }
2722
2723        let codec = event
2724            .payload
2725            .get("payload_codec")
2726            .and_then(Value::as_str)
2727            .unwrap_or(fallback_codec);
2728        let raw = event
2729            .payload
2730            .get("value")
2731            .or_else(|| event.payload.get("input"))
2732            .or_else(|| event.payload.get("arguments"));
2733        let decoded = match raw.filter(|value| !value.is_null()) {
2734            Some(value) => decode_wire_value(value, codec)?,
2735            None => resume_signal
2736                .filter(|signal| resume_signal_matches_event(signal, event, signal_name))
2737                .map(|signal| Value::Array(signal.arguments.clone()))
2738                .unwrap_or_else(|| Value::Array(Vec::new())),
2739        };
2740        let args = match normalize_arguments(decoded) {
2741            Value::Array(values) => values,
2742            _ => unreachable!("normalize_arguments always returns an array"),
2743        };
2744        signals.push(args);
2745    }
2746
2747    Ok(signals)
2748}
2749
2750fn hydrate_query_history_from_export(task: &mut QueryTask) -> Result<()> {
2751    let Some(export_events) = task
2752        .history_export
2753        .as_ref()
2754        .and_then(|export| export.get("history_events"))
2755        .and_then(Value::as_array)
2756    else {
2757        return Ok(());
2758    };
2759
2760    if export_events.len() > task.history_events.len() {
2761        task.history_events = serde_json::from_value(Value::Array(export_events.clone()))?;
2762    }
2763
2764    Ok(())
2765}
2766
2767fn enrich_query_history_from_export(task: &mut QueryTask) -> Result<()> {
2768    let Some(export) = task.history_export.as_ref() else {
2769        return Ok(());
2770    };
2771    let signals = export
2772        .get("signals")
2773        .and_then(Value::as_array)
2774        .cloned()
2775        .unwrap_or_default();
2776    let activities = export
2777        .get("activities")
2778        .and_then(Value::as_array)
2779        .cloned()
2780        .unwrap_or_default();
2781    let export_codec = export
2782        .get("payloads")
2783        .and_then(|payloads| payloads.get("codec"))
2784        .and_then(Value::as_str)
2785        .unwrap_or(&task.payload_codec)
2786        .to_string();
2787    let mut signal_name_offsets: HashMap<String, usize> = HashMap::new();
2788
2789    for event in &mut task.history_events {
2790        if event.event_type == "ActivityCompleted" {
2791            let sequence = event
2792                .payload
2793                .get("sequence")
2794                .or_else(|| event.payload.get("workflow_sequence"))
2795                .and_then(value_as_u64);
2796            let Some(activity) = sequence.and_then(|sequence| {
2797                activities.iter().find(|activity| {
2798                    activity.get("sequence").and_then(value_as_u64) == Some(sequence)
2799                })
2800            }) else {
2801                continue;
2802            };
2803            let Some(payload) = event.payload.as_object_mut() else {
2804                continue;
2805            };
2806            if missing_payload(payload.get("result")) {
2807                if let Some(result) = activity
2808                    .get("result")
2809                    .filter(|value| !missing_payload(Some(value)))
2810                {
2811                    payload.insert("result".to_string(), result.clone());
2812                }
2813            }
2814            for field in ["payload_codec", "activity_type"] {
2815                if payload
2816                    .get(field)
2817                    .and_then(Value::as_str)
2818                    .unwrap_or_default()
2819                    .is_empty()
2820                {
2821                    if let Some(value) = activity.get(field) {
2822                        payload.insert(field.to_string(), value.clone());
2823                    }
2824                }
2825            }
2826            continue;
2827        }
2828
2829        if event.event_type != "SignalReceived" && event.event_type != "SignalApplied" {
2830            continue;
2831        }
2832        let signal_id = event.payload.get("signal_id").and_then(Value::as_str);
2833        let command_id = event
2834            .payload
2835            .get("workflow_command_id")
2836            .or_else(|| event.raw.get("workflow_command_id"))
2837            .and_then(Value::as_str);
2838        let signal_name = event
2839            .payload
2840            .get("signal_name")
2841            .and_then(Value::as_str)
2842            .unwrap_or_default()
2843            .to_string();
2844        let matched = signals
2845            .iter()
2846            .find(|signal| {
2847                signal_id.is_some() && signal.get("id").and_then(Value::as_str) == signal_id
2848            })
2849            .or_else(|| {
2850                signals.iter().find(|signal| {
2851                    command_id.is_some()
2852                        && signal.get("command_id").and_then(Value::as_str) == command_id
2853                })
2854            })
2855            .or_else(|| {
2856                let offset = signal_name_offsets.entry(signal_name.clone()).or_default();
2857                let signal = signals
2858                    .iter()
2859                    .filter(|signal| {
2860                        signal.get("name").and_then(Value::as_str) == Some(signal_name.as_str())
2861                    })
2862                    .nth(*offset);
2863                if signal.is_some() {
2864                    *offset += 1;
2865                }
2866                signal
2867            });
2868        let Some(signal) = matched else {
2869            continue;
2870        };
2871        let signal_codec = signal
2872            .get("payload_codec")
2873            .and_then(Value::as_str)
2874            .unwrap_or(&export_codec);
2875        let Some(payload) = event.payload.as_object_mut() else {
2876            continue;
2877        };
2878        if missing_payload(payload.get("arguments")) {
2879            if let Some(arguments) = signal
2880                .get("arguments")
2881                .filter(|value| !missing_payload(Some(value)))
2882            {
2883                let envelope = match arguments {
2884                    Value::String(blob) => json!({"codec": signal_codec, "blob": blob}),
2885                    other => other.clone(),
2886                };
2887                payload.insert("arguments".to_string(), envelope);
2888            }
2889        }
2890        if payload
2891            .get("payload_codec")
2892            .and_then(Value::as_str)
2893            .unwrap_or_default()
2894            .is_empty()
2895        {
2896            payload.insert("payload_codec".to_string(), json!(signal_codec));
2897        }
2898    }
2899
2900    Ok(())
2901}
2902
2903fn missing_payload(value: Option<&Value>) -> bool {
2904    match value {
2905        None | Some(Value::Null) => true,
2906        Some(Value::String(value)) => value.is_empty(),
2907        Some(_) => false,
2908    }
2909}
2910
2911fn query_signal_events(task: &QueryTask) -> Result<Vec<QuerySignal>> {
2912    let export_signals = task
2913        .history_export
2914        .as_ref()
2915        .and_then(|export| export.get("signals"))
2916        .and_then(Value::as_array)
2917        .cloned()
2918        .unwrap_or_default();
2919    let export_codec = task
2920        .history_export
2921        .as_ref()
2922        .and_then(|export| export.get("payloads"))
2923        .and_then(|payloads| payloads.get("codec"))
2924        .and_then(Value::as_str)
2925        .unwrap_or(&task.payload_codec);
2926    let mut name_offsets: HashMap<String, usize> = HashMap::new();
2927    let mut signals = Vec::new();
2928
2929    for event in &task.history_events {
2930        if event.event_type != "SignalApplied" && event.event_type != "SignalReceived" {
2931            continue;
2932        }
2933
2934        let name = event
2935            .payload
2936            .get("signal_name")
2937            .and_then(Value::as_str)
2938            .unwrap_or_default();
2939        if name.is_empty() {
2940            continue;
2941        }
2942        let signal_id = event.payload.get("signal_id").and_then(Value::as_str);
2943        let command_id = event
2944            .payload
2945            .get("workflow_command_id")
2946            .or_else(|| event.raw.get("workflow_command_id"))
2947            .and_then(Value::as_str);
2948        let matched_export = export_signals
2949            .iter()
2950            .find(|candidate| {
2951                signal_id.is_some() && candidate.get("id").and_then(Value::as_str) == signal_id
2952            })
2953            .or_else(|| {
2954                export_signals.iter().find(|candidate| {
2955                    command_id.is_some()
2956                        && candidate.get("command_id").and_then(Value::as_str) == command_id
2957                })
2958            })
2959            .or_else(|| {
2960                let offset = name_offsets.entry(name.to_string()).or_default();
2961                let candidate = export_signals
2962                    .iter()
2963                    .filter(|candidate| candidate.get("name").and_then(Value::as_str) == Some(name))
2964                    .nth(*offset);
2965                if candidate.is_some() {
2966                    *offset += 1;
2967                }
2968                candidate
2969            });
2970        let codec = event
2971            .payload
2972            .get("payload_codec")
2973            .and_then(Value::as_str)
2974            .or_else(|| {
2975                matched_export
2976                    .and_then(|signal| signal.get("payload_codec"))
2977                    .and_then(Value::as_str)
2978            })
2979            .unwrap_or(export_codec);
2980        let raw_arguments = event
2981            .payload
2982            .get("value")
2983            .or_else(|| event.payload.get("input"))
2984            .or_else(|| event.payload.get("arguments"))
2985            .filter(|value| !value.is_null())
2986            .or_else(|| matched_export.and_then(|signal| signal.get("arguments")));
2987        let arguments = decode_query_signal_arguments(raw_arguments, codec)?;
2988        let workflow_sequence = event
2989            .payload
2990            .get("workflow_sequence")
2991            .and_then(value_as_u64)
2992            .or_else(|| {
2993                matched_export
2994                    .and_then(|signal| signal.get("workflow_sequence"))
2995                    .and_then(value_as_u64)
2996            });
2997
2998        signals.push(QuerySignal {
2999            id: signal_id.map(str::to_string).or_else(|| {
3000                matched_export
3001                    .and_then(|signal| signal.get("id"))
3002                    .and_then(Value::as_str)
3003                    .map(str::to_string)
3004            }),
3005            name: name.to_string(),
3006            arguments,
3007            workflow_sequence,
3008        });
3009    }
3010
3011    if signals.is_empty() {
3012        for signal in export_signals {
3013            if signal.get("status").and_then(Value::as_str) == Some("rejected") {
3014                continue;
3015            }
3016            let Some(name) = signal.get("name").and_then(Value::as_str) else {
3017                continue;
3018            };
3019            let codec = signal
3020                .get("payload_codec")
3021                .and_then(Value::as_str)
3022                .unwrap_or(export_codec);
3023            let arguments = decode_query_signal_arguments(signal.get("arguments"), codec)?;
3024            signals.push(QuerySignal {
3025                id: signal.get("id").and_then(Value::as_str).map(str::to_string),
3026                name: name.to_string(),
3027                arguments,
3028                workflow_sequence: signal.get("workflow_sequence").and_then(value_as_u64),
3029            });
3030        }
3031        signals.sort_by_key(|signal| signal.workflow_sequence.unwrap_or(u64::MAX));
3032    }
3033
3034    Ok(signals)
3035}
3036
3037fn decode_query_signal_arguments(raw: Option<&Value>, codec: &str) -> Result<Vec<Value>> {
3038    let decoded = match raw.filter(|value| !value.is_null()) {
3039        Some(value) => decode_wire_value(value, codec)?,
3040        None => Value::Array(Vec::new()),
3041    };
3042    let Value::Array(arguments) = normalize_arguments(decoded) else {
3043        unreachable!("normalize_arguments always returns an array");
3044    };
3045    Ok(arguments)
3046}
3047
3048fn value_as_u64(value: &Value) -> Option<u64> {
3049    value
3050        .as_u64()
3051        .or_else(|| value.as_str().and_then(|value| value.parse().ok()))
3052}
3053
3054fn resume_signal_matches_event(
3055    resume_signal: &ResumeSignal,
3056    event: &HistoryEvent,
3057    signal_name: &str,
3058) -> bool {
3059    if resume_signal.signal_name != signal_name {
3060        return false;
3061    }
3062
3063    match (
3064        resume_signal.signal_id.as_deref(),
3065        event.payload.get("signal_id").and_then(Value::as_str),
3066    ) {
3067        (Some(resume_id), Some(event_id)) => resume_id == event_id,
3068        _ => true,
3069    }
3070}
3071
3072#[cfg(test)]
3073mod tests {
3074    use super::*;
3075    use std::{
3076        io::{Read, Write},
3077        net::{SocketAddr, TcpListener, TcpStream},
3078        thread,
3079    };
3080
3081    #[derive(Clone, Debug, Default, PartialEq)]
3082    struct ReplayCounterState {
3083        loaded: Option<String>,
3084        count: i64,
3085        finished: bool,
3086    }
3087
3088    fn replay_counter_worker() -> Worker {
3089        let client = Client::new("http://127.0.0.1:8080").expect("client");
3090        let mut worker = Worker::new(client, "rust-workers");
3091        worker.register_replayed_workflow(
3092            "replay-counter",
3093            ReplayCounterState::default,
3094            |ctx, _input, state| async move {
3095                let loaded = ctx.activity("load-counter", json!([])).await?;
3096                state.update(|current| {
3097                    current.loaded = loaded.as_str().map(str::to_string);
3098                })?;
3099                for _ in 0..2 {
3100                    let signal = ctx.wait_signal("increment").await?;
3101                    let amount = signal.first().and_then(Value::as_i64).unwrap_or_default();
3102                    state.update(|current| current.count += amount)?;
3103                }
3104                state.update(|current| current.finished = true)?;
3105                state.read(|current| Ok(json!(current.count)))?
3106            },
3107        );
3108        worker.register_replayed_query::<ReplayCounterState, _, _>(
3109            "replay-counter",
3110            "current",
3111            |_ctx, state, _args| async move {
3112                Ok(json!({
3113                    "loaded": state.loaded,
3114                    "count": state.count,
3115                    "finished": state.finished,
3116                }))
3117            },
3118        );
3119        worker.register_replayed_query::<ReplayCounterState, _, _>(
3120            "replay-counter",
3121            "detached-mutation",
3122            |_ctx, state, _args| async move {
3123                let mut detached = (*state).clone();
3124                detached.count = 999;
3125                Ok(json!(detached.count))
3126            },
3127        );
3128        worker.register_replayed_query::<ReplayCounterState, _, _>(
3129            "replay-counter",
3130            "failed-mutation",
3131            |_ctx, state, _args| async move {
3132                let mut detached = (*state).clone();
3133                detached.count = 999;
3134                Err(Error::WorkerLoop("query refused".to_string()))
3135            },
3136        );
3137        worker
3138    }
3139
3140    fn replay_counter_query(
3141        query_name: &str,
3142        history_events: Value,
3143        run_status: &str,
3144    ) -> QueryTask {
3145        serde_json::from_value(json!({
3146            "query_task_id": format!("query-{query_name}"),
3147            "workflow_type": "replay-counter",
3148            "query_name": query_name,
3149            "payload_codec": "json",
3150            "workflow_arguments": {"codec": "json", "blob": "[]"},
3151            "query_arguments": {"codec": "json", "blob": "[]"},
3152            "history_events": history_events,
3153            "run_status": run_status,
3154        }))
3155        .expect("query task")
3156    }
3157
3158    #[test]
3159    fn avro_generic_wrapper_round_trips_json_values() {
3160        let value = json!({"greeting": "hello", "count": 3, "ok": true});
3161        let envelope = PayloadEnvelope::avro(&value).expect("encode");
3162        assert_eq!(envelope.codec, DEFAULT_CODEC);
3163        assert_eq!(decode_payload::<Value>(&envelope).expect("decode"), value);
3164    }
3165
3166    #[test]
3167    fn json_codec_remains_plain_json() {
3168        let value = json!({"greeting": "hello", "count": 3, "ok": true});
3169        let envelope = PayloadEnvelope::json(&value).expect("encode");
3170
3171        assert_eq!(envelope.codec, JSON_CODEC);
3172        assert_eq!(envelope.blob, serde_json::to_string(&value).expect("json"));
3173        assert_eq!(decode_payload::<Value>(&envelope).expect("decode"), value);
3174    }
3175
3176    #[test]
3177    fn typed_avro_payload_without_schema_context_keeps_diagnostic() {
3178        let envelope = PayloadEnvelope {
3179            codec: DEFAULT_CODEC.to_string(),
3180            blob: BASE64.encode([0x01]),
3181        };
3182
3183        let error = decode_payload::<Value>(&envelope).expect_err("typed payload must fail");
3184        assert_eq!(
3185            error.to_string(),
3186            "codec error: typed avro payloads require a schema context; v1 supports the generic wrapper"
3187        );
3188    }
3189
3190    #[test]
3191    fn workflow_context_schedules_activity_until_completion_is_in_history() {
3192        let ctx = WorkflowContext {
3193            state: Arc::new(Mutex::new(WorkflowState {
3194                history: Vec::new(),
3195                task_queue: "rust-workers".to_string(),
3196                payload_codec: DEFAULT_CODEC.to_string(),
3197                resume_signal: None,
3198                activity_cursor: 0,
3199                signal_cursors: HashMap::new(),
3200                commands: Vec::new(),
3201            })),
3202        };
3203
3204        let mut call = Box::pin(ctx.activity("hello.activity", json!(["Ada"])));
3205        let mut task_context = TaskContext::from_waker(noop_waker_ref());
3206        assert!(matches!(
3207            call.as_mut().poll(&mut task_context),
3208            Poll::Pending
3209        ));
3210
3211        let commands = ctx.take_commands().expect("commands");
3212        assert_eq!(commands[0]["type"], "schedule_activity");
3213        assert_eq!(commands[0]["activity_type"], "hello.activity");
3214    }
3215
3216    #[test]
3217    fn rust_hello_world_uses_signal_arguments_from_resume_payload() {
3218        let client = Client::new("http://127.0.0.1:8080").expect("client");
3219        let mut worker = Worker::new(client, "rust-workers");
3220
3221        worker.register_workflow("rust.hello_workflow", |ctx, _input| async move {
3222            let signal = ctx.wait_signal("start").await?;
3223            let name = signal
3224                .first()
3225                .and_then(|value| value.as_str())
3226                .unwrap_or("world");
3227            let greeting = ctx.activity("rust.hello_activity", json!([name])).await?;
3228            Ok(json!({
3229                "greeting": greeting,
3230                "language": "rust"
3231            }))
3232        });
3233
3234        let signal_arguments =
3235            encode_value_envelope(&json!(["Rust"]), DEFAULT_CODEC).expect("signal arguments");
3236        let task = WorkflowTask {
3237            task_id: "wft-rust-signal-1".to_string(),
3238            workflow_id: Some("wf-rust-hello".to_string()),
3239            run_id: Some("run-rust-hello".to_string()),
3240            workflow_type: "rust.hello_workflow".to_string(),
3241            payload_codec: DEFAULT_CODEC.to_string(),
3242            arguments: Some(encode_value_envelope(&json!([]), DEFAULT_CODEC).expect("input")),
3243            history_events: vec![HistoryEvent {
3244                event_type: "SignalReceived".to_string(),
3245                payload: json!({
3246                    "signal_id": "sig-rust-1",
3247                    "signal_name": "start"
3248                }),
3249                raw: HashMap::new(),
3250            }],
3251            total_history_events: Some(1),
3252            next_history_page_token: None,
3253            workflow_task_attempt: 1,
3254            workflow_signal_id: Some("sig-rust-1".to_string()),
3255            signal_name: Some("start".to_string()),
3256            signal_arguments: Some(signal_arguments),
3257            lease_owner: Some("rust-worker".to_string()),
3258        };
3259
3260        let commands = worker.execute_workflow_task(task).expect("workflow task");
3261
3262        assert_eq!(commands.len(), 1);
3263        assert_eq!(commands[0]["type"], "schedule_activity");
3264        assert_eq!(commands[0]["activity_type"], "rust.hello_activity");
3265        assert_eq!(
3266            decode_wire_value(&commands[0]["arguments"], DEFAULT_CODEC).expect("activity args"),
3267            json!(["Rust"])
3268        );
3269    }
3270
3271    #[test]
3272    fn workflow_task_appends_paginated_history_events() {
3273        let mut task = WorkflowTask {
3274            task_id: "wft-rust-pages-1".to_string(),
3275            workflow_id: Some("wf-rust-pages".to_string()),
3276            run_id: Some("run-rust-pages".to_string()),
3277            workflow_type: "rust.hello_workflow".to_string(),
3278            payload_codec: DEFAULT_CODEC.to_string(),
3279            arguments: Some(encode_value_envelope(&json!([]), DEFAULT_CODEC).expect("input")),
3280            history_events: vec![HistoryEvent {
3281                event_type: "WorkflowStarted".to_string(),
3282                payload: json!({}),
3283                raw: HashMap::new(),
3284            }],
3285            total_history_events: Some(3),
3286            next_history_page_token: Some("MQ==".to_string()),
3287            workflow_task_attempt: 1,
3288            workflow_signal_id: None,
3289            signal_name: None,
3290            signal_arguments: None,
3291            lease_owner: Some("rust-worker".to_string()),
3292        };
3293
3294        task.append_history_page(WorkflowTaskHistoryPage {
3295            history_events: vec![
3296                HistoryEvent {
3297                    event_type: "SignalReceived".to_string(),
3298                    payload: json!({
3299                        "signal_id": "sig-rust-1",
3300                        "signal_name": "start",
3301                        "arguments": encode_value_envelope(&json!(["Rust"]), DEFAULT_CODEC)
3302                            .expect("signal arguments")
3303                    }),
3304                    raw: HashMap::new(),
3305                },
3306                HistoryEvent {
3307                    event_type: "MarkerRecorded".to_string(),
3308                    payload: json!({"sequence": 3}),
3309                    raw: HashMap::new(),
3310                },
3311            ],
3312            total_history_events: Some(3),
3313            next_history_page_token: None,
3314        });
3315
3316        assert_eq!(task.history_events.len(), 3);
3317        assert_eq!(task.total_history_events, Some(3));
3318        assert_eq!(task.next_history_page_token, None);
3319
3320        let signals =
3321            signal_values(&task.history_events, "start", DEFAULT_CODEC, None).expect("signals");
3322        assert_eq!(signals, vec![vec![json!("Rust")]]);
3323    }
3324
3325    #[tokio::test]
3326    async fn query_handler_reads_ordered_cross_codec_signals_without_commands() {
3327        let client = Client::new("http://127.0.0.1:8080").expect("client");
3328        let mut worker = Worker::new(client, "rust-workers");
3329        worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
3330        worker.register_query("counter", "current", |ctx, _args| async move {
3331            let mut count = 0_i64;
3332            for signal in ctx.signal_events() {
3333                let value = signal
3334                    .arguments
3335                    .first()
3336                    .and_then(Value::as_i64)
3337                    .unwrap_or_default();
3338                match signal.name.as_str() {
3339                    "increment" => count += value,
3340                    "set" => count = value,
3341                    _ => {}
3342                }
3343            }
3344            Ok(json!(count))
3345        });
3346
3347        let task = QueryTask {
3348            query_task_id: "query-rust-counter".to_string(),
3349            query_task_attempt: 1,
3350            lease_owner: Some("rust-worker".to_string()),
3351            workflow_id: Some("counter-1".to_string()),
3352            run_id: Some("run-counter-1".to_string()),
3353            workflow_type: "counter".to_string(),
3354            query_name: "current".to_string(),
3355            payload_codec: DEFAULT_CODEC.to_string(),
3356            workflow_arguments: Some(
3357                encode_value_envelope(&json!([]), DEFAULT_CODEC).expect("workflow input"),
3358            ),
3359            query_arguments: Some(
3360                encode_value_envelope(&json!([]), DEFAULT_CODEC).expect("query arguments"),
3361            ),
3362            history_events: vec![
3363                HistoryEvent {
3364                    event_type: "SignalReceived".to_string(),
3365                    payload: json!({
3366                        "signal_id": "php-signal-1",
3367                        "signal_name": "increment",
3368                        "workflow_sequence": 1,
3369                        "payload_codec": DEFAULT_CODEC,
3370                        "arguments": encode_value_envelope(&json!([3]), DEFAULT_CODEC).expect("php avro signal")
3371                    }),
3372                    raw: HashMap::new(),
3373                },
3374                HistoryEvent {
3375                    event_type: "SignalReceived".to_string(),
3376                    payload: json!({
3377                        "signal_id": "python-signal-2",
3378                        "signal_name": "increment",
3379                        "workflow_sequence": 2,
3380                        "payload_codec": JSON_CODEC,
3381                        "arguments": encode_value_envelope(&json!([5]), JSON_CODEC).expect("python json signal")
3382                    }),
3383                    raw: HashMap::new(),
3384                },
3385                HistoryEvent {
3386                    event_type: "SignalReceived".to_string(),
3387                    payload: json!({
3388                        "signal_id": "rust-signal-3",
3389                        "signal_name": "set",
3390                        "workflow_sequence": 3,
3391                        "payload_codec": DEFAULT_CODEC,
3392                        "arguments": encode_value_envelope(&json!([0]), DEFAULT_CODEC).expect("rust avro signal")
3393                    }),
3394                    raw: HashMap::new(),
3395                },
3396            ],
3397            history_export: None,
3398            run_status: Some("completed".to_string()),
3399        };
3400
3401        let result = worker.execute_query_task(task).await.expect("query result");
3402        assert_eq!(result, json!(0));
3403    }
3404
3405    #[tokio::test]
3406    async fn replayed_queries_read_running_completed_and_cold_restarted_instance_state() {
3407        let worker = replay_counter_worker();
3408        let running_history = json!([
3409            {
3410                "type": "ActivityCompleted",
3411                "payload": {
3412                    "sequence": 1,
3413                    "activity_type": "load-counter",
3414                    "payload_codec": "json",
3415                    "result": {"codec": "json", "blob": "\"loaded\""}
3416                }
3417            },
3418            {
3419                "type": "SignalReceived",
3420                "payload": {
3421                    "signal_id": "signal-3",
3422                    "signal_name": "increment",
3423                    "payload_codec": "json",
3424                    "arguments": {"codec": "json", "blob": "[3]"}
3425                }
3426            }
3427        ]);
3428
3429        let running = worker
3430            .execute_query_task(replay_counter_query(
3431                "current",
3432                running_history.clone(),
3433                "running",
3434            ))
3435            .await
3436            .expect("running replay query");
3437        assert_eq!(
3438            running,
3439            json!({"loaded": "loaded", "count": 3, "finished": false})
3440        );
3441
3442        let detached = worker
3443            .execute_query_task(replay_counter_query(
3444                "detached-mutation",
3445                running_history.clone(),
3446                "running",
3447            ))
3448            .await
3449            .expect("query mutates only its detached state clone");
3450        assert_eq!(detached, json!(999));
3451        let failed = worker
3452            .execute_query_task(replay_counter_query(
3453                "failed-mutation",
3454                running_history.clone(),
3455                "running",
3456            ))
3457            .await
3458            .expect_err("failed query");
3459        assert_eq!(failed.reason, "query_rejected");
3460        let unchanged = worker
3461            .execute_query_task(replay_counter_query("current", running_history, "running"))
3462            .await
3463            .expect("later query reconstructs unchanged state");
3464        assert_eq!(unchanged, running);
3465
3466        let restarted_worker = replay_counter_worker();
3467        let restarted_task: QueryTask = serde_json::from_value(json!({
3468            "query_task_id": "query-after-restart",
3469            "workflow_id": "counter-1",
3470            "run_id": "run-counter-1",
3471            "workflow_type": "replay-counter",
3472            "query_name": "current",
3473            "payload_codec": "json",
3474            "workflow_arguments": {"codec": "json", "blob": "[]"},
3475            "query_arguments": {"codec": "json", "blob": "[]"},
3476            "history_events": [],
3477            "history_export": {
3478                "payloads": {"codec": "json"},
3479                "history_events": [
3480                    {
3481                        "type": "ActivityCompleted",
3482                        "payload": {
3483                            "sequence": 1,
3484                            "activity_type": "load-counter",
3485                            "payload_codec": "json",
3486                            "result": null
3487                        }
3488                    },
3489                    {
3490                        "type": "SignalReceived",
3491                        "payload": {"signal_id": "signal-3", "signal_name": "increment"}
3492                    },
3493                    {
3494                        "type": "SignalReceived",
3495                        "payload": {"signal_id": "signal-5", "signal_name": "increment"}
3496                    }
3497                ],
3498                "activities": [{
3499                    "sequence": 1,
3500                    "activity_type": "load-counter",
3501                    "payload_codec": "json",
3502                    "result": {"codec": "json", "blob": "\"loaded\""}
3503                }],
3504                "signals": [
3505                    {
3506                        "id": "signal-3",
3507                        "name": "increment",
3508                        "payload_codec": "json",
3509                        "arguments": "[3]"
3510                    },
3511                    {
3512                        "id": "signal-5",
3513                        "name": "increment",
3514                        "payload_codec": "json",
3515                        "arguments": "[5]"
3516                    }
3517                ]
3518            },
3519            "run_status": "completed"
3520        }))
3521        .expect("cold replay query task");
3522        let completed = restarted_worker
3523            .execute_query_task(restarted_task)
3524            .await
3525            .expect("completed cold replay query");
3526        assert_eq!(
3527            completed,
3528            json!({"loaded": "loaded", "count": 8, "finished": true})
3529        );
3530    }
3531
3532    #[tokio::test]
3533    async fn replayed_query_replay_failures_are_machine_readable() {
3534        let worker = replay_counter_worker();
3535        let task = replay_counter_query(
3536            "current",
3537            json!([{
3538                "type": "ActivityCompleted",
3539                "payload": {
3540                    "sequence": 1,
3541                    "payload_codec": "json",
3542                    "result": {"codec": "json", "blob": "{"}
3543                }
3544            }]),
3545            "running",
3546        );
3547        let failure = worker
3548            .execute_query_task(task)
3549            .await
3550            .expect_err("invalid replay history payload");
3551        assert_eq!(failure.reason, "query_workflow_state_unavailable");
3552        assert_eq!(failure.failure_type, "QueryWorkflowStateUnavailable");
3553    }
3554
3555    #[tokio::test]
3556    async fn query_task_restores_compact_history_from_export() {
3557        let client = Client::new("http://127.0.0.1:8080").expect("client");
3558        let mut worker = Worker::new(client, "rust-workers");
3559        worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
3560        worker.register_query("counter", "current", |ctx, _args| async move {
3561            Ok(json!(ctx.signals("increment")[0][0]))
3562        });
3563        let task: QueryTask = serde_json::from_value(json!({
3564            "query_task_id": "query-export",
3565            "workflow_type": "counter",
3566            "query_name": "current",
3567            "payload_codec": "json",
3568            "workflow_arguments": {"codec": "json", "blob": "[]"},
3569            "query_arguments": {"codec": "json", "blob": "[]"},
3570            "history_events": [],
3571            "history_export": {
3572                "payloads": {"codec": "json"},
3573                "history_events": [{
3574                    "type": "SignalReceived",
3575                    "payload": {"signal_id": "signal-export", "signal_name": "increment"}
3576                }],
3577                "signals": [{
3578                    "id": "signal-export",
3579                    "name": "increment",
3580                    "status": "applied",
3581                    "workflow_sequence": 1,
3582                    "payload_codec": "json",
3583                    "arguments": "[9]"
3584                }]
3585            }
3586        }))
3587        .expect("query task");
3588
3589        let result = worker.execute_query_task(task).await.expect("query result");
3590        assert_eq!(result, json!(9));
3591    }
3592
3593    #[tokio::test]
3594    async fn query_task_failures_have_stable_reasons() {
3595        let client = Client::new("http://127.0.0.1:8080").expect("client");
3596        let mut worker = Worker::new(client, "rust-workers");
3597        worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
3598        worker.register_query(
3599            "counter",
3600            "current",
3601            |_ctx, _args| async move { Ok(json!(0)) },
3602        );
3603
3604        let base_task = QueryTask {
3605            query_task_id: "query-errors".to_string(),
3606            query_task_attempt: 1,
3607            lease_owner: None,
3608            workflow_id: Some("counter-errors".to_string()),
3609            run_id: Some("run-errors".to_string()),
3610            workflow_type: "counter".to_string(),
3611            query_name: "missing".to_string(),
3612            payload_codec: JSON_CODEC.to_string(),
3613            workflow_arguments: Some(json!({"codec": "json", "blob": "[]"})),
3614            query_arguments: Some(json!({"codec": "json", "blob": "[]"})),
3615            history_events: Vec::new(),
3616            history_export: None,
3617            run_status: Some("running".to_string()),
3618        };
3619
3620        let unknown = worker
3621            .execute_query_task(base_task.clone())
3622            .await
3623            .expect_err("unknown query");
3624        assert_eq!(unknown.reason, "rejected_unknown_query");
3625
3626        let mut malformed = base_task;
3627        malformed.query_name = "current".to_string();
3628        malformed.query_arguments = Some(json!({"codec": "json", "blob": "{"}));
3629        let malformed = worker
3630            .execute_query_task(malformed)
3631            .await
3632            .expect_err("malformed payload");
3633        assert_eq!(malformed.reason, "query_payload_decode_failed");
3634
3635        let client = Client::new("http://127.0.0.1:8080").expect("client");
3636        let mut unavailable_worker = Worker::new(client, "rust-workers");
3637        unavailable_worker
3638            .register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
3639        let unavailable_task: QueryTask = serde_json::from_value(json!({
3640            "query_task_id": "query-unavailable",
3641            "workflow_type": "counter",
3642            "query_name": "current",
3643            "payload_codec": "json",
3644            "workflow_arguments": {"codec": "json", "blob": "[]"},
3645            "query_arguments": {"codec": "json", "blob": "[]"}
3646        }))
3647        .expect("query task");
3648        let unavailable = unavailable_worker
3649            .execute_query_task(unavailable_task)
3650            .await
3651            .expect_err("query handler unavailable");
3652        assert_eq!(unavailable.reason, "query_handler_unavailable");
3653    }
3654
3655    #[tokio::test]
3656    async fn client_query_decodes_result_and_typed_failure() {
3657        let server = MockWorkerServer::start();
3658        let client = Client::builder(server.base_url())
3659            .timeout(Duration::from_secs(2))
3660            .build()
3661            .expect("client");
3662
3663        let result = client
3664            .query_workflow("counter-1", "current", json!([]))
3665            .await
3666            .expect("query result");
3667        assert_eq!(result, json!({"count": 8}));
3668
3669        let error = client
3670            .query_workflow("counter-1", "missing", json!([]))
3671            .await
3672            .expect_err("unknown query");
3673        let Error::QueryFailed(failure) = error else {
3674            panic!("expected typed query failure");
3675        };
3676        assert_eq!(failure.status, 404);
3677        assert_eq!(failure.reason, "rejected_unknown_query");
3678    }
3679
3680    #[tokio::test]
3681    async fn baseline_worker_endpoints_send_the_baseline_protocol() {
3682        let server = MockWorkerServer::start();
3683        let client = Client::builder(server.base_url())
3684            .timeout(Duration::from_secs(2))
3685            .build()
3686            .expect("client");
3687
3688        client
3689            .register_worker("capture-worker", "capture", vec![], vec![], 1, 1)
3690            .await
3691            .expect("register");
3692        client
3693            .heartbeat_worker("capture-worker", 1, 1)
3694            .await
3695            .expect("heartbeat");
3696        client
3697            .poll_workflow_task("capture-worker", "capture", Duration::from_millis(10))
3698            .await
3699            .expect("workflow poll");
3700        client
3701            .poll_activity_task("capture-worker", "capture", Duration::from_millis(10))
3702            .await
3703            .expect("activity poll");
3704
3705        for path in [
3706            "/api/worker/register",
3707            "/api/worker/heartbeat",
3708            "/api/worker/workflow-tasks/poll",
3709            "/api/worker/activity-tasks/poll",
3710        ] {
3711            assert_eq!(
3712                server.worker_protocol_for(path).as_deref(),
3713                Some(WORKER_PROTOCOL_VERSION),
3714                "unexpected protocol for {path}"
3715            );
3716        }
3717
3718        assert_eq!(
3719            server.request_body("/api/worker/workflow-tasks/poll")["timeout_seconds"],
3720            1
3721        );
3722        assert_eq!(
3723            server.request_body("/api/worker/activity-tasks/poll")["timeout_seconds"],
3724            1
3725        );
3726    }
3727
3728    #[tokio::test]
3729    async fn query_task_endpoints_send_the_query_feature_protocol() {
3730        let server = MockWorkerServer::start();
3731        let client = Client::builder(server.base_url())
3732            .timeout(Duration::from_secs(2))
3733            .build()
3734            .expect("client");
3735
3736        client
3737            .poll_query_task("capture-worker", "capture", Duration::from_millis(10))
3738            .await
3739            .expect("query poll");
3740        client
3741            .complete_query_task("query-capture", "capture-worker", 1, json!(8), JSON_CODEC)
3742            .await
3743            .expect("query complete");
3744        client
3745            .fail_query_task(
3746                "query-capture",
3747                "capture-worker",
3748                1,
3749                "failed",
3750                "query_rejected",
3751                "QueryFailed",
3752            )
3753            .await
3754            .expect("query fail");
3755
3756        for path in [
3757            "/api/worker/query-tasks/poll",
3758            "/api/worker/query-tasks/query-capture/complete",
3759            "/api/worker/query-tasks/query-capture/fail",
3760        ] {
3761            assert_eq!(
3762                server.worker_protocol_for(path).as_deref(),
3763                Some(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION),
3764                "unexpected protocol for {path}"
3765            );
3766        }
3767
3768        assert_eq!(
3769            server.request_body("/api/worker/query-tasks/poll")["timeout_seconds"],
3770            1
3771        );
3772    }
3773
3774    #[tokio::test]
3775    async fn query_protocol_rejection_from_older_server_is_typed() {
3776        let server = MockWorkerServer::reject_query_protocol();
3777        let client = Client::builder(server.base_url())
3778            .timeout(Duration::from_secs(2))
3779            .build()
3780            .expect("client");
3781
3782        let error = client
3783            .poll_query_task("capture-worker", "capture", Duration::from_millis(10))
3784            .await
3785            .expect_err("server below query protocol floor must reject");
3786        let Error::Protocol(failure) = error else {
3787            panic!("expected typed protocol failure");
3788        };
3789
3790        assert_eq!(failure.status, 400);
3791        assert_eq!(failure.reason, "unsupported_protocol_version");
3792        assert_eq!(failure.supported_version.as_deref(), Some("1.7"));
3793        assert_eq!(
3794            failure.requested_version.as_deref(),
3795            Some(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION)
3796        );
3797        assert_eq!(
3798            server
3799                .worker_protocol_for("/api/worker/query-tasks/poll")
3800                .as_deref(),
3801            Some(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION)
3802        );
3803    }
3804
3805    #[tokio::test]
3806    async fn run_once_without_query_handlers_keeps_pre_query_server_compatibility() {
3807        let server = MockWorkerServer::reject_query_protocol();
3808        let client = Client::builder(server.base_url())
3809            .timeout(Duration::from_secs(2))
3810            .build()
3811            .expect("client");
3812        let mut worker = Worker::new(client, "rust-workers")
3813            .worker_id("baseline-worker")
3814            .poll_timeout(Duration::from_millis(10));
3815
3816        worker.register_workflow("baseline.workflow", |_ctx, _input| async move {
3817            Ok(Value::Null)
3818        });
3819
3820        assert_eq!(worker.run_once().await.expect("baseline run once"), 0);
3821        assert_eq!(
3822            server
3823                .worker_protocol_for("/api/worker/workflow-tasks/poll")
3824                .as_deref(),
3825            Some(WORKER_PROTOCOL_VERSION)
3826        );
3827        assert_eq!(
3828            server.worker_protocol_for("/api/worker/query-tasks/poll"),
3829            None,
3830            "a worker without query handlers must not use the query-task endpoint"
3831        );
3832    }
3833
3834    #[tokio::test]
3835    async fn completion_time_query_rejection_is_typed_without_stopping_worker() {
3836        let server = MockWorkerServer::reject_query_completion();
3837        let client = Client::builder(server.base_url())
3838            .timeout(Duration::from_secs(2))
3839            .build()
3840            .expect("client");
3841
3842        let error = client
3843            .complete_query_task("query-late", "late-worker", 1, json!(8), JSON_CODEC)
3844            .await
3845            .expect_err("expired completion must be rejected");
3846        let Error::QueryFailed(failure) = error else {
3847            panic!("expected typed query failure");
3848        };
3849        assert_eq!(failure.status, 409);
3850        assert_eq!(failure.reason, "query_task_timed_out");
3851
3852        let mut worker = Worker::new(client, "rust-workers")
3853            .worker_id("late-worker")
3854            .poll_timeout(Duration::from_millis(10));
3855        worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
3856        worker.register_query(
3857            "counter",
3858            "current",
3859            |_ctx, _args| async move { Ok(json!(8)) },
3860        );
3861
3862        assert_eq!(worker.run_once().await.expect("late task is handled"), 1);
3863        assert_eq!(
3864            worker
3865                .run_once()
3866                .await
3867                .expect("worker continues after late completion"),
3868            0
3869        );
3870        assert_eq!(
3871            server.request_count("/api/worker/query-tasks/query-late/complete"),
3872            2
3873        );
3874        assert_eq!(
3875            server.request_count("/api/worker/query-tasks/query-late/fail"),
3876            0,
3877            "a server completion rejection must not be reported as an encoding failure"
3878        );
3879    }
3880
3881    #[tokio::test]
3882    async fn activity_only_worker_can_shutdown_without_workflow_poller() {
3883        let server = MockWorkerServer::start();
3884        let client = Client::builder(server.base_url())
3885            .timeout(Duration::from_secs(2))
3886            .build()
3887            .expect("client");
3888        let mut worker = Worker::new(client, "rust-workers")
3889            .worker_id("activity-only-worker")
3890            .poll_timeout(Duration::from_millis(10));
3891
3892        worker.register_activity(
3893            "activity.only",
3894            |_ctx, _args| async move { Ok(Value::Null) },
3895        );
3896
3897        worker.run_until(async {}).await.expect("run worker");
3898    }
3899
3900    #[tokio::test]
3901    async fn workflow_only_worker_can_shutdown_without_activity_poller() {
3902        let server = MockWorkerServer::start();
3903        let client = Client::builder(server.base_url())
3904            .timeout(Duration::from_secs(2))
3905            .build()
3906            .expect("client");
3907        let mut worker = Worker::new(client, "rust-workers")
3908            .worker_id("workflow-only-worker")
3909            .poll_timeout(Duration::from_millis(10));
3910
3911        worker.register_workflow(
3912            "workflow.only",
3913            |_ctx, _input| async move { Ok(Value::Null) },
3914        );
3915
3916        worker.run_until(async {}).await.expect("run worker");
3917    }
3918
3919    #[tokio::test]
3920    async fn worker_heartbeat_observer_receives_server_acknowledgements() {
3921        let server = MockWorkerServer::start();
3922        let client = Client::builder(server.base_url())
3923            .timeout(Duration::from_secs(2))
3924            .build()
3925            .expect("client");
3926        let observations = Arc::new(Mutex::new(Vec::new()));
3927        let observed = Arc::clone(&observations);
3928        let mut worker = Worker::new(client, "rust-workers")
3929            .worker_id("observed-heartbeat-worker")
3930            .poll_timeout(Duration::from_millis(10))
3931            .on_worker_heartbeat(move |observation| {
3932                observed
3933                    .lock()
3934                    .expect("heartbeat observations")
3935                    .push(observation.clone());
3936            });
3937
3938        worker.register_workflow("workflow.observed", |_ctx, _input| async move {
3939            Ok(Value::Null)
3940        });
3941        worker
3942            .run_until(tokio::time::sleep(Duration::from_millis(20)))
3943            .await
3944            .expect("run worker");
3945
3946        let observations = observations.lock().expect("heartbeat observations");
3947        let first = observations.first().expect("heartbeat acknowledgement");
3948        assert_eq!(first.worker_id, "observed-heartbeat-worker");
3949        assert_eq!(first.task_queue, "rust-workers");
3950        assert!(first.acknowledged_at_unix_millis > 0);
3951        assert_eq!(first.acknowledgement, json!({}));
3952    }
3953
3954    #[tokio::test]
3955    async fn worker_retries_poll_and_heartbeat_transport_failures_independently() {
3956        let server = MockWorkerServer::transient_worker_failures();
3957        let client = Client::builder(server.base_url())
3958            .timeout(Duration::from_secs(2))
3959            .build()
3960            .expect("client");
3961        let mut worker = Worker::new(client, "rust-workers")
3962            .worker_id("retry-worker")
3963            .poll_timeout(Duration::from_millis(10))
3964            .retry_policy(WorkerRetryPolicy {
3965                max_retries: 2,
3966                initial_backoff: Duration::from_millis(1),
3967                max_backoff: Duration::from_millis(1),
3968            });
3969        worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
3970        worker.register_activity(
3971            "counter.activity",
3972            |_ctx, _input| async move { Ok(Value::Null) },
3973        );
3974        worker.register_query(
3975            "counter",
3976            "current",
3977            |_ctx, _args| async move { Ok(json!(8)) },
3978        );
3979
3980        worker
3981            .run_until(tokio::time::sleep(Duration::from_millis(75)))
3982            .await
3983            .expect("transient failures must not stop the worker");
3984
3985        for path in [
3986            "/api/worker/heartbeat",
3987            "/api/worker/workflow-tasks/poll",
3988            "/api/worker/activity-tasks/poll",
3989            "/api/worker/query-tasks/poll",
3990        ] {
3991            assert!(
3992                server.request_count(path) >= 2,
3993                "{path} must continue after its transient failure"
3994            );
3995        }
3996    }
3997
3998    #[tokio::test]
3999    async fn worker_bounds_transport_retries() {
4000        let server = MockWorkerServer::unavailable_polls();
4001        let client = Client::builder(server.base_url())
4002            .timeout(Duration::from_secs(2))
4003            .build()
4004            .expect("client");
4005        let mut worker = Worker::new(client, "rust-workers")
4006            .worker_id("bounded-retry-worker")
4007            .poll_timeout(Duration::from_millis(10))
4008            .retry_policy(WorkerRetryPolicy {
4009                max_retries: 2,
4010                initial_backoff: Duration::from_millis(1),
4011                max_backoff: Duration::from_millis(1),
4012            });
4013        worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
4014
4015        let error = worker.run().await.expect_err("retry bound must terminate");
4016        assert!(matches!(error, Error::Transport(_)));
4017        assert_eq!(
4018            server.request_count("/api/worker/workflow-tasks/poll"),
4019            3,
4020            "one initial request plus two retries"
4021        );
4022    }
4023
4024    #[tokio::test]
4025    async fn worker_does_not_retry_authentication_failures() {
4026        let server = MockWorkerServer::unauthorized_polls();
4027        let client = Client::builder(server.base_url())
4028            .timeout(Duration::from_secs(2))
4029            .build()
4030            .expect("client");
4031        let mut worker = Worker::new(client, "rust-workers")
4032            .worker_id("unauthorized-worker")
4033            .poll_timeout(Duration::from_millis(10));
4034        worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
4035
4036        let error = worker
4037            .run()
4038            .await
4039            .expect_err("authentication must terminate");
4040        let Error::Http { status, body } = error else {
4041            panic!("expected stable HTTP authentication error");
4042        };
4043        assert_eq!(status, reqwest::StatusCode::UNAUTHORIZED);
4044        assert!(body.contains("authentication_failed"));
4045        assert_eq!(
4046            server.request_count("/api/worker/workflow-tasks/poll"),
4047            1,
4048            "authentication failures must not be retried"
4049        );
4050    }
4051
4052    #[derive(Clone, Debug)]
4053    struct CapturedRequest {
4054        path: String,
4055        worker_protocol: Option<String>,
4056        body: String,
4057    }
4058
4059    struct MockWorkerServer {
4060        addr: SocketAddr,
4061        stop: Arc<AtomicBool>,
4062        requests: Arc<Mutex<Vec<CapturedRequest>>>,
4063        thread: Option<thread::JoinHandle<()>>,
4064    }
4065
4066    #[derive(Clone, Copy, Default)]
4067    struct MockWorkerBehavior {
4068        reject_query_protocol: bool,
4069        reject_query_completion: bool,
4070        poll_failures_per_path: usize,
4071        heartbeat_failures: usize,
4072        unauthorized_polls: bool,
4073    }
4074
4075    impl MockWorkerServer {
4076        fn start() -> Self {
4077            Self::start_with_behavior(MockWorkerBehavior::default())
4078        }
4079
4080        fn reject_query_protocol() -> Self {
4081            Self::start_with_behavior(MockWorkerBehavior {
4082                reject_query_protocol: true,
4083                ..MockWorkerBehavior::default()
4084            })
4085        }
4086
4087        fn reject_query_completion() -> Self {
4088            Self::start_with_behavior(MockWorkerBehavior {
4089                reject_query_completion: true,
4090                ..MockWorkerBehavior::default()
4091            })
4092        }
4093
4094        fn transient_worker_failures() -> Self {
4095            Self::start_with_behavior(MockWorkerBehavior {
4096                poll_failures_per_path: 1,
4097                heartbeat_failures: 1,
4098                ..MockWorkerBehavior::default()
4099            })
4100        }
4101
4102        fn unavailable_polls() -> Self {
4103            Self::start_with_behavior(MockWorkerBehavior {
4104                poll_failures_per_path: usize::MAX,
4105                ..MockWorkerBehavior::default()
4106            })
4107        }
4108
4109        fn unauthorized_polls() -> Self {
4110            Self::start_with_behavior(MockWorkerBehavior {
4111                unauthorized_polls: true,
4112                ..MockWorkerBehavior::default()
4113            })
4114        }
4115
4116        fn start_with_behavior(behavior: MockWorkerBehavior) -> Self {
4117            let listener = TcpListener::bind("127.0.0.1:0").expect("bind mock server");
4118            listener
4119                .set_nonblocking(true)
4120                .expect("configure mock listener");
4121            let addr = listener.local_addr().expect("mock server address");
4122            let stop = Arc::new(AtomicBool::new(false));
4123            let server_stop = Arc::clone(&stop);
4124            let requests = Arc::new(Mutex::new(Vec::new()));
4125            let server_requests = Arc::clone(&requests);
4126            let thread = thread::spawn(move || {
4127                while !server_stop.load(Ordering::SeqCst) {
4128                    match listener.accept() {
4129                        Ok((mut stream, _)) => {
4130                            handle_mock_worker_request(&mut stream, &server_requests, behavior)
4131                        }
4132                        Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
4133                            thread::sleep(Duration::from_millis(5));
4134                        }
4135                        Err(_) => break,
4136                    }
4137                }
4138            });
4139
4140            Self {
4141                addr,
4142                stop,
4143                requests,
4144                thread: Some(thread),
4145            }
4146        }
4147
4148        fn base_url(&self) -> String {
4149            format!("http://{}", self.addr)
4150        }
4151
4152        fn worker_protocol_for(&self, path: &str) -> Option<String> {
4153            self.requests
4154                .lock()
4155                .expect("captured requests")
4156                .iter()
4157                .find(|request| request.path == path)
4158                .and_then(|request| request.worker_protocol.clone())
4159        }
4160
4161        fn request_count(&self, path: &str) -> usize {
4162            self.requests
4163                .lock()
4164                .expect("captured requests")
4165                .iter()
4166                .filter(|request| request.path == path)
4167                .count()
4168        }
4169
4170        fn request_body(&self, path: &str) -> Value {
4171            let requests = self.requests.lock().expect("captured requests");
4172            let body = &requests
4173                .iter()
4174                .find(|request| request.path == path)
4175                .unwrap_or_else(|| panic!("missing request for {path}"))
4176                .body;
4177            serde_json::from_str(body).unwrap_or_else(|error| {
4178                panic!("invalid JSON request body for {path}: {error}: {body:?}")
4179            })
4180        }
4181    }
4182
4183    impl Drop for MockWorkerServer {
4184        fn drop(&mut self) {
4185            self.stop.store(true, Ordering::SeqCst);
4186            let _ = TcpStream::connect(self.addr);
4187
4188            if let Some(thread) = self.thread.take() {
4189                thread.join().expect("join mock server");
4190            }
4191        }
4192    }
4193
4194    fn handle_mock_worker_request(
4195        stream: &mut TcpStream,
4196        requests: &Arc<Mutex<Vec<CapturedRequest>>>,
4197        behavior: MockWorkerBehavior,
4198    ) {
4199        let _ = stream.set_read_timeout(Some(Duration::from_millis(200)));
4200        let mut buffer = [0_u8; 8192];
4201        let mut request = Vec::new();
4202
4203        loop {
4204            match stream.read(&mut buffer) {
4205                Ok(0) => break,
4206                Ok(read) => {
4207                    request.extend_from_slice(&buffer[..read]);
4208                    if mock_request_is_complete(&request) {
4209                        break;
4210                    }
4211                }
4212                Err(error)
4213                    if matches!(
4214                        error.kind(),
4215                        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
4216                    ) =>
4217                {
4218                    break;
4219                }
4220                Err(_) => return,
4221            }
4222        }
4223
4224        let request = String::from_utf8_lossy(&request);
4225        let body = request
4226            .split_once("\r\n\r\n")
4227            .map(|(_, body)| body)
4228            .unwrap_or_default();
4229        let path = request
4230            .lines()
4231            .next()
4232            .and_then(|line| line.split_whitespace().nth(1))
4233            .unwrap_or_default();
4234        let worker_protocol = request.lines().find_map(|line| {
4235            let (name, value) = line.split_once(':')?;
4236            name.eq_ignore_ascii_case("X-Durable-Workflow-Protocol-Version")
4237                .then(|| value.trim().to_string())
4238        });
4239        let request_number = {
4240            let mut requests = requests.lock().expect("captured requests");
4241            requests.push(CapturedRequest {
4242                path: path.to_string(),
4243                worker_protocol: worker_protocol.clone(),
4244                body: body.to_string(),
4245            });
4246            requests
4247                .iter()
4248                .filter(|request| request.path == path)
4249                .count()
4250        };
4251
4252        let is_poll = matches!(
4253            path,
4254            "/api/worker/workflow-tasks/poll"
4255                | "/api/worker/activity-tasks/poll"
4256                | "/api/worker/query-tasks/poll"
4257        );
4258        if is_poll && request_number <= behavior.poll_failures_per_path {
4259            return;
4260        }
4261        if path == "/api/worker/heartbeat" && request_number <= behavior.heartbeat_failures {
4262            return;
4263        }
4264        if behavior.unauthorized_polls && is_poll {
4265            write_mock_response(
4266                stream,
4267                "401 Unauthorized",
4268                r#"{"reason":"authentication_failed","message":"invalid worker token"}"#,
4269            );
4270            return;
4271        }
4272
4273        if behavior.reject_query_protocol && path.starts_with("/api/worker/query-tasks/") {
4274            let requested_version = worker_protocol.as_deref().unwrap_or("missing");
4275            let body = format!(
4276                r#"{{"reason":"unsupported_protocol_version","message":"Query tasks require worker protocol 1.8 or newer.","supported_version":"1.7","requested_version":"{requested_version}"}}"#
4277            );
4278            write_mock_response(stream, "400 Bad Request", &body);
4279            return;
4280        }
4281
4282        if behavior.reject_query_completion && path == "/api/worker/query-tasks/query-late/complete"
4283        {
4284            write_mock_response(
4285                stream,
4286                "409 Conflict",
4287                r#"{"reason":"query_task_timed_out","message":"query task timed out before completion"}"#,
4288            );
4289            return;
4290        }
4291
4292        let (status, body) = match path {
4293            "/api/worker/register" => (
4294                "200 OK",
4295                r#"{"worker_id":"mock-worker","registered":true,"heartbeat_interval_seconds":3600}"#,
4296            ),
4297            "/api/worker/heartbeat" => ("200 OK", "{}"),
4298            "/api/worker/activity-tasks/poll" | "/api/worker/workflow-tasks/poll" => {
4299                ("200 OK", r#"{"task":null}"#)
4300            }
4301            "/api/worker/query-tasks/poll"
4302                if behavior.reject_query_completion && request_number == 1 =>
4303            {
4304                (
4305                    "200 OK",
4306                    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"}}"#,
4307                )
4308            }
4309            "/api/worker/query-tasks/poll" => ("200 OK", r#"{"task":null}"#),
4310            "/api/worker/query-tasks/query-capture/complete"
4311            | "/api/worker/query-tasks/query-capture/fail" => ("200 OK", "{}"),
4312            "/api/workflows/counter-1/query/current" => (
4313                "200 OK",
4314                r#"{"workflow_id":"counter-1","query_name":"current","result":{"count":8},"result_envelope":{"codec":"json","blob":"{\"count\":8}"}}"#,
4315            ),
4316            "/api/workflows/counter-1/query/missing" => (
4317                "404 Not Found",
4318                r#"{"workflow_id":"counter-1","query_name":"missing","reason":"rejected_unknown_query","message":"unknown query"}"#,
4319            ),
4320            _ => ("404 Not Found", r#"{"message":"not found"}"#),
4321        };
4322        write_mock_response(stream, status, body);
4323    }
4324
4325    fn mock_request_is_complete(request: &[u8]) -> bool {
4326        let Some(header_end) = request
4327            .windows(4)
4328            .position(|window| window == b"\r\n\r\n")
4329            .map(|position| position + 4)
4330        else {
4331            return false;
4332        };
4333        let headers = String::from_utf8_lossy(&request[..header_end]);
4334        let content_length = headers.lines().find_map(|line| {
4335            let (name, value) = line.split_once(':')?;
4336            name.eq_ignore_ascii_case("content-length")
4337                .then(|| value.trim().parse::<usize>().ok())
4338                .flatten()
4339        });
4340
4341        request.len() >= header_end + content_length.unwrap_or(0)
4342    }
4343
4344    fn write_mock_response(stream: &mut TcpStream, status: &str, body: &str) {
4345        let response = format!(
4346            "HTTP/1.1 {status}\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}",
4347            body.len()
4348        );
4349
4350        let _ = stream.write_all(response.as_bytes());
4351        let _ = stream.flush();
4352    }
4353}