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