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