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