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 and signal workflows,
4//! register a Rust worker, poll workflow and activity tasks, heartbeat worker
5//! and activity liveness, and exchange JSON-native payloads through the same
6//! `avro` generic wrapper used by the existing first-party SDKs.
7
8use std::{
9    collections::HashMap,
10    future::Future,
11    pin::Pin,
12    sync::{
13        atomic::{AtomicBool, Ordering},
14        Arc, Mutex, OnceLock,
15    },
16    task::{Context as TaskContext, Poll},
17    time::{Duration, Instant, SystemTime, UNIX_EPOCH},
18};
19
20use apache_avro::{from_avro_datum, from_value, to_avro_datum, to_value, Schema};
21use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
22use futures_util::{future::OptionFuture, task::noop_waker_ref};
23use serde::{de::DeserializeOwned, Deserialize, Serialize};
24pub use serde_json::{json, Value};
25use thiserror::Error;
26
27pub const WORKER_PROTOCOL_VERSION: &str = "1.2";
28pub const CONTROL_PLANE_VERSION: &str = "2";
29pub const DEFAULT_CODEC: &str = "avro";
30pub const JSON_CODEC: &str = "json";
31pub const SDK_VERSION: &str = concat!("durable-workflow-rust/", env!("CARGO_PKG_VERSION"));
32
33const AVRO_PAYLOAD_SCHEMA_JSON: &str = r#"{"type":"record","name":"Payload","namespace":"durable_workflow","fields":[{"name":"json","type":"string"},{"name":"version","type":"int","default":1}]}"#;
34const AVRO_PAYLOAD_VERSION: i32 = 1;
35
36static AVRO_PAYLOAD_SCHEMA: OnceLock<std::result::Result<Schema, String>> = OnceLock::new();
37
38pub type Result<T> = std::result::Result<T, Error>;
39
40#[derive(Debug, Error)]
41pub enum Error {
42    #[error("transport error: {0}")]
43    Transport(#[from] reqwest::Error),
44    #[error("json error: {0}")]
45    Json(#[from] serde_json::Error),
46    #[error("http {status}: {body}")]
47    Http {
48        status: reqwest::StatusCode,
49        body: String,
50    },
51    #[error("codec error: {0}")]
52    Codec(String),
53    #[error("workflow handler {0:?} is not registered")]
54    WorkflowNotRegistered(String),
55    #[error("activity handler {0:?} is not registered")]
56    ActivityNotRegistered(String),
57    #[error("workflow future yielded without emitting a durable command")]
58    WorkflowYieldedWithoutCommand,
59    #[error("workflow state lock is poisoned")]
60    WorkflowStatePoisoned,
61    #[error("operation timed out")]
62    Timeout,
63    #[error("worker loop error: {0}")]
64    WorkerLoop(String),
65}
66
67#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
68pub struct PayloadEnvelope {
69    pub codec: String,
70    pub blob: String,
71}
72
73impl PayloadEnvelope {
74    pub fn avro<T: Serialize>(value: &T) -> Result<Self> {
75        encode_payload(value, DEFAULT_CODEC)
76    }
77
78    pub fn json<T: Serialize>(value: &T) -> Result<Self> {
79        encode_payload(value, JSON_CODEC)
80    }
81}
82
83pub fn encode_payload<T: Serialize>(value: &T, codec: &str) -> Result<PayloadEnvelope> {
84    let value = serde_json::to_value(value)?;
85    let blob = encode_value_blob(&value, codec)?;
86
87    Ok(PayloadEnvelope {
88        codec: codec.to_string(),
89        blob,
90    })
91}
92
93pub fn decode_payload<T: DeserializeOwned>(envelope: &PayloadEnvelope) -> Result<T> {
94    let value = decode_blob(&envelope.blob, &envelope.codec)?;
95    Ok(serde_json::from_value(value)?)
96}
97
98fn encode_value_envelope(value: &Value, codec: &str) -> Result<Value> {
99    Ok(serde_json::to_value(encode_payload(value, codec)?)?)
100}
101
102fn encode_value_blob(value: &Value, codec: &str) -> Result<String> {
103    match codec {
104        JSON_CODEC => Ok(serde_json::to_string(value)?),
105        DEFAULT_CODEC => encode_avro_generic(value),
106        other => Err(Error::Codec(format!("unsupported payload codec {other:?}"))),
107    }
108}
109
110fn decode_wire_value(value: &Value, fallback_codec: &str) -> Result<Value> {
111    if value.is_null() {
112        return Ok(Value::Null);
113    }
114
115    if let Some(object) = value.as_object() {
116        if let (Some(codec), Some(blob)) = (
117            object.get("codec").and_then(Value::as_str),
118            object.get("blob").and_then(Value::as_str),
119        ) {
120            return decode_blob(blob, codec);
121        }
122    }
123
124    if let Some(blob) = value.as_str() {
125        return decode_blob(blob, fallback_codec);
126    }
127
128    Ok(value.clone())
129}
130
131fn decode_blob(blob: &str, codec: &str) -> Result<Value> {
132    match codec {
133        JSON_CODEC => Ok(serde_json::from_str(blob)?),
134        DEFAULT_CODEC => decode_avro_generic(blob),
135        other => Err(Error::Codec(format!("unsupported payload codec {other:?}"))),
136    }
137}
138
139fn encode_avro_generic(value: &Value) -> Result<String> {
140    let json = serde_json::to_string(value)?;
141    let datum = to_value(AvroPayload {
142        json,
143        version: AVRO_PAYLOAD_VERSION,
144    })
145    .map_err(|err| Error::Codec(format!("could not convert avro generic wrapper: {err}")))?;
146    let datum = to_avro_datum(avro_payload_schema()?, datum)
147        .map_err(|err| Error::Codec(format!("could not encode avro generic wrapper: {err}")))?;
148
149    let mut bytes = Vec::with_capacity(datum.len() + 1);
150    bytes.push(0x00);
151    bytes.extend_from_slice(&datum);
152    Ok(BASE64.encode(bytes))
153}
154
155fn decode_avro_generic(blob: &str) -> Result<Value> {
156    let bytes = BASE64
157        .decode(blob)
158        .map_err(|err| Error::Codec(format!("invalid avro base64 payload: {err}")))?;
159
160    if bytes.is_empty() {
161        return Err(Error::Codec("avro payload is empty".to_string()));
162    }
163
164    match bytes[0] {
165        0x00 => {}
166        0x01 => {
167            return Err(Error::Codec(
168                "typed avro payloads require a schema context; v1 supports the generic wrapper"
169                    .to_string(),
170            ));
171        }
172        other => {
173            return Err(Error::Codec(format!(
174                "unknown avro payload prefix 0x{other:02x}"
175            )));
176        }
177    }
178
179    let mut datum = &bytes[1..];
180    let datum = from_avro_datum(avro_payload_schema()?, &mut datum, None)
181        .map_err(|err| Error::Codec(format!("could not decode avro generic wrapper: {err}")))?;
182    let payload: AvroPayload = from_value(&datum)
183        .map_err(|err| Error::Codec(format!("invalid avro generic wrapper record: {err}")))?;
184
185    if payload.version != AVRO_PAYLOAD_VERSION {
186        return Err(Error::Codec(format!(
187            "unsupported avro generic wrapper version {}",
188            payload.version
189        )));
190    }
191
192    Ok(serde_json::from_str(&payload.json)?)
193}
194
195#[derive(Debug, Serialize, Deserialize)]
196struct AvroPayload {
197    json: String,
198    version: i32,
199}
200
201fn avro_payload_schema() -> Result<&'static Schema> {
202    match AVRO_PAYLOAD_SCHEMA.get_or_init(|| {
203        Schema::parse_str(AVRO_PAYLOAD_SCHEMA_JSON)
204            .map_err(|err| format!("could not parse avro payload schema: {err}"))
205    }) {
206        Ok(schema) => Ok(schema),
207        Err(message) => Err(Error::Codec(message.clone())),
208    }
209}
210
211#[derive(Clone, Debug)]
212pub struct Client {
213    http: reqwest::Client,
214    base_url: String,
215    token: Option<String>,
216    control_token: Option<String>,
217    worker_token: Option<String>,
218    namespace: String,
219}
220
221impl Client {
222    pub fn new(base_url: impl Into<String>) -> Result<Self> {
223        Self::builder(base_url).build()
224    }
225
226    pub fn builder(base_url: impl Into<String>) -> ClientBuilder {
227        ClientBuilder {
228            base_url: base_url.into(),
229            token: None,
230            control_token: None,
231            worker_token: None,
232            namespace: "default".to_string(),
233            timeout: Duration::from_secs(60),
234        }
235    }
236
237    pub async fn health(&self) -> Result<Value> {
238        self.request_json(
239            reqwest::Method::GET,
240            "/health",
241            false,
242            Option::<&Value>::None,
243        )
244        .await
245    }
246
247    pub async fn cluster_info(&self) -> Result<Value> {
248        self.request_json(
249            reqwest::Method::GET,
250            "/cluster/info",
251            false,
252            Option::<&Value>::None,
253        )
254        .await
255    }
256
257    pub async fn start_workflow<T: Serialize>(
258        &self,
259        workflow_type: &str,
260        task_queue: &str,
261        workflow_id: &str,
262        input: T,
263    ) -> Result<WorkflowHandle> {
264        let input = serde_json::to_value(input)?;
265        let input_envelope = encode_value_envelope(&normalize_arguments(input), DEFAULT_CODEC)?;
266        let body = json!({
267            "workflow_id": workflow_id,
268            "workflow_type": workflow_type,
269            "task_queue": task_queue,
270            "input": input_envelope,
271            "execution_timeout_seconds": 3600,
272            "run_timeout_seconds": 600
273        });
274
275        let data: Value = self
276            .request_json(reqwest::Method::POST, "/workflows", false, Some(&body))
277            .await?;
278
279        Ok(WorkflowHandle {
280            client: self.clone(),
281            workflow_id: data
282                .get("workflow_id")
283                .and_then(Value::as_str)
284                .unwrap_or(workflow_id)
285                .to_string(),
286            run_id: data
287                .get("run_id")
288                .and_then(Value::as_str)
289                .map(str::to_string),
290            workflow_type: data
291                .get("workflow_type")
292                .and_then(Value::as_str)
293                .unwrap_or(workflow_type)
294                .to_string(),
295        })
296    }
297
298    pub async fn signal_workflow<T: Serialize>(
299        &self,
300        workflow_id: &str,
301        signal_name: &str,
302        input: T,
303    ) -> Result<Value> {
304        let input = serde_json::to_value(input)?;
305        let input_envelope = encode_value_envelope(&normalize_arguments(input), DEFAULT_CODEC)?;
306        let body = json!({
307            "input": input_envelope
308        });
309        let path = format!("/workflows/{workflow_id}/signal/{signal_name}");
310        self.request_json(reqwest::Method::POST, &path, false, Some(&body))
311            .await
312    }
313
314    pub async fn describe_workflow(&self, workflow_id: &str) -> Result<WorkflowDescription> {
315        let path = format!("/workflows/{workflow_id}");
316        let mut data: WorkflowDescription = self
317            .request_json(reqwest::Method::GET, &path, false, Option::<&Value>::None)
318            .await?;
319        data.decode_payloads()?;
320        Ok(data)
321    }
322
323    pub async fn register_worker(
324        &self,
325        worker_id: &str,
326        task_queue: &str,
327        supported_workflow_types: Vec<String>,
328        supported_activity_types: Vec<String>,
329        max_concurrent_workflow_tasks: usize,
330        max_concurrent_activity_tasks: usize,
331    ) -> Result<RegisterWorkerResponse> {
332        let body = json!({
333            "worker_id": worker_id,
334            "task_queue": task_queue,
335            "runtime": "rust",
336            "sdk_version": SDK_VERSION,
337            "supported_workflow_types": supported_workflow_types,
338            "supported_activity_types": supported_activity_types,
339            "max_concurrent_workflow_tasks": max_concurrent_workflow_tasks,
340            "max_concurrent_activity_tasks": max_concurrent_activity_tasks
341        });
342
343        self.request_json(reqwest::Method::POST, "/worker/register", true, Some(&body))
344            .await
345    }
346
347    pub async fn heartbeat_worker(
348        &self,
349        worker_id: &str,
350        workflow_available: usize,
351        activity_available: usize,
352    ) -> Result<Value> {
353        let body = json!({
354            "worker_id": worker_id,
355            "task_slots": {
356                "workflow_available": workflow_available,
357                "activity_available": activity_available
358            },
359            "process_metrics": {
360                "process_id": std::process::id(),
361                "process_uptime_seconds": 0
362            }
363        });
364
365        self.request_json(
366            reqwest::Method::POST,
367            "/worker/heartbeat",
368            true,
369            Some(&body),
370        )
371        .await
372    }
373
374    pub async fn poll_workflow_task(
375        &self,
376        worker_id: &str,
377        task_queue: &str,
378        timeout: Duration,
379    ) -> Result<Option<WorkflowTask>> {
380        Ok(self
381            .poll_workflow_task_response(worker_id, task_queue, timeout)
382            .await?
383            .task)
384    }
385
386    pub async fn poll_workflow_task_response(
387        &self,
388        worker_id: &str,
389        task_queue: &str,
390        timeout: Duration,
391    ) -> Result<PollWorkflowTaskResponse> {
392        let body = json!({
393            "worker_id": worker_id,
394            "task_queue": task_queue,
395        });
396        let mut data: PollWorkflowTaskResponse = self
397            .request_json_with_timeout(
398                reqwest::Method::POST,
399                "/worker/workflow-tasks/poll",
400                true,
401                Some(&body),
402                timeout + Duration::from_secs(5),
403            )
404            .await?;
405
406        if let Some(task) = data.task.as_mut() {
407            self.fetch_remaining_workflow_history(worker_id, task)
408                .await?;
409        }
410
411        Ok(data)
412    }
413
414    async fn fetch_remaining_workflow_history(
415        &self,
416        worker_id: &str,
417        task: &mut WorkflowTask,
418    ) -> Result<()> {
419        let mut next_token = task.next_history_page_token.clone();
420
421        while let Some(token) = next_token.take().filter(|token| !token.is_empty()) {
422            let lease_owner = task
423                .lease_owner
424                .clone()
425                .unwrap_or_else(|| worker_id.to_string());
426            let page = self
427                .workflow_task_history_page(
428                    &task.task_id,
429                    &lease_owner,
430                    task.workflow_task_attempt,
431                    &token,
432                )
433                .await?;
434
435            task.append_history_page(page);
436
437            if task.next_history_page_token.as_deref() == Some(token.as_str()) {
438                return Err(Error::Codec(
439                    "workflow history pagination returned the same page token".to_string(),
440                ));
441            }
442
443            next_token = task.next_history_page_token.clone();
444        }
445
446        Ok(())
447    }
448
449    async fn workflow_task_history_page(
450        &self,
451        task_id: &str,
452        lease_owner: &str,
453        workflow_task_attempt: u64,
454        next_history_page_token: &str,
455    ) -> Result<WorkflowTaskHistoryPage> {
456        let body = json!({
457            "lease_owner": lease_owner,
458            "workflow_task_attempt": workflow_task_attempt,
459            "next_history_page_token": next_history_page_token
460        });
461        let path = format!("/worker/workflow-tasks/{task_id}/history");
462
463        self.request_json(reqwest::Method::POST, &path, true, Some(&body))
464            .await
465    }
466
467    pub async fn complete_workflow_task(
468        &self,
469        task_id: &str,
470        lease_owner: &str,
471        workflow_task_attempt: u64,
472        commands: Vec<Value>,
473    ) -> Result<Value> {
474        let body = json!({
475            "lease_owner": lease_owner,
476            "workflow_task_attempt": workflow_task_attempt,
477            "commands": commands
478        });
479        let path = format!("/worker/workflow-tasks/{task_id}/complete");
480        self.request_json(reqwest::Method::POST, &path, true, Some(&body))
481            .await
482    }
483
484    pub async fn fail_workflow_task(
485        &self,
486        task_id: &str,
487        lease_owner: &str,
488        workflow_task_attempt: u64,
489        message: impl Into<String>,
490    ) -> Result<Value> {
491        let body = json!({
492            "lease_owner": lease_owner,
493            "workflow_task_attempt": workflow_task_attempt,
494            "failure": {
495                "message": message.into(),
496                "type": "RustWorkflowTaskFailure"
497            }
498        });
499        let path = format!("/worker/workflow-tasks/{task_id}/fail");
500        self.request_json(reqwest::Method::POST, &path, true, Some(&body))
501            .await
502    }
503
504    pub async fn poll_activity_task(
505        &self,
506        worker_id: &str,
507        task_queue: &str,
508        timeout: Duration,
509    ) -> Result<Option<ActivityTask>> {
510        let body = json!({
511            "worker_id": worker_id,
512            "task_queue": task_queue,
513        });
514        let data: PollActivityTaskResponse = self
515            .request_json_with_timeout(
516                reqwest::Method::POST,
517                "/worker/activity-tasks/poll",
518                true,
519                Some(&body),
520                timeout + Duration::from_secs(5),
521            )
522            .await?;
523        Ok(data.task)
524    }
525
526    pub async fn complete_activity_task(
527        &self,
528        task_id: &str,
529        activity_attempt_id: &str,
530        lease_owner: &str,
531        result: Value,
532        codec: &str,
533    ) -> Result<Value> {
534        let result = encode_value_envelope(&result, codec)?;
535        let body = json!({
536            "activity_attempt_id": activity_attempt_id,
537            "lease_owner": lease_owner,
538            "result": result
539        });
540        let path = format!("/worker/activity-tasks/{task_id}/complete");
541        self.request_json(reqwest::Method::POST, &path, true, Some(&body))
542            .await
543    }
544
545    pub async fn fail_activity_task(
546        &self,
547        task_id: &str,
548        activity_attempt_id: &str,
549        lease_owner: &str,
550        message: impl Into<String>,
551        non_retryable: bool,
552    ) -> Result<Value> {
553        let body = json!({
554            "activity_attempt_id": activity_attempt_id,
555            "lease_owner": lease_owner,
556            "failure": {
557                "message": message.into(),
558                "type": "RustActivityFailure",
559                "non_retryable": non_retryable
560            }
561        });
562        let path = format!("/worker/activity-tasks/{task_id}/fail");
563        self.request_json(reqwest::Method::POST, &path, true, Some(&body))
564            .await
565    }
566
567    pub async fn heartbeat_activity_task(
568        &self,
569        task_id: &str,
570        activity_attempt_id: &str,
571        lease_owner: &str,
572        details: Value,
573    ) -> Result<ActivityHeartbeatResponse> {
574        let body = json!({
575            "activity_attempt_id": activity_attempt_id,
576            "lease_owner": lease_owner,
577            "details": details
578        });
579        let path = format!("/worker/activity-tasks/{task_id}/heartbeat");
580        self.request_json(reqwest::Method::POST, &path, true, Some(&body))
581            .await
582    }
583
584    async fn request_json<T: DeserializeOwned, B: Serialize + ?Sized>(
585        &self,
586        method: reqwest::Method,
587        path: &str,
588        worker: bool,
589        body: Option<&B>,
590    ) -> Result<T> {
591        self.request_json_with_timeout(method, path, worker, body, Duration::from_secs(60))
592            .await
593    }
594
595    async fn request_json_with_timeout<T: DeserializeOwned, B: Serialize + ?Sized>(
596        &self,
597        method: reqwest::Method,
598        path: &str,
599        worker: bool,
600        body: Option<&B>,
601        timeout: Duration,
602    ) -> Result<T> {
603        let mut request = self
604            .http
605            .request(method, format!("{}/api{}", self.base_url, path))
606            .timeout(timeout)
607            .header(reqwest::header::ACCEPT, "application/json")
608            .header(reqwest::header::CONTENT_TYPE, "application/json")
609            .header("X-Namespace", &self.namespace);
610
611        if worker {
612            request = request.header(
613                "X-Durable-Workflow-Protocol-Version",
614                WORKER_PROTOCOL_VERSION,
615            );
616        } else {
617            request = request.header(
618                "X-Durable-Workflow-Control-Plane-Version",
619                CONTROL_PLANE_VERSION,
620            );
621        }
622
623        if let Some(token) = self.auth_token(worker) {
624            request = request.bearer_auth(token);
625        }
626
627        if let Some(body) = body {
628            request = request.json(body);
629        }
630
631        let response = request.send().await?;
632        let status = response.status();
633        let bytes = response.bytes().await?;
634
635        if !status.is_success() {
636            let body = String::from_utf8_lossy(&bytes).to_string();
637            return Err(Error::Http { status, body });
638        }
639
640        if bytes.is_empty() {
641            return Ok(serde_json::from_value(Value::Null)?);
642        }
643
644        Ok(serde_json::from_slice(&bytes)?)
645    }
646
647    fn auth_token(&self, worker: bool) -> Option<&str> {
648        if worker {
649            self.worker_token
650                .as_deref()
651                .or(self.token.as_deref())
652                .or(self.control_token.as_deref())
653        } else {
654            self.control_token
655                .as_deref()
656                .or(self.token.as_deref())
657                .or(self.worker_token.as_deref())
658        }
659    }
660}
661
662#[derive(Debug)]
663pub struct ClientBuilder {
664    base_url: String,
665    token: Option<String>,
666    control_token: Option<String>,
667    worker_token: Option<String>,
668    namespace: String,
669    timeout: Duration,
670}
671
672impl ClientBuilder {
673    pub fn token(mut self, token: Option<String>) -> Self {
674        self.token = token;
675        self
676    }
677
678    pub fn control_token(mut self, token: Option<String>) -> Self {
679        self.control_token = token;
680        self
681    }
682
683    pub fn worker_token(mut self, token: Option<String>) -> Self {
684        self.worker_token = token;
685        self
686    }
687
688    pub fn namespace(mut self, namespace: impl Into<String>) -> Self {
689        self.namespace = namespace.into();
690        self
691    }
692
693    pub fn timeout(mut self, timeout: Duration) -> Self {
694        self.timeout = timeout;
695        self
696    }
697
698    pub fn build(self) -> Result<Client> {
699        Ok(Client {
700            http: reqwest::Client::builder().timeout(self.timeout).build()?,
701            base_url: self.base_url.trim_end_matches('/').to_string(),
702            token: self.token,
703            control_token: self.control_token,
704            worker_token: self.worker_token,
705            namespace: self.namespace,
706        })
707    }
708}
709
710#[derive(Clone, Debug)]
711pub struct WorkflowHandle {
712    client: Client,
713    pub workflow_id: String,
714    pub run_id: Option<String>,
715    pub workflow_type: String,
716}
717
718impl WorkflowHandle {
719    pub async fn describe(&self) -> Result<WorkflowDescription> {
720        self.client.describe_workflow(&self.workflow_id).await
721    }
722
723    pub async fn signal<T: Serialize>(&self, signal_name: &str, input: T) -> Result<Value> {
724        self.client
725            .signal_workflow(&self.workflow_id, signal_name, input)
726            .await
727    }
728
729    pub async fn result(&self, options: WorkflowResultOptions) -> Result<Value> {
730        let started = Instant::now();
731
732        loop {
733            let description = self.describe().await?;
734            if description.is_completed() {
735                return Ok(description.output.unwrap_or(Value::Null));
736            }
737
738            if description.is_terminal() {
739                return Err(Error::Codec(format!(
740                    "workflow {} closed with status {:?}",
741                    self.workflow_id, description.status
742                )));
743            }
744
745            if started.elapsed() >= options.timeout {
746                return Err(Error::Timeout);
747            }
748
749            tokio::time::sleep(options.poll_interval).await;
750        }
751    }
752}
753
754#[derive(Clone, Copy, Debug)]
755pub struct WorkflowResultOptions {
756    pub poll_interval: Duration,
757    pub timeout: Duration,
758}
759
760impl Default for WorkflowResultOptions {
761    fn default() -> Self {
762        Self {
763            poll_interval: Duration::from_millis(500),
764            timeout: Duration::from_secs(30),
765        }
766    }
767}
768
769#[derive(Clone, Debug, Deserialize)]
770pub struct WorkflowDescription {
771    pub workflow_id: Option<String>,
772    pub run_id: Option<String>,
773    pub workflow_type: Option<String>,
774    pub status: Option<String>,
775    #[serde(default)]
776    pub output: Option<Value>,
777    #[serde(default)]
778    pub output_envelope: Option<Value>,
779    #[serde(flatten)]
780    pub raw: HashMap<String, Value>,
781}
782
783impl WorkflowDescription {
784    pub fn is_completed(&self) -> bool {
785        matches!(self.status.as_deref(), Some("completed" | "Completed"))
786    }
787
788    pub fn is_terminal(&self) -> bool {
789        matches!(
790            self.status.as_deref(),
791            Some(
792                "completed"
793                    | "Completed"
794                    | "failed"
795                    | "Failed"
796                    | "cancelled"
797                    | "Cancelled"
798                    | "terminated"
799                    | "Terminated"
800                    | "timed_out"
801                    | "TimedOut",
802            )
803        )
804    }
805
806    fn decode_payloads(&mut self) -> Result<()> {
807        if let Some(envelope) = &self.output_envelope {
808            self.output = Some(decode_wire_value(envelope, DEFAULT_CODEC)?);
809        }
810
811        Ok(())
812    }
813}
814
815#[derive(Clone, Debug, Deserialize)]
816pub struct RegisterWorkerResponse {
817    pub worker_id: String,
818    pub registered: bool,
819    #[serde(default)]
820    pub heartbeat_interval_seconds: Option<u64>,
821    #[serde(default)]
822    pub protocol_version: Option<String>,
823    #[serde(default)]
824    pub server_capabilities: Option<Value>,
825}
826
827#[derive(Clone, Debug, Deserialize)]
828pub struct PollWorkflowTaskResponse {
829    #[serde(default)]
830    pub task: Option<WorkflowTask>,
831    #[serde(default)]
832    pub poll_status: Option<String>,
833    #[serde(default)]
834    pub reason: Option<String>,
835    #[serde(default)]
836    pub protocol_version: Option<String>,
837    #[serde(default)]
838    pub server_capabilities: Option<Value>,
839}
840
841#[derive(Clone, Debug, Deserialize)]
842struct PollActivityTaskResponse {
843    #[serde(default)]
844    task: Option<ActivityTask>,
845}
846
847#[derive(Clone, Debug, Deserialize)]
848pub struct WorkflowTask {
849    pub task_id: String,
850    #[serde(default)]
851    pub workflow_id: Option<String>,
852    #[serde(default)]
853    pub run_id: Option<String>,
854    pub workflow_type: String,
855    #[serde(default = "default_payload_codec")]
856    pub payload_codec: String,
857    #[serde(default)]
858    pub arguments: Option<Value>,
859    #[serde(default)]
860    pub history_events: Vec<HistoryEvent>,
861    #[serde(default)]
862    pub total_history_events: Option<u64>,
863    #[serde(default)]
864    pub next_history_page_token: Option<String>,
865    #[serde(default = "default_workflow_task_attempt")]
866    pub workflow_task_attempt: u64,
867    #[serde(default)]
868    pub workflow_signal_id: Option<String>,
869    #[serde(default)]
870    pub signal_name: Option<String>,
871    #[serde(default)]
872    pub signal_arguments: Option<Value>,
873    #[serde(default)]
874    pub lease_owner: Option<String>,
875}
876
877impl WorkflowTask {
878    fn append_history_page(&mut self, page: WorkflowTaskHistoryPage) {
879        self.history_events.extend(page.history_events);
880
881        if page.total_history_events.is_some() {
882            self.total_history_events = page.total_history_events;
883        }
884
885        self.next_history_page_token = page
886            .next_history_page_token
887            .filter(|token| !token.is_empty());
888    }
889}
890
891#[derive(Clone, Debug, Deserialize)]
892struct WorkflowTaskHistoryPage {
893    #[serde(default)]
894    history_events: Vec<HistoryEvent>,
895    #[serde(default)]
896    total_history_events: Option<u64>,
897    #[serde(default)]
898    next_history_page_token: Option<String>,
899}
900
901#[derive(Clone, Debug, Deserialize)]
902pub struct ActivityTask {
903    pub task_id: String,
904    #[serde(default)]
905    pub activity_attempt_id: Option<String>,
906    #[serde(default)]
907    pub attempt_id: Option<String>,
908    pub activity_type: String,
909    #[serde(default = "default_payload_codec")]
910    pub payload_codec: String,
911    #[serde(default)]
912    pub arguments: Option<Value>,
913    #[serde(default = "default_attempt_number")]
914    pub attempt_number: u64,
915    #[serde(default)]
916    pub lease_owner: Option<String>,
917}
918
919#[derive(Clone, Debug, Deserialize)]
920pub struct HistoryEvent {
921    pub event_type: String,
922    #[serde(default)]
923    pub payload: Value,
924    #[serde(flatten)]
925    pub raw: HashMap<String, Value>,
926}
927
928#[derive(Clone, Debug, Deserialize)]
929pub struct ActivityHeartbeatResponse {
930    #[serde(default)]
931    pub cancel_requested: bool,
932    #[serde(default)]
933    pub heartbeat_recorded: bool,
934}
935
936fn default_payload_codec() -> String {
937    DEFAULT_CODEC.to_string()
938}
939
940fn default_workflow_task_attempt() -> u64 {
941    1
942}
943
944fn default_attempt_number() -> u64 {
945    1
946}
947
948type WorkflowFuture = Pin<Box<dyn Future<Output = Result<Value>> + Send + 'static>>;
949type WorkflowHandler = Arc<dyn Fn(WorkflowContext, Value) -> WorkflowFuture + Send + Sync>;
950type ActivityFuture = Pin<Box<dyn Future<Output = Result<Value>> + Send + 'static>>;
951type ActivityHandler = Arc<dyn Fn(ActivityContext, Value) -> ActivityFuture + Send + Sync>;
952type WorkerHeartbeatObserver = Arc<dyn Fn(&WorkerHeartbeatObservation) + Send + Sync>;
953
954#[derive(Clone, Debug)]
955pub struct WorkerHeartbeatObservation {
956    pub worker_id: String,
957    pub task_queue: String,
958    pub acknowledged_at_unix_millis: u64,
959    pub acknowledgement: Value,
960}
961
962#[derive(Clone)]
963pub struct Worker {
964    client: Client,
965    worker_id: String,
966    task_queue: String,
967    workflows: HashMap<String, WorkflowHandler>,
968    activities: HashMap<String, ActivityHandler>,
969    max_concurrent_workflow_tasks: usize,
970    max_concurrent_activity_tasks: usize,
971    poll_timeout: Duration,
972    heartbeat_interval: Duration,
973    heartbeat_observer: Option<WorkerHeartbeatObserver>,
974}
975
976impl Worker {
977    pub fn new(client: Client, task_queue: impl Into<String>) -> Self {
978        Self {
979            client,
980            worker_id: default_worker_id(),
981            task_queue: task_queue.into(),
982            workflows: HashMap::new(),
983            activities: HashMap::new(),
984            max_concurrent_workflow_tasks: 10,
985            max_concurrent_activity_tasks: 10,
986            poll_timeout: Duration::from_secs(30),
987            heartbeat_interval: Duration::from_secs(60),
988            heartbeat_observer: None,
989        }
990    }
991
992    pub fn worker_id(mut self, worker_id: impl Into<String>) -> Self {
993        self.worker_id = worker_id.into();
994        self
995    }
996
997    pub fn poll_timeout(mut self, timeout: Duration) -> Self {
998        self.poll_timeout = timeout;
999        self
1000    }
1001
1002    pub fn heartbeat_interval(mut self, interval: Duration) -> Self {
1003        self.heartbeat_interval = interval;
1004        self
1005    }
1006
1007    pub fn on_worker_heartbeat<F>(mut self, observer: F) -> Self
1008    where
1009        F: Fn(&WorkerHeartbeatObservation) + Send + Sync + 'static,
1010    {
1011        self.heartbeat_observer = Some(Arc::new(observer));
1012        self
1013    }
1014
1015    pub fn max_concurrent_workflow_tasks(mut self, count: usize) -> Self {
1016        self.max_concurrent_workflow_tasks = count.max(1);
1017        self
1018    }
1019
1020    pub fn max_concurrent_activity_tasks(mut self, count: usize) -> Self {
1021        self.max_concurrent_activity_tasks = count.max(1);
1022        self
1023    }
1024
1025    pub fn register_workflow<F, Fut>(&mut self, workflow_type: impl Into<String>, handler: F)
1026    where
1027        F: Fn(WorkflowContext, Value) -> Fut + Send + Sync + 'static,
1028        Fut: Future<Output = Result<Value>> + Send + 'static,
1029    {
1030        self.workflows.insert(
1031            workflow_type.into(),
1032            Arc::new(move |ctx, input| Box::pin(handler(ctx, input))),
1033        );
1034    }
1035
1036    pub fn register_activity<F, Fut>(&mut self, activity_type: impl Into<String>, handler: F)
1037    where
1038        F: Fn(ActivityContext, Value) -> Fut + Send + Sync + 'static,
1039        Fut: Future<Output = Result<Value>> + Send + 'static,
1040    {
1041        self.activities.insert(
1042            activity_type.into(),
1043            Arc::new(move |ctx, args| Box::pin(handler(ctx, args))),
1044        );
1045    }
1046
1047    pub async fn register(&self) -> Result<RegisterWorkerResponse> {
1048        self.client
1049            .register_worker(
1050                &self.worker_id,
1051                &self.task_queue,
1052                self.workflows.keys().cloned().collect(),
1053                self.activities.keys().cloned().collect(),
1054                self.max_concurrent_workflow_tasks,
1055                self.max_concurrent_activity_tasks,
1056            )
1057            .await
1058    }
1059
1060    pub async fn run(&self) -> Result<()> {
1061        self.run_until(std::future::pending::<()>()).await
1062    }
1063
1064    pub async fn run_until<F>(&self, shutdown: F) -> Result<()>
1065    where
1066        F: Future<Output = ()>,
1067    {
1068        let registration = self.register().await?;
1069        let mut heartbeat = tokio::time::interval(Duration::from_secs(
1070            registration
1071                .heartbeat_interval_seconds
1072                .unwrap_or(self.heartbeat_interval.as_secs().max(1)),
1073        ));
1074        tokio::pin!(shutdown);
1075        let stop = Arc::new(AtomicBool::new(false));
1076        // Poll responses may already have leased server-side work by the time
1077        // they become ready, so each poller owns its responses through
1078        // completion or failure instead of racing raw polls in this select.
1079        let mut workflow_poller = (!self.workflows.is_empty()).then(|| {
1080            let worker = self.clone();
1081            let stop = Arc::clone(&stop);
1082            tokio::spawn(async move { worker.poll_workflows_until_stopped(stop).await })
1083        });
1084        let mut activity_poller = (!self.activities.is_empty()).then(|| {
1085            let worker = self.clone();
1086            let stop = Arc::clone(&stop);
1087            tokio::spawn(async move { worker.poll_activities_until_stopped(stop).await })
1088        });
1089
1090        loop {
1091            tokio::select! {
1092                _ = &mut shutdown => {
1093                    stop.store(true, Ordering::SeqCst);
1094                    break;
1095                }
1096                _ = heartbeat.tick() => {
1097                    match self.client
1098                        .heartbeat_worker(
1099                            &self.worker_id,
1100                            self.max_concurrent_workflow_tasks,
1101                            self.max_concurrent_activity_tasks,
1102                        )
1103                        .await
1104                    {
1105                        Ok(acknowledgement) => {
1106                            if let Some(observer) = &self.heartbeat_observer {
1107                                observer(&WorkerHeartbeatObservation {
1108                                    worker_id: self.worker_id.clone(),
1109                                    task_queue: self.task_queue.clone(),
1110                                    acknowledged_at_unix_millis: SystemTime::now()
1111                                        .duration_since(UNIX_EPOCH)
1112                                        .unwrap_or_default()
1113                                        .as_millis()
1114                                        .min(u64::MAX as u128)
1115                                        as u64,
1116                                    acknowledgement,
1117                                });
1118                            }
1119                        }
1120                        Err(error) => {
1121                            stop.store(true, Ordering::SeqCst);
1122                            join_pollers(workflow_poller.take(), activity_poller.take()).await?;
1123                            return Err(error);
1124                        }
1125                    }
1126                }
1127                result = OptionFuture::from(workflow_poller.as_mut()), if workflow_poller.is_some() => {
1128                    workflow_poller = None;
1129                    stop.store(true, Ordering::SeqCst);
1130                    let poller_result = optional_poller_result("workflow", result);
1131                    let join_result =
1132                        join_pollers(workflow_poller.take(), activity_poller.take()).await;
1133                    poller_result?;
1134                    join_result?;
1135                    return Err(Error::WorkerLoop(
1136                        "workflow poller stopped unexpectedly".to_string(),
1137                    ));
1138                }
1139                result = OptionFuture::from(activity_poller.as_mut()), if activity_poller.is_some() => {
1140                    activity_poller = None;
1141                    stop.store(true, Ordering::SeqCst);
1142                    let poller_result = optional_poller_result("activity", result);
1143                    let join_result =
1144                        join_pollers(workflow_poller.take(), activity_poller.take()).await;
1145                    poller_result?;
1146                    join_result?;
1147                    return Err(Error::WorkerLoop(
1148                        "activity poller stopped unexpectedly".to_string(),
1149                    ));
1150                }
1151            }
1152        }
1153
1154        join_pollers(workflow_poller.take(), activity_poller.take()).await
1155    }
1156
1157    pub async fn run_once(&self) -> Result<usize> {
1158        let mut handled = 0;
1159        if self.poll_workflow_once().await? {
1160            handled += 1;
1161        }
1162        if self.poll_activity_once().await? {
1163            handled += 1;
1164        }
1165        Ok(handled)
1166    }
1167
1168    async fn poll_workflow_once(&self) -> Result<bool> {
1169        let Some(task) = self
1170            .client
1171            .poll_workflow_task(&self.worker_id, &self.task_queue, self.poll_timeout)
1172            .await?
1173        else {
1174            return Ok(false);
1175        };
1176
1177        let task_id = task.task_id.clone();
1178        let attempt = task.workflow_task_attempt;
1179        let lease_owner = task
1180            .lease_owner
1181            .clone()
1182            .unwrap_or_else(|| self.worker_id.clone());
1183
1184        match self.execute_workflow_task(task) {
1185            Ok(commands) => {
1186                self.client
1187                    .complete_workflow_task(&task_id, &lease_owner, attempt, commands)
1188                    .await?;
1189            }
1190            Err(error) => {
1191                self.client
1192                    .fail_workflow_task(&task_id, &lease_owner, attempt, error.to_string())
1193                    .await?;
1194            }
1195        }
1196
1197        Ok(true)
1198    }
1199
1200    async fn poll_workflows_until_stopped(self, stop: Arc<AtomicBool>) -> Result<()> {
1201        while !stop.load(Ordering::SeqCst) {
1202            self.poll_workflow_once().await?;
1203        }
1204
1205        Ok(())
1206    }
1207
1208    async fn poll_activity_once(&self) -> Result<bool> {
1209        let Some(task) = self
1210            .client
1211            .poll_activity_task(&self.worker_id, &self.task_queue, self.poll_timeout)
1212            .await?
1213        else {
1214            return Ok(false);
1215        };
1216
1217        let task_id = task.task_id.clone();
1218        let attempt_id = task
1219            .activity_attempt_id
1220            .clone()
1221            .or(task.attempt_id.clone())
1222            .unwrap_or_default();
1223        let lease_owner = task
1224            .lease_owner
1225            .clone()
1226            .unwrap_or_else(|| self.worker_id.clone());
1227        let codec = task.payload_codec.clone();
1228        let result = self.execute_activity_task(task).await;
1229        match result {
1230            Ok(value) => {
1231                self.client
1232                    .complete_activity_task(&task_id, &attempt_id, &lease_owner, value, &codec)
1233                    .await?;
1234            }
1235            Err(error) => {
1236                self.client
1237                    .fail_activity_task(
1238                        &task_id,
1239                        &attempt_id,
1240                        &lease_owner,
1241                        error.to_string(),
1242                        false,
1243                    )
1244                    .await?;
1245            }
1246        }
1247
1248        Ok(true)
1249    }
1250
1251    async fn poll_activities_until_stopped(self, stop: Arc<AtomicBool>) -> Result<()> {
1252        while !stop.load(Ordering::SeqCst) {
1253            self.poll_activity_once().await?;
1254        }
1255
1256        Ok(())
1257    }
1258
1259    fn execute_workflow_task(&self, task: WorkflowTask) -> Result<Vec<Value>> {
1260        let handler = self
1261            .workflows
1262            .get(&task.workflow_type)
1263            .ok_or_else(|| Error::WorkflowNotRegistered(task.workflow_type.clone()))?;
1264        let input = decode_task_arguments(task.arguments.as_ref(), &task.payload_codec)?;
1265        let resume_signal = decode_resume_signal(&task)?;
1266        let state = Arc::new(Mutex::new(WorkflowState {
1267            history: task.history_events,
1268            task_queue: self.task_queue.clone(),
1269            payload_codec: task.payload_codec.clone(),
1270            resume_signal,
1271            activity_cursor: 0,
1272            signal_cursors: HashMap::new(),
1273            commands: Vec::new(),
1274        }));
1275        let ctx = WorkflowContext { state };
1276        let mut future = handler(ctx.clone(), input);
1277        let mut cx = TaskContext::from_waker(noop_waker_ref());
1278
1279        match future.as_mut().poll(&mut cx) {
1280            Poll::Ready(Ok(result)) => {
1281                let result = encode_value_envelope(&result, &task.payload_codec)?;
1282                Ok(vec![json!({
1283                    "type": "complete_workflow",
1284                    "result": result
1285                })])
1286            }
1287            Poll::Ready(Err(error)) => Err(error),
1288            Poll::Pending => {
1289                let commands = ctx.take_commands()?;
1290                if commands.is_empty() {
1291                    Err(Error::WorkflowYieldedWithoutCommand)
1292                } else {
1293                    Ok(commands)
1294                }
1295            }
1296        }
1297    }
1298
1299    async fn execute_activity_task(&self, task: ActivityTask) -> Result<Value> {
1300        let handler = self
1301            .activities
1302            .get(&task.activity_type)
1303            .ok_or_else(|| Error::ActivityNotRegistered(task.activity_type.clone()))?;
1304        let args = decode_task_arguments(task.arguments.as_ref(), &task.payload_codec)?;
1305        let attempt_id = task
1306            .activity_attempt_id
1307            .clone()
1308            .or(task.attempt_id.clone())
1309            .unwrap_or_default();
1310        let lease_owner = task
1311            .lease_owner
1312            .clone()
1313            .unwrap_or_else(|| self.worker_id.clone());
1314        let ctx = ActivityContext {
1315            client: self.client.clone(),
1316            task_id: task.task_id,
1317            activity_attempt_id: attempt_id,
1318            lease_owner,
1319            activity_type: task.activity_type,
1320            attempt_number: task.attempt_number,
1321            task_queue: self.task_queue.clone(),
1322            worker_id: self.worker_id.clone(),
1323        };
1324
1325        handler(ctx, args).await
1326    }
1327}
1328
1329fn poller_result(
1330    kind: &str,
1331    result: std::result::Result<Result<()>, tokio::task::JoinError>,
1332) -> Result<()> {
1333    match result {
1334        Ok(result) => result,
1335        Err(error) => Err(Error::WorkerLoop(format!(
1336            "{kind} poller join error: {error}"
1337        ))),
1338    }
1339}
1340
1341fn optional_poller_result(
1342    kind: &str,
1343    result: Option<std::result::Result<Result<()>, tokio::task::JoinError>>,
1344) -> Result<()> {
1345    match result {
1346        Some(result) => poller_result(kind, result),
1347        None => Ok(()),
1348    }
1349}
1350
1351async fn join_pollers(
1352    workflow_poller: Option<tokio::task::JoinHandle<Result<()>>>,
1353    activity_poller: Option<tokio::task::JoinHandle<Result<()>>>,
1354) -> Result<()> {
1355    let mut first_error = None;
1356
1357    if let Some(handle) = workflow_poller {
1358        if let Err(error) = poller_result("workflow", handle.await) {
1359            first_error.get_or_insert(error);
1360        }
1361    }
1362
1363    if let Some(handle) = activity_poller {
1364        if let Err(error) = poller_result("activity", handle.await) {
1365            first_error.get_or_insert(error);
1366        }
1367    }
1368
1369    if let Some(error) = first_error {
1370        Err(error)
1371    } else {
1372        Ok(())
1373    }
1374}
1375
1376fn default_worker_id() -> String {
1377    let millis = SystemTime::now()
1378        .duration_since(UNIX_EPOCH)
1379        .unwrap_or_default()
1380        .as_millis();
1381    format!("rust-worker-{}-{millis}", std::process::id())
1382}
1383
1384#[derive(Clone, Debug)]
1385pub struct WorkflowContext {
1386    state: Arc<Mutex<WorkflowState>>,
1387}
1388
1389impl WorkflowContext {
1390    pub fn activity<T: Serialize>(
1391        &self,
1392        activity_type: impl Into<String>,
1393        args: T,
1394    ) -> ActivityCall {
1395        self.activity_on_queue(activity_type, None::<String>, args)
1396    }
1397
1398    pub fn activity_on_queue<T, Q>(
1399        &self,
1400        activity_type: impl Into<String>,
1401        task_queue: Option<Q>,
1402        args: T,
1403    ) -> ActivityCall
1404    where
1405        T: Serialize,
1406        Q: Into<String>,
1407    {
1408        ActivityCall {
1409            ctx: self.clone(),
1410            activity_type: activity_type.into(),
1411            task_queue: task_queue.map(Into::into),
1412            args: Some(serde_json::to_value(args).map_err(Error::from)),
1413            scheduled: false,
1414        }
1415    }
1416
1417    pub fn wait_signal(&self, signal_name: impl Into<String>) -> SignalCall {
1418        SignalCall {
1419            ctx: self.clone(),
1420            signal_name: signal_name.into(),
1421            opened_wait: false,
1422        }
1423    }
1424
1425    fn take_commands(&self) -> Result<Vec<Value>> {
1426        let mut state = self
1427            .state
1428            .lock()
1429            .map_err(|_| Error::WorkflowStatePoisoned)?;
1430        Ok(std::mem::take(&mut state.commands))
1431    }
1432}
1433
1434#[derive(Debug)]
1435struct WorkflowState {
1436    history: Vec<HistoryEvent>,
1437    task_queue: String,
1438    payload_codec: String,
1439    resume_signal: Option<ResumeSignal>,
1440    activity_cursor: usize,
1441    signal_cursors: HashMap<String, usize>,
1442    commands: Vec<Value>,
1443}
1444
1445#[derive(Clone, Debug)]
1446struct ResumeSignal {
1447    signal_id: Option<String>,
1448    signal_name: String,
1449    arguments: Vec<Value>,
1450}
1451
1452pub struct ActivityCall {
1453    ctx: WorkflowContext,
1454    activity_type: String,
1455    task_queue: Option<String>,
1456    args: Option<Result<Value>>,
1457    scheduled: bool,
1458}
1459
1460impl Future for ActivityCall {
1461    type Output = Result<Value>;
1462
1463    fn poll(mut self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
1464        let ctx = self.ctx.clone();
1465        let mut state = match ctx.state.lock() {
1466            Ok(state) => state,
1467            Err(_) => return Poll::Ready(Err(Error::WorkflowStatePoisoned)),
1468        };
1469
1470        let completed = match completed_activity_results(&state.history, &state.payload_codec) {
1471            Ok(completed) => completed,
1472            Err(error) => return Poll::Ready(Err(error)),
1473        };
1474
1475        if state.activity_cursor < completed.len() {
1476            let value = completed[state.activity_cursor].clone();
1477            state.activity_cursor += 1;
1478            return Poll::Ready(Ok(value));
1479        }
1480
1481        if !self.scheduled {
1482            let task_queue = self
1483                .task_queue
1484                .clone()
1485                .unwrap_or_else(|| state.task_queue.clone());
1486            let args = match self.args.take().unwrap_or(Ok(Value::Null)) {
1487                Ok(args) => args,
1488                Err(error) => return Poll::Ready(Err(error)),
1489            };
1490            let arguments = normalize_arguments(args);
1491            let envelope = match encode_value_envelope(&arguments, &state.payload_codec) {
1492                Ok(envelope) => envelope,
1493                Err(error) => return Poll::Ready(Err(error)),
1494            };
1495
1496            state.commands.push(json!({
1497                "type": "schedule_activity",
1498                "activity_type": self.activity_type.clone(),
1499                "queue": task_queue,
1500                "arguments": envelope
1501            }));
1502            self.scheduled = true;
1503        }
1504
1505        Poll::Pending
1506    }
1507}
1508
1509pub struct SignalCall {
1510    ctx: WorkflowContext,
1511    signal_name: String,
1512    opened_wait: bool,
1513}
1514
1515impl Future for SignalCall {
1516    type Output = Result<Vec<Value>>;
1517
1518    fn poll(mut self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
1519        let ctx = self.ctx.clone();
1520        let mut state = match ctx.state.lock() {
1521            Ok(state) => state,
1522            Err(_) => return Poll::Ready(Err(Error::WorkflowStatePoisoned)),
1523        };
1524
1525        let signals = match signal_values(
1526            &state.history,
1527            &self.signal_name,
1528            &state.payload_codec,
1529            state.resume_signal.as_ref(),
1530        ) {
1531            Ok(signals) => signals,
1532            Err(error) => return Poll::Ready(Err(error)),
1533        };
1534        let cursor = *state.signal_cursors.get(&self.signal_name).unwrap_or(&0);
1535
1536        if cursor < signals.len() {
1537            state
1538                .signal_cursors
1539                .insert(self.signal_name.clone(), cursor + 1);
1540            return Poll::Ready(Ok(signals[cursor].clone()));
1541        }
1542
1543        if !self.opened_wait {
1544            state.commands.push(json!({
1545                "type": "open_condition_wait",
1546                "condition_key": format!("signal:{}", self.signal_name)
1547            }));
1548            self.opened_wait = true;
1549        }
1550
1551        Poll::Pending
1552    }
1553}
1554
1555#[derive(Clone, Debug)]
1556pub struct ActivityContext {
1557    client: Client,
1558    pub task_id: String,
1559    pub activity_attempt_id: String,
1560    pub lease_owner: String,
1561    pub activity_type: String,
1562    pub attempt_number: u64,
1563    pub task_queue: String,
1564    pub worker_id: String,
1565}
1566
1567impl ActivityContext {
1568    pub async fn heartbeat<T: Serialize>(&self, details: T) -> Result<ActivityHeartbeatResponse> {
1569        self.client
1570            .heartbeat_activity_task(
1571                &self.task_id,
1572                &self.activity_attempt_id,
1573                &self.lease_owner,
1574                serde_json::to_value(details)?,
1575            )
1576            .await
1577    }
1578}
1579
1580fn decode_task_arguments(value: Option<&Value>, codec: &str) -> Result<Value> {
1581    match value {
1582        Some(value) => Ok(normalize_arguments(decode_wire_value(value, codec)?)),
1583        None => Ok(Value::Array(Vec::new())),
1584    }
1585}
1586
1587fn decode_resume_signal(task: &WorkflowTask) -> Result<Option<ResumeSignal>> {
1588    let Some(signal_name) = task
1589        .signal_name
1590        .as_deref()
1591        .filter(|value| !value.is_empty())
1592    else {
1593        return Ok(None);
1594    };
1595    let Some(arguments) = task.signal_arguments.as_ref() else {
1596        return Ok(None);
1597    };
1598
1599    let decoded = normalize_arguments(decode_wire_value(arguments, &task.payload_codec)?);
1600    let Value::Array(arguments) = decoded else {
1601        unreachable!("normalize_arguments always returns an array");
1602    };
1603
1604    Ok(Some(ResumeSignal {
1605        signal_id: task.workflow_signal_id.clone(),
1606        signal_name: signal_name.to_string(),
1607        arguments,
1608    }))
1609}
1610
1611fn normalize_arguments(value: Value) -> Value {
1612    match value {
1613        Value::Null => Value::Array(Vec::new()),
1614        Value::Array(_) => value,
1615        other => Value::Array(vec![other]),
1616    }
1617}
1618
1619fn completed_activity_results(events: &[HistoryEvent], fallback_codec: &str) -> Result<Vec<Value>> {
1620    let mut results = Vec::new();
1621
1622    for event in events {
1623        if event.event_type != "ActivityCompleted" {
1624            continue;
1625        }
1626
1627        let codec = event
1628            .payload
1629            .get("payload_codec")
1630            .and_then(Value::as_str)
1631            .unwrap_or(fallback_codec);
1632        let result = event.payload.get("result").unwrap_or(&Value::Null);
1633        results.push(decode_wire_value(result, codec)?);
1634    }
1635
1636    Ok(results)
1637}
1638
1639fn signal_values(
1640    events: &[HistoryEvent],
1641    signal_name: &str,
1642    fallback_codec: &str,
1643    resume_signal: Option<&ResumeSignal>,
1644) -> Result<Vec<Vec<Value>>> {
1645    let mut signals = Vec::new();
1646
1647    for event in events {
1648        if event.event_type != "SignalApplied" && event.event_type != "SignalReceived" {
1649            continue;
1650        }
1651
1652        if event.payload.get("signal_name").and_then(Value::as_str) != Some(signal_name) {
1653            continue;
1654        }
1655
1656        let codec = event
1657            .payload
1658            .get("payload_codec")
1659            .and_then(Value::as_str)
1660            .unwrap_or(fallback_codec);
1661        let raw = event
1662            .payload
1663            .get("value")
1664            .or_else(|| event.payload.get("input"))
1665            .or_else(|| event.payload.get("arguments"));
1666        let decoded = match raw.filter(|value| !value.is_null()) {
1667            Some(value) => decode_wire_value(value, codec)?,
1668            None => resume_signal
1669                .filter(|signal| resume_signal_matches_event(signal, event, signal_name))
1670                .map(|signal| Value::Array(signal.arguments.clone()))
1671                .unwrap_or_else(|| Value::Array(Vec::new())),
1672        };
1673        let args = match normalize_arguments(decoded) {
1674            Value::Array(values) => values,
1675            _ => unreachable!("normalize_arguments always returns an array"),
1676        };
1677        signals.push(args);
1678    }
1679
1680    Ok(signals)
1681}
1682
1683fn resume_signal_matches_event(
1684    resume_signal: &ResumeSignal,
1685    event: &HistoryEvent,
1686    signal_name: &str,
1687) -> bool {
1688    if resume_signal.signal_name != signal_name {
1689        return false;
1690    }
1691
1692    match (
1693        resume_signal.signal_id.as_deref(),
1694        event.payload.get("signal_id").and_then(Value::as_str),
1695    ) {
1696        (Some(resume_id), Some(event_id)) => resume_id == event_id,
1697        _ => true,
1698    }
1699}
1700
1701#[cfg(test)]
1702mod tests {
1703    use super::*;
1704    use std::{
1705        io::{Read, Write},
1706        net::{SocketAddr, TcpListener, TcpStream},
1707        thread,
1708    };
1709
1710    #[test]
1711    fn avro_generic_wrapper_round_trips_json_values() {
1712        let value = json!({"greeting": "hello", "count": 3, "ok": true});
1713        let envelope = PayloadEnvelope::avro(&value).expect("encode");
1714        assert_eq!(envelope.codec, DEFAULT_CODEC);
1715        assert_eq!(decode_payload::<Value>(&envelope).expect("decode"), value);
1716    }
1717
1718    #[test]
1719    fn json_codec_remains_plain_json() {
1720        let value = json!({"greeting": "hello", "count": 3, "ok": true});
1721        let envelope = PayloadEnvelope::json(&value).expect("encode");
1722
1723        assert_eq!(envelope.codec, JSON_CODEC);
1724        assert_eq!(envelope.blob, serde_json::to_string(&value).expect("json"));
1725        assert_eq!(decode_payload::<Value>(&envelope).expect("decode"), value);
1726    }
1727
1728    #[test]
1729    fn typed_avro_payload_without_schema_context_keeps_diagnostic() {
1730        let envelope = PayloadEnvelope {
1731            codec: DEFAULT_CODEC.to_string(),
1732            blob: BASE64.encode([0x01]),
1733        };
1734
1735        let error = decode_payload::<Value>(&envelope).expect_err("typed payload must fail");
1736        assert_eq!(
1737            error.to_string(),
1738            "codec error: typed avro payloads require a schema context; v1 supports the generic wrapper"
1739        );
1740    }
1741
1742    #[test]
1743    fn workflow_context_schedules_activity_until_completion_is_in_history() {
1744        let ctx = WorkflowContext {
1745            state: Arc::new(Mutex::new(WorkflowState {
1746                history: Vec::new(),
1747                task_queue: "rust-workers".to_string(),
1748                payload_codec: DEFAULT_CODEC.to_string(),
1749                resume_signal: None,
1750                activity_cursor: 0,
1751                signal_cursors: HashMap::new(),
1752                commands: Vec::new(),
1753            })),
1754        };
1755
1756        let mut call = Box::pin(ctx.activity("hello.activity", json!(["Ada"])));
1757        let mut task_context = TaskContext::from_waker(noop_waker_ref());
1758        assert!(matches!(
1759            call.as_mut().poll(&mut task_context),
1760            Poll::Pending
1761        ));
1762
1763        let commands = ctx.take_commands().expect("commands");
1764        assert_eq!(commands[0]["type"], "schedule_activity");
1765        assert_eq!(commands[0]["activity_type"], "hello.activity");
1766    }
1767
1768    #[test]
1769    fn rust_hello_world_uses_signal_arguments_from_resume_payload() {
1770        let client = Client::new("http://127.0.0.1:8080").expect("client");
1771        let mut worker = Worker::new(client, "rust-workers");
1772
1773        worker.register_workflow("rust.hello_workflow", |ctx, _input| async move {
1774            let signal = ctx.wait_signal("start").await?;
1775            let name = signal
1776                .first()
1777                .and_then(|value| value.as_str())
1778                .unwrap_or("world");
1779            let greeting = ctx.activity("rust.hello_activity", json!([name])).await?;
1780            Ok(json!({
1781                "greeting": greeting,
1782                "language": "rust"
1783            }))
1784        });
1785
1786        let signal_arguments =
1787            encode_value_envelope(&json!(["Rust"]), DEFAULT_CODEC).expect("signal arguments");
1788        let task = WorkflowTask {
1789            task_id: "wft-rust-signal-1".to_string(),
1790            workflow_id: Some("wf-rust-hello".to_string()),
1791            run_id: Some("run-rust-hello".to_string()),
1792            workflow_type: "rust.hello_workflow".to_string(),
1793            payload_codec: DEFAULT_CODEC.to_string(),
1794            arguments: Some(encode_value_envelope(&json!([]), DEFAULT_CODEC).expect("input")),
1795            history_events: vec![HistoryEvent {
1796                event_type: "SignalReceived".to_string(),
1797                payload: json!({
1798                    "signal_id": "sig-rust-1",
1799                    "signal_name": "start"
1800                }),
1801                raw: HashMap::new(),
1802            }],
1803            total_history_events: Some(1),
1804            next_history_page_token: None,
1805            workflow_task_attempt: 1,
1806            workflow_signal_id: Some("sig-rust-1".to_string()),
1807            signal_name: Some("start".to_string()),
1808            signal_arguments: Some(signal_arguments),
1809            lease_owner: Some("rust-worker".to_string()),
1810        };
1811
1812        let commands = worker.execute_workflow_task(task).expect("workflow task");
1813
1814        assert_eq!(commands.len(), 1);
1815        assert_eq!(commands[0]["type"], "schedule_activity");
1816        assert_eq!(commands[0]["activity_type"], "rust.hello_activity");
1817        assert_eq!(
1818            decode_wire_value(&commands[0]["arguments"], DEFAULT_CODEC).expect("activity args"),
1819            json!(["Rust"])
1820        );
1821    }
1822
1823    #[test]
1824    fn workflow_task_appends_paginated_history_events() {
1825        let mut task = WorkflowTask {
1826            task_id: "wft-rust-pages-1".to_string(),
1827            workflow_id: Some("wf-rust-pages".to_string()),
1828            run_id: Some("run-rust-pages".to_string()),
1829            workflow_type: "rust.hello_workflow".to_string(),
1830            payload_codec: DEFAULT_CODEC.to_string(),
1831            arguments: Some(encode_value_envelope(&json!([]), DEFAULT_CODEC).expect("input")),
1832            history_events: vec![HistoryEvent {
1833                event_type: "WorkflowStarted".to_string(),
1834                payload: json!({}),
1835                raw: HashMap::new(),
1836            }],
1837            total_history_events: Some(3),
1838            next_history_page_token: Some("MQ==".to_string()),
1839            workflow_task_attempt: 1,
1840            workflow_signal_id: None,
1841            signal_name: None,
1842            signal_arguments: None,
1843            lease_owner: Some("rust-worker".to_string()),
1844        };
1845
1846        task.append_history_page(WorkflowTaskHistoryPage {
1847            history_events: vec![
1848                HistoryEvent {
1849                    event_type: "SignalReceived".to_string(),
1850                    payload: json!({
1851                        "signal_id": "sig-rust-1",
1852                        "signal_name": "start",
1853                        "arguments": encode_value_envelope(&json!(["Rust"]), DEFAULT_CODEC)
1854                            .expect("signal arguments")
1855                    }),
1856                    raw: HashMap::new(),
1857                },
1858                HistoryEvent {
1859                    event_type: "MarkerRecorded".to_string(),
1860                    payload: json!({"sequence": 3}),
1861                    raw: HashMap::new(),
1862                },
1863            ],
1864            total_history_events: Some(3),
1865            next_history_page_token: None,
1866        });
1867
1868        assert_eq!(task.history_events.len(), 3);
1869        assert_eq!(task.total_history_events, Some(3));
1870        assert_eq!(task.next_history_page_token, None);
1871
1872        let signals =
1873            signal_values(&task.history_events, "start", DEFAULT_CODEC, None).expect("signals");
1874        assert_eq!(signals, vec![vec![json!("Rust")]]);
1875    }
1876
1877    #[tokio::test]
1878    async fn activity_only_worker_can_shutdown_without_workflow_poller() {
1879        let server = MockWorkerServer::start();
1880        let client = Client::builder(server.base_url())
1881            .timeout(Duration::from_secs(2))
1882            .build()
1883            .expect("client");
1884        let mut worker = Worker::new(client, "rust-workers")
1885            .worker_id("activity-only-worker")
1886            .poll_timeout(Duration::from_millis(10));
1887
1888        worker.register_activity(
1889            "activity.only",
1890            |_ctx, _args| async move { Ok(Value::Null) },
1891        );
1892
1893        worker.run_until(async {}).await.expect("run worker");
1894    }
1895
1896    #[tokio::test]
1897    async fn workflow_only_worker_can_shutdown_without_activity_poller() {
1898        let server = MockWorkerServer::start();
1899        let client = Client::builder(server.base_url())
1900            .timeout(Duration::from_secs(2))
1901            .build()
1902            .expect("client");
1903        let mut worker = Worker::new(client, "rust-workers")
1904            .worker_id("workflow-only-worker")
1905            .poll_timeout(Duration::from_millis(10));
1906
1907        worker.register_workflow(
1908            "workflow.only",
1909            |_ctx, _input| async move { Ok(Value::Null) },
1910        );
1911
1912        worker.run_until(async {}).await.expect("run worker");
1913    }
1914
1915    #[tokio::test]
1916    async fn worker_heartbeat_observer_receives_server_acknowledgements() {
1917        let server = MockWorkerServer::start();
1918        let client = Client::builder(server.base_url())
1919            .timeout(Duration::from_secs(2))
1920            .build()
1921            .expect("client");
1922        let observations = Arc::new(Mutex::new(Vec::new()));
1923        let observed = Arc::clone(&observations);
1924        let mut worker = Worker::new(client, "rust-workers")
1925            .worker_id("observed-heartbeat-worker")
1926            .poll_timeout(Duration::from_millis(10))
1927            .on_worker_heartbeat(move |observation| {
1928                observed
1929                    .lock()
1930                    .expect("heartbeat observations")
1931                    .push(observation.clone());
1932            });
1933
1934        worker.register_workflow("workflow.observed", |_ctx, _input| async move {
1935            Ok(Value::Null)
1936        });
1937        worker
1938            .run_until(tokio::time::sleep(Duration::from_millis(20)))
1939            .await
1940            .expect("run worker");
1941
1942        let observations = observations.lock().expect("heartbeat observations");
1943        let first = observations.first().expect("heartbeat acknowledgement");
1944        assert_eq!(first.worker_id, "observed-heartbeat-worker");
1945        assert_eq!(first.task_queue, "rust-workers");
1946        assert!(first.acknowledged_at_unix_millis > 0);
1947        assert_eq!(first.acknowledgement, json!({}));
1948    }
1949
1950    struct MockWorkerServer {
1951        addr: SocketAddr,
1952        stop: Arc<AtomicBool>,
1953        thread: Option<thread::JoinHandle<()>>,
1954    }
1955
1956    impl MockWorkerServer {
1957        fn start() -> Self {
1958            let listener = TcpListener::bind("127.0.0.1:0").expect("bind mock server");
1959            listener
1960                .set_nonblocking(true)
1961                .expect("configure mock listener");
1962            let addr = listener.local_addr().expect("mock server address");
1963            let stop = Arc::new(AtomicBool::new(false));
1964            let server_stop = Arc::clone(&stop);
1965            let thread = thread::spawn(move || {
1966                while !server_stop.load(Ordering::SeqCst) {
1967                    match listener.accept() {
1968                        Ok((mut stream, _)) => handle_mock_worker_request(&mut stream),
1969                        Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
1970                            thread::sleep(Duration::from_millis(5));
1971                        }
1972                        Err(_) => break,
1973                    }
1974                }
1975            });
1976
1977            Self {
1978                addr,
1979                stop,
1980                thread: Some(thread),
1981            }
1982        }
1983
1984        fn base_url(&self) -> String {
1985            format!("http://{}", self.addr)
1986        }
1987    }
1988
1989    impl Drop for MockWorkerServer {
1990        fn drop(&mut self) {
1991            self.stop.store(true, Ordering::SeqCst);
1992            let _ = TcpStream::connect(self.addr);
1993
1994            if let Some(thread) = self.thread.take() {
1995                thread.join().expect("join mock server");
1996            }
1997        }
1998    }
1999
2000    fn handle_mock_worker_request(stream: &mut TcpStream) {
2001        let _ = stream.set_read_timeout(Some(Duration::from_millis(200)));
2002        let mut buffer = [0_u8; 8192];
2003        let mut request = Vec::new();
2004
2005        loop {
2006            match stream.read(&mut buffer) {
2007                Ok(0) => break,
2008                Ok(read) => {
2009                    request.extend_from_slice(&buffer[..read]);
2010                    if request.windows(4).any(|window| window == b"\r\n\r\n") {
2011                        break;
2012                    }
2013                }
2014                Err(error)
2015                    if matches!(
2016                        error.kind(),
2017                        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
2018                    ) =>
2019                {
2020                    break;
2021                }
2022                Err(_) => return,
2023            }
2024        }
2025
2026        let request = String::from_utf8_lossy(&request);
2027        let path = request
2028            .lines()
2029            .next()
2030            .and_then(|line| line.split_whitespace().nth(1))
2031            .unwrap_or_default();
2032        let (status, body) = match path {
2033            "/api/worker/register" => (
2034                "200 OK",
2035                r#"{"worker_id":"mock-worker","registered":true,"heartbeat_interval_seconds":3600}"#,
2036            ),
2037            "/api/worker/heartbeat" => ("200 OK", "{}"),
2038            "/api/worker/activity-tasks/poll" | "/api/worker/workflow-tasks/poll" => {
2039                ("200 OK", r#"{"task":null}"#)
2040            }
2041            _ => ("404 Not Found", r#"{"message":"not found"}"#),
2042        };
2043        let response = format!(
2044            "HTTP/1.1 {status}\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}",
2045            body.len()
2046        );
2047
2048        let _ = stream.write_all(response.as_bytes());
2049        let _ = stream.flush();
2050    }
2051}