-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcross_queue_workflow.rs
More file actions
162 lines (142 loc) · 5.82 KB
/
Copy pathcross_queue_workflow.rs
File metadata and controls
162 lines (142 loc) · 5.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
//! Coordinate a parent task with child work in another queue.
//!
//! The parent checkpoints a typed `TaskRef` to the child, then deliberately fails.
//! On retry, the checkpoint replays that same child reference, so the parent can await
//! the exact task it already created instead of spawning another one. The child spawn
//! also uses an idempotency key to cover the smaller crash window between creating the
//! child and committing the parent checkpoint.
//!
//! Separate queues are intentional: Steda rejects same-queue waits because a finite
//! worker pool could otherwise deadlock with every slot occupied by parents waiting
//! for children that need those same slots.
/// Shared setup and finite-worker helpers.
mod common;
use std::{
sync::{
Arc,
atomic::{AtomicUsize, Ordering},
},
time::Duration,
};
use common::RunningWorker;
use serde::{Deserialize, Serialize};
use steda::{Error, Queue, Result, RetryStrategy, Step, Task, TaskContext, TaskRef};
/// Input for the child email task.
#[derive(Debug, Deserialize, Serialize)]
struct EmailReceiptInput {
/// Order whose receipt is being sent.
order_id: String,
/// Destination email address.
address: String,
}
/// Result returned by the child email task.
#[derive(Debug, Deserialize, Serialize)]
struct EmailReceiptOutput {
/// Identifier assigned to the sent message.
message_id: String,
}
/// Child task definition.
const EMAIL_RECEIPT: Task<EmailReceiptInput, EmailReceiptOutput> = Task::new("email-receipt");
/// Input for the parent order task.
#[derive(Debug, Deserialize, Serialize)]
struct CompleteOrderInput {
/// Order whose receipt is being sent.
order_id: String,
/// Address that should receive the receipt.
email: String,
}
/// Parent result after the child task completes.
#[derive(Debug, Deserialize, Serialize)]
struct CompleteOrderOutput {
/// Order whose receipt is being sent.
order_id: String,
/// Message identifier returned by the child task.
receipt_message_id: String,
}
/// Parent task definition.
const COMPLETE_ORDER: Task<CompleteOrderInput, CompleteOrderOutput> = Task::new("complete-order");
/// Checkpoint used for child task creation.
const SPAWN_RECEIPT: Step<TaskRef<EmailReceiptInput, EmailReceiptOutput>> =
Step::new("spawn-receipt");
/// Completes one order workflow attempt and waits for the receipt task.
async fn complete_order(
input: CompleteOrderInput,
ctx: TaskContext,
child_queue: Queue,
) -> Result<CompleteOrderOutput> {
let order_id = input.order_id.clone();
let email_address = input.email.clone();
let receipt_queue = child_queue.clone();
let child_order_id = order_id.clone();
let child_idempotency_key = format!("receipt:{}", ctx.task_id());
// Checkpoint child creation so a parent retry cannot create a second logical child.
let child = ctx
.step(SPAWN_RECEIPT, async move || {
let child = receipt_queue
.spawn(
EMAIL_RECEIPT,
EmailReceiptInput { order_id: child_order_id.clone(), address: email_address },
)
.idempotency_key(child_idempotency_key)
.await?;
Ok(child.task_ref())
})
.await?;
// Simulate a transient parent failure after the child reference is durable.
if ctx.attempt() == 1 {
println!("parent attempt 1 checkpointed the child task, then simulated a restart");
return Err(Error::Other("order worker restarted after spawning receipt".to_owned()));
}
// The retry recovered the same typed child reference from the checkpoint above.
println!("parent attempt 2 reused the checkpointed child task");
let receipt = ctx.await_task(&child).timeout(Duration::from_secs(10)).await?;
Ok(CompleteOrderOutput { order_id: input.order_id, receipt_message_id: receipt.message_id })
}
/// Run the cross-queue dependency example.
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<()> {
let steda = common::connect().await?;
let orders = steda.queue("example-orders")?;
let email = steda.queue("example-email")?;
orders.create().await?;
email.create().await?;
let receipt_runs = Arc::new(AtomicUsize::new(0));
let worker_receipt_runs = Arc::clone(&receipt_runs);
let email_worker = email
.worker()
.task(EMAIL_RECEIPT, move |input: EmailReceiptInput, _ctx: TaskContext| {
let worker_receipt_runs = Arc::clone(&worker_receipt_runs);
async move {
worker_receipt_runs.fetch_add(1, Ordering::SeqCst);
println!("sending one receipt for {} to {}", input.order_id, input.address);
Ok(EmailReceiptOutput { message_id: "MSG-1001".to_owned() })
}
})
.build()?;
let email_worker = RunningWorker::start(email_worker);
let child_queue = email.clone();
let orders_worker = orders
.worker()
.task(COMPLETE_ORDER, move |input, ctx| complete_order(input, ctx, child_queue.clone()))
.build()?;
let orders_worker = RunningWorker::start(orders_worker);
let task = orders
.spawn(
COMPLETE_ORDER,
CompleteOrderInput {
order_id: "ORD-1001".to_owned(),
email: "buyer@example.invalid".to_owned(),
},
)
.max_attempts(2)
.retry_strategy(RetryStrategy::fixed(Duration::from_millis(250)))
.await?;
let completed = task.result_with_timeout(Duration::from_secs(15)).await?;
assert_eq!(receipt_runs.load(Ordering::SeqCst), 1);
println!("order {} completed", completed.order_id);
println!("receipt: {}", completed.receipt_message_id);
println!("receipt task executions: {}", receipt_runs.load(Ordering::SeqCst));
orders_worker.stop().await?;
email_worker.stop().await?;
Ok(())
}