1use std::{
11 any::{Any, TypeId},
12 collections::{BTreeMap, HashMap},
13 future::Future,
14 pin::Pin,
15 sync::{
16 atomic::{AtomicBool, Ordering},
17 Arc, Mutex, OnceLock,
18 },
19 task::{Context as TaskContext, Poll},
20 time::{Duration, Instant, SystemTime, UNIX_EPOCH},
21};
22
23use apache_avro::{from_avro_datum, from_value, to_avro_datum, to_value, Schema};
24use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
25use futures_util::{future::OptionFuture, task::noop_waker_ref};
26use serde::{de::DeserializeOwned, Deserialize, Serialize};
27pub use serde_json::{json, Value};
28use thiserror::Error;
29
30pub const WORKER_PROTOCOL_VERSION: &str = "1.2";
31pub const CONTROL_PLANE_VERSION: &str = "2";
32pub const DEFAULT_CODEC: &str = "avro";
33pub const JSON_CODEC: &str = "json";
34pub const SDK_VERSION: &str = concat!("durable-workflow-rust/", env!("CARGO_PKG_VERSION"));
35pub const QUERY_TASKS_CAPABILITY: &str = "query_tasks";
37pub const QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION: &str = "1.8";
39
40const MAX_LONG_POLL_TIMEOUT_SECONDS: u64 = 60;
41
42const QUERY_TASK_FINAL_REJECTION_REASONS: &[&str] = &[
43 "lease_expired",
44 "query_task_not_found",
45 "query_task_not_leased",
46 "query_task_timed_out",
47];
48
49const AVRO_PAYLOAD_SCHEMA_JSON: &str = r#"{"type":"record","name":"Payload","namespace":"durable_workflow","fields":[{"name":"json","type":"string"},{"name":"version","type":"int","default":1}]}"#;
50const AVRO_PAYLOAD_VERSION: i32 = 1;
51
52static AVRO_PAYLOAD_SCHEMA: OnceLock<std::result::Result<Schema, String>> = OnceLock::new();
53
54#[derive(Clone, Copy)]
55enum RequestProtocol {
56 ControlPlane,
57 Worker(&'static str),
58}
59
60impl RequestProtocol {
61 fn is_worker(self) -> bool {
62 matches!(self, Self::Worker(_))
63 }
64}
65
66pub type Result<T> = std::result::Result<T, Error>;
67
68#[derive(Debug, Error)]
69pub enum Error {
70 #[error("transport error: {0}")]
71 Transport(#[from] reqwest::Error),
72 #[error("json error: {0}")]
73 Json(#[from] serde_json::Error),
74 #[error("http {status}: {body}")]
75 Http {
76 status: reqwest::StatusCode,
77 body: String,
78 },
79 #[error("codec error: {0}")]
80 Codec(String),
81 #[error(transparent)]
82 QueryFailed(QueryFailure),
83 #[error(transparent)]
84 Protocol(ProtocolFailure),
85 #[error(transparent)]
86 NonDeterministicReplay(ReplayFailure),
87 #[error("workflow handler {0:?} is not registered")]
88 WorkflowNotRegistered(String),
89 #[error("activity handler {0:?} is not registered")]
90 ActivityNotRegistered(String),
91 #[error("workflow future yielded without emitting a durable command")]
92 WorkflowYieldedWithoutCommand,
93 #[error("workflow state lock is poisoned")]
94 WorkflowStatePoisoned,
95 #[error("timer duration is too large for the worker protocol")]
96 TimerDurationOverflow,
97 #[error("operation timed out")]
98 Timeout,
99 #[error("worker loop error: {0}")]
100 WorkerLoop(String),
101}
102
103#[derive(Clone, Debug, Error)]
106#[error("non-deterministic workflow replay ({reason}) at sequence {sequence:?}: {message}")]
107pub struct ReplayFailure {
108 pub reason: String,
109 pub sequence: Option<u64>,
110 pub expected: Option<String>,
111 pub actual: Option<String>,
112 pub message: String,
113}
114
115impl ReplayFailure {
116 fn new(
117 reason: impl Into<String>,
118 sequence: Option<u64>,
119 expected: Option<String>,
120 actual: Option<String>,
121 message: impl Into<String>,
122 ) -> Self {
123 Self {
124 reason: reason.into(),
125 sequence,
126 expected,
127 actual,
128 message: message.into(),
129 }
130 }
131}
132
133#[derive(Clone, Debug, Error)]
135#[error("query failed ({reason}, HTTP {status}): {message}")]
136pub struct QueryFailure {
137 pub status: u16,
138 pub reason: String,
139 pub message: String,
140 pub body: Value,
141}
142
143#[derive(Clone, Debug, Error)]
145#[error("protocol rejected ({reason}, HTTP {status}): {message}")]
146pub struct ProtocolFailure {
147 pub status: u16,
148 pub reason: String,
149 pub message: String,
150 pub supported_version: Option<String>,
151 pub requested_version: Option<String>,
152 pub body: Value,
153}
154
155#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
156pub struct PayloadEnvelope {
157 pub codec: String,
158 pub blob: String,
159}
160
161impl PayloadEnvelope {
162 pub fn avro<T: Serialize>(value: &T) -> Result<Self> {
163 encode_payload(value, DEFAULT_CODEC)
164 }
165
166 pub fn json<T: Serialize>(value: &T) -> Result<Self> {
167 encode_payload(value, JSON_CODEC)
168 }
169}
170
171pub fn encode_payload<T: Serialize>(value: &T, codec: &str) -> Result<PayloadEnvelope> {
172 let value = serde_json::to_value(value)?;
173 let blob = encode_value_blob(&value, codec)?;
174
175 Ok(PayloadEnvelope {
176 codec: codec.to_string(),
177 blob,
178 })
179}
180
181pub fn decode_payload<T: DeserializeOwned>(envelope: &PayloadEnvelope) -> Result<T> {
182 let value = decode_blob(&envelope.blob, &envelope.codec)?;
183 Ok(serde_json::from_value(value)?)
184}
185
186fn encode_value_envelope(value: &Value, codec: &str) -> Result<Value> {
187 Ok(serde_json::to_value(encode_payload(value, codec)?)?)
188}
189
190fn encode_value_blob(value: &Value, codec: &str) -> Result<String> {
191 match codec {
192 JSON_CODEC => Ok(serde_json::to_string(value)?),
193 DEFAULT_CODEC => encode_avro_generic(value),
194 other => Err(Error::Codec(format!("unsupported payload codec {other:?}"))),
195 }
196}
197
198fn decode_wire_value(value: &Value, fallback_codec: &str) -> Result<Value> {
199 if value.is_null() {
200 return Ok(Value::Null);
201 }
202
203 if let Some(object) = value.as_object() {
204 if let (Some(codec), Some(blob)) = (
205 object.get("codec").and_then(Value::as_str),
206 object.get("blob").and_then(Value::as_str),
207 ) {
208 return decode_blob(blob, codec);
209 }
210 }
211
212 if let Some(blob) = value.as_str() {
213 return decode_blob(blob, fallback_codec);
214 }
215
216 Ok(value.clone())
217}
218
219fn decode_blob(blob: &str, codec: &str) -> Result<Value> {
220 match codec {
221 JSON_CODEC => Ok(serde_json::from_str(blob)?),
222 DEFAULT_CODEC => decode_avro_generic(blob),
223 other => Err(Error::Codec(format!("unsupported payload codec {other:?}"))),
224 }
225}
226
227fn encode_avro_generic(value: &Value) -> Result<String> {
228 let json = serde_json::to_string(value)?;
229 let datum = to_value(AvroPayload {
230 json,
231 version: AVRO_PAYLOAD_VERSION,
232 })
233 .map_err(|err| Error::Codec(format!("could not convert avro generic wrapper: {err}")))?;
234 let datum = to_avro_datum(avro_payload_schema()?, datum)
235 .map_err(|err| Error::Codec(format!("could not encode avro generic wrapper: {err}")))?;
236
237 let mut bytes = Vec::with_capacity(datum.len() + 1);
238 bytes.push(0x00);
239 bytes.extend_from_slice(&datum);
240 Ok(BASE64.encode(bytes))
241}
242
243fn decode_avro_generic(blob: &str) -> Result<Value> {
244 let bytes = BASE64
245 .decode(blob)
246 .map_err(|err| Error::Codec(format!("invalid avro base64 payload: {err}")))?;
247
248 if bytes.is_empty() {
249 return Err(Error::Codec("avro payload is empty".to_string()));
250 }
251
252 match bytes[0] {
253 0x00 => {}
254 0x01 => {
255 return Err(Error::Codec(
256 "typed avro payloads require a schema context; v1 supports the generic wrapper"
257 .to_string(),
258 ));
259 }
260 other => {
261 return Err(Error::Codec(format!(
262 "unknown avro payload prefix 0x{other:02x}"
263 )));
264 }
265 }
266
267 let mut datum = &bytes[1..];
268 let datum = from_avro_datum(avro_payload_schema()?, &mut datum, None)
269 .map_err(|err| Error::Codec(format!("could not decode avro generic wrapper: {err}")))?;
270 let payload: AvroPayload = from_value(&datum)
271 .map_err(|err| Error::Codec(format!("invalid avro generic wrapper record: {err}")))?;
272
273 if payload.version != AVRO_PAYLOAD_VERSION {
274 return Err(Error::Codec(format!(
275 "unsupported avro generic wrapper version {}",
276 payload.version
277 )));
278 }
279
280 Ok(serde_json::from_str(&payload.json)?)
281}
282
283#[derive(Debug, Serialize, Deserialize)]
284struct AvroPayload {
285 json: String,
286 version: i32,
287}
288
289fn avro_payload_schema() -> Result<&'static Schema> {
290 match AVRO_PAYLOAD_SCHEMA.get_or_init(|| {
291 Schema::parse_str(AVRO_PAYLOAD_SCHEMA_JSON)
292 .map_err(|err| format!("could not parse avro payload schema: {err}"))
293 }) {
294 Ok(schema) => Ok(schema),
295 Err(message) => Err(Error::Codec(message.clone())),
296 }
297}
298
299#[derive(Clone, Debug)]
300pub struct Client {
301 http: reqwest::Client,
302 base_url: String,
303 token: Option<String>,
304 control_token: Option<String>,
305 worker_token: Option<String>,
306 namespace: String,
307}
308
309impl Client {
310 pub fn new(base_url: impl Into<String>) -> Result<Self> {
311 Self::builder(base_url).build()
312 }
313
314 pub fn builder(base_url: impl Into<String>) -> ClientBuilder {
315 ClientBuilder {
316 base_url: base_url.into(),
317 token: None,
318 control_token: None,
319 worker_token: None,
320 namespace: "default".to_string(),
321 timeout: Duration::from_secs(60),
322 }
323 }
324
325 pub async fn health(&self) -> Result<Value> {
326 self.request_json(
327 reqwest::Method::GET,
328 "/health",
329 RequestProtocol::ControlPlane,
330 Option::<&Value>::None,
331 )
332 .await
333 }
334
335 pub async fn cluster_info(&self) -> Result<Value> {
336 self.request_json(
337 reqwest::Method::GET,
338 "/cluster/info",
339 RequestProtocol::ControlPlane,
340 Option::<&Value>::None,
341 )
342 .await
343 }
344
345 pub async fn start_workflow<T: Serialize>(
346 &self,
347 workflow_type: &str,
348 task_queue: &str,
349 workflow_id: &str,
350 input: T,
351 ) -> Result<WorkflowHandle> {
352 let input = serde_json::to_value(input)?;
353 let input_envelope = encode_value_envelope(&normalize_arguments(input), DEFAULT_CODEC)?;
354 let body = json!({
355 "workflow_id": workflow_id,
356 "workflow_type": workflow_type,
357 "task_queue": task_queue,
358 "input": input_envelope,
359 "execution_timeout_seconds": 3600,
360 "run_timeout_seconds": 600
361 });
362
363 let data: Value = self
364 .request_json(
365 reqwest::Method::POST,
366 "/workflows",
367 RequestProtocol::ControlPlane,
368 Some(&body),
369 )
370 .await?;
371
372 Ok(WorkflowHandle {
373 client: self.clone(),
374 workflow_id: data
375 .get("workflow_id")
376 .and_then(Value::as_str)
377 .unwrap_or(workflow_id)
378 .to_string(),
379 run_id: data
380 .get("run_id")
381 .and_then(Value::as_str)
382 .map(str::to_string),
383 workflow_type: data
384 .get("workflow_type")
385 .and_then(Value::as_str)
386 .unwrap_or(workflow_type)
387 .to_string(),
388 })
389 }
390
391 pub async fn signal_workflow<T: Serialize>(
392 &self,
393 workflow_id: &str,
394 signal_name: &str,
395 input: T,
396 ) -> Result<Value> {
397 let input = serde_json::to_value(input)?;
398 let input_envelope = encode_value_envelope(&normalize_arguments(input), DEFAULT_CODEC)?;
399 let body = json!({
400 "input": input_envelope
401 });
402 let path = format!("/workflows/{workflow_id}/signal/{signal_name}");
403 self.request_json(
404 reqwest::Method::POST,
405 &path,
406 RequestProtocol::ControlPlane,
407 Some(&body),
408 )
409 .await
410 }
411
412 pub async fn query_workflow<T: Serialize>(
418 &self,
419 workflow_id: &str,
420 query_name: &str,
421 input: T,
422 ) -> Result<Value> {
423 let input = serde_json::to_value(input)?;
424 let input_envelope = encode_value_envelope(&normalize_arguments(input), DEFAULT_CODEC)?;
425 let body = json!({
426 "input": input_envelope
427 });
428 let path = format!("/workflows/{workflow_id}/query/{query_name}");
429 let response: Value = match self
430 .request_json(
431 reqwest::Method::POST,
432 &path,
433 RequestProtocol::ControlPlane,
434 Some(&body),
435 )
436 .await
437 {
438 Ok(response) => response,
439 Err(Error::Http { status, body }) => {
440 return Err(Error::QueryFailed(query_failure(status, body)));
441 }
442 Err(error) => return Err(error),
443 };
444
445 if let Some(envelope) = response
446 .get("result_envelope")
447 .filter(|envelope| !envelope.is_null())
448 {
449 return decode_wire_value(envelope, DEFAULT_CODEC);
450 }
451
452 Ok(response.get("result").cloned().unwrap_or(Value::Null))
453 }
454
455 pub async fn describe_workflow(&self, workflow_id: &str) -> Result<WorkflowDescription> {
456 let path = format!("/workflows/{workflow_id}");
457 let mut data: WorkflowDescription = self
458 .request_json(
459 reqwest::Method::GET,
460 &path,
461 RequestProtocol::ControlPlane,
462 Option::<&Value>::None,
463 )
464 .await?;
465 data.decode_payloads()?;
466 Ok(data)
467 }
468
469 pub async fn register_worker(
470 &self,
471 worker_id: &str,
472 task_queue: &str,
473 supported_workflow_types: Vec<String>,
474 supported_activity_types: Vec<String>,
475 max_concurrent_workflow_tasks: usize,
476 max_concurrent_activity_tasks: usize,
477 ) -> Result<RegisterWorkerResponse> {
478 self.register_worker_with_capabilities(
479 worker_id,
480 task_queue,
481 supported_workflow_types,
482 supported_activity_types,
483 max_concurrent_workflow_tasks,
484 max_concurrent_activity_tasks,
485 Vec::new(),
486 )
487 .await
488 }
489
490 pub async fn register_worker_with_capabilities(
492 &self,
493 worker_id: &str,
494 task_queue: &str,
495 supported_workflow_types: Vec<String>,
496 supported_activity_types: Vec<String>,
497 max_concurrent_workflow_tasks: usize,
498 max_concurrent_activity_tasks: usize,
499 capabilities: Vec<String>,
500 ) -> Result<RegisterWorkerResponse> {
501 let body = json!({
502 "worker_id": worker_id,
503 "task_queue": task_queue,
504 "runtime": "rust",
505 "sdk_version": SDK_VERSION,
506 "supported_workflow_types": supported_workflow_types,
507 "supported_activity_types": supported_activity_types,
508 "capabilities": capabilities,
509 "max_concurrent_workflow_tasks": max_concurrent_workflow_tasks,
510 "max_concurrent_activity_tasks": max_concurrent_activity_tasks
511 });
512
513 self.request_json(
514 reqwest::Method::POST,
515 "/worker/register",
516 RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
517 Some(&body),
518 )
519 .await
520 }
521
522 pub async fn poll_query_task(
524 &self,
525 worker_id: &str,
526 task_queue: &str,
527 timeout: Duration,
528 ) -> Result<Option<QueryTask>> {
529 let timeout_seconds = long_poll_timeout_seconds(timeout);
530 let body = json!({
531 "worker_id": worker_id,
532 "task_queue": task_queue,
533 "poll_request_id": unique_request_id("rust-query-poll"),
534 "timeout_seconds": timeout_seconds,
535 });
536 let data: PollQueryTaskResponse = self
537 .request_json_with_timeout(
538 reqwest::Method::POST,
539 "/worker/query-tasks/poll",
540 RequestProtocol::Worker(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION),
541 Some(&body),
542 timeout + Duration::from_secs(5),
543 )
544 .await?;
545 Ok(data.task)
546 }
547
548 pub async fn complete_query_task(
550 &self,
551 query_task_id: &str,
552 lease_owner: &str,
553 query_task_attempt: u64,
554 result: Value,
555 codec: &str,
556 ) -> Result<Value> {
557 let result_envelope = encode_value_envelope(&result, codec)?;
558 self.complete_query_task_with_envelope(
559 query_task_id,
560 lease_owner,
561 query_task_attempt,
562 result,
563 result_envelope,
564 )
565 .await
566 }
567
568 async fn complete_query_task_with_envelope(
569 &self,
570 query_task_id: &str,
571 lease_owner: &str,
572 query_task_attempt: u64,
573 result: Value,
574 result_envelope: Value,
575 ) -> Result<Value> {
576 let body = json!({
577 "lease_owner": lease_owner,
578 "query_task_attempt": query_task_attempt,
579 "result": result,
580 "result_envelope": result_envelope,
581 });
582 let path = format!("/worker/query-tasks/{query_task_id}/complete");
583 let response = self
584 .request_json(
585 reqwest::Method::POST,
586 &path,
587 RequestProtocol::Worker(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION),
588 Some(&body),
589 )
590 .await;
591 query_task_response(response)
592 }
593
594 pub async fn fail_query_task(
596 &self,
597 query_task_id: &str,
598 lease_owner: &str,
599 query_task_attempt: u64,
600 message: impl Into<String>,
601 reason: impl Into<String>,
602 failure_type: impl Into<String>,
603 ) -> Result<Value> {
604 let body = json!({
605 "lease_owner": lease_owner,
606 "query_task_attempt": query_task_attempt,
607 "failure": {
608 "message": message.into(),
609 "reason": reason.into(),
610 "type": failure_type.into(),
611 }
612 });
613 let path = format!("/worker/query-tasks/{query_task_id}/fail");
614 let response = self
615 .request_json(
616 reqwest::Method::POST,
617 &path,
618 RequestProtocol::Worker(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION),
619 Some(&body),
620 )
621 .await;
622 query_task_response(response)
623 }
624
625 pub async fn heartbeat_worker(
626 &self,
627 worker_id: &str,
628 workflow_available: usize,
629 activity_available: usize,
630 ) -> Result<Value> {
631 let body = json!({
632 "worker_id": worker_id,
633 "task_slots": {
634 "workflow_available": workflow_available,
635 "activity_available": activity_available
636 },
637 "process_metrics": {
638 "process_id": std::process::id(),
639 "process_uptime_seconds": 0
640 }
641 });
642
643 self.request_json(
644 reqwest::Method::POST,
645 "/worker/heartbeat",
646 RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
647 Some(&body),
648 )
649 .await
650 }
651
652 pub async fn poll_workflow_task(
653 &self,
654 worker_id: &str,
655 task_queue: &str,
656 timeout: Duration,
657 ) -> Result<Option<WorkflowTask>> {
658 Ok(self
659 .poll_workflow_task_response(worker_id, task_queue, timeout)
660 .await?
661 .task)
662 }
663
664 pub async fn poll_workflow_task_response(
665 &self,
666 worker_id: &str,
667 task_queue: &str,
668 timeout: Duration,
669 ) -> Result<PollWorkflowTaskResponse> {
670 let body = json!({
671 "worker_id": worker_id,
672 "task_queue": task_queue,
673 "timeout_seconds": long_poll_timeout_seconds(timeout),
674 });
675 let mut data: PollWorkflowTaskResponse = self
676 .request_json_with_timeout(
677 reqwest::Method::POST,
678 "/worker/workflow-tasks/poll",
679 RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
680 Some(&body),
681 timeout + Duration::from_secs(5),
682 )
683 .await?;
684
685 if let Some(task) = data.task.as_mut() {
686 self.fetch_remaining_workflow_history(worker_id, task)
687 .await?;
688 }
689
690 Ok(data)
691 }
692
693 async fn fetch_remaining_workflow_history(
694 &self,
695 worker_id: &str,
696 task: &mut WorkflowTask,
697 ) -> Result<()> {
698 let mut next_token = task.next_history_page_token.clone();
699
700 while let Some(token) = next_token.take().filter(|token| !token.is_empty()) {
701 let lease_owner = task
702 .lease_owner
703 .clone()
704 .unwrap_or_else(|| worker_id.to_string());
705 let page = self
706 .workflow_task_history_page(
707 &task.task_id,
708 &lease_owner,
709 task.workflow_task_attempt,
710 &token,
711 )
712 .await?;
713
714 task.append_history_page(page);
715
716 if task.next_history_page_token.as_deref() == Some(token.as_str()) {
717 return Err(Error::Codec(
718 "workflow history pagination returned the same page token".to_string(),
719 ));
720 }
721
722 next_token = task.next_history_page_token.clone();
723 }
724
725 Ok(())
726 }
727
728 async fn workflow_task_history_page(
729 &self,
730 task_id: &str,
731 lease_owner: &str,
732 workflow_task_attempt: u64,
733 next_history_page_token: &str,
734 ) -> Result<WorkflowTaskHistoryPage> {
735 let body = json!({
736 "lease_owner": lease_owner,
737 "workflow_task_attempt": workflow_task_attempt,
738 "next_history_page_token": next_history_page_token
739 });
740 let path = format!("/worker/workflow-tasks/{task_id}/history");
741
742 self.request_json(
743 reqwest::Method::POST,
744 &path,
745 RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
746 Some(&body),
747 )
748 .await
749 }
750
751 pub async fn complete_workflow_task(
752 &self,
753 task_id: &str,
754 lease_owner: &str,
755 workflow_task_attempt: u64,
756 commands: Vec<Value>,
757 ) -> Result<Value> {
758 let body = json!({
759 "lease_owner": lease_owner,
760 "workflow_task_attempt": workflow_task_attempt,
761 "commands": commands
762 });
763 let path = format!("/worker/workflow-tasks/{task_id}/complete");
764 self.request_json(
765 reqwest::Method::POST,
766 &path,
767 RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
768 Some(&body),
769 )
770 .await
771 }
772
773 pub async fn fail_workflow_task(
774 &self,
775 task_id: &str,
776 lease_owner: &str,
777 workflow_task_attempt: u64,
778 message: impl Into<String>,
779 ) -> Result<Value> {
780 let body = json!({
781 "lease_owner": lease_owner,
782 "workflow_task_attempt": workflow_task_attempt,
783 "failure": {
784 "message": message.into(),
785 "type": "RustWorkflowTaskFailure"
786 }
787 });
788 let path = format!("/worker/workflow-tasks/{task_id}/fail");
789 self.request_json(
790 reqwest::Method::POST,
791 &path,
792 RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
793 Some(&body),
794 )
795 .await
796 }
797
798 pub async fn poll_activity_task(
799 &self,
800 worker_id: &str,
801 task_queue: &str,
802 timeout: Duration,
803 ) -> Result<Option<ActivityTask>> {
804 let body = json!({
805 "worker_id": worker_id,
806 "task_queue": task_queue,
807 "timeout_seconds": long_poll_timeout_seconds(timeout),
808 });
809 let data: PollActivityTaskResponse = self
810 .request_json_with_timeout(
811 reqwest::Method::POST,
812 "/worker/activity-tasks/poll",
813 RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
814 Some(&body),
815 timeout + Duration::from_secs(5),
816 )
817 .await?;
818 Ok(data.task)
819 }
820
821 pub async fn complete_activity_task(
822 &self,
823 task_id: &str,
824 activity_attempt_id: &str,
825 lease_owner: &str,
826 result: Value,
827 codec: &str,
828 ) -> Result<Value> {
829 let result = encode_value_envelope(&result, codec)?;
830 let body = json!({
831 "activity_attempt_id": activity_attempt_id,
832 "lease_owner": lease_owner,
833 "result": result
834 });
835 let path = format!("/worker/activity-tasks/{task_id}/complete");
836 self.request_json(
837 reqwest::Method::POST,
838 &path,
839 RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
840 Some(&body),
841 )
842 .await
843 }
844
845 pub async fn fail_activity_task(
846 &self,
847 task_id: &str,
848 activity_attempt_id: &str,
849 lease_owner: &str,
850 message: impl Into<String>,
851 non_retryable: bool,
852 ) -> Result<Value> {
853 let body = json!({
854 "activity_attempt_id": activity_attempt_id,
855 "lease_owner": lease_owner,
856 "failure": {
857 "message": message.into(),
858 "type": "RustActivityFailure",
859 "non_retryable": non_retryable
860 }
861 });
862 let path = format!("/worker/activity-tasks/{task_id}/fail");
863 self.request_json(
864 reqwest::Method::POST,
865 &path,
866 RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
867 Some(&body),
868 )
869 .await
870 }
871
872 pub async fn heartbeat_activity_task(
873 &self,
874 task_id: &str,
875 activity_attempt_id: &str,
876 lease_owner: &str,
877 details: Value,
878 ) -> Result<ActivityHeartbeatResponse> {
879 let body = json!({
880 "activity_attempt_id": activity_attempt_id,
881 "lease_owner": lease_owner,
882 "details": details
883 });
884 let path = format!("/worker/activity-tasks/{task_id}/heartbeat");
885 self.request_json(
886 reqwest::Method::POST,
887 &path,
888 RequestProtocol::Worker(WORKER_PROTOCOL_VERSION),
889 Some(&body),
890 )
891 .await
892 }
893
894 async fn request_json<T: DeserializeOwned, B: Serialize + ?Sized>(
895 &self,
896 method: reqwest::Method,
897 path: &str,
898 protocol: RequestProtocol,
899 body: Option<&B>,
900 ) -> Result<T> {
901 self.request_json_with_timeout(method, path, protocol, body, Duration::from_secs(60))
902 .await
903 }
904
905 async fn request_json_with_timeout<T: DeserializeOwned, B: Serialize + ?Sized>(
906 &self,
907 method: reqwest::Method,
908 path: &str,
909 protocol: RequestProtocol,
910 body: Option<&B>,
911 timeout: Duration,
912 ) -> Result<T> {
913 let mut request = self
914 .http
915 .request(method, format!("{}/api{}", self.base_url, path))
916 .timeout(timeout)
917 .header(reqwest::header::ACCEPT, "application/json")
918 .header(reqwest::header::CONTENT_TYPE, "application/json")
919 .header("X-Namespace", &self.namespace);
920
921 match protocol {
922 RequestProtocol::Worker(version) => {
923 request = request.header("X-Durable-Workflow-Protocol-Version", version);
924 }
925 RequestProtocol::ControlPlane => {
926 request = request.header(
927 "X-Durable-Workflow-Control-Plane-Version",
928 CONTROL_PLANE_VERSION,
929 );
930 }
931 }
932
933 if let Some(token) = self.auth_token(protocol.is_worker()) {
934 request = request.bearer_auth(token);
935 }
936
937 if let Some(body) = body {
938 request = request.json(body);
939 }
940
941 let response = request.send().await?;
942 let status = response.status();
943 let bytes = response.bytes().await?;
944
945 if !status.is_success() {
946 let body = String::from_utf8_lossy(&bytes).to_string();
947 if let Some(protocol) = protocol_failure(status, &body) {
948 return Err(Error::Protocol(protocol));
949 }
950 return Err(Error::Http { status, body });
951 }
952
953 if bytes.is_empty() {
954 return Ok(serde_json::from_value(Value::Null)?);
955 }
956
957 Ok(serde_json::from_slice(&bytes)?)
958 }
959
960 fn auth_token(&self, worker: bool) -> Option<&str> {
961 if worker {
962 self.worker_token
963 .as_deref()
964 .or(self.token.as_deref())
965 .or(self.control_token.as_deref())
966 } else {
967 self.control_token
968 .as_deref()
969 .or(self.token.as_deref())
970 .or(self.worker_token.as_deref())
971 }
972 }
973}
974
975fn query_failure(status: reqwest::StatusCode, raw_body: String) -> QueryFailure {
976 let body = serde_json::from_str(&raw_body).unwrap_or_else(|_| json!({"message": raw_body}));
977 let reason = body
978 .get("reason")
979 .and_then(Value::as_str)
980 .unwrap_or("query_rejected")
981 .to_string();
982 let message = body
983 .get("message")
984 .or_else(|| body.get("error"))
985 .and_then(Value::as_str)
986 .unwrap_or("workflow query was rejected")
987 .to_string();
988
989 QueryFailure {
990 status: status.as_u16(),
991 reason,
992 message,
993 body,
994 }
995}
996
997fn query_task_response(response: Result<Value>) -> Result<Value> {
998 match response {
999 Err(Error::Http { status, body }) => Err(Error::QueryFailed(query_failure(status, body))),
1000 response => response,
1001 }
1002}
1003
1004fn query_task_rejection_is_final(error: &Error) -> bool {
1005 matches!(
1006 error,
1007 Error::QueryFailed(failure)
1008 if QUERY_TASK_FINAL_REJECTION_REASONS.contains(&failure.reason.as_str())
1009 )
1010}
1011
1012fn protocol_failure(status: reqwest::StatusCode, raw_body: &str) -> Option<ProtocolFailure> {
1013 let body: Value = serde_json::from_str(raw_body).ok()?;
1014 let reason = body.get("reason")?.as_str()?;
1015 if !matches!(
1016 reason,
1017 "missing_protocol_version"
1018 | "unsupported_protocol_version"
1019 | "missing_control_plane_version"
1020 | "unsupported_control_plane_version"
1021 ) {
1022 return None;
1023 }
1024
1025 Some(ProtocolFailure {
1026 status: status.as_u16(),
1027 reason: reason.to_string(),
1028 message: body
1029 .get("message")
1030 .or_else(|| body.get("error"))
1031 .and_then(Value::as_str)
1032 .unwrap_or("protocol version rejected")
1033 .to_string(),
1034 supported_version: body
1035 .get("supported_version")
1036 .and_then(Value::as_str)
1037 .map(str::to_string),
1038 requested_version: body
1039 .get("requested_version")
1040 .and_then(Value::as_str)
1041 .map(str::to_string),
1042 body,
1043 })
1044}
1045
1046fn long_poll_timeout_seconds(timeout: Duration) -> u64 {
1047 timeout
1048 .as_secs()
1049 .saturating_add(u64::from(timeout.subsec_nanos() > 0))
1050 .min(MAX_LONG_POLL_TIMEOUT_SECONDS)
1051}
1052
1053fn worker_operation_is_retryable(error: &Error) -> bool {
1054 match error {
1055 Error::Transport(error) => {
1056 error.is_timeout() || error.is_connect() || error.is_request() || error.is_body()
1057 }
1058 Error::Http { status, .. } => {
1059 matches!(
1060 *status,
1061 reqwest::StatusCode::REQUEST_TIMEOUT | reqwest::StatusCode::TOO_MANY_REQUESTS
1062 ) || status.is_server_error()
1063 }
1064 _ => false,
1065 }
1066}
1067
1068fn worker_retry_delay(policy: WorkerRetryPolicy, retry: usize) -> Duration {
1069 let exponent = retry.saturating_sub(1).min(31) as u32;
1070 policy
1071 .initial_backoff
1072 .saturating_mul(1_u32 << exponent)
1073 .min(policy.max_backoff)
1074}
1075
1076#[derive(Debug)]
1077pub struct ClientBuilder {
1078 base_url: String,
1079 token: Option<String>,
1080 control_token: Option<String>,
1081 worker_token: Option<String>,
1082 namespace: String,
1083 timeout: Duration,
1084}
1085
1086impl ClientBuilder {
1087 pub fn token(mut self, token: Option<String>) -> Self {
1088 self.token = token;
1089 self
1090 }
1091
1092 pub fn control_token(mut self, token: Option<String>) -> Self {
1093 self.control_token = token;
1094 self
1095 }
1096
1097 pub fn worker_token(mut self, token: Option<String>) -> Self {
1098 self.worker_token = token;
1099 self
1100 }
1101
1102 pub fn namespace(mut self, namespace: impl Into<String>) -> Self {
1103 self.namespace = namespace.into();
1104 self
1105 }
1106
1107 pub fn timeout(mut self, timeout: Duration) -> Self {
1108 self.timeout = timeout;
1109 self
1110 }
1111
1112 pub fn build(self) -> Result<Client> {
1113 Ok(Client {
1114 http: reqwest::Client::builder().timeout(self.timeout).build()?,
1115 base_url: self.base_url.trim_end_matches('/').to_string(),
1116 token: self.token,
1117 control_token: self.control_token,
1118 worker_token: self.worker_token,
1119 namespace: self.namespace,
1120 })
1121 }
1122}
1123
1124#[derive(Clone, Debug)]
1125pub struct WorkflowHandle {
1126 client: Client,
1127 pub workflow_id: String,
1128 pub run_id: Option<String>,
1129 pub workflow_type: String,
1130}
1131
1132impl WorkflowHandle {
1133 pub async fn describe(&self) -> Result<WorkflowDescription> {
1134 self.client.describe_workflow(&self.workflow_id).await
1135 }
1136
1137 pub async fn signal<T: Serialize>(&self, signal_name: &str, input: T) -> Result<Value> {
1138 self.client
1139 .signal_workflow(&self.workflow_id, signal_name, input)
1140 .await
1141 }
1142
1143 pub async fn query<T: Serialize>(&self, query_name: &str, input: T) -> Result<Value> {
1145 self.client
1146 .query_workflow(&self.workflow_id, query_name, input)
1147 .await
1148 }
1149
1150 pub async fn result(&self, options: WorkflowResultOptions) -> Result<Value> {
1151 let started = Instant::now();
1152
1153 loop {
1154 let description = self.describe().await?;
1155 if description.is_completed() {
1156 return Ok(description.output.unwrap_or(Value::Null));
1157 }
1158
1159 if description.is_terminal() {
1160 return Err(Error::Codec(format!(
1161 "workflow {} closed with status {:?}",
1162 self.workflow_id, description.status
1163 )));
1164 }
1165
1166 if started.elapsed() >= options.timeout {
1167 return Err(Error::Timeout);
1168 }
1169
1170 tokio::time::sleep(options.poll_interval).await;
1171 }
1172 }
1173}
1174
1175#[derive(Clone, Copy, Debug)]
1176pub struct WorkflowResultOptions {
1177 pub poll_interval: Duration,
1178 pub timeout: Duration,
1179}
1180
1181impl Default for WorkflowResultOptions {
1182 fn default() -> Self {
1183 Self {
1184 poll_interval: Duration::from_millis(500),
1185 timeout: Duration::from_secs(30),
1186 }
1187 }
1188}
1189
1190#[derive(Clone, Debug, Deserialize)]
1191pub struct WorkflowDescription {
1192 pub workflow_id: Option<String>,
1193 pub run_id: Option<String>,
1194 pub workflow_type: Option<String>,
1195 pub status: Option<String>,
1196 #[serde(default)]
1197 pub output: Option<Value>,
1198 #[serde(default)]
1199 pub output_envelope: Option<Value>,
1200 #[serde(flatten)]
1201 pub raw: HashMap<String, Value>,
1202}
1203
1204impl WorkflowDescription {
1205 pub fn is_completed(&self) -> bool {
1206 matches!(self.status.as_deref(), Some("completed" | "Completed"))
1207 }
1208
1209 pub fn is_terminal(&self) -> bool {
1210 matches!(
1211 self.status.as_deref(),
1212 Some(
1213 "completed"
1214 | "Completed"
1215 | "failed"
1216 | "Failed"
1217 | "cancelled"
1218 | "Cancelled"
1219 | "terminated"
1220 | "Terminated"
1221 | "timed_out"
1222 | "TimedOut",
1223 )
1224 )
1225 }
1226
1227 fn decode_payloads(&mut self) -> Result<()> {
1228 if let Some(envelope) = &self.output_envelope {
1229 self.output = Some(decode_wire_value(envelope, DEFAULT_CODEC)?);
1230 }
1231
1232 Ok(())
1233 }
1234}
1235
1236#[derive(Clone, Debug, Deserialize)]
1237pub struct RegisterWorkerResponse {
1238 pub worker_id: String,
1239 pub registered: bool,
1240 #[serde(default)]
1241 pub heartbeat_interval_seconds: Option<u64>,
1242 #[serde(default)]
1243 pub protocol_version: Option<String>,
1244 #[serde(default)]
1245 pub server_capabilities: Option<Value>,
1246}
1247
1248#[derive(Clone, Debug, Deserialize)]
1249pub struct PollWorkflowTaskResponse {
1250 #[serde(default)]
1251 pub task: Option<WorkflowTask>,
1252 #[serde(default)]
1253 pub poll_status: Option<String>,
1254 #[serde(default)]
1255 pub reason: Option<String>,
1256 #[serde(default)]
1257 pub protocol_version: Option<String>,
1258 #[serde(default)]
1259 pub server_capabilities: Option<Value>,
1260}
1261
1262#[derive(Clone, Debug, Deserialize)]
1263struct PollActivityTaskResponse {
1264 #[serde(default)]
1265 task: Option<ActivityTask>,
1266}
1267
1268#[derive(Clone, Debug, Deserialize)]
1269struct PollQueryTaskResponse {
1270 #[serde(default)]
1271 task: Option<QueryTask>,
1272}
1273
1274#[derive(Clone, Debug, Deserialize)]
1276pub struct QueryTask {
1277 pub query_task_id: String,
1278 #[serde(default = "default_workflow_task_attempt")]
1279 pub query_task_attempt: u64,
1280 #[serde(default)]
1281 pub lease_owner: Option<String>,
1282 #[serde(default)]
1283 pub workflow_id: Option<String>,
1284 #[serde(default)]
1285 pub run_id: Option<String>,
1286 pub workflow_type: String,
1287 pub query_name: String,
1288 #[serde(default = "default_payload_codec")]
1289 pub payload_codec: String,
1290 #[serde(default)]
1291 pub workflow_arguments: Option<Value>,
1292 #[serde(default)]
1293 pub query_arguments: Option<Value>,
1294 #[serde(default)]
1295 pub history_events: Vec<HistoryEvent>,
1296 #[serde(default)]
1297 pub history_export: Option<Value>,
1298 #[serde(default)]
1299 pub run_status: Option<String>,
1300}
1301
1302#[derive(Clone, Debug, Deserialize)]
1303pub struct WorkflowTask {
1304 pub task_id: String,
1305 #[serde(default)]
1306 pub workflow_id: Option<String>,
1307 #[serde(default)]
1308 pub run_id: Option<String>,
1309 pub workflow_type: String,
1310 #[serde(default = "default_payload_codec")]
1311 pub payload_codec: String,
1312 #[serde(default)]
1313 pub arguments: Option<Value>,
1314 #[serde(default)]
1315 pub history_events: Vec<HistoryEvent>,
1316 #[serde(default)]
1317 pub total_history_events: Option<u64>,
1318 #[serde(default)]
1319 pub next_history_page_token: Option<String>,
1320 #[serde(default = "default_workflow_task_attempt")]
1321 pub workflow_task_attempt: u64,
1322 #[serde(default)]
1323 pub workflow_signal_id: Option<String>,
1324 #[serde(default)]
1325 pub signal_name: Option<String>,
1326 #[serde(default)]
1327 pub signal_arguments: Option<Value>,
1328 #[serde(default)]
1329 pub lease_owner: Option<String>,
1330}
1331
1332impl WorkflowTask {
1333 fn append_history_page(&mut self, page: WorkflowTaskHistoryPage) {
1334 self.history_events.extend(page.history_events);
1335
1336 if page.total_history_events.is_some() {
1337 self.total_history_events = page.total_history_events;
1338 }
1339
1340 self.next_history_page_token = page
1341 .next_history_page_token
1342 .filter(|token| !token.is_empty());
1343 }
1344}
1345
1346#[derive(Clone, Debug, Deserialize)]
1347struct WorkflowTaskHistoryPage {
1348 #[serde(default)]
1349 history_events: Vec<HistoryEvent>,
1350 #[serde(default)]
1351 total_history_events: Option<u64>,
1352 #[serde(default)]
1353 next_history_page_token: Option<String>,
1354}
1355
1356#[derive(Clone, Debug, Deserialize)]
1357pub struct ActivityTask {
1358 pub task_id: String,
1359 #[serde(default)]
1360 pub activity_attempt_id: Option<String>,
1361 #[serde(default)]
1362 pub attempt_id: Option<String>,
1363 pub activity_type: String,
1364 #[serde(default = "default_payload_codec")]
1365 pub payload_codec: String,
1366 #[serde(default)]
1367 pub arguments: Option<Value>,
1368 #[serde(default = "default_attempt_number")]
1369 pub attempt_number: u64,
1370 #[serde(default)]
1371 pub lease_owner: Option<String>,
1372}
1373
1374#[derive(Clone, Debug, Deserialize)]
1375pub struct HistoryEvent {
1376 #[serde(alias = "type")]
1377 pub event_type: String,
1378 #[serde(default)]
1379 pub payload: Value,
1380 #[serde(flatten)]
1381 pub raw: HashMap<String, Value>,
1382}
1383
1384#[derive(Clone, Debug, PartialEq)]
1386pub struct QuerySignal {
1387 pub id: Option<String>,
1388 pub name: String,
1389 pub arguments: Vec<Value>,
1390 pub workflow_sequence: Option<u64>,
1391}
1392
1393#[derive(Clone, Debug)]
1399pub struct QueryContext {
1400 pub workflow_id: Option<String>,
1401 pub run_id: Option<String>,
1402 pub workflow_type: String,
1403 pub run_status: Option<String>,
1404 workflow_input: Value,
1405 history_events: Arc<Vec<HistoryEvent>>,
1406 signal_events: Arc<Vec<QuerySignal>>,
1407}
1408
1409impl QueryContext {
1410 pub fn workflow_input(&self) -> &Value {
1412 &self.workflow_input
1413 }
1414
1415 pub fn history_events(&self) -> &[HistoryEvent] {
1417 self.history_events.as_slice()
1418 }
1419
1420 pub fn signal_events(&self) -> &[QuerySignal] {
1422 self.signal_events.as_slice()
1423 }
1424
1425 pub fn signals(&self, signal_name: &str) -> Vec<Vec<Value>> {
1427 self.signal_events
1428 .iter()
1429 .filter(|signal| signal.name == signal_name)
1430 .map(|signal| signal.arguments.clone())
1431 .collect()
1432 }
1433}
1434
1435#[derive(Clone, Debug, Deserialize)]
1436pub struct ActivityHeartbeatResponse {
1437 #[serde(default)]
1438 pub cancel_requested: bool,
1439 #[serde(default)]
1440 pub heartbeat_recorded: bool,
1441}
1442
1443fn default_payload_codec() -> String {
1444 DEFAULT_CODEC.to_string()
1445}
1446
1447fn default_workflow_task_attempt() -> u64 {
1448 1
1449}
1450
1451fn default_attempt_number() -> u64 {
1452 1
1453}
1454
1455type WorkflowFuture = Pin<Box<dyn Future<Output = Result<Value>> + Send + 'static>>;
1456type WorkflowHandler = Arc<dyn Fn(WorkflowContext, Value) -> WorkflowFuture + Send + Sync>;
1457type ErasedWorkflowState = Arc<dyn Any + Send + Sync>;
1458type WorkflowStateSnapshot = Arc<dyn Fn() -> Result<ErasedWorkflowState> + Send + Sync>;
1459type ReplayedWorkflowHandler =
1460 Arc<dyn Fn(WorkflowContext, Value) -> ReplayedWorkflowInvocation + Send + Sync>;
1461type ActivityFuture = Pin<Box<dyn Future<Output = Result<Value>> + Send + 'static>>;
1462type ActivityHandler = Arc<dyn Fn(ActivityContext, Value) -> ActivityFuture + Send + Sync>;
1463type QueryFuture = Pin<Box<dyn Future<Output = Result<Value>> + Send + 'static>>;
1464type QueryHandler = Arc<dyn Fn(QueryContext, Value) -> QueryFuture + Send + Sync>;
1465type ReplayedQueryHandler = Arc<
1466 dyn Fn(QueryContext, ErasedWorkflowState, Value) -> std::result::Result<QueryFuture, String>
1467 + Send
1468 + Sync,
1469>;
1470type WorkerHeartbeatObserver = Arc<dyn Fn(&WorkerHeartbeatObservation) + Send + Sync>;
1471
1472struct ReplayedWorkflowInvocation {
1473 future: WorkflowFuture,
1474 snapshot: WorkflowStateSnapshot,
1475}
1476
1477#[derive(Clone)]
1478struct RegisteredWorkflow {
1479 execute: WorkflowHandler,
1480 replay: Option<ReplayedWorkflowHandler>,
1481 state_type: Option<TypeId>,
1482}
1483
1484#[derive(Clone)]
1485enum RegisteredQuery {
1486 Snapshot(QueryHandler),
1487 Replayed {
1488 state_type: TypeId,
1489 handler: ReplayedQueryHandler,
1490 },
1491}
1492
1493#[derive(Clone, Debug)]
1494pub struct WorkerHeartbeatObservation {
1495 pub worker_id: String,
1496 pub task_queue: String,
1497 pub acknowledged_at_unix_millis: u64,
1498 pub acknowledgement: Value,
1499}
1500
1501#[derive(Clone, Copy, Debug)]
1508pub struct WorkerRetryPolicy {
1509 pub max_retries: usize,
1511 pub initial_backoff: Duration,
1513 pub max_backoff: Duration,
1515}
1516
1517impl Default for WorkerRetryPolicy {
1518 fn default() -> Self {
1519 Self {
1520 max_retries: 5,
1521 initial_backoff: Duration::from_millis(100),
1522 max_backoff: Duration::from_secs(5),
1523 }
1524 }
1525}
1526
1527#[derive(Clone)]
1528pub struct Worker {
1529 client: Client,
1530 worker_id: String,
1531 task_queue: String,
1532 workflows: HashMap<String, RegisteredWorkflow>,
1533 activities: HashMap<String, ActivityHandler>,
1534 queries: HashMap<String, HashMap<String, RegisteredQuery>>,
1535 max_concurrent_workflow_tasks: usize,
1536 max_concurrent_activity_tasks: usize,
1537 poll_timeout: Duration,
1538 heartbeat_interval: Duration,
1539 retry_policy: WorkerRetryPolicy,
1540 heartbeat_observer: Option<WorkerHeartbeatObserver>,
1541}
1542
1543impl Worker {
1544 pub fn new(client: Client, task_queue: impl Into<String>) -> Self {
1545 Self {
1546 client,
1547 worker_id: default_worker_id(),
1548 task_queue: task_queue.into(),
1549 workflows: HashMap::new(),
1550 activities: HashMap::new(),
1551 queries: HashMap::new(),
1552 max_concurrent_workflow_tasks: 10,
1553 max_concurrent_activity_tasks: 10,
1554 poll_timeout: Duration::from_secs(30),
1555 heartbeat_interval: Duration::from_secs(60),
1556 retry_policy: WorkerRetryPolicy::default(),
1557 heartbeat_observer: None,
1558 }
1559 }
1560
1561 pub fn worker_id(mut self, worker_id: impl Into<String>) -> Self {
1562 self.worker_id = worker_id.into();
1563 self
1564 }
1565
1566 pub fn poll_timeout(mut self, timeout: Duration) -> Self {
1567 self.poll_timeout = timeout;
1568 self
1569 }
1570
1571 pub fn heartbeat_interval(mut self, interval: Duration) -> Self {
1572 self.heartbeat_interval = interval;
1573 self
1574 }
1575
1576 pub fn retry_policy(mut self, policy: WorkerRetryPolicy) -> Self {
1578 self.retry_policy = policy;
1579 self
1580 }
1581
1582 pub fn on_worker_heartbeat<F>(mut self, observer: F) -> Self
1583 where
1584 F: Fn(&WorkerHeartbeatObservation) + Send + Sync + 'static,
1585 {
1586 self.heartbeat_observer = Some(Arc::new(observer));
1587 self
1588 }
1589
1590 pub fn max_concurrent_workflow_tasks(mut self, count: usize) -> Self {
1591 self.max_concurrent_workflow_tasks = count.max(1);
1592 self
1593 }
1594
1595 pub fn max_concurrent_activity_tasks(mut self, count: usize) -> Self {
1596 self.max_concurrent_activity_tasks = count.max(1);
1597 self
1598 }
1599
1600 pub fn register_workflow<F, Fut>(&mut self, workflow_type: impl Into<String>, handler: F)
1601 where
1602 F: Fn(WorkflowContext, Value) -> Fut + Send + Sync + 'static,
1603 Fut: Future<Output = Result<Value>> + Send + 'static,
1604 {
1605 self.workflows.insert(
1606 workflow_type.into(),
1607 RegisteredWorkflow {
1608 execute: Arc::new(move |ctx, input| Box::pin(handler(ctx, input))),
1609 replay: None,
1610 state_type: None,
1611 },
1612 );
1613 }
1614
1615 pub fn register_replayed_workflow<S, Factory, F, Fut>(
1623 &mut self,
1624 workflow_type: impl Into<String>,
1625 state_factory: Factory,
1626 handler: F,
1627 ) where
1628 S: Clone + Send + Sync + 'static,
1629 Factory: Fn() -> S + Send + Sync + 'static,
1630 F: Fn(WorkflowContext, Value, WorkflowInstance<S>) -> Fut + Send + Sync + 'static,
1631 Fut: Future<Output = Result<Value>> + Send + 'static,
1632 {
1633 let state_factory = Arc::new(state_factory);
1634 let handler = Arc::new(handler);
1635
1636 let execute_factory = Arc::clone(&state_factory);
1637 let execute_handler = Arc::clone(&handler);
1638 let execute = Arc::new(move |ctx: WorkflowContext, input: Value| {
1639 let state = WorkflowInstance::new(execute_factory());
1640 let future = execute_handler(ctx, input, state);
1641 Box::pin(future) as WorkflowFuture
1642 });
1643
1644 let replay = Arc::new(move |ctx: WorkflowContext, input: Value| {
1645 let state = WorkflowInstance::new(state_factory());
1646 let snapshot_state = state.clone();
1647 let snapshot: WorkflowStateSnapshot =
1648 Arc::new(move || Ok(Arc::new(snapshot_state.snapshot()?) as ErasedWorkflowState));
1649 let future = handler(ctx, input, state);
1650 ReplayedWorkflowInvocation {
1651 future: Box::pin(future),
1652 snapshot,
1653 }
1654 });
1655
1656 self.workflows.insert(
1657 workflow_type.into(),
1658 RegisteredWorkflow {
1659 execute,
1660 replay: Some(replay),
1661 state_type: Some(TypeId::of::<S>()),
1662 },
1663 );
1664 }
1665
1666 pub fn register_activity<F, Fut>(&mut self, activity_type: impl Into<String>, handler: F)
1667 where
1668 F: Fn(ActivityContext, Value) -> Fut + Send + Sync + 'static,
1669 Fut: Future<Output = Result<Value>> + Send + 'static,
1670 {
1671 self.activities.insert(
1672 activity_type.into(),
1673 Arc::new(move |ctx, args| Box::pin(handler(ctx, args))),
1674 );
1675 }
1676
1677 pub fn register_query<F, Fut>(
1683 &mut self,
1684 workflow_type: impl Into<String>,
1685 query_name: impl Into<String>,
1686 handler: F,
1687 ) where
1688 F: Fn(QueryContext, Value) -> Fut + Send + Sync + 'static,
1689 Fut: Future<Output = Result<Value>> + Send + 'static,
1690 {
1691 self.queries
1692 .entry(workflow_type.into())
1693 .or_default()
1694 .insert(
1695 query_name.into(),
1696 RegisteredQuery::Snapshot(Arc::new(move |ctx, args| Box::pin(handler(ctx, args)))),
1697 );
1698 }
1699
1700 pub fn register_replayed_query<S, F, Fut>(
1707 &mut self,
1708 workflow_type: impl Into<String>,
1709 query_name: impl Into<String>,
1710 handler: F,
1711 ) where
1712 S: Clone + Send + Sync + 'static,
1713 F: Fn(QueryContext, Arc<S>, Value) -> Fut + Send + Sync + 'static,
1714 Fut: Future<Output = Result<Value>> + Send + 'static,
1715 {
1716 let handler = Arc::new(handler);
1717 let erased_handler: ReplayedQueryHandler = Arc::new(move |ctx, state, args| {
1718 let state = state.downcast::<S>().map_err(|_| {
1719 "registered query state type does not match the replayed workflow state".to_string()
1720 })?;
1721 Ok(Box::pin(handler(ctx, state, args)))
1722 });
1723
1724 self.queries
1725 .entry(workflow_type.into())
1726 .or_default()
1727 .insert(
1728 query_name.into(),
1729 RegisteredQuery::Replayed {
1730 state_type: TypeId::of::<S>(),
1731 handler: erased_handler,
1732 },
1733 );
1734 }
1735
1736 pub async fn register(&self) -> Result<RegisterWorkerResponse> {
1737 self.client
1738 .register_worker_with_capabilities(
1739 &self.worker_id,
1740 &self.task_queue,
1741 self.workflows.keys().cloned().collect(),
1742 self.activities.keys().cloned().collect(),
1743 self.max_concurrent_workflow_tasks,
1744 self.max_concurrent_activity_tasks,
1745 (!self.queries.is_empty())
1746 .then(|| QUERY_TASKS_CAPABILITY.to_string())
1747 .into_iter()
1748 .collect(),
1749 )
1750 .await
1751 }
1752
1753 pub async fn run(&self) -> Result<()> {
1759 self.run_until(std::future::pending::<()>()).await
1760 }
1761
1762 pub async fn run_until<F>(&self, shutdown: F) -> Result<()>
1766 where
1767 F: Future<Output = ()>,
1768 {
1769 let registration = self.register().await?;
1770 let mut heartbeat = tokio::time::interval(Duration::from_secs(
1771 registration
1772 .heartbeat_interval_seconds
1773 .unwrap_or(self.heartbeat_interval.as_secs().max(1)),
1774 ));
1775 tokio::pin!(shutdown);
1776 let stop = Arc::new(AtomicBool::new(false));
1777 let mut workflow_poller = (!self.workflows.is_empty()).then(|| {
1781 let worker = self.clone();
1782 let stop = Arc::clone(&stop);
1783 tokio::spawn(async move { worker.poll_workflows_until_stopped(stop).await })
1784 });
1785 let mut activity_poller = (!self.activities.is_empty()).then(|| {
1786 let worker = self.clone();
1787 let stop = Arc::clone(&stop);
1788 tokio::spawn(async move { worker.poll_activities_until_stopped(stop).await })
1789 });
1790 let mut query_poller = (!self.queries.is_empty()).then(|| {
1791 let worker = self.clone();
1792 let stop = Arc::clone(&stop);
1793 tokio::spawn(async move { worker.poll_queries_until_stopped(stop).await })
1794 });
1795
1796 loop {
1797 tokio::select! {
1798 _ = &mut shutdown => {
1799 stop.store(true, Ordering::SeqCst);
1800 break;
1801 }
1802 _ = heartbeat.tick() => {
1803 match self.retry_worker_operation(|| {
1804 self.client.heartbeat_worker(
1805 &self.worker_id,
1806 self.max_concurrent_workflow_tasks,
1807 self.max_concurrent_activity_tasks,
1808 )
1809 }).await
1810 {
1811 Ok(acknowledgement) => {
1812 if let Some(observer) = &self.heartbeat_observer {
1813 observer(&WorkerHeartbeatObservation {
1814 worker_id: self.worker_id.clone(),
1815 task_queue: self.task_queue.clone(),
1816 acknowledged_at_unix_millis: SystemTime::now()
1817 .duration_since(UNIX_EPOCH)
1818 .unwrap_or_default()
1819 .as_millis()
1820 .min(u64::MAX as u128)
1821 as u64,
1822 acknowledgement,
1823 });
1824 }
1825 }
1826 Err(error) => {
1827 stop.store(true, Ordering::SeqCst);
1828 join_pollers(workflow_poller.take(), activity_poller.take(), query_poller.take()).await?;
1829 return Err(error);
1830 }
1831 }
1832 }
1833 result = OptionFuture::from(workflow_poller.as_mut()), if workflow_poller.is_some() => {
1834 workflow_poller = None;
1835 stop.store(true, Ordering::SeqCst);
1836 let poller_result = optional_poller_result("workflow", result);
1837 let join_result =
1838 join_pollers(workflow_poller.take(), activity_poller.take(), query_poller.take()).await;
1839 poller_result?;
1840 join_result?;
1841 return Err(Error::WorkerLoop(
1842 "workflow poller stopped unexpectedly".to_string(),
1843 ));
1844 }
1845 result = OptionFuture::from(activity_poller.as_mut()), if activity_poller.is_some() => {
1846 activity_poller = None;
1847 stop.store(true, Ordering::SeqCst);
1848 let poller_result = optional_poller_result("activity", result);
1849 let join_result =
1850 join_pollers(workflow_poller.take(), activity_poller.take(), query_poller.take()).await;
1851 poller_result?;
1852 join_result?;
1853 return Err(Error::WorkerLoop(
1854 "activity poller stopped unexpectedly".to_string(),
1855 ));
1856 }
1857 result = OptionFuture::from(query_poller.as_mut()), if query_poller.is_some() => {
1858 query_poller = None;
1859 stop.store(true, Ordering::SeqCst);
1860 let poller_result = optional_poller_result("query", result);
1861 let join_result =
1862 join_pollers(workflow_poller.take(), activity_poller.take(), query_poller.take()).await;
1863 poller_result?;
1864 join_result?;
1865 return Err(Error::WorkerLoop(
1866 "query poller stopped unexpectedly".to_string(),
1867 ));
1868 }
1869 }
1870 }
1871
1872 join_pollers(
1873 workflow_poller.take(),
1874 activity_poller.take(),
1875 query_poller.take(),
1876 )
1877 .await
1878 }
1879
1880 pub async fn run_once(&self) -> Result<usize> {
1881 let mut handled = 0;
1882 if self.poll_workflow_once().await? {
1883 handled += 1;
1884 }
1885 if self.poll_activity_once().await? {
1886 handled += 1;
1887 }
1888 if !self.queries.is_empty() && self.poll_query_once().await? {
1889 handled += 1;
1890 }
1891 Ok(handled)
1892 }
1893
1894 async fn poll_workflow_once(&self) -> Result<bool> {
1895 let Some(task) = self
1896 .retry_worker_operation(|| {
1897 self.client
1898 .poll_workflow_task(&self.worker_id, &self.task_queue, self.poll_timeout)
1899 })
1900 .await?
1901 else {
1902 return Ok(false);
1903 };
1904
1905 let task_id = task.task_id.clone();
1906 let attempt = task.workflow_task_attempt;
1907 let lease_owner = task
1908 .lease_owner
1909 .clone()
1910 .unwrap_or_else(|| self.worker_id.clone());
1911
1912 match self.execute_workflow_task(task) {
1913 Ok(commands) => {
1914 self.client
1915 .complete_workflow_task(&task_id, &lease_owner, attempt, commands)
1916 .await?;
1917 }
1918 Err(error) => {
1919 self.client
1920 .fail_workflow_task(&task_id, &lease_owner, attempt, error.to_string())
1921 .await?;
1922 }
1923 }
1924
1925 Ok(true)
1926 }
1927
1928 async fn poll_workflows_until_stopped(self, stop: Arc<AtomicBool>) -> Result<()> {
1929 while !stop.load(Ordering::SeqCst) {
1930 self.poll_workflow_once().await?;
1931 }
1932
1933 Ok(())
1934 }
1935
1936 async fn poll_activity_once(&self) -> Result<bool> {
1937 let Some(task) = self
1938 .retry_worker_operation(|| {
1939 self.client
1940 .poll_activity_task(&self.worker_id, &self.task_queue, self.poll_timeout)
1941 })
1942 .await?
1943 else {
1944 return Ok(false);
1945 };
1946
1947 let task_id = task.task_id.clone();
1948 let attempt_id = task
1949 .activity_attempt_id
1950 .clone()
1951 .or(task.attempt_id.clone())
1952 .unwrap_or_default();
1953 let lease_owner = task
1954 .lease_owner
1955 .clone()
1956 .unwrap_or_else(|| self.worker_id.clone());
1957 let codec = task.payload_codec.clone();
1958 let result = self.execute_activity_task(task).await;
1959 match result {
1960 Ok(value) => {
1961 self.client
1962 .complete_activity_task(&task_id, &attempt_id, &lease_owner, value, &codec)
1963 .await?;
1964 }
1965 Err(error) => {
1966 self.client
1967 .fail_activity_task(
1968 &task_id,
1969 &attempt_id,
1970 &lease_owner,
1971 error.to_string(),
1972 false,
1973 )
1974 .await?;
1975 }
1976 }
1977
1978 Ok(true)
1979 }
1980
1981 async fn poll_activities_until_stopped(self, stop: Arc<AtomicBool>) -> Result<()> {
1982 while !stop.load(Ordering::SeqCst) {
1983 self.poll_activity_once().await?;
1984 }
1985
1986 Ok(())
1987 }
1988
1989 async fn poll_query_once(&self) -> Result<bool> {
1990 let Some(task) = self
1991 .retry_worker_operation(|| {
1992 self.client
1993 .poll_query_task(&self.worker_id, &self.task_queue, self.poll_timeout)
1994 })
1995 .await?
1996 else {
1997 return Ok(false);
1998 };
1999
2000 let query_task_id = task.query_task_id.clone();
2001 let attempt = task.query_task_attempt;
2002 let lease_owner = task
2003 .lease_owner
2004 .clone()
2005 .unwrap_or_else(|| self.worker_id.clone());
2006 let codec = task.payload_codec.clone();
2007
2008 match self.execute_query_task(task).await {
2009 Ok(value) => {
2010 let result_envelope = match encode_value_envelope(&value, &codec) {
2011 Ok(result_envelope) => result_envelope,
2012 Err(error) => {
2013 let failure = self
2014 .client
2015 .fail_query_task(
2016 &query_task_id,
2017 &lease_owner,
2018 attempt,
2019 error.to_string(),
2020 "query_result_encode_failed",
2021 "QueryResultEncodeFailed",
2022 )
2023 .await;
2024 if let Err(error) = failure {
2025 if !query_task_rejection_is_final(&error) {
2026 return Err(error);
2027 }
2028 }
2029 return Ok(true);
2030 }
2031 };
2032
2033 if let Err(error) = self
2034 .client
2035 .complete_query_task_with_envelope(
2036 &query_task_id,
2037 &lease_owner,
2038 attempt,
2039 value,
2040 result_envelope,
2041 )
2042 .await
2043 {
2044 if !query_task_rejection_is_final(&error) {
2045 return Err(error);
2046 }
2047 }
2048 }
2049 Err(failure) => {
2050 let result = self
2051 .client
2052 .fail_query_task(
2053 &query_task_id,
2054 &lease_owner,
2055 attempt,
2056 failure.message,
2057 failure.reason,
2058 failure.failure_type,
2059 )
2060 .await;
2061 if let Err(error) = result {
2062 if !query_task_rejection_is_final(&error) {
2063 return Err(error);
2064 }
2065 }
2066 }
2067 }
2068
2069 Ok(true)
2070 }
2071
2072 async fn poll_queries_until_stopped(self, stop: Arc<AtomicBool>) -> Result<()> {
2073 while !stop.load(Ordering::SeqCst) {
2074 self.poll_query_once().await?;
2075 }
2076
2077 Ok(())
2078 }
2079
2080 async fn retry_worker_operation<T, F, Fut>(&self, mut operation: F) -> Result<T>
2081 where
2082 F: FnMut() -> Fut,
2083 Fut: Future<Output = Result<T>>,
2084 {
2085 let mut retries = 0;
2086
2087 loop {
2088 match operation().await {
2089 Err(error)
2090 if worker_operation_is_retryable(&error)
2091 && retries < self.retry_policy.max_retries =>
2092 {
2093 retries += 1;
2094 tokio::time::sleep(worker_retry_delay(self.retry_policy, retries)).await;
2095 }
2096 result => return result,
2097 }
2098 }
2099 }
2100
2101 async fn execute_query_task(
2102 &self,
2103 mut task: QueryTask,
2104 ) -> std::result::Result<Value, QueryTaskExecutionFailure> {
2105 if !matches!(task.payload_codec.as_str(), DEFAULT_CODEC | JSON_CODEC) {
2106 return Err(QueryTaskExecutionFailure::new(
2107 "query_payload_decode_failed",
2108 format!(
2109 "cannot decode query payload with unsupported codec {:?}",
2110 task.payload_codec
2111 ),
2112 "QueryPayloadDecodeFailed",
2113 ));
2114 }
2115
2116 if !self.workflows.contains_key(&task.workflow_type) {
2117 return Err(QueryTaskExecutionFailure::new(
2118 "query_workflow_type_not_registered",
2119 format!("no workflow registered for type {:?}", task.workflow_type),
2120 "WorkflowTypeNotRegistered",
2121 ));
2122 }
2123
2124 let Some(handlers) = self.queries.get(&task.workflow_type) else {
2125 return Err(QueryTaskExecutionFailure::new(
2126 "query_handler_unavailable",
2127 format!(
2128 "query handlers are unavailable for workflow type {:?}",
2129 task.workflow_type
2130 ),
2131 "QueryHandlerUnavailable",
2132 ));
2133 };
2134 let Some(query) = handlers.get(&task.query_name) else {
2135 return Err(QueryTaskExecutionFailure::new(
2136 "rejected_unknown_query",
2137 format!("unknown query {:?}", task.query_name),
2138 "QueryFailed",
2139 ));
2140 };
2141
2142 let args = decode_task_arguments(task.query_arguments.as_ref(), &task.payload_codec)
2143 .map_err(|error| {
2144 QueryTaskExecutionFailure::new(
2145 "query_payload_decode_failed",
2146 format!("cannot decode query arguments: {error}"),
2147 "QueryPayloadDecodeFailed",
2148 )
2149 })?;
2150 let workflow_input =
2151 decode_task_arguments(task.workflow_arguments.as_ref(), &task.payload_codec).map_err(
2152 |error| {
2153 QueryTaskExecutionFailure::new(
2154 "query_workflow_state_unavailable",
2155 format!("cannot decode workflow start input: {error}"),
2156 "QueryWorkflowStateUnavailable",
2157 )
2158 },
2159 )?;
2160 hydrate_query_history_from_export(&mut task).map_err(|error| {
2161 QueryTaskExecutionFailure::new(
2162 "query_workflow_state_unavailable",
2163 format!("cannot restore query history snapshot: {error}"),
2164 "QueryWorkflowStateUnavailable",
2165 )
2166 })?;
2167 enrich_query_history_from_export(&mut task).map_err(|error| {
2168 QueryTaskExecutionFailure::new(
2169 "query_workflow_state_unavailable",
2170 format!("cannot restore compact query history payloads: {error}"),
2171 "QueryWorkflowStateUnavailable",
2172 )
2173 })?;
2174 let signal_events = query_signal_events(&task).map_err(|error| {
2175 QueryTaskExecutionFailure::new(
2176 "query_workflow_state_unavailable",
2177 format!("cannot decode committed workflow signals: {error}"),
2178 "QueryWorkflowStateUnavailable",
2179 )
2180 })?;
2181 let history_events = Arc::new(std::mem::take(&mut task.history_events));
2182 let context = QueryContext {
2183 workflow_id: task.workflow_id,
2184 run_id: task.run_id,
2185 workflow_type: task.workflow_type.clone(),
2186 run_status: task.run_status,
2187 workflow_input,
2188 history_events: Arc::clone(&history_events),
2189 signal_events: Arc::new(signal_events),
2190 };
2191
2192 let future = match query {
2193 RegisteredQuery::Snapshot(handler) => handler(context, args),
2194 RegisteredQuery::Replayed {
2195 state_type,
2196 handler,
2197 } => {
2198 let workflow = self
2199 .workflows
2200 .get(&task.workflow_type)
2201 .expect("workflow registration was checked above");
2202 if workflow.state_type != Some(*state_type) {
2203 return Err(QueryTaskExecutionFailure::new(
2204 "query_workflow_state_unavailable",
2205 "replayed query state type does not match its workflow registration",
2206 "QueryWorkflowStateUnavailable",
2207 ));
2208 }
2209 let replay = workflow.replay.as_ref().ok_or_else(|| {
2210 QueryTaskExecutionFailure::new(
2211 "query_workflow_state_unavailable",
2212 format!(
2213 "workflow type {:?} is not registered for instance-state replay",
2214 task.workflow_type
2215 ),
2216 "QueryWorkflowStateUnavailable",
2217 )
2218 })?;
2219 let workflow_state = Arc::new(Mutex::new(
2220 WorkflowState::new(
2221 history_events.as_ref().clone(),
2222 self.task_queue.clone(),
2223 task.payload_codec,
2224 None,
2225 )
2226 .map_err(|error| {
2227 QueryTaskExecutionFailure::new(
2228 "query_workflow_state_unavailable",
2229 format!("workflow replay failed before query: {error}"),
2230 "QueryWorkflowStateUnavailable",
2231 )
2232 })?,
2233 ));
2234 let workflow_context = WorkflowContext {
2235 state: workflow_state,
2236 };
2237 let mut invocation =
2238 replay(workflow_context.clone(), context.workflow_input.clone());
2239 let mut cx = TaskContext::from_waker(noop_waker_ref());
2240 match invocation.future.as_mut().poll(&mut cx) {
2241 Poll::Ready(Ok(_)) => {
2242 workflow_context
2243 .ensure_history_consumed()
2244 .map_err(|error| {
2245 QueryTaskExecutionFailure::new(
2246 "query_workflow_state_unavailable",
2247 format!("workflow replay failed before query: {error}"),
2248 "QueryWorkflowStateUnavailable",
2249 )
2250 })?;
2251 }
2252 Poll::Ready(Err(error)) => {
2253 return Err(QueryTaskExecutionFailure::new(
2254 "query_workflow_state_unavailable",
2255 format!("workflow replay failed before query: {error}"),
2256 "QueryWorkflowStateUnavailable",
2257 ));
2258 }
2259 Poll::Pending => {
2260 let commands = workflow_context.take_commands().map_err(|error| {
2261 QueryTaskExecutionFailure::new(
2262 "query_workflow_state_unavailable",
2263 format!("workflow replay failed before query: {error}"),
2264 "QueryWorkflowStateUnavailable",
2265 )
2266 })?;
2267 if commands.is_empty()
2268 && !workflow_context
2269 .matched_recorded_pending()
2270 .map_err(|error| {
2271 QueryTaskExecutionFailure::new(
2272 "query_workflow_state_unavailable",
2273 format!("workflow replay failed before query: {error}"),
2274 "QueryWorkflowStateUnavailable",
2275 )
2276 })?
2277 {
2278 return Err(QueryTaskExecutionFailure::new(
2279 "query_workflow_state_unavailable",
2280 "workflow replay yielded without a durable command",
2281 "QueryWorkflowStateUnavailable",
2282 ));
2283 }
2284 }
2285 }
2286 let state = (invocation.snapshot)().map_err(|error| {
2287 QueryTaskExecutionFailure::new(
2288 "query_workflow_state_unavailable",
2289 format!("cannot snapshot replayed workflow state: {error}"),
2290 "QueryWorkflowStateUnavailable",
2291 )
2292 })?;
2293 handler(context, state, args).map_err(|message| {
2294 QueryTaskExecutionFailure::new(
2295 "query_workflow_state_unavailable",
2296 message,
2297 "QueryWorkflowStateUnavailable",
2298 )
2299 })?
2300 }
2301 };
2302
2303 future.await.map_err(|error| {
2304 QueryTaskExecutionFailure::new("query_rejected", error.to_string(), "QueryFailed")
2305 })
2306 }
2307
2308 fn execute_workflow_task(&self, task: WorkflowTask) -> Result<Vec<Value>> {
2309 let workflow = self
2310 .workflows
2311 .get(&task.workflow_type)
2312 .ok_or_else(|| Error::WorkflowNotRegistered(task.workflow_type.clone()))?;
2313 let input = decode_task_arguments(task.arguments.as_ref(), &task.payload_codec)?;
2314 let resume_signal = decode_resume_signal(&task)?;
2315 let state = Arc::new(Mutex::new(WorkflowState::new(
2316 task.history_events,
2317 self.task_queue.clone(),
2318 task.payload_codec.clone(),
2319 resume_signal,
2320 )?));
2321 let ctx = WorkflowContext { state };
2322 let mut future = (workflow.execute)(ctx.clone(), input);
2323 let mut cx = TaskContext::from_waker(noop_waker_ref());
2324
2325 match future.as_mut().poll(&mut cx) {
2326 Poll::Ready(Ok(result)) => {
2327 ctx.ensure_history_consumed()?;
2328 let result = encode_value_envelope(&result, &task.payload_codec)?;
2329 Ok(vec![json!({
2330 "type": "complete_workflow",
2331 "result": result
2332 })])
2333 }
2334 Poll::Ready(Err(error)) => Err(error),
2335 Poll::Pending => {
2336 let commands = ctx.take_commands()?;
2337 if commands.is_empty() && !ctx.matched_recorded_pending()? {
2338 Err(Error::WorkflowYieldedWithoutCommand)
2339 } else {
2340 Ok(commands)
2341 }
2342 }
2343 }
2344 }
2345
2346 async fn execute_activity_task(&self, task: ActivityTask) -> Result<Value> {
2347 let handler = self
2348 .activities
2349 .get(&task.activity_type)
2350 .ok_or_else(|| Error::ActivityNotRegistered(task.activity_type.clone()))?;
2351 let args = decode_task_arguments(task.arguments.as_ref(), &task.payload_codec)?;
2352 let attempt_id = task
2353 .activity_attempt_id
2354 .clone()
2355 .or(task.attempt_id.clone())
2356 .unwrap_or_default();
2357 let lease_owner = task
2358 .lease_owner
2359 .clone()
2360 .unwrap_or_else(|| self.worker_id.clone());
2361 let ctx = ActivityContext {
2362 client: self.client.clone(),
2363 task_id: task.task_id,
2364 activity_attempt_id: attempt_id,
2365 lease_owner,
2366 activity_type: task.activity_type,
2367 attempt_number: task.attempt_number,
2368 task_queue: self.task_queue.clone(),
2369 worker_id: self.worker_id.clone(),
2370 };
2371
2372 handler(ctx, args).await
2373 }
2374}
2375
2376fn poller_result(
2377 kind: &str,
2378 result: std::result::Result<Result<()>, tokio::task::JoinError>,
2379) -> Result<()> {
2380 match result {
2381 Ok(result) => result,
2382 Err(error) => Err(Error::WorkerLoop(format!(
2383 "{kind} poller join error: {error}"
2384 ))),
2385 }
2386}
2387
2388fn optional_poller_result(
2389 kind: &str,
2390 result: Option<std::result::Result<Result<()>, tokio::task::JoinError>>,
2391) -> Result<()> {
2392 match result {
2393 Some(result) => poller_result(kind, result),
2394 None => Ok(()),
2395 }
2396}
2397
2398async fn join_pollers(
2399 workflow_poller: Option<tokio::task::JoinHandle<Result<()>>>,
2400 activity_poller: Option<tokio::task::JoinHandle<Result<()>>>,
2401 query_poller: Option<tokio::task::JoinHandle<Result<()>>>,
2402) -> Result<()> {
2403 let mut first_error = None;
2404
2405 if let Some(handle) = workflow_poller {
2406 if let Err(error) = poller_result("workflow", handle.await) {
2407 first_error.get_or_insert(error);
2408 }
2409 }
2410
2411 if let Some(handle) = activity_poller {
2412 if let Err(error) = poller_result("activity", handle.await) {
2413 first_error.get_or_insert(error);
2414 }
2415 }
2416
2417 if let Some(handle) = query_poller {
2418 if let Err(error) = poller_result("query", handle.await) {
2419 first_error.get_or_insert(error);
2420 }
2421 }
2422
2423 if let Some(error) = first_error {
2424 Err(error)
2425 } else {
2426 Ok(())
2427 }
2428}
2429
2430fn default_worker_id() -> String {
2431 let millis = SystemTime::now()
2432 .duration_since(UNIX_EPOCH)
2433 .unwrap_or_default()
2434 .as_millis();
2435 format!("rust-worker-{}-{millis}", std::process::id())
2436}
2437
2438fn unique_request_id(prefix: &str) -> String {
2439 let nanos = SystemTime::now()
2440 .duration_since(UNIX_EPOCH)
2441 .unwrap_or_default()
2442 .as_nanos();
2443 format!("{prefix}-{}-{nanos}", std::process::id())
2444}
2445
2446#[derive(Debug)]
2447struct QueryTaskExecutionFailure {
2448 reason: String,
2449 message: String,
2450 failure_type: String,
2451}
2452
2453impl QueryTaskExecutionFailure {
2454 fn new(
2455 reason: impl Into<String>,
2456 message: impl Into<String>,
2457 failure_type: impl Into<String>,
2458 ) -> Self {
2459 Self {
2460 reason: reason.into(),
2461 message: message.into(),
2462 failure_type: failure_type.into(),
2463 }
2464 }
2465}
2466
2467#[derive(Clone, Debug)]
2473pub struct WorkflowInstance<S> {
2474 state: Arc<Mutex<S>>,
2475}
2476
2477impl<S> WorkflowInstance<S> {
2478 fn new(state: S) -> Self {
2479 Self {
2480 state: Arc::new(Mutex::new(state)),
2481 }
2482 }
2483
2484 pub fn read<R>(&self, reader: impl FnOnce(&S) -> R) -> Result<R> {
2486 let state = self
2487 .state
2488 .lock()
2489 .map_err(|_| Error::WorkflowStatePoisoned)?;
2490 Ok(reader(&state))
2491 }
2492
2493 pub fn update<R>(&self, transition: impl FnOnce(&mut S) -> R) -> Result<R> {
2495 let mut state = self
2496 .state
2497 .lock()
2498 .map_err(|_| Error::WorkflowStatePoisoned)?;
2499 Ok(transition(&mut state))
2500 }
2501}
2502
2503impl<S: Clone> WorkflowInstance<S> {
2504 fn snapshot(&self) -> Result<S> {
2505 self.read(Clone::clone)
2506 }
2507}
2508
2509#[derive(Clone, Debug)]
2510pub struct WorkflowContext {
2511 state: Arc<Mutex<WorkflowState>>,
2512}
2513
2514impl WorkflowContext {
2515 pub fn activity<T: Serialize>(
2516 &self,
2517 activity_type: impl Into<String>,
2518 args: T,
2519 ) -> ActivityCall {
2520 self.activity_on_queue(activity_type, None::<String>, args)
2521 }
2522
2523 pub fn activity_on_queue<T, Q>(
2524 &self,
2525 activity_type: impl Into<String>,
2526 task_queue: Option<Q>,
2527 args: T,
2528 ) -> ActivityCall
2529 where
2530 T: Serialize,
2531 Q: Into<String>,
2532 {
2533 ActivityCall {
2534 ctx: self.clone(),
2535 activity_type: activity_type.into(),
2536 task_queue: task_queue.map(Into::into),
2537 args: Some(serde_json::to_value(args).map_err(Error::from)),
2538 scheduled: false,
2539 }
2540 }
2541
2542 pub fn wait_signal(&self, signal_name: impl Into<String>) -> SignalCall {
2543 SignalCall {
2544 ctx: self.clone(),
2545 signal_name: signal_name.into(),
2546 opened_wait: false,
2547 matched_pending: false,
2548 }
2549 }
2550
2551 pub fn sleep(&self, duration: Duration) -> TimerCall {
2572 let delay_seconds = duration
2573 .as_secs()
2574 .checked_add(u64::from(duration.subsec_nanos() > 0));
2575 TimerCall {
2576 ctx: self.clone(),
2577 delay_seconds,
2578 scheduled: false,
2579 matched_pending: false,
2580 }
2581 }
2582
2583 pub fn start_timer(&self, duration: Duration) -> TimerCall {
2585 self.sleep(duration)
2586 }
2587
2588 fn take_commands(&self) -> Result<Vec<Value>> {
2589 let mut state = self
2590 .state
2591 .lock()
2592 .map_err(|_| Error::WorkflowStatePoisoned)?;
2593 Ok(std::mem::take(&mut state.commands))
2594 }
2595
2596 fn matched_recorded_pending(&self) -> Result<bool> {
2597 let state = self
2598 .state
2599 .lock()
2600 .map_err(|_| Error::WorkflowStatePoisoned)?;
2601 Ok(state.matched_recorded_pending)
2602 }
2603
2604 fn ensure_history_consumed(&self) -> Result<()> {
2605 let state = self
2606 .state
2607 .lock()
2608 .map_err(|_| Error::WorkflowStatePoisoned)?;
2609 if let Some(command) = state.recorded_commands.get(state.command_cursor) {
2610 return Err(Error::NonDeterministicReplay(ReplayFailure::new(
2611 "recorded_commands_unconsumed",
2612 Some(command.sequence()),
2613 Some(command.shape().to_string()),
2614 Some("workflow completion".to_string()),
2615 "workflow completed before consuming all recorded durable commands",
2616 )));
2617 }
2618 Ok(())
2619 }
2620}
2621
2622#[derive(Debug)]
2623struct WorkflowState {
2624 history: Vec<HistoryEvent>,
2625 task_queue: String,
2626 payload_codec: String,
2627 resume_signal: Option<ResumeSignal>,
2628 recorded_commands: Vec<RecordedCommand>,
2629 command_cursor: usize,
2630 matched_recorded_pending: bool,
2631 signal_cursors: HashMap<String, usize>,
2632 commands: Vec<Value>,
2633}
2634
2635impl WorkflowState {
2636 fn new(
2637 history: Vec<HistoryEvent>,
2638 task_queue: String,
2639 payload_codec: String,
2640 resume_signal: Option<ResumeSignal>,
2641 ) -> Result<Self> {
2642 let recorded_commands = recorded_commands(&history, &payload_codec)?;
2643 Ok(Self {
2644 history,
2645 task_queue,
2646 payload_codec,
2647 resume_signal,
2648 recorded_commands,
2649 command_cursor: 0,
2650 matched_recorded_pending: false,
2651 signal_cursors: HashMap::new(),
2652 commands: Vec::new(),
2653 })
2654 }
2655}
2656
2657#[derive(Clone, Debug)]
2658enum RecordedCommand {
2659 Activity {
2660 sequence: u64,
2661 activity_type: Option<String>,
2662 result: Option<Value>,
2663 },
2664 Timer {
2665 sequence: u64,
2666 delay_seconds: u64,
2667 fired: bool,
2668 },
2669 SignalWait {
2670 sequence: u64,
2671 signal_name: Option<String>,
2672 },
2673}
2674
2675impl RecordedCommand {
2676 fn sequence(&self) -> u64 {
2677 match self {
2678 Self::Activity { sequence, .. }
2679 | Self::Timer { sequence, .. }
2680 | Self::SignalWait { sequence, .. } => *sequence,
2681 }
2682 }
2683
2684 fn shape(&self) -> &'static str {
2685 match self {
2686 Self::Activity { .. } => "activity",
2687 Self::Timer { .. } => "timer",
2688 Self::SignalWait { .. } => "signal wait",
2689 }
2690 }
2691}
2692
2693#[derive(Clone, Debug)]
2694struct ResumeSignal {
2695 signal_id: Option<String>,
2696 signal_name: String,
2697 arguments: Vec<Value>,
2698}
2699
2700pub struct ActivityCall {
2701 ctx: WorkflowContext,
2702 activity_type: String,
2703 task_queue: Option<String>,
2704 args: Option<Result<Value>>,
2705 scheduled: bool,
2706}
2707
2708impl Future for ActivityCall {
2709 type Output = Result<Value>;
2710
2711 fn poll(mut self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
2712 let ctx = self.ctx.clone();
2713 let mut state = match ctx.state.lock() {
2714 Ok(state) => state,
2715 Err(_) => return Poll::Ready(Err(Error::WorkflowStatePoisoned)),
2716 };
2717
2718 if self.scheduled {
2719 return Poll::Pending;
2720 }
2721
2722 if let Some(recorded) = state.recorded_commands.get(state.command_cursor).cloned() {
2723 let sequence = recorded.sequence();
2724 match recorded {
2725 RecordedCommand::Activity {
2726 activity_type,
2727 result,
2728 ..
2729 } => {
2730 if let Some(recorded_type) = activity_type {
2731 if recorded_type != self.activity_type {
2732 return Poll::Ready(Err(Error::NonDeterministicReplay(
2733 ReplayFailure::new(
2734 "recorded_command_detail_mismatch",
2735 Some(sequence),
2736 Some(format!("activity:{recorded_type}")),
2737 Some(format!("activity:{}", self.activity_type)),
2738 "recorded activity type differs from the current workflow command",
2739 ),
2740 )));
2741 }
2742 }
2743 state.command_cursor += 1;
2744 if let Some(value) = result {
2745 return Poll::Ready(Ok(value));
2746 }
2747 state.matched_recorded_pending = true;
2748 self.scheduled = true;
2749 return Poll::Pending;
2750 }
2751 other => {
2752 return Poll::Ready(Err(command_mismatch(
2753 &other,
2754 format!("activity:{}", self.activity_type),
2755 )));
2756 }
2757 }
2758 }
2759
2760 if !self.scheduled {
2761 let task_queue = self
2762 .task_queue
2763 .clone()
2764 .unwrap_or_else(|| state.task_queue.clone());
2765 let args = match self.args.take().unwrap_or(Ok(Value::Null)) {
2766 Ok(args) => args,
2767 Err(error) => return Poll::Ready(Err(error)),
2768 };
2769 let arguments = normalize_arguments(args);
2770 let envelope = match encode_value_envelope(&arguments, &state.payload_codec) {
2771 Ok(envelope) => envelope,
2772 Err(error) => return Poll::Ready(Err(error)),
2773 };
2774
2775 state.commands.push(json!({
2776 "type": "schedule_activity",
2777 "activity_type": self.activity_type.clone(),
2778 "queue": task_queue,
2779 "arguments": envelope
2780 }));
2781 self.scheduled = true;
2782 }
2783
2784 Poll::Pending
2785 }
2786}
2787
2788pub struct TimerCall {
2790 ctx: WorkflowContext,
2791 delay_seconds: Option<u64>,
2792 scheduled: bool,
2793 matched_pending: bool,
2794}
2795
2796impl Future for TimerCall {
2797 type Output = Result<()>;
2798
2799 fn poll(mut self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
2800 if self.matched_pending {
2801 return Poll::Pending;
2802 }
2803
2804 let ctx = self.ctx.clone();
2805 let Some(requested_delay) = self.delay_seconds else {
2806 return Poll::Ready(Err(Error::TimerDurationOverflow));
2807 };
2808 let mut state = match ctx.state.lock() {
2809 Ok(state) => state,
2810 Err(_) => return Poll::Ready(Err(Error::WorkflowStatePoisoned)),
2811 };
2812
2813 if let Some(recorded) = state.recorded_commands.get(state.command_cursor).cloned() {
2814 match recorded {
2815 RecordedCommand::Timer {
2816 sequence,
2817 delay_seconds,
2818 fired,
2819 ..
2820 } => {
2821 if delay_seconds != requested_delay {
2822 return Poll::Ready(Err(Error::NonDeterministicReplay(
2823 ReplayFailure::new(
2824 "timer_delay_mismatch",
2825 Some(sequence),
2826 Some(format!("timer:{delay_seconds}s")),
2827 Some(format!("timer:{requested_delay}s")),
2828 "recorded timer delay differs from the current workflow command",
2829 ),
2830 )));
2831 }
2832 state.command_cursor += 1;
2833 if fired {
2834 return Poll::Ready(Ok(()));
2835 }
2836 state.matched_recorded_pending = true;
2837 self.scheduled = true;
2838 self.matched_pending = true;
2839 return Poll::Pending;
2840 }
2841 other => return Poll::Ready(Err(command_mismatch(&other, "timer"))),
2842 }
2843 }
2844
2845 if !self.scheduled {
2846 state.commands.push(json!({
2847 "type": "start_timer",
2848 "delay_seconds": requested_delay,
2849 }));
2850 self.scheduled = true;
2851 }
2852
2853 Poll::Pending
2854 }
2855}
2856
2857fn command_mismatch(recorded: &RecordedCommand, actual: impl Into<String>) -> Error {
2858 Error::NonDeterministicReplay(ReplayFailure::new(
2859 "recorded_command_mismatch",
2860 Some(recorded.sequence()),
2861 Some(recorded.shape().to_string()),
2862 Some(actual.into()),
2863 "current workflow command does not match the recorded durable command sequence",
2864 ))
2865}
2866
2867pub struct SignalCall {
2868 ctx: WorkflowContext,
2869 signal_name: String,
2870 opened_wait: bool,
2871 matched_pending: bool,
2872}
2873
2874impl Future for SignalCall {
2875 type Output = Result<Vec<Value>>;
2876
2877 fn poll(mut self: Pin<&mut Self>, _cx: &mut TaskContext<'_>) -> Poll<Self::Output> {
2878 if self.matched_pending {
2879 return Poll::Pending;
2880 }
2881
2882 let ctx = self.ctx.clone();
2883 let mut state = match ctx.state.lock() {
2884 Ok(state) => state,
2885 Err(_) => return Poll::Ready(Err(Error::WorkflowStatePoisoned)),
2886 };
2887
2888 let signals = match signal_values(
2889 &state.history,
2890 &self.signal_name,
2891 &state.payload_codec,
2892 state.resume_signal.as_ref(),
2893 ) {
2894 Ok(signals) => signals,
2895 Err(error) => return Poll::Ready(Err(error)),
2896 };
2897 let cursor = *state.signal_cursors.get(&self.signal_name).unwrap_or(&0);
2898
2899 if let Some(recorded) = state.recorded_commands.get(state.command_cursor).cloned() {
2900 match recorded {
2901 RecordedCommand::SignalWait {
2902 sequence,
2903 signal_name,
2904 } => {
2905 if let Some(recorded_name) = signal_name {
2906 if recorded_name != self.signal_name {
2907 return Poll::Ready(Err(Error::NonDeterministicReplay(
2908 ReplayFailure::new(
2909 "recorded_command_detail_mismatch",
2910 Some(sequence),
2911 Some(format!("signal wait:{recorded_name}")),
2912 Some(format!("signal wait:{}", self.signal_name)),
2913 "recorded signal name differs from the current workflow command",
2914 ),
2915 )));
2916 }
2917 }
2918
2919 state.command_cursor += 1;
2920 if cursor < signals.len() {
2921 state
2922 .signal_cursors
2923 .insert(self.signal_name.clone(), cursor + 1);
2924 return Poll::Ready(Ok(signals[cursor].clone()));
2925 }
2926
2927 state.matched_recorded_pending = true;
2928 self.opened_wait = true;
2929 self.matched_pending = true;
2930 return Poll::Pending;
2931 }
2932 other => {
2933 return Poll::Ready(Err(command_mismatch(
2934 &other,
2935 format!("signal wait:{}", self.signal_name),
2936 )));
2937 }
2938 }
2939 }
2940
2941 if cursor < signals.len() {
2942 state
2943 .signal_cursors
2944 .insert(self.signal_name.clone(), cursor + 1);
2945 return Poll::Ready(Ok(signals[cursor].clone()));
2946 }
2947
2948 if !self.opened_wait {
2949 state.commands.push(json!({
2950 "type": "open_condition_wait",
2951 "condition_key": format!("signal:{}", self.signal_name)
2952 }));
2953 self.opened_wait = true;
2954 }
2955
2956 Poll::Pending
2957 }
2958}
2959
2960#[derive(Clone, Debug)]
2961pub struct ActivityContext {
2962 client: Client,
2963 pub task_id: String,
2964 pub activity_attempt_id: String,
2965 pub lease_owner: String,
2966 pub activity_type: String,
2967 pub attempt_number: u64,
2968 pub task_queue: String,
2969 pub worker_id: String,
2970}
2971
2972impl ActivityContext {
2973 pub async fn heartbeat<T: Serialize>(&self, details: T) -> Result<ActivityHeartbeatResponse> {
2974 self.client
2975 .heartbeat_activity_task(
2976 &self.task_id,
2977 &self.activity_attempt_id,
2978 &self.lease_owner,
2979 serde_json::to_value(details)?,
2980 )
2981 .await
2982 }
2983}
2984
2985fn decode_task_arguments(value: Option<&Value>, codec: &str) -> Result<Value> {
2986 match value {
2987 Some(value) => Ok(normalize_arguments(decode_wire_value(value, codec)?)),
2988 None => Ok(Value::Array(Vec::new())),
2989 }
2990}
2991
2992fn decode_resume_signal(task: &WorkflowTask) -> Result<Option<ResumeSignal>> {
2993 let Some(signal_name) = task
2994 .signal_name
2995 .as_deref()
2996 .filter(|value| !value.is_empty())
2997 else {
2998 return Ok(None);
2999 };
3000 let Some(arguments) = task.signal_arguments.as_ref() else {
3001 return Ok(None);
3002 };
3003
3004 let decoded = normalize_arguments(decode_wire_value(arguments, &task.payload_codec)?);
3005 let Value::Array(arguments) = decoded else {
3006 unreachable!("normalize_arguments always returns an array");
3007 };
3008
3009 Ok(Some(ResumeSignal {
3010 signal_id: task.workflow_signal_id.clone(),
3011 signal_name: signal_name.to_string(),
3012 arguments,
3013 }))
3014}
3015
3016fn normalize_arguments(value: Value) -> Value {
3017 match value {
3018 Value::Null => Value::Array(Vec::new()),
3019 Value::Array(_) => value,
3020 other => Value::Array(vec![other]),
3021 }
3022}
3023
3024fn recorded_commands(
3025 events: &[HistoryEvent],
3026 fallback_codec: &str,
3027) -> Result<Vec<RecordedCommand>> {
3028 let mut events_by_sequence: BTreeMap<u64, Vec<&HistoryEvent>> = BTreeMap::new();
3029
3030 for event in events {
3031 let is_activity = matches!(
3032 event.event_type.as_str(),
3033 "ActivityScheduled"
3034 | "ActivityStarted"
3035 | "ActivityHeartbeatRecorded"
3036 | "ActivityRetryScheduled"
3037 | "ActivityCompleted"
3038 | "ActivityFailed"
3039 | "ActivityCancelled"
3040 | "ActivityTimedOut"
3041 );
3042 let is_workflow_timer = matches!(
3043 event.event_type.as_str(),
3044 "TimerScheduled" | "TimerCancelled" | "TimerFired"
3045 ) && !is_internal_timer_event(event);
3046 let is_signal_wait = is_recorded_signal_wait_event(event);
3047 if !is_activity && !is_workflow_timer && !is_signal_wait {
3048 continue;
3049 }
3050
3051 let sequence = durable_event_sequence(event).ok_or_else(|| {
3052 Error::NonDeterministicReplay(ReplayFailure::new(
3053 "durable_command_sequence_missing",
3054 None,
3055 Some("positive workflow sequence".to_string()),
3056 Some(event.event_type.clone()),
3057 "durable command history event has no workflow sequence",
3058 ))
3059 })?;
3060 if sequence == 0 {
3061 return Err(Error::NonDeterministicReplay(ReplayFailure::new(
3062 "durable_command_sequence_invalid",
3063 Some(sequence),
3064 Some("positive workflow sequence".to_string()),
3065 Some(sequence.to_string()),
3066 "durable command history uses an invalid workflow sequence",
3067 )));
3068 }
3069 events_by_sequence.entry(sequence).or_default().push(event);
3070 }
3071
3072 events_by_sequence
3073 .into_iter()
3074 .map(|(sequence, sequence_events)| {
3075 let activity_events: Vec<_> = sequence_events
3076 .iter()
3077 .copied()
3078 .filter(|event| event.event_type.starts_with("Activity"))
3079 .collect();
3080 let timer_events: Vec<_> = sequence_events
3081 .iter()
3082 .copied()
3083 .filter(|event| event.event_type.starts_with("Timer"))
3084 .collect();
3085 let signal_wait_events: Vec<_> = sequence_events
3086 .iter()
3087 .copied()
3088 .filter(|event| is_recorded_signal_wait_event(event))
3089 .collect();
3090
3091 let command_kind_count = usize::from(!activity_events.is_empty())
3092 + usize::from(!timer_events.is_empty())
3093 + usize::from(!signal_wait_events.is_empty());
3094 if command_kind_count > 1 {
3095 let actual = [
3096 (!activity_events.is_empty()).then_some("activity"),
3097 (!timer_events.is_empty()).then_some("timer"),
3098 (!signal_wait_events.is_empty()).then_some("signal wait"),
3099 ]
3100 .into_iter()
3101 .flatten()
3102 .collect::<Vec<_>>()
3103 .join(" and ");
3104 return Err(invalid_recorded_history(
3105 "durable_command_sequence_collision",
3106 sequence,
3107 "one durable command kind",
3108 &actual,
3109 "one workflow sequence records more than one durable command kind",
3110 ));
3111 }
3112
3113 if !activity_events.is_empty() {
3114 let scheduled_count = activity_events
3115 .iter()
3116 .filter(|event| event.event_type == "ActivityScheduled")
3117 .count();
3118 if scheduled_count > 1 {
3119 return Err(invalid_recorded_history(
3120 "duplicate_activity_schedule",
3121 sequence,
3122 "at most one ActivityScheduled event",
3123 "multiple ActivityScheduled events",
3124 "activity history schedules more than one command at one workflow sequence",
3125 ));
3126 }
3127 let activity_type = activity_events.iter().find_map(|event| {
3128 event
3129 .payload
3130 .get("activity_type")
3131 .or_else(|| event.payload.get("activity_name"))
3132 .and_then(Value::as_str)
3133 .map(str::to_string)
3134 });
3135 if activity_events.iter().filter_map(|event| {
3136 event
3137 .payload
3138 .get("activity_type")
3139 .or_else(|| event.payload.get("activity_name"))
3140 .and_then(Value::as_str)
3141 }).any(|candidate| Some(candidate) != activity_type.as_deref()) {
3142 return Err(invalid_recorded_history(
3143 "activity_identity_mismatch",
3144 sequence,
3145 activity_type.as_deref().unwrap_or("one activity identity"),
3146 "conflicting activity identities",
3147 "activity lifecycle events at one workflow sequence disagree on identity",
3148 ));
3149 }
3150 let completed: Vec<_> = activity_events
3151 .iter()
3152 .copied()
3153 .filter(|event| event.event_type == "ActivityCompleted")
3154 .collect();
3155 if completed.len() > 1 {
3156 return Err(invalid_recorded_history(
3157 "duplicate_activity_completion",
3158 sequence,
3159 "one ActivityCompleted event",
3160 "multiple ActivityCompleted events",
3161 "activity history contains more than one completion for a workflow sequence",
3162 ));
3163 }
3164 let result = completed
3165 .first()
3166 .map(|event| {
3167 let codec = event
3168 .payload
3169 .get("payload_codec")
3170 .and_then(Value::as_str)
3171 .unwrap_or(fallback_codec);
3172 decode_wire_value(
3173 event.payload.get("result").unwrap_or(&Value::Null),
3174 codec,
3175 )
3176 })
3177 .transpose()?;
3178 return Ok(RecordedCommand::Activity {
3179 sequence,
3180 activity_type,
3181 result,
3182 });
3183 }
3184
3185 if !signal_wait_events.is_empty() {
3186 let opened_count = signal_wait_events
3187 .iter()
3188 .filter(|event| {
3189 matches!(
3190 event.event_type.as_str(),
3191 "SignalWaitOpened" | "ConditionWaitOpened"
3192 )
3193 })
3194 .count();
3195 if opened_count > 1 {
3196 return Err(invalid_recorded_history(
3197 "duplicate_signal_wait_open",
3198 sequence,
3199 "at most one signal wait open event",
3200 "multiple signal wait open events",
3201 "signal history opens more than one durable wait at one workflow sequence",
3202 ));
3203 }
3204
3205 let signal_name = signal_wait_events
3206 .iter()
3207 .find_map(|event| recorded_signal_wait_name(event));
3208 if signal_wait_events
3209 .iter()
3210 .filter_map(|event| recorded_signal_wait_name(event))
3211 .any(|candidate| Some(candidate.as_str()) != signal_name.as_deref())
3212 {
3213 return Err(invalid_recorded_history(
3214 "signal_wait_identity_mismatch",
3215 sequence,
3216 signal_name.as_deref().unwrap_or("one signal name"),
3217 "conflicting signal names",
3218 "signal wait lifecycle events at one workflow sequence disagree on identity",
3219 ));
3220 }
3221 return Ok(RecordedCommand::SignalWait {
3222 sequence,
3223 signal_name,
3224 });
3225 }
3226
3227 let scheduled: Vec<_> = timer_events
3228 .iter()
3229 .copied()
3230 .filter(|event| event.event_type == "TimerScheduled")
3231 .collect();
3232 let fired: Vec<_> = timer_events
3233 .iter()
3234 .copied()
3235 .filter(|event| event.event_type == "TimerFired")
3236 .collect();
3237 if scheduled.len() != 1 {
3238 return Err(invalid_recorded_history(
3239 "timer_schedule_missing_or_duplicate",
3240 sequence,
3241 "one TimerScheduled event",
3242 &format!("{} TimerScheduled events", scheduled.len()),
3243 "timer replay requires exactly one recorded schedule event",
3244 ));
3245 }
3246 if fired.len() > 1 {
3247 return Err(invalid_recorded_history(
3248 "duplicate_timer_fire",
3249 sequence,
3250 "at most one TimerFired event",
3251 "multiple TimerFired events",
3252 "timer history contains more than one fire event for a workflow sequence",
3253 ));
3254 }
3255
3256 let scheduled = scheduled[0];
3257 let timer_id = required_history_string(scheduled, "timer_id", sequence)?;
3258 let delay_seconds = required_history_u64(scheduled, "delay_seconds", sequence)?;
3259 if let Some(fired) = fired.first() {
3260 let fired_timer_id = required_history_string(fired, "timer_id", sequence)?;
3261 if fired_timer_id != timer_id {
3262 return Err(invalid_recorded_history(
3263 "timer_identity_mismatch",
3264 sequence,
3265 &timer_id,
3266 &fired_timer_id,
3267 "TimerFired does not correspond to the recorded TimerScheduled event",
3268 ));
3269 }
3270 let fired_delay = required_history_u64(fired, "delay_seconds", sequence)?;
3271 if fired_delay != delay_seconds {
3272 return Err(invalid_recorded_history(
3273 "timer_history_delay_mismatch",
3274 sequence,
3275 &delay_seconds.to_string(),
3276 &fired_delay.to_string(),
3277 "TimerScheduled and TimerFired record different delays",
3278 ));
3279 }
3280 }
3281
3282 Ok(RecordedCommand::Timer {
3283 sequence,
3284 delay_seconds,
3285 fired: !fired.is_empty(),
3286 })
3287 })
3288 .collect()
3289}
3290
3291fn durable_event_sequence(event: &HistoryEvent) -> Option<u64> {
3292 event
3293 .payload
3294 .get("sequence")
3295 .or_else(|| event.payload.get("workflow_sequence"))
3296 .or_else(|| event.raw.get("sequence"))
3297 .or_else(|| event.raw.get("workflow_sequence"))
3298 .and_then(value_as_u64)
3299}
3300
3301fn is_internal_timer_event(event: &HistoryEvent) -> bool {
3302 matches!(
3303 event
3304 .payload
3305 .get("timer_kind")
3306 .or_else(|| event.raw.get("timer_kind"))
3307 .and_then(Value::as_str),
3308 Some("condition_timeout" | "signal_timeout")
3309 )
3310}
3311
3312fn recorded_signal_wait_name(event: &HistoryEvent) -> Option<String> {
3313 match event.event_type.as_str() {
3314 "SignalWaitOpened" | "SignalApplied" => event
3315 .payload
3316 .get("signal_name")
3317 .or_else(|| event.raw.get("signal_name"))
3318 .and_then(Value::as_str)
3319 .filter(|value| !value.is_empty())
3320 .map(str::to_string),
3321 "ConditionWaitOpened" | "ConditionWaitSatisfied" | "ConditionWaitTimedOut" => event
3322 .payload
3323 .get("condition_key")
3324 .or_else(|| event.raw.get("condition_key"))
3325 .and_then(Value::as_str)
3326 .and_then(|key| key.strip_prefix("signal:"))
3327 .filter(|value| !value.is_empty())
3328 .map(str::to_string),
3329 _ => None,
3330 }
3331}
3332
3333fn is_recorded_signal_wait_event(event: &HistoryEvent) -> bool {
3334 match event.event_type.as_str() {
3335 "SignalWaitOpened" | "SignalApplied" => true,
3336 "ConditionWaitOpened" | "ConditionWaitSatisfied" | "ConditionWaitTimedOut" => event
3337 .payload
3338 .get("condition_key")
3339 .or_else(|| event.raw.get("condition_key"))
3340 .and_then(Value::as_str)
3341 .is_some_and(|key| key.starts_with("signal:")),
3342 _ => false,
3343 }
3344}
3345
3346fn required_history_string(event: &HistoryEvent, field: &str, sequence: u64) -> Result<String> {
3347 event
3348 .payload
3349 .get(field)
3350 .and_then(Value::as_str)
3351 .filter(|value| !value.is_empty())
3352 .map(str::to_string)
3353 .ok_or_else(|| {
3354 invalid_recorded_history(
3355 "timer_history_field_missing",
3356 sequence,
3357 field,
3358 &event.event_type,
3359 "timer history is missing a required identity field",
3360 )
3361 })
3362}
3363
3364fn required_history_u64(event: &HistoryEvent, field: &str, sequence: u64) -> Result<u64> {
3365 event
3366 .payload
3367 .get(field)
3368 .and_then(value_as_u64)
3369 .ok_or_else(|| {
3370 invalid_recorded_history(
3371 "timer_history_field_missing",
3372 sequence,
3373 field,
3374 &event.event_type,
3375 "timer history is missing a required numeric field",
3376 )
3377 })
3378}
3379
3380fn invalid_recorded_history(
3381 reason: &str,
3382 sequence: u64,
3383 expected: &str,
3384 actual: &str,
3385 message: &str,
3386) -> Error {
3387 Error::NonDeterministicReplay(ReplayFailure::new(
3388 reason,
3389 Some(sequence),
3390 Some(expected.to_string()),
3391 Some(actual.to_string()),
3392 message,
3393 ))
3394}
3395
3396fn signal_values(
3397 events: &[HistoryEvent],
3398 signal_name: &str,
3399 fallback_codec: &str,
3400 resume_signal: Option<&ResumeSignal>,
3401) -> Result<Vec<Vec<Value>>> {
3402 let mut signals = Vec::new();
3403
3404 for event in events {
3405 if event.event_type != "SignalApplied" && event.event_type != "SignalReceived" {
3406 continue;
3407 }
3408
3409 if event.payload.get("signal_name").and_then(Value::as_str) != Some(signal_name) {
3410 continue;
3411 }
3412
3413 let codec = event
3414 .payload
3415 .get("payload_codec")
3416 .and_then(Value::as_str)
3417 .unwrap_or(fallback_codec);
3418 let raw = event
3419 .payload
3420 .get("value")
3421 .or_else(|| event.payload.get("input"))
3422 .or_else(|| event.payload.get("arguments"));
3423 let decoded = match raw.filter(|value| !value.is_null()) {
3424 Some(value) => decode_wire_value(value, codec)?,
3425 None => resume_signal
3426 .filter(|signal| resume_signal_matches_event(signal, event, signal_name))
3427 .map(|signal| Value::Array(signal.arguments.clone()))
3428 .unwrap_or_else(|| Value::Array(Vec::new())),
3429 };
3430 let args = match normalize_arguments(decoded) {
3431 Value::Array(values) => values,
3432 _ => unreachable!("normalize_arguments always returns an array"),
3433 };
3434 signals.push(args);
3435 }
3436
3437 Ok(signals)
3438}
3439
3440fn hydrate_query_history_from_export(task: &mut QueryTask) -> Result<()> {
3441 let Some(export_events) = task
3442 .history_export
3443 .as_ref()
3444 .and_then(|export| export.get("history_events"))
3445 .and_then(Value::as_array)
3446 else {
3447 return Ok(());
3448 };
3449
3450 if export_events.len() > task.history_events.len() {
3451 task.history_events = serde_json::from_value(Value::Array(export_events.clone()))?;
3452 }
3453
3454 Ok(())
3455}
3456
3457fn enrich_query_history_from_export(task: &mut QueryTask) -> Result<()> {
3458 let Some(export) = task.history_export.as_ref() else {
3459 return Ok(());
3460 };
3461 let signals = export
3462 .get("signals")
3463 .and_then(Value::as_array)
3464 .cloned()
3465 .unwrap_or_default();
3466 let activities = export
3467 .get("activities")
3468 .and_then(Value::as_array)
3469 .cloned()
3470 .unwrap_or_default();
3471 let export_codec = export
3472 .get("payloads")
3473 .and_then(|payloads| payloads.get("codec"))
3474 .and_then(Value::as_str)
3475 .unwrap_or(&task.payload_codec)
3476 .to_string();
3477 let mut signal_name_offsets: HashMap<String, usize> = HashMap::new();
3478
3479 for event in &mut task.history_events {
3480 if event.event_type == "ActivityCompleted" {
3481 let sequence = event
3482 .payload
3483 .get("sequence")
3484 .or_else(|| event.payload.get("workflow_sequence"))
3485 .and_then(value_as_u64);
3486 let Some(activity) = sequence.and_then(|sequence| {
3487 activities.iter().find(|activity| {
3488 activity.get("sequence").and_then(value_as_u64) == Some(sequence)
3489 })
3490 }) else {
3491 continue;
3492 };
3493 let Some(payload) = event.payload.as_object_mut() else {
3494 continue;
3495 };
3496 if missing_payload(payload.get("result")) {
3497 if let Some(result) = activity
3498 .get("result")
3499 .filter(|value| !missing_payload(Some(value)))
3500 {
3501 payload.insert("result".to_string(), result.clone());
3502 }
3503 }
3504 for field in ["payload_codec", "activity_type"] {
3505 if payload
3506 .get(field)
3507 .and_then(Value::as_str)
3508 .unwrap_or_default()
3509 .is_empty()
3510 {
3511 if let Some(value) = activity.get(field) {
3512 payload.insert(field.to_string(), value.clone());
3513 }
3514 }
3515 }
3516 continue;
3517 }
3518
3519 if event.event_type != "SignalReceived" && event.event_type != "SignalApplied" {
3520 continue;
3521 }
3522 let signal_id = event.payload.get("signal_id").and_then(Value::as_str);
3523 let command_id = event
3524 .payload
3525 .get("workflow_command_id")
3526 .or_else(|| event.raw.get("workflow_command_id"))
3527 .and_then(Value::as_str);
3528 let signal_name = event
3529 .payload
3530 .get("signal_name")
3531 .and_then(Value::as_str)
3532 .unwrap_or_default()
3533 .to_string();
3534 let matched = signals
3535 .iter()
3536 .find(|signal| {
3537 signal_id.is_some() && signal.get("id").and_then(Value::as_str) == signal_id
3538 })
3539 .or_else(|| {
3540 signals.iter().find(|signal| {
3541 command_id.is_some()
3542 && signal.get("command_id").and_then(Value::as_str) == command_id
3543 })
3544 })
3545 .or_else(|| {
3546 let offset = signal_name_offsets.entry(signal_name.clone()).or_default();
3547 let signal = signals
3548 .iter()
3549 .filter(|signal| {
3550 signal.get("name").and_then(Value::as_str) == Some(signal_name.as_str())
3551 })
3552 .nth(*offset);
3553 if signal.is_some() {
3554 *offset += 1;
3555 }
3556 signal
3557 });
3558 let Some(signal) = matched else {
3559 continue;
3560 };
3561 let signal_codec = signal
3562 .get("payload_codec")
3563 .and_then(Value::as_str)
3564 .unwrap_or(&export_codec);
3565 let Some(payload) = event.payload.as_object_mut() else {
3566 continue;
3567 };
3568 if missing_payload(payload.get("arguments")) {
3569 if let Some(arguments) = signal
3570 .get("arguments")
3571 .filter(|value| !missing_payload(Some(value)))
3572 {
3573 let envelope = match arguments {
3574 Value::String(blob) => json!({"codec": signal_codec, "blob": blob}),
3575 other => other.clone(),
3576 };
3577 payload.insert("arguments".to_string(), envelope);
3578 }
3579 }
3580 if payload
3581 .get("payload_codec")
3582 .and_then(Value::as_str)
3583 .unwrap_or_default()
3584 .is_empty()
3585 {
3586 payload.insert("payload_codec".to_string(), json!(signal_codec));
3587 }
3588 }
3589
3590 Ok(())
3591}
3592
3593fn missing_payload(value: Option<&Value>) -> bool {
3594 match value {
3595 None | Some(Value::Null) => true,
3596 Some(Value::String(value)) => value.is_empty(),
3597 Some(_) => false,
3598 }
3599}
3600
3601fn query_signal_events(task: &QueryTask) -> Result<Vec<QuerySignal>> {
3602 let export_signals = task
3603 .history_export
3604 .as_ref()
3605 .and_then(|export| export.get("signals"))
3606 .and_then(Value::as_array)
3607 .cloned()
3608 .unwrap_or_default();
3609 let export_codec = task
3610 .history_export
3611 .as_ref()
3612 .and_then(|export| export.get("payloads"))
3613 .and_then(|payloads| payloads.get("codec"))
3614 .and_then(Value::as_str)
3615 .unwrap_or(&task.payload_codec);
3616 let mut name_offsets: HashMap<String, usize> = HashMap::new();
3617 let mut signals = Vec::new();
3618
3619 for event in &task.history_events {
3620 if event.event_type != "SignalApplied" && event.event_type != "SignalReceived" {
3621 continue;
3622 }
3623
3624 let name = event
3625 .payload
3626 .get("signal_name")
3627 .and_then(Value::as_str)
3628 .unwrap_or_default();
3629 if name.is_empty() {
3630 continue;
3631 }
3632 let signal_id = event.payload.get("signal_id").and_then(Value::as_str);
3633 let command_id = event
3634 .payload
3635 .get("workflow_command_id")
3636 .or_else(|| event.raw.get("workflow_command_id"))
3637 .and_then(Value::as_str);
3638 let matched_export = export_signals
3639 .iter()
3640 .find(|candidate| {
3641 signal_id.is_some() && candidate.get("id").and_then(Value::as_str) == signal_id
3642 })
3643 .or_else(|| {
3644 export_signals.iter().find(|candidate| {
3645 command_id.is_some()
3646 && candidate.get("command_id").and_then(Value::as_str) == command_id
3647 })
3648 })
3649 .or_else(|| {
3650 let offset = name_offsets.entry(name.to_string()).or_default();
3651 let candidate = export_signals
3652 .iter()
3653 .filter(|candidate| candidate.get("name").and_then(Value::as_str) == Some(name))
3654 .nth(*offset);
3655 if candidate.is_some() {
3656 *offset += 1;
3657 }
3658 candidate
3659 });
3660 let codec = event
3661 .payload
3662 .get("payload_codec")
3663 .and_then(Value::as_str)
3664 .or_else(|| {
3665 matched_export
3666 .and_then(|signal| signal.get("payload_codec"))
3667 .and_then(Value::as_str)
3668 })
3669 .unwrap_or(export_codec);
3670 let raw_arguments = event
3671 .payload
3672 .get("value")
3673 .or_else(|| event.payload.get("input"))
3674 .or_else(|| event.payload.get("arguments"))
3675 .filter(|value| !value.is_null())
3676 .or_else(|| matched_export.and_then(|signal| signal.get("arguments")));
3677 let arguments = decode_query_signal_arguments(raw_arguments, codec)?;
3678 let workflow_sequence = event
3679 .payload
3680 .get("workflow_sequence")
3681 .and_then(value_as_u64)
3682 .or_else(|| {
3683 matched_export
3684 .and_then(|signal| signal.get("workflow_sequence"))
3685 .and_then(value_as_u64)
3686 });
3687
3688 signals.push(QuerySignal {
3689 id: signal_id.map(str::to_string).or_else(|| {
3690 matched_export
3691 .and_then(|signal| signal.get("id"))
3692 .and_then(Value::as_str)
3693 .map(str::to_string)
3694 }),
3695 name: name.to_string(),
3696 arguments,
3697 workflow_sequence,
3698 });
3699 }
3700
3701 if signals.is_empty() {
3702 for signal in export_signals {
3703 if signal.get("status").and_then(Value::as_str) == Some("rejected") {
3704 continue;
3705 }
3706 let Some(name) = signal.get("name").and_then(Value::as_str) else {
3707 continue;
3708 };
3709 let codec = signal
3710 .get("payload_codec")
3711 .and_then(Value::as_str)
3712 .unwrap_or(export_codec);
3713 let arguments = decode_query_signal_arguments(signal.get("arguments"), codec)?;
3714 signals.push(QuerySignal {
3715 id: signal.get("id").and_then(Value::as_str).map(str::to_string),
3716 name: name.to_string(),
3717 arguments,
3718 workflow_sequence: signal.get("workflow_sequence").and_then(value_as_u64),
3719 });
3720 }
3721 signals.sort_by_key(|signal| signal.workflow_sequence.unwrap_or(u64::MAX));
3722 }
3723
3724 Ok(signals)
3725}
3726
3727fn decode_query_signal_arguments(raw: Option<&Value>, codec: &str) -> Result<Vec<Value>> {
3728 let decoded = match raw.filter(|value| !value.is_null()) {
3729 Some(value) => decode_wire_value(value, codec)?,
3730 None => Value::Array(Vec::new()),
3731 };
3732 let Value::Array(arguments) = normalize_arguments(decoded) else {
3733 unreachable!("normalize_arguments always returns an array");
3734 };
3735 Ok(arguments)
3736}
3737
3738fn value_as_u64(value: &Value) -> Option<u64> {
3739 value
3740 .as_u64()
3741 .or_else(|| value.as_str().and_then(|value| value.parse().ok()))
3742}
3743
3744fn resume_signal_matches_event(
3745 resume_signal: &ResumeSignal,
3746 event: &HistoryEvent,
3747 signal_name: &str,
3748) -> bool {
3749 if resume_signal.signal_name != signal_name {
3750 return false;
3751 }
3752
3753 match (
3754 resume_signal.signal_id.as_deref(),
3755 event.payload.get("signal_id").and_then(Value::as_str),
3756 ) {
3757 (Some(resume_id), Some(event_id)) => resume_id == event_id,
3758 _ => true,
3759 }
3760}
3761
3762#[cfg(test)]
3763mod tests {
3764 use super::*;
3765 use std::{
3766 io::{Read, Write},
3767 net::{SocketAddr, TcpListener, TcpStream},
3768 thread,
3769 };
3770
3771 #[derive(Clone, Debug, Default, PartialEq)]
3772 struct ReplayCounterState {
3773 loaded: Option<String>,
3774 count: i64,
3775 finished: bool,
3776 }
3777
3778 fn replay_counter_worker() -> Worker {
3779 let client = Client::new("http://127.0.0.1:8080").expect("client");
3780 let mut worker = Worker::new(client, "rust-workers");
3781 worker.register_replayed_workflow(
3782 "replay-counter",
3783 ReplayCounterState::default,
3784 |ctx, _input, state| async move {
3785 let loaded = ctx.activity("load-counter", json!([])).await?;
3786 state.update(|current| {
3787 current.loaded = loaded.as_str().map(str::to_string);
3788 })?;
3789 for _ in 0..2 {
3790 let signal = ctx.wait_signal("increment").await?;
3791 let amount = signal.first().and_then(Value::as_i64).unwrap_or_default();
3792 state.update(|current| current.count += amount)?;
3793 }
3794 state.update(|current| current.finished = true)?;
3795 state.read(|current| Ok(json!(current.count)))?
3796 },
3797 );
3798 worker.register_replayed_query::<ReplayCounterState, _, _>(
3799 "replay-counter",
3800 "current",
3801 |_ctx, state, _args| async move {
3802 Ok(json!({
3803 "loaded": state.loaded,
3804 "count": state.count,
3805 "finished": state.finished,
3806 }))
3807 },
3808 );
3809 worker.register_replayed_query::<ReplayCounterState, _, _>(
3810 "replay-counter",
3811 "detached-mutation",
3812 |_ctx, state, _args| async move {
3813 let mut detached = (*state).clone();
3814 detached.count = 999;
3815 Ok(json!(detached.count))
3816 },
3817 );
3818 worker.register_replayed_query::<ReplayCounterState, _, _>(
3819 "replay-counter",
3820 "failed-mutation",
3821 |_ctx, state, _args| async move {
3822 let mut detached = (*state).clone();
3823 detached.count = 999;
3824 Err(Error::WorkerLoop("query refused".to_string()))
3825 },
3826 );
3827 worker
3828 }
3829
3830 fn replay_counter_query(
3831 query_name: &str,
3832 history_events: Value,
3833 run_status: &str,
3834 ) -> QueryTask {
3835 serde_json::from_value(json!({
3836 "query_task_id": format!("query-{query_name}"),
3837 "workflow_type": "replay-counter",
3838 "query_name": query_name,
3839 "payload_codec": "json",
3840 "workflow_arguments": {"codec": "json", "blob": "[]"},
3841 "query_arguments": {"codec": "json", "blob": "[]"},
3842 "history_events": history_events,
3843 "run_status": run_status,
3844 }))
3845 .expect("query task")
3846 }
3847
3848 fn workflow_context(history: Vec<HistoryEvent>) -> WorkflowContext {
3849 WorkflowContext {
3850 state: Arc::new(Mutex::new(
3851 WorkflowState::new(
3852 history,
3853 "rust-workers".to_string(),
3854 JSON_CODEC.to_string(),
3855 None,
3856 )
3857 .expect("valid workflow history"),
3858 )),
3859 }
3860 }
3861
3862 fn history_event(event_type: &str, payload: Value) -> HistoryEvent {
3863 HistoryEvent {
3864 event_type: event_type.to_string(),
3865 payload,
3866 raw: HashMap::new(),
3867 }
3868 }
3869
3870 #[test]
3871 fn avro_generic_wrapper_round_trips_json_values() {
3872 let value = json!({"greeting": "hello", "count": 3, "ok": true});
3873 let envelope = PayloadEnvelope::avro(&value).expect("encode");
3874 assert_eq!(envelope.codec, DEFAULT_CODEC);
3875 assert_eq!(decode_payload::<Value>(&envelope).expect("decode"), value);
3876 }
3877
3878 #[test]
3879 fn json_codec_remains_plain_json() {
3880 let value = json!({"greeting": "hello", "count": 3, "ok": true});
3881 let envelope = PayloadEnvelope::json(&value).expect("encode");
3882
3883 assert_eq!(envelope.codec, JSON_CODEC);
3884 assert_eq!(envelope.blob, serde_json::to_string(&value).expect("json"));
3885 assert_eq!(decode_payload::<Value>(&envelope).expect("decode"), value);
3886 }
3887
3888 #[test]
3889 fn typed_avro_payload_without_schema_context_keeps_diagnostic() {
3890 let envelope = PayloadEnvelope {
3891 codec: DEFAULT_CODEC.to_string(),
3892 blob: BASE64.encode([0x01]),
3893 };
3894
3895 let error = decode_payload::<Value>(&envelope).expect_err("typed payload must fail");
3896 assert_eq!(
3897 error.to_string(),
3898 "codec error: typed avro payloads require a schema context; v1 supports the generic wrapper"
3899 );
3900 }
3901
3902 #[test]
3903 fn workflow_context_schedules_activity_until_completion_is_in_history() {
3904 let ctx = WorkflowContext {
3905 state: Arc::new(Mutex::new(
3906 WorkflowState::new(
3907 Vec::new(),
3908 "rust-workers".to_string(),
3909 DEFAULT_CODEC.to_string(),
3910 None,
3911 )
3912 .expect("workflow state"),
3913 )),
3914 };
3915
3916 let mut call = Box::pin(ctx.activity("hello.activity", json!(["Ada"])));
3917 let mut task_context = TaskContext::from_waker(noop_waker_ref());
3918 assert!(matches!(
3919 call.as_mut().poll(&mut task_context),
3920 Poll::Pending
3921 ));
3922
3923 let commands = ctx.take_commands().expect("commands");
3924 assert_eq!(commands[0]["type"], "schedule_activity");
3925 assert_eq!(commands[0]["activity_type"], "hello.activity");
3926 }
3927
3928 #[test]
3929 fn workflow_sleep_emits_one_durable_timer_and_rounds_up() {
3930 let ctx = workflow_context(Vec::new());
3931 let mut sleep = Box::pin(ctx.sleep(Duration::from_millis(1_001)));
3932 let mut task_context = TaskContext::from_waker(noop_waker_ref());
3933
3934 assert!(matches!(
3935 sleep.as_mut().poll(&mut task_context),
3936 Poll::Pending
3937 ));
3938 assert!(matches!(
3939 sleep.as_mut().poll(&mut task_context),
3940 Poll::Pending
3941 ));
3942
3943 let commands = ctx.take_commands().expect("timer command");
3944 assert_eq!(
3945 commands,
3946 vec![json!({
3947 "type": "start_timer",
3948 "delay_seconds": 2,
3949 })]
3950 );
3951 }
3952
3953 #[test]
3954 fn workflow_sleep_replays_matching_schedule_and_fire_without_a_command() {
3955 let history = vec![
3956 history_event(
3957 "TimerScheduled",
3958 json!({
3959 "sequence": 1,
3960 "timer_id": "timer-1",
3961 "delay_seconds": 5,
3962 "fire_at": "2026-07-11T12:00:05Z",
3963 }),
3964 ),
3965 history_event(
3966 "TimerFired",
3967 json!({
3968 "sequence": 1,
3969 "timer_id": "timer-1",
3970 "delay_seconds": 5,
3971 "fire_at": "2026-07-11T12:00:05Z",
3972 "fired_at": "2026-07-11T12:00:05Z",
3973 }),
3974 ),
3975 ];
3976
3977 for _restart in 0..2 {
3978 let ctx = workflow_context(history.clone());
3979 let mut sleep = Box::pin(ctx.sleep(Duration::from_secs(5)));
3980 let mut task_context = TaskContext::from_waker(noop_waker_ref());
3981 assert!(matches!(
3982 sleep.as_mut().poll(&mut task_context),
3983 Poll::Ready(Ok(()))
3984 ));
3985 assert!(ctx.take_commands().expect("commands").is_empty());
3986 ctx.ensure_history_consumed().expect("history consumed");
3987 }
3988 }
3989
3990 #[test]
3991 fn workflow_sleep_rejects_changed_delay_during_replay() {
3992 let ctx = workflow_context(vec![
3993 history_event(
3994 "TimerScheduled",
3995 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
3996 ),
3997 history_event(
3998 "TimerFired",
3999 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4000 ),
4001 ]);
4002 let mut sleep = Box::pin(ctx.sleep(Duration::from_secs(500)));
4003 let mut task_context = TaskContext::from_waker(noop_waker_ref());
4004
4005 let Poll::Ready(Err(Error::NonDeterministicReplay(failure))) =
4006 sleep.as_mut().poll(&mut task_context)
4007 else {
4008 panic!("changed timer delay must be rejected");
4009 };
4010 assert_eq!(failure.reason, "timer_delay_mismatch");
4011 assert_eq!(failure.sequence, Some(1));
4012 }
4013
4014 #[test]
4015 fn workflow_history_rejects_unpaired_or_mismatched_timer_events() {
4016 let lone_fire = WorkflowState::new(
4017 vec![history_event(
4018 "TimerFired",
4019 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4020 )],
4021 "rust-workers".to_string(),
4022 JSON_CODEC.to_string(),
4023 None,
4024 )
4025 .expect_err("TimerFired requires TimerScheduled");
4026 assert!(matches!(
4027 lone_fire,
4028 Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
4029 if reason == "timer_schedule_missing_or_duplicate"
4030 ));
4031
4032 let wrong_identity = WorkflowState::new(
4033 vec![
4034 history_event(
4035 "TimerScheduled",
4036 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4037 ),
4038 history_event(
4039 "TimerFired",
4040 json!({"sequence": 1, "timer_id": "timer-2", "delay_seconds": 5}),
4041 ),
4042 ],
4043 "rust-workers".to_string(),
4044 JSON_CODEC.to_string(),
4045 None,
4046 )
4047 .expect_err("fire must match scheduled timer identity");
4048 assert!(matches!(
4049 wrong_identity,
4050 Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
4051 if reason == "timer_identity_mismatch"
4052 ));
4053
4054 let duplicate_fire = WorkflowState::new(
4055 vec![
4056 history_event(
4057 "TimerScheduled",
4058 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4059 ),
4060 history_event(
4061 "TimerFired",
4062 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4063 ),
4064 history_event(
4065 "TimerFired",
4066 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4067 ),
4068 ],
4069 "rust-workers".to_string(),
4070 JSON_CODEC.to_string(),
4071 None,
4072 )
4073 .expect_err("a durable timer cannot fire twice");
4074 assert!(matches!(
4075 duplicate_fire,
4076 Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
4077 if reason == "duplicate_timer_fire"
4078 ));
4079
4080 let wrong_fired_delay = WorkflowState::new(
4081 vec![
4082 history_event(
4083 "TimerScheduled",
4084 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4085 ),
4086 history_event(
4087 "TimerFired",
4088 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 6}),
4089 ),
4090 ],
4091 "rust-workers".to_string(),
4092 JSON_CODEC.to_string(),
4093 None,
4094 )
4095 .expect_err("timer schedule and fire delays must agree");
4096 assert!(matches!(
4097 wrong_fired_delay,
4098 Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
4099 if reason == "timer_history_delay_mismatch"
4100 ));
4101 }
4102
4103 #[test]
4104 fn replay_rejects_activity_moved_before_recorded_timer() {
4105 let ctx = workflow_context(vec![
4106 history_event(
4107 "TimerScheduled",
4108 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4109 ),
4110 history_event(
4111 "TimerFired",
4112 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4113 ),
4114 history_event(
4115 "ActivityCompleted",
4116 json!({
4117 "sequence": 2,
4118 "activity_type": "after-timer",
4119 "payload_codec": "json",
4120 "result": {"codec": "json", "blob": "\"done\""},
4121 }),
4122 ),
4123 ]);
4124 let mut activity = Box::pin(ctx.activity("after-timer", json!([])));
4125 let mut task_context = TaskContext::from_waker(noop_waker_ref());
4126
4127 let Poll::Ready(Err(Error::NonDeterministicReplay(failure))) =
4128 activity.as_mut().poll(&mut task_context)
4129 else {
4130 panic!("reordered durable command must be rejected");
4131 };
4132 assert_eq!(failure.reason, "recorded_command_mismatch");
4133 assert_eq!(failure.sequence, Some(1));
4134 assert_eq!(failure.expected.as_deref(), Some("timer"));
4135 assert_eq!(failure.actual.as_deref(), Some("activity:after-timer"));
4136 }
4137
4138 #[test]
4139 fn replay_orders_signal_waits_and_timers_in_one_command_stream() {
4140 let signal_then_timer = vec![
4141 history_event(
4142 "ConditionWaitOpened",
4143 json!({"sequence": 1, "condition_key": "signal:go"}),
4144 ),
4145 history_event(
4146 "SignalReceived",
4147 json!({
4148 "signal_name": "go",
4149 "arguments": ["now"],
4150 }),
4151 ),
4152 history_event(
4153 "TimerScheduled",
4154 json!({"sequence": 2, "timer_id": "timer-2", "delay_seconds": 5}),
4155 ),
4156 history_event(
4157 "TimerFired",
4158 json!({"sequence": 2, "timer_id": "timer-2", "delay_seconds": 5}),
4159 ),
4160 ];
4161
4162 let ctx = workflow_context(signal_then_timer.clone());
4163 let mut signal = Box::pin(ctx.wait_signal("go"));
4164 let mut task_context = TaskContext::from_waker(noop_waker_ref());
4165 assert!(matches!(
4166 signal.as_mut().poll(&mut task_context),
4167 Poll::Ready(Ok(arguments)) if arguments == vec![json!("now")]
4168 ));
4169 let mut timer = Box::pin(ctx.sleep(Duration::from_secs(5)));
4170 assert!(matches!(
4171 timer.as_mut().poll(&mut task_context),
4172 Poll::Ready(Ok(()))
4173 ));
4174 ctx.ensure_history_consumed()
4175 .expect("signal and timer history consumed in order");
4176
4177 let reordered = workflow_context(signal_then_timer);
4178 let mut timer_first = Box::pin(reordered.sleep(Duration::from_secs(5)));
4179 let Poll::Ready(Err(Error::NonDeterministicReplay(failure))) =
4180 timer_first.as_mut().poll(&mut task_context)
4181 else {
4182 panic!("timer cannot consume signal-wait-first history");
4183 };
4184 assert_eq!(failure.reason, "recorded_command_mismatch");
4185 assert_eq!(failure.sequence, Some(1));
4186 assert_eq!(failure.expected.as_deref(), Some("signal wait"));
4187
4188 let timer_then_signal = vec![
4189 history_event(
4190 "TimerScheduled",
4191 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4192 ),
4193 history_event(
4194 "TimerFired",
4195 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4196 ),
4197 history_event(
4198 "ConditionWaitOpened",
4199 json!({"sequence": 2, "condition_key": "signal:go"}),
4200 ),
4201 history_event(
4202 "SignalReceived",
4203 json!({"signal_name": "go", "arguments": []}),
4204 ),
4205 ];
4206 let reordered = workflow_context(timer_then_signal);
4207 let mut signal_first = Box::pin(reordered.wait_signal("go"));
4208 let Poll::Ready(Err(Error::NonDeterministicReplay(failure))) =
4209 signal_first.as_mut().poll(&mut task_context)
4210 else {
4211 panic!("signal wait cannot consume timer-first history");
4212 };
4213 assert_eq!(failure.reason, "recorded_command_mismatch");
4214 assert_eq!(failure.sequence, Some(1));
4215 assert_eq!(failure.expected.as_deref(), Some("timer"));
4216 }
4217
4218 #[test]
4219 fn workflow_history_rejects_duplicate_or_colliding_command_sequences() {
4220 let duplicate_timer = WorkflowState::new(
4221 vec![
4222 history_event(
4223 "TimerScheduled",
4224 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4225 ),
4226 history_event(
4227 "TimerScheduled",
4228 json!({"sequence": 1, "timer_id": "timer-2", "delay_seconds": 5}),
4229 ),
4230 ],
4231 "rust-workers".to_string(),
4232 JSON_CODEC.to_string(),
4233 None,
4234 )
4235 .expect_err("one workflow sequence cannot schedule two timers");
4236 assert!(matches!(
4237 duplicate_timer,
4238 Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
4239 if reason == "timer_schedule_missing_or_duplicate"
4240 ));
4241
4242 let colliding_kinds = WorkflowState::new(
4243 vec![
4244 history_event(
4245 "TimerScheduled",
4246 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4247 ),
4248 history_event(
4249 "ActivityCompleted",
4250 json!({"sequence": 1, "activity_type": "same-sequence"}),
4251 ),
4252 ],
4253 "rust-workers".to_string(),
4254 JSON_CODEC.to_string(),
4255 None,
4256 )
4257 .expect_err("one workflow sequence cannot identify two command kinds");
4258 assert!(matches!(
4259 colliding_kinds,
4260 Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
4261 if reason == "durable_command_sequence_collision"
4262 ));
4263
4264 let duplicate_signal_wait = WorkflowState::new(
4265 vec![
4266 history_event(
4267 "SignalWaitOpened",
4268 json!({"sequence": 1, "signal_name": "go"}),
4269 ),
4270 history_event(
4271 "SignalWaitOpened",
4272 json!({"sequence": 1, "signal_name": "go"}),
4273 ),
4274 ],
4275 "rust-workers".to_string(),
4276 JSON_CODEC.to_string(),
4277 None,
4278 )
4279 .expect_err("one workflow sequence cannot open two signal waits");
4280 assert!(matches!(
4281 duplicate_signal_wait,
4282 Error::NonDeterministicReplay(ReplayFailure { ref reason, .. })
4283 if reason == "duplicate_signal_wait_open"
4284 ));
4285 }
4286
4287 #[test]
4288 fn workflow_sleep_rejects_unrepresentable_rounded_duration() {
4289 let ctx = workflow_context(Vec::new());
4290 let mut sleep = Box::pin(ctx.start_timer(Duration::new(u64::MAX, 1)));
4291 let mut task_context = TaskContext::from_waker(noop_waker_ref());
4292 assert!(matches!(
4293 sleep.as_mut().poll(&mut task_context),
4294 Poll::Ready(Err(Error::TimerDurationOverflow))
4295 ));
4296 assert!(ctx.take_commands().expect("commands").is_empty());
4297 }
4298
4299 #[test]
4300 fn workflow_task_replay_completes_without_rescheduling_recorded_commands() {
4301 let client = Client::new("http://127.0.0.1:8080").expect("client");
4302 let mut worker = Worker::new(client, "rust-workers");
4303 worker.register_workflow("rust.timer", |ctx, _input| async move {
4304 ctx.sleep(Duration::from_secs(5)).await?;
4305 ctx.activity("after-timer", json!([])).await
4306 });
4307
4308 let task = |history_events| WorkflowTask {
4309 task_id: "wft-rust-timer-1".to_string(),
4310 workflow_id: Some("wf-rust-timer".to_string()),
4311 run_id: Some("run-rust-timer".to_string()),
4312 workflow_type: "rust.timer".to_string(),
4313 payload_codec: JSON_CODEC.to_string(),
4314 arguments: Some(json!({"codec": "json", "blob": "[]"})),
4315 history_events,
4316 total_history_events: None,
4317 next_history_page_token: None,
4318 workflow_task_attempt: 1,
4319 workflow_signal_id: None,
4320 signal_name: None,
4321 signal_arguments: None,
4322 lease_owner: Some("rust-worker".to_string()),
4323 };
4324
4325 let initial = worker
4326 .execute_workflow_task(task(Vec::new()))
4327 .expect("initial timer task");
4328 assert_eq!(
4329 initial,
4330 vec![json!({"type": "start_timer", "delay_seconds": 5})]
4331 );
4332
4333 let replayed = worker
4334 .execute_workflow_task(task(vec![
4335 history_event(
4336 "TimerScheduled",
4337 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4338 ),
4339 history_event(
4340 "TimerFired",
4341 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4342 ),
4343 history_event(
4344 "ActivityCompleted",
4345 json!({
4346 "sequence": 2,
4347 "activity_type": "after-timer",
4348 "payload_codec": "json",
4349 "result": {"codec": "json", "blob": "\"done\""},
4350 }),
4351 ),
4352 ]))
4353 .expect("replayed workflow task");
4354 assert_eq!(replayed.len(), 1);
4355 assert_eq!(replayed[0]["type"], "complete_workflow");
4356 assert_eq!(
4357 decode_wire_value(&replayed[0]["result"], JSON_CODEC).expect("result"),
4358 json!("done")
4359 );
4360 }
4361
4362 #[test]
4363 fn workflow_task_replay_keeps_recorded_unfired_timer_pending_without_rescheduling() {
4364 let client = Client::new("http://127.0.0.1:8080").expect("client");
4365 let mut worker = Worker::new(client, "rust-workers");
4366 worker.register_workflow("rust.timer.pending", |ctx, _input| async move {
4367 ctx.sleep(Duration::from_secs(5)).await?;
4368 Ok(json!({"status": "timer fired"}))
4369 });
4370
4371 let task = WorkflowTask {
4372 task_id: "wft-rust-timer-pending".to_string(),
4373 workflow_id: Some("wf-rust-timer".to_string()),
4374 run_id: Some("run-rust-timer".to_string()),
4375 workflow_type: "rust.timer.pending".to_string(),
4376 payload_codec: JSON_CODEC.to_string(),
4377 arguments: Some(json!({"codec": "json", "blob": "[]"})),
4378 history_events: vec![history_event(
4379 "TimerScheduled",
4380 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4381 )],
4382 total_history_events: Some(1),
4383 next_history_page_token: None,
4384 workflow_task_attempt: 1,
4385 workflow_signal_id: None,
4386 signal_name: None,
4387 signal_arguments: None,
4388 lease_owner: Some("rust-worker".to_string()),
4389 };
4390
4391 for _redelivery_or_restart in 0..2 {
4392 let commands = worker
4393 .execute_workflow_task(task.clone())
4394 .expect("recorded timer remains pending");
4395 assert!(
4396 commands.is_empty(),
4397 "recorded timer must not be rescheduled"
4398 );
4399 }
4400 }
4401
4402 #[test]
4403 fn workflow_task_rejects_recorded_command_removed_from_workflow_code() {
4404 let client = Client::new("http://127.0.0.1:8080").expect("client");
4405 let mut worker = Worker::new(client, "rust-workers");
4406 worker.register_workflow("rust.timer.removed", |_ctx, _input| async move {
4407 Ok(json!({"status": "completed"}))
4408 });
4409 let task = WorkflowTask {
4410 task_id: "wft-rust-timer-removed".to_string(),
4411 workflow_id: Some("wf-rust-timer".to_string()),
4412 run_id: Some("run-rust-timer".to_string()),
4413 workflow_type: "rust.timer.removed".to_string(),
4414 payload_codec: JSON_CODEC.to_string(),
4415 arguments: Some(json!({"codec": "json", "blob": "[]"})),
4416 history_events: vec![
4417 history_event(
4418 "TimerScheduled",
4419 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4420 ),
4421 history_event(
4422 "TimerFired",
4423 json!({"sequence": 1, "timer_id": "timer-1", "delay_seconds": 5}),
4424 ),
4425 ],
4426 total_history_events: Some(2),
4427 next_history_page_token: None,
4428 workflow_task_attempt: 1,
4429 workflow_signal_id: None,
4430 signal_name: None,
4431 signal_arguments: None,
4432 lease_owner: Some("rust-worker".to_string()),
4433 };
4434
4435 let Error::NonDeterministicReplay(failure) = worker
4436 .execute_workflow_task(task)
4437 .expect_err("removed timer must fail replay")
4438 else {
4439 panic!("expected typed replay failure");
4440 };
4441 assert_eq!(failure.reason, "recorded_commands_unconsumed");
4442 assert_eq!(failure.sequence, Some(1));
4443 }
4444
4445 #[test]
4446 fn rust_hello_world_uses_signal_arguments_from_resume_payload() {
4447 let client = Client::new("http://127.0.0.1:8080").expect("client");
4448 let mut worker = Worker::new(client, "rust-workers");
4449
4450 worker.register_workflow("rust.hello_workflow", |ctx, _input| async move {
4451 let signal = ctx.wait_signal("start").await?;
4452 let name = signal
4453 .first()
4454 .and_then(|value| value.as_str())
4455 .unwrap_or("world");
4456 let greeting = ctx.activity("rust.hello_activity", json!([name])).await?;
4457 Ok(json!({
4458 "greeting": greeting,
4459 "language": "rust"
4460 }))
4461 });
4462
4463 let signal_arguments =
4464 encode_value_envelope(&json!(["Rust"]), DEFAULT_CODEC).expect("signal arguments");
4465 let task = WorkflowTask {
4466 task_id: "wft-rust-signal-1".to_string(),
4467 workflow_id: Some("wf-rust-hello".to_string()),
4468 run_id: Some("run-rust-hello".to_string()),
4469 workflow_type: "rust.hello_workflow".to_string(),
4470 payload_codec: DEFAULT_CODEC.to_string(),
4471 arguments: Some(encode_value_envelope(&json!([]), DEFAULT_CODEC).expect("input")),
4472 history_events: vec![HistoryEvent {
4473 event_type: "SignalReceived".to_string(),
4474 payload: json!({
4475 "signal_id": "sig-rust-1",
4476 "signal_name": "start"
4477 }),
4478 raw: HashMap::new(),
4479 }],
4480 total_history_events: Some(1),
4481 next_history_page_token: None,
4482 workflow_task_attempt: 1,
4483 workflow_signal_id: Some("sig-rust-1".to_string()),
4484 signal_name: Some("start".to_string()),
4485 signal_arguments: Some(signal_arguments),
4486 lease_owner: Some("rust-worker".to_string()),
4487 };
4488
4489 let commands = worker.execute_workflow_task(task).expect("workflow task");
4490
4491 assert_eq!(commands.len(), 1);
4492 assert_eq!(commands[0]["type"], "schedule_activity");
4493 assert_eq!(commands[0]["activity_type"], "rust.hello_activity");
4494 assert_eq!(
4495 decode_wire_value(&commands[0]["arguments"], DEFAULT_CODEC).expect("activity args"),
4496 json!(["Rust"])
4497 );
4498 }
4499
4500 #[test]
4501 fn workflow_task_appends_paginated_history_events() {
4502 let mut task = WorkflowTask {
4503 task_id: "wft-rust-pages-1".to_string(),
4504 workflow_id: Some("wf-rust-pages".to_string()),
4505 run_id: Some("run-rust-pages".to_string()),
4506 workflow_type: "rust.hello_workflow".to_string(),
4507 payload_codec: DEFAULT_CODEC.to_string(),
4508 arguments: Some(encode_value_envelope(&json!([]), DEFAULT_CODEC).expect("input")),
4509 history_events: vec![HistoryEvent {
4510 event_type: "WorkflowStarted".to_string(),
4511 payload: json!({}),
4512 raw: HashMap::new(),
4513 }],
4514 total_history_events: Some(3),
4515 next_history_page_token: Some("MQ==".to_string()),
4516 workflow_task_attempt: 1,
4517 workflow_signal_id: None,
4518 signal_name: None,
4519 signal_arguments: None,
4520 lease_owner: Some("rust-worker".to_string()),
4521 };
4522
4523 task.append_history_page(WorkflowTaskHistoryPage {
4524 history_events: vec![
4525 HistoryEvent {
4526 event_type: "SignalReceived".to_string(),
4527 payload: json!({
4528 "signal_id": "sig-rust-1",
4529 "signal_name": "start",
4530 "arguments": encode_value_envelope(&json!(["Rust"]), DEFAULT_CODEC)
4531 .expect("signal arguments")
4532 }),
4533 raw: HashMap::new(),
4534 },
4535 HistoryEvent {
4536 event_type: "MarkerRecorded".to_string(),
4537 payload: json!({"sequence": 3}),
4538 raw: HashMap::new(),
4539 },
4540 ],
4541 total_history_events: Some(3),
4542 next_history_page_token: None,
4543 });
4544
4545 assert_eq!(task.history_events.len(), 3);
4546 assert_eq!(task.total_history_events, Some(3));
4547 assert_eq!(task.next_history_page_token, None);
4548
4549 let signals =
4550 signal_values(&task.history_events, "start", DEFAULT_CODEC, None).expect("signals");
4551 assert_eq!(signals, vec![vec![json!("Rust")]]);
4552 }
4553
4554 #[tokio::test]
4555 async fn query_handler_reads_ordered_cross_codec_signals_without_commands() {
4556 let client = Client::new("http://127.0.0.1:8080").expect("client");
4557 let mut worker = Worker::new(client, "rust-workers");
4558 worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
4559 worker.register_query("counter", "current", |ctx, _args| async move {
4560 let mut count = 0_i64;
4561 for signal in ctx.signal_events() {
4562 let value = signal
4563 .arguments
4564 .first()
4565 .and_then(Value::as_i64)
4566 .unwrap_or_default();
4567 match signal.name.as_str() {
4568 "increment" => count += value,
4569 "set" => count = value,
4570 _ => {}
4571 }
4572 }
4573 Ok(json!(count))
4574 });
4575
4576 let task = QueryTask {
4577 query_task_id: "query-rust-counter".to_string(),
4578 query_task_attempt: 1,
4579 lease_owner: Some("rust-worker".to_string()),
4580 workflow_id: Some("counter-1".to_string()),
4581 run_id: Some("run-counter-1".to_string()),
4582 workflow_type: "counter".to_string(),
4583 query_name: "current".to_string(),
4584 payload_codec: DEFAULT_CODEC.to_string(),
4585 workflow_arguments: Some(
4586 encode_value_envelope(&json!([]), DEFAULT_CODEC).expect("workflow input"),
4587 ),
4588 query_arguments: Some(
4589 encode_value_envelope(&json!([]), DEFAULT_CODEC).expect("query arguments"),
4590 ),
4591 history_events: vec![
4592 HistoryEvent {
4593 event_type: "SignalReceived".to_string(),
4594 payload: json!({
4595 "signal_id": "php-signal-1",
4596 "signal_name": "increment",
4597 "workflow_sequence": 1,
4598 "payload_codec": DEFAULT_CODEC,
4599 "arguments": encode_value_envelope(&json!([3]), DEFAULT_CODEC).expect("php avro signal")
4600 }),
4601 raw: HashMap::new(),
4602 },
4603 HistoryEvent {
4604 event_type: "SignalReceived".to_string(),
4605 payload: json!({
4606 "signal_id": "python-signal-2",
4607 "signal_name": "increment",
4608 "workflow_sequence": 2,
4609 "payload_codec": JSON_CODEC,
4610 "arguments": encode_value_envelope(&json!([5]), JSON_CODEC).expect("python json signal")
4611 }),
4612 raw: HashMap::new(),
4613 },
4614 HistoryEvent {
4615 event_type: "SignalReceived".to_string(),
4616 payload: json!({
4617 "signal_id": "rust-signal-3",
4618 "signal_name": "set",
4619 "workflow_sequence": 3,
4620 "payload_codec": DEFAULT_CODEC,
4621 "arguments": encode_value_envelope(&json!([0]), DEFAULT_CODEC).expect("rust avro signal")
4622 }),
4623 raw: HashMap::new(),
4624 },
4625 ],
4626 history_export: None,
4627 run_status: Some("completed".to_string()),
4628 };
4629
4630 let result = worker.execute_query_task(task).await.expect("query result");
4631 assert_eq!(result, json!(0));
4632 }
4633
4634 #[tokio::test]
4635 async fn replayed_queries_read_running_completed_and_cold_restarted_instance_state() {
4636 let worker = replay_counter_worker();
4637 let running_history = json!([
4638 {
4639 "type": "ActivityCompleted",
4640 "payload": {
4641 "sequence": 1,
4642 "activity_type": "load-counter",
4643 "payload_codec": "json",
4644 "result": {"codec": "json", "blob": "\"loaded\""}
4645 }
4646 },
4647 {
4648 "type": "SignalReceived",
4649 "payload": {
4650 "signal_id": "signal-3",
4651 "signal_name": "increment",
4652 "payload_codec": "json",
4653 "arguments": {"codec": "json", "blob": "[3]"}
4654 }
4655 }
4656 ]);
4657
4658 let running = worker
4659 .execute_query_task(replay_counter_query(
4660 "current",
4661 running_history.clone(),
4662 "running",
4663 ))
4664 .await
4665 .expect("running replay query");
4666 assert_eq!(
4667 running,
4668 json!({"loaded": "loaded", "count": 3, "finished": false})
4669 );
4670
4671 let detached = worker
4672 .execute_query_task(replay_counter_query(
4673 "detached-mutation",
4674 running_history.clone(),
4675 "running",
4676 ))
4677 .await
4678 .expect("query mutates only its detached state clone");
4679 assert_eq!(detached, json!(999));
4680 let failed = worker
4681 .execute_query_task(replay_counter_query(
4682 "failed-mutation",
4683 running_history.clone(),
4684 "running",
4685 ))
4686 .await
4687 .expect_err("failed query");
4688 assert_eq!(failed.reason, "query_rejected");
4689 let unchanged = worker
4690 .execute_query_task(replay_counter_query("current", running_history, "running"))
4691 .await
4692 .expect("later query reconstructs unchanged state");
4693 assert_eq!(unchanged, running);
4694
4695 let restarted_worker = replay_counter_worker();
4696 let restarted_task: QueryTask = serde_json::from_value(json!({
4697 "query_task_id": "query-after-restart",
4698 "workflow_id": "counter-1",
4699 "run_id": "run-counter-1",
4700 "workflow_type": "replay-counter",
4701 "query_name": "current",
4702 "payload_codec": "json",
4703 "workflow_arguments": {"codec": "json", "blob": "[]"},
4704 "query_arguments": {"codec": "json", "blob": "[]"},
4705 "history_events": [],
4706 "history_export": {
4707 "payloads": {"codec": "json"},
4708 "history_events": [
4709 {
4710 "type": "ActivityCompleted",
4711 "payload": {
4712 "sequence": 1,
4713 "activity_type": "load-counter",
4714 "payload_codec": "json",
4715 "result": null
4716 }
4717 },
4718 {
4719 "type": "SignalReceived",
4720 "payload": {"signal_id": "signal-3", "signal_name": "increment"}
4721 },
4722 {
4723 "type": "SignalReceived",
4724 "payload": {"signal_id": "signal-5", "signal_name": "increment"}
4725 }
4726 ],
4727 "activities": [{
4728 "sequence": 1,
4729 "activity_type": "load-counter",
4730 "payload_codec": "json",
4731 "result": {"codec": "json", "blob": "\"loaded\""}
4732 }],
4733 "signals": [
4734 {
4735 "id": "signal-3",
4736 "name": "increment",
4737 "payload_codec": "json",
4738 "arguments": "[3]"
4739 },
4740 {
4741 "id": "signal-5",
4742 "name": "increment",
4743 "payload_codec": "json",
4744 "arguments": "[5]"
4745 }
4746 ]
4747 },
4748 "run_status": "completed"
4749 }))
4750 .expect("cold replay query task");
4751 let completed = restarted_worker
4752 .execute_query_task(restarted_task)
4753 .await
4754 .expect("completed cold replay query");
4755 assert_eq!(
4756 completed,
4757 json!({"loaded": "loaded", "count": 8, "finished": true})
4758 );
4759 }
4760
4761 #[tokio::test]
4762 async fn replayed_query_replay_failures_are_machine_readable() {
4763 let worker = replay_counter_worker();
4764 let task = replay_counter_query(
4765 "current",
4766 json!([{
4767 "type": "ActivityCompleted",
4768 "payload": {
4769 "sequence": 1,
4770 "payload_codec": "json",
4771 "result": {"codec": "json", "blob": "{"}
4772 }
4773 }]),
4774 "running",
4775 );
4776 let failure = worker
4777 .execute_query_task(task)
4778 .await
4779 .expect_err("invalid replay history payload");
4780 assert_eq!(failure.reason, "query_workflow_state_unavailable");
4781 assert_eq!(failure.failure_type, "QueryWorkflowStateUnavailable");
4782 }
4783
4784 #[tokio::test]
4785 async fn query_task_restores_compact_history_from_export() {
4786 let client = Client::new("http://127.0.0.1:8080").expect("client");
4787 let mut worker = Worker::new(client, "rust-workers");
4788 worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
4789 worker.register_query("counter", "current", |ctx, _args| async move {
4790 Ok(json!(ctx.signals("increment")[0][0]))
4791 });
4792 let task: QueryTask = serde_json::from_value(json!({
4793 "query_task_id": "query-export",
4794 "workflow_type": "counter",
4795 "query_name": "current",
4796 "payload_codec": "json",
4797 "workflow_arguments": {"codec": "json", "blob": "[]"},
4798 "query_arguments": {"codec": "json", "blob": "[]"},
4799 "history_events": [],
4800 "history_export": {
4801 "payloads": {"codec": "json"},
4802 "history_events": [{
4803 "type": "SignalReceived",
4804 "payload": {"signal_id": "signal-export", "signal_name": "increment"}
4805 }],
4806 "signals": [{
4807 "id": "signal-export",
4808 "name": "increment",
4809 "status": "applied",
4810 "workflow_sequence": 1,
4811 "payload_codec": "json",
4812 "arguments": "[9]"
4813 }]
4814 }
4815 }))
4816 .expect("query task");
4817
4818 let result = worker.execute_query_task(task).await.expect("query result");
4819 assert_eq!(result, json!(9));
4820 }
4821
4822 #[tokio::test]
4823 async fn query_task_failures_have_stable_reasons() {
4824 let client = Client::new("http://127.0.0.1:8080").expect("client");
4825 let mut worker = Worker::new(client, "rust-workers");
4826 worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
4827 worker.register_query(
4828 "counter",
4829 "current",
4830 |_ctx, _args| async move { Ok(json!(0)) },
4831 );
4832
4833 let base_task = QueryTask {
4834 query_task_id: "query-errors".to_string(),
4835 query_task_attempt: 1,
4836 lease_owner: None,
4837 workflow_id: Some("counter-errors".to_string()),
4838 run_id: Some("run-errors".to_string()),
4839 workflow_type: "counter".to_string(),
4840 query_name: "missing".to_string(),
4841 payload_codec: JSON_CODEC.to_string(),
4842 workflow_arguments: Some(json!({"codec": "json", "blob": "[]"})),
4843 query_arguments: Some(json!({"codec": "json", "blob": "[]"})),
4844 history_events: Vec::new(),
4845 history_export: None,
4846 run_status: Some("running".to_string()),
4847 };
4848
4849 let unknown = worker
4850 .execute_query_task(base_task.clone())
4851 .await
4852 .expect_err("unknown query");
4853 assert_eq!(unknown.reason, "rejected_unknown_query");
4854
4855 let mut malformed = base_task;
4856 malformed.query_name = "current".to_string();
4857 malformed.query_arguments = Some(json!({"codec": "json", "blob": "{"}));
4858 let malformed = worker
4859 .execute_query_task(malformed)
4860 .await
4861 .expect_err("malformed payload");
4862 assert_eq!(malformed.reason, "query_payload_decode_failed");
4863
4864 let client = Client::new("http://127.0.0.1:8080").expect("client");
4865 let mut unavailable_worker = Worker::new(client, "rust-workers");
4866 unavailable_worker
4867 .register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
4868 let unavailable_task: QueryTask = serde_json::from_value(json!({
4869 "query_task_id": "query-unavailable",
4870 "workflow_type": "counter",
4871 "query_name": "current",
4872 "payload_codec": "json",
4873 "workflow_arguments": {"codec": "json", "blob": "[]"},
4874 "query_arguments": {"codec": "json", "blob": "[]"}
4875 }))
4876 .expect("query task");
4877 let unavailable = unavailable_worker
4878 .execute_query_task(unavailable_task)
4879 .await
4880 .expect_err("query handler unavailable");
4881 assert_eq!(unavailable.reason, "query_handler_unavailable");
4882 }
4883
4884 #[tokio::test]
4885 async fn client_query_decodes_result_and_typed_failure() {
4886 let server = MockWorkerServer::start();
4887 let client = Client::builder(server.base_url())
4888 .timeout(Duration::from_secs(2))
4889 .build()
4890 .expect("client");
4891
4892 let result = client
4893 .query_workflow("counter-1", "current", json!([]))
4894 .await
4895 .expect("query result");
4896 assert_eq!(result, json!({"count": 8}));
4897
4898 let error = client
4899 .query_workflow("counter-1", "missing", json!([]))
4900 .await
4901 .expect_err("unknown query");
4902 let Error::QueryFailed(failure) = error else {
4903 panic!("expected typed query failure");
4904 };
4905 assert_eq!(failure.status, 404);
4906 assert_eq!(failure.reason, "rejected_unknown_query");
4907 }
4908
4909 #[tokio::test]
4910 async fn baseline_worker_endpoints_send_the_baseline_protocol() {
4911 let server = MockWorkerServer::start();
4912 let client = Client::builder(server.base_url())
4913 .timeout(Duration::from_secs(2))
4914 .build()
4915 .expect("client");
4916
4917 client
4918 .register_worker("capture-worker", "capture", vec![], vec![], 1, 1)
4919 .await
4920 .expect("register");
4921 client
4922 .heartbeat_worker("capture-worker", 1, 1)
4923 .await
4924 .expect("heartbeat");
4925 client
4926 .poll_workflow_task("capture-worker", "capture", Duration::from_millis(10))
4927 .await
4928 .expect("workflow poll");
4929 client
4930 .poll_activity_task("capture-worker", "capture", Duration::from_millis(10))
4931 .await
4932 .expect("activity poll");
4933
4934 for path in [
4935 "/api/worker/register",
4936 "/api/worker/heartbeat",
4937 "/api/worker/workflow-tasks/poll",
4938 "/api/worker/activity-tasks/poll",
4939 ] {
4940 assert_eq!(
4941 server.worker_protocol_for(path).as_deref(),
4942 Some(WORKER_PROTOCOL_VERSION),
4943 "unexpected protocol for {path}"
4944 );
4945 }
4946
4947 assert_eq!(
4948 server.request_body("/api/worker/workflow-tasks/poll")["timeout_seconds"],
4949 1
4950 );
4951 assert_eq!(
4952 server.request_body("/api/worker/activity-tasks/poll")["timeout_seconds"],
4953 1
4954 );
4955 }
4956
4957 #[tokio::test]
4958 async fn query_task_endpoints_send_the_query_feature_protocol() {
4959 let server = MockWorkerServer::start();
4960 let client = Client::builder(server.base_url())
4961 .timeout(Duration::from_secs(2))
4962 .build()
4963 .expect("client");
4964
4965 client
4966 .poll_query_task("capture-worker", "capture", Duration::from_millis(10))
4967 .await
4968 .expect("query poll");
4969 client
4970 .complete_query_task("query-capture", "capture-worker", 1, json!(8), JSON_CODEC)
4971 .await
4972 .expect("query complete");
4973 client
4974 .fail_query_task(
4975 "query-capture",
4976 "capture-worker",
4977 1,
4978 "failed",
4979 "query_rejected",
4980 "QueryFailed",
4981 )
4982 .await
4983 .expect("query fail");
4984
4985 for path in [
4986 "/api/worker/query-tasks/poll",
4987 "/api/worker/query-tasks/query-capture/complete",
4988 "/api/worker/query-tasks/query-capture/fail",
4989 ] {
4990 assert_eq!(
4991 server.worker_protocol_for(path).as_deref(),
4992 Some(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION),
4993 "unexpected protocol for {path}"
4994 );
4995 }
4996
4997 assert_eq!(
4998 server.request_body("/api/worker/query-tasks/poll")["timeout_seconds"],
4999 1
5000 );
5001 }
5002
5003 #[tokio::test]
5004 async fn query_protocol_rejection_from_older_server_is_typed() {
5005 let server = MockWorkerServer::reject_query_protocol();
5006 let client = Client::builder(server.base_url())
5007 .timeout(Duration::from_secs(2))
5008 .build()
5009 .expect("client");
5010
5011 let error = client
5012 .poll_query_task("capture-worker", "capture", Duration::from_millis(10))
5013 .await
5014 .expect_err("server below query protocol floor must reject");
5015 let Error::Protocol(failure) = error else {
5016 panic!("expected typed protocol failure");
5017 };
5018
5019 assert_eq!(failure.status, 400);
5020 assert_eq!(failure.reason, "unsupported_protocol_version");
5021 assert_eq!(failure.supported_version.as_deref(), Some("1.7"));
5022 assert_eq!(
5023 failure.requested_version.as_deref(),
5024 Some(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION)
5025 );
5026 assert_eq!(
5027 server
5028 .worker_protocol_for("/api/worker/query-tasks/poll")
5029 .as_deref(),
5030 Some(QUERY_TASK_MINIMUM_WORKER_PROTOCOL_VERSION)
5031 );
5032 }
5033
5034 #[tokio::test]
5035 async fn run_once_without_query_handlers_keeps_pre_query_server_compatibility() {
5036 let server = MockWorkerServer::reject_query_protocol();
5037 let client = Client::builder(server.base_url())
5038 .timeout(Duration::from_secs(2))
5039 .build()
5040 .expect("client");
5041 let mut worker = Worker::new(client, "rust-workers")
5042 .worker_id("baseline-worker")
5043 .poll_timeout(Duration::from_millis(10));
5044
5045 worker.register_workflow("baseline.workflow", |_ctx, _input| async move {
5046 Ok(Value::Null)
5047 });
5048
5049 assert_eq!(worker.run_once().await.expect("baseline run once"), 0);
5050 assert_eq!(
5051 server
5052 .worker_protocol_for("/api/worker/workflow-tasks/poll")
5053 .as_deref(),
5054 Some(WORKER_PROTOCOL_VERSION)
5055 );
5056 assert_eq!(
5057 server.worker_protocol_for("/api/worker/query-tasks/poll"),
5058 None,
5059 "a worker without query handlers must not use the query-task endpoint"
5060 );
5061 }
5062
5063 #[tokio::test]
5064 async fn completion_time_query_rejection_is_typed_without_stopping_worker() {
5065 let server = MockWorkerServer::reject_query_completion();
5066 let client = Client::builder(server.base_url())
5067 .timeout(Duration::from_secs(2))
5068 .build()
5069 .expect("client");
5070
5071 let error = client
5072 .complete_query_task("query-late", "late-worker", 1, json!(8), JSON_CODEC)
5073 .await
5074 .expect_err("expired completion must be rejected");
5075 let Error::QueryFailed(failure) = error else {
5076 panic!("expected typed query failure");
5077 };
5078 assert_eq!(failure.status, 409);
5079 assert_eq!(failure.reason, "query_task_timed_out");
5080
5081 let mut worker = Worker::new(client, "rust-workers")
5082 .worker_id("late-worker")
5083 .poll_timeout(Duration::from_millis(10));
5084 worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
5085 worker.register_query(
5086 "counter",
5087 "current",
5088 |_ctx, _args| async move { Ok(json!(8)) },
5089 );
5090
5091 assert_eq!(worker.run_once().await.expect("late task is handled"), 1);
5092 assert_eq!(
5093 worker
5094 .run_once()
5095 .await
5096 .expect("worker continues after late completion"),
5097 0
5098 );
5099 assert_eq!(
5100 server.request_count("/api/worker/query-tasks/query-late/complete"),
5101 2
5102 );
5103 assert_eq!(
5104 server.request_count("/api/worker/query-tasks/query-late/fail"),
5105 0,
5106 "a server completion rejection must not be reported as an encoding failure"
5107 );
5108 }
5109
5110 #[tokio::test]
5111 async fn activity_only_worker_can_shutdown_without_workflow_poller() {
5112 let server = MockWorkerServer::start();
5113 let client = Client::builder(server.base_url())
5114 .timeout(Duration::from_secs(2))
5115 .build()
5116 .expect("client");
5117 let mut worker = Worker::new(client, "rust-workers")
5118 .worker_id("activity-only-worker")
5119 .poll_timeout(Duration::from_millis(10));
5120
5121 worker.register_activity(
5122 "activity.only",
5123 |_ctx, _args| async move { Ok(Value::Null) },
5124 );
5125
5126 worker.run_until(async {}).await.expect("run worker");
5127 }
5128
5129 #[tokio::test]
5130 async fn workflow_only_worker_can_shutdown_without_activity_poller() {
5131 let server = MockWorkerServer::start();
5132 let client = Client::builder(server.base_url())
5133 .timeout(Duration::from_secs(2))
5134 .build()
5135 .expect("client");
5136 let mut worker = Worker::new(client, "rust-workers")
5137 .worker_id("workflow-only-worker")
5138 .poll_timeout(Duration::from_millis(10));
5139
5140 worker.register_workflow(
5141 "workflow.only",
5142 |_ctx, _input| async move { Ok(Value::Null) },
5143 );
5144
5145 worker.run_until(async {}).await.expect("run worker");
5146 }
5147
5148 #[tokio::test]
5149 async fn worker_heartbeat_observer_receives_server_acknowledgements() {
5150 let server = MockWorkerServer::start();
5151 let client = Client::builder(server.base_url())
5152 .timeout(Duration::from_secs(2))
5153 .build()
5154 .expect("client");
5155 let observations = Arc::new(Mutex::new(Vec::new()));
5156 let observed = Arc::clone(&observations);
5157 let mut worker = Worker::new(client, "rust-workers")
5158 .worker_id("observed-heartbeat-worker")
5159 .poll_timeout(Duration::from_millis(10))
5160 .on_worker_heartbeat(move |observation| {
5161 observed
5162 .lock()
5163 .expect("heartbeat observations")
5164 .push(observation.clone());
5165 });
5166
5167 worker.register_workflow("workflow.observed", |_ctx, _input| async move {
5168 Ok(Value::Null)
5169 });
5170 worker
5171 .run_until(tokio::time::sleep(Duration::from_millis(20)))
5172 .await
5173 .expect("run worker");
5174
5175 let observations = observations.lock().expect("heartbeat observations");
5176 let first = observations.first().expect("heartbeat acknowledgement");
5177 assert_eq!(first.worker_id, "observed-heartbeat-worker");
5178 assert_eq!(first.task_queue, "rust-workers");
5179 assert!(first.acknowledged_at_unix_millis > 0);
5180 assert_eq!(first.acknowledgement, json!({}));
5181 }
5182
5183 #[tokio::test]
5184 async fn worker_retries_poll_and_heartbeat_transport_failures_independently() {
5185 let server = MockWorkerServer::transient_worker_failures();
5186 let client = Client::builder(server.base_url())
5187 .timeout(Duration::from_secs(2))
5188 .build()
5189 .expect("client");
5190 let mut worker = Worker::new(client, "rust-workers")
5191 .worker_id("retry-worker")
5192 .poll_timeout(Duration::from_millis(10))
5193 .retry_policy(WorkerRetryPolicy {
5194 max_retries: 2,
5195 initial_backoff: Duration::from_millis(1),
5196 max_backoff: Duration::from_millis(1),
5197 });
5198 worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
5199 worker.register_activity(
5200 "counter.activity",
5201 |_ctx, _input| async move { Ok(Value::Null) },
5202 );
5203 worker.register_query(
5204 "counter",
5205 "current",
5206 |_ctx, _args| async move { Ok(json!(8)) },
5207 );
5208
5209 worker
5210 .run_until(tokio::time::sleep(Duration::from_millis(75)))
5211 .await
5212 .expect("transient failures must not stop the worker");
5213
5214 for path in [
5215 "/api/worker/heartbeat",
5216 "/api/worker/workflow-tasks/poll",
5217 "/api/worker/activity-tasks/poll",
5218 "/api/worker/query-tasks/poll",
5219 ] {
5220 assert!(
5221 server.request_count(path) >= 2,
5222 "{path} must continue after its transient failure"
5223 );
5224 }
5225 }
5226
5227 #[tokio::test]
5228 async fn worker_bounds_transport_retries() {
5229 let server = MockWorkerServer::unavailable_polls();
5230 let client = Client::builder(server.base_url())
5231 .timeout(Duration::from_secs(2))
5232 .build()
5233 .expect("client");
5234 let mut worker = Worker::new(client, "rust-workers")
5235 .worker_id("bounded-retry-worker")
5236 .poll_timeout(Duration::from_millis(10))
5237 .retry_policy(WorkerRetryPolicy {
5238 max_retries: 2,
5239 initial_backoff: Duration::from_millis(1),
5240 max_backoff: Duration::from_millis(1),
5241 });
5242 worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
5243
5244 let error = worker.run().await.expect_err("retry bound must terminate");
5245 assert!(matches!(error, Error::Transport(_)));
5246 assert_eq!(
5247 server.request_count("/api/worker/workflow-tasks/poll"),
5248 3,
5249 "one initial request plus two retries"
5250 );
5251 }
5252
5253 #[tokio::test]
5254 async fn worker_does_not_retry_authentication_failures() {
5255 let server = MockWorkerServer::unauthorized_polls();
5256 let client = Client::builder(server.base_url())
5257 .timeout(Duration::from_secs(2))
5258 .build()
5259 .expect("client");
5260 let mut worker = Worker::new(client, "rust-workers")
5261 .worker_id("unauthorized-worker")
5262 .poll_timeout(Duration::from_millis(10));
5263 worker.register_workflow("counter", |_ctx, _input| async move { Ok(Value::Null) });
5264
5265 let error = worker
5266 .run()
5267 .await
5268 .expect_err("authentication must terminate");
5269 let Error::Http { status, body } = error else {
5270 panic!("expected stable HTTP authentication error");
5271 };
5272 assert_eq!(status, reqwest::StatusCode::UNAUTHORIZED);
5273 assert!(body.contains("authentication_failed"));
5274 assert_eq!(
5275 server.request_count("/api/worker/workflow-tasks/poll"),
5276 1,
5277 "authentication failures must not be retried"
5278 );
5279 }
5280
5281 #[derive(Clone, Debug)]
5282 struct CapturedRequest {
5283 path: String,
5284 worker_protocol: Option<String>,
5285 body: String,
5286 }
5287
5288 struct MockWorkerServer {
5289 addr: SocketAddr,
5290 stop: Arc<AtomicBool>,
5291 requests: Arc<Mutex<Vec<CapturedRequest>>>,
5292 thread: Option<thread::JoinHandle<()>>,
5293 }
5294
5295 #[derive(Clone, Copy, Default)]
5296 struct MockWorkerBehavior {
5297 reject_query_protocol: bool,
5298 reject_query_completion: bool,
5299 poll_failures_per_path: usize,
5300 heartbeat_failures: usize,
5301 unauthorized_polls: bool,
5302 }
5303
5304 impl MockWorkerServer {
5305 fn start() -> Self {
5306 Self::start_with_behavior(MockWorkerBehavior::default())
5307 }
5308
5309 fn reject_query_protocol() -> Self {
5310 Self::start_with_behavior(MockWorkerBehavior {
5311 reject_query_protocol: true,
5312 ..MockWorkerBehavior::default()
5313 })
5314 }
5315
5316 fn reject_query_completion() -> Self {
5317 Self::start_with_behavior(MockWorkerBehavior {
5318 reject_query_completion: true,
5319 ..MockWorkerBehavior::default()
5320 })
5321 }
5322
5323 fn transient_worker_failures() -> Self {
5324 Self::start_with_behavior(MockWorkerBehavior {
5325 poll_failures_per_path: 1,
5326 heartbeat_failures: 1,
5327 ..MockWorkerBehavior::default()
5328 })
5329 }
5330
5331 fn unavailable_polls() -> Self {
5332 Self::start_with_behavior(MockWorkerBehavior {
5333 poll_failures_per_path: usize::MAX,
5334 ..MockWorkerBehavior::default()
5335 })
5336 }
5337
5338 fn unauthorized_polls() -> Self {
5339 Self::start_with_behavior(MockWorkerBehavior {
5340 unauthorized_polls: true,
5341 ..MockWorkerBehavior::default()
5342 })
5343 }
5344
5345 fn start_with_behavior(behavior: MockWorkerBehavior) -> Self {
5346 let listener = TcpListener::bind("127.0.0.1:0").expect("bind mock server");
5347 listener
5348 .set_nonblocking(true)
5349 .expect("configure mock listener");
5350 let addr = listener.local_addr().expect("mock server address");
5351 let stop = Arc::new(AtomicBool::new(false));
5352 let server_stop = Arc::clone(&stop);
5353 let requests = Arc::new(Mutex::new(Vec::new()));
5354 let server_requests = Arc::clone(&requests);
5355 let thread = thread::spawn(move || {
5356 while !server_stop.load(Ordering::SeqCst) {
5357 match listener.accept() {
5358 Ok((mut stream, _)) => {
5359 handle_mock_worker_request(&mut stream, &server_requests, behavior)
5360 }
5361 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
5362 thread::sleep(Duration::from_millis(5));
5363 }
5364 Err(_) => break,
5365 }
5366 }
5367 });
5368
5369 Self {
5370 addr,
5371 stop,
5372 requests,
5373 thread: Some(thread),
5374 }
5375 }
5376
5377 fn base_url(&self) -> String {
5378 format!("http://{}", self.addr)
5379 }
5380
5381 fn worker_protocol_for(&self, path: &str) -> Option<String> {
5382 self.requests
5383 .lock()
5384 .expect("captured requests")
5385 .iter()
5386 .find(|request| request.path == path)
5387 .and_then(|request| request.worker_protocol.clone())
5388 }
5389
5390 fn request_count(&self, path: &str) -> usize {
5391 self.requests
5392 .lock()
5393 .expect("captured requests")
5394 .iter()
5395 .filter(|request| request.path == path)
5396 .count()
5397 }
5398
5399 fn request_body(&self, path: &str) -> Value {
5400 let requests = self.requests.lock().expect("captured requests");
5401 let body = &requests
5402 .iter()
5403 .find(|request| request.path == path)
5404 .unwrap_or_else(|| panic!("missing request for {path}"))
5405 .body;
5406 serde_json::from_str(body).unwrap_or_else(|error| {
5407 panic!("invalid JSON request body for {path}: {error}: {body:?}")
5408 })
5409 }
5410 }
5411
5412 impl Drop for MockWorkerServer {
5413 fn drop(&mut self) {
5414 self.stop.store(true, Ordering::SeqCst);
5415 let _ = TcpStream::connect(self.addr);
5416
5417 if let Some(thread) = self.thread.take() {
5418 thread.join().expect("join mock server");
5419 }
5420 }
5421 }
5422
5423 fn handle_mock_worker_request(
5424 stream: &mut TcpStream,
5425 requests: &Arc<Mutex<Vec<CapturedRequest>>>,
5426 behavior: MockWorkerBehavior,
5427 ) {
5428 let _ = stream.set_read_timeout(Some(Duration::from_millis(200)));
5429 let mut buffer = [0_u8; 8192];
5430 let mut request = Vec::new();
5431
5432 loop {
5433 match stream.read(&mut buffer) {
5434 Ok(0) => break,
5435 Ok(read) => {
5436 request.extend_from_slice(&buffer[..read]);
5437 if mock_request_is_complete(&request) {
5438 break;
5439 }
5440 }
5441 Err(error)
5442 if matches!(
5443 error.kind(),
5444 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
5445 ) =>
5446 {
5447 break;
5448 }
5449 Err(_) => return,
5450 }
5451 }
5452
5453 let request = String::from_utf8_lossy(&request);
5454 let body = request
5455 .split_once("\r\n\r\n")
5456 .map(|(_, body)| body)
5457 .unwrap_or_default();
5458 let path = request
5459 .lines()
5460 .next()
5461 .and_then(|line| line.split_whitespace().nth(1))
5462 .unwrap_or_default();
5463 let worker_protocol = request.lines().find_map(|line| {
5464 let (name, value) = line.split_once(':')?;
5465 name.eq_ignore_ascii_case("X-Durable-Workflow-Protocol-Version")
5466 .then(|| value.trim().to_string())
5467 });
5468 let request_number = {
5469 let mut requests = requests.lock().expect("captured requests");
5470 requests.push(CapturedRequest {
5471 path: path.to_string(),
5472 worker_protocol: worker_protocol.clone(),
5473 body: body.to_string(),
5474 });
5475 requests
5476 .iter()
5477 .filter(|request| request.path == path)
5478 .count()
5479 };
5480
5481 let is_poll = matches!(
5482 path,
5483 "/api/worker/workflow-tasks/poll"
5484 | "/api/worker/activity-tasks/poll"
5485 | "/api/worker/query-tasks/poll"
5486 );
5487 if is_poll && request_number <= behavior.poll_failures_per_path {
5488 return;
5489 }
5490 if path == "/api/worker/heartbeat" && request_number <= behavior.heartbeat_failures {
5491 return;
5492 }
5493 if behavior.unauthorized_polls && is_poll {
5494 write_mock_response(
5495 stream,
5496 "401 Unauthorized",
5497 r#"{"reason":"authentication_failed","message":"invalid worker token"}"#,
5498 );
5499 return;
5500 }
5501
5502 if behavior.reject_query_protocol && path.starts_with("/api/worker/query-tasks/") {
5503 let requested_version = worker_protocol.as_deref().unwrap_or("missing");
5504 let body = format!(
5505 r#"{{"reason":"unsupported_protocol_version","message":"Query tasks require worker protocol 1.8 or newer.","supported_version":"1.7","requested_version":"{requested_version}"}}"#
5506 );
5507 write_mock_response(stream, "400 Bad Request", &body);
5508 return;
5509 }
5510
5511 if behavior.reject_query_completion && path == "/api/worker/query-tasks/query-late/complete"
5512 {
5513 write_mock_response(
5514 stream,
5515 "409 Conflict",
5516 r#"{"reason":"query_task_timed_out","message":"query task timed out before completion"}"#,
5517 );
5518 return;
5519 }
5520
5521 let (status, body) = match path {
5522 "/api/worker/register" => (
5523 "200 OK",
5524 r#"{"worker_id":"mock-worker","registered":true,"heartbeat_interval_seconds":3600}"#,
5525 ),
5526 "/api/worker/heartbeat" => ("200 OK", "{}"),
5527 "/api/worker/activity-tasks/poll" | "/api/worker/workflow-tasks/poll" => {
5528 ("200 OK", r#"{"task":null}"#)
5529 }
5530 "/api/worker/query-tasks/poll"
5531 if behavior.reject_query_completion && request_number == 1 =>
5532 {
5533 (
5534 "200 OK",
5535 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"}}"#,
5536 )
5537 }
5538 "/api/worker/query-tasks/poll" => ("200 OK", r#"{"task":null}"#),
5539 "/api/worker/query-tasks/query-capture/complete"
5540 | "/api/worker/query-tasks/query-capture/fail" => ("200 OK", "{}"),
5541 "/api/workflows/counter-1/query/current" => (
5542 "200 OK",
5543 r#"{"workflow_id":"counter-1","query_name":"current","result":{"count":8},"result_envelope":{"codec":"json","blob":"{\"count\":8}"}}"#,
5544 ),
5545 "/api/workflows/counter-1/query/missing" => (
5546 "404 Not Found",
5547 r#"{"workflow_id":"counter-1","query_name":"missing","reason":"rejected_unknown_query","message":"unknown query"}"#,
5548 ),
5549 _ => ("404 Not Found", r#"{"message":"not found"}"#),
5550 };
5551 write_mock_response(stream, status, body);
5552 }
5553
5554 fn mock_request_is_complete(request: &[u8]) -> bool {
5555 let Some(header_end) = request
5556 .windows(4)
5557 .position(|window| window == b"\r\n\r\n")
5558 .map(|position| position + 4)
5559 else {
5560 return false;
5561 };
5562 let headers = String::from_utf8_lossy(&request[..header_end]);
5563 let content_length = headers.lines().find_map(|line| {
5564 let (name, value) = line.split_once(':')?;
5565 name.eq_ignore_ascii_case("content-length")
5566 .then(|| value.trim().parse::<usize>().ok())
5567 .flatten()
5568 });
5569
5570 request.len() >= header_end + content_length.unwrap_or(0)
5571 }
5572
5573 fn write_mock_response(stream: &mut TcpStream, status: &str, body: &str) {
5574 let response = format!(
5575 "HTTP/1.1 {status}\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}",
5576 body.len()
5577 );
5578
5579 let _ = stream.write_all(response.as_bytes());
5580 let _ = stream.flush();
5581 }
5582}