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