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