1use 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 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}