本页译自英文,由模型翻译。若与英文原文存在差异,以英文原文为准。
确认与投递保证
本指南用处理器的结果结算 JetStream 投递,为慢处理器设定 ack_wait,处理投递了两次的消息,并说明 Core NATS 不保证什么。
处理器用它返回的值结算自己的投递,运行时在处理器结束后发送结算。 在 JetStream 消费者上,每种结果都是一个服务器据以行动的确认:
| 结果 | 发给服务器 | 服务器做什么 |
|---|---|---|
ack | +ACK | 这个消费者不再保留这条消息 |
retry | -NAK | 立即再次投递它 |
retry_after(delay) | 带延迟的 -NAK | 扣留它,延迟过后再次投递 |
drop | +TERM | 不再投递它 |
| 没有结算 | 什么也不发 | ack_wait 过后再次投递它 |
重新投递保留消息的流序号,服务器统计它的投递次数。
示例是一个支付服务。 它通过银行扣收已授权的付款,并把每次扣款通知给看板。
结算一笔付款
- 宏
- 手写
/// How long a busy bank asks its callers to wait.
const BACKOFF: Duration = Duration::from_secs(30);
#[derive(Debug, PartialEq, Serialize, Deserialize, Outgoing)]
#[outgoing(name = "payments.authorized")]
struct Payment {
id: u64,
card: String,
amount_cents: u64,
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Outgoing)]
#[outgoing(name = "payments.captured")]
struct Captured {
payment: u64,
amount_cents: u64,
}
#[subscriber(
JetStreamSubject::new("payments.authorized", "PAYMENTS")
.durable("capture")
.ack_wait(NonZeroDuration::from_secs(nonzero!(60))),
reply
)]
async fn capture(
payment: &Payment,
Ctx(delivered): Ctx<Delivered>,
State(bank): State<Arc<Bank>>,
) -> Result<Captured, HandlerOutcome> {
if delivered.is_some_and(|count| count > 1) {
tracing::info!(payment.id, ?delivered, "payment delivered again");
}
match bank.capture(payment).await {
Ok(()) => Ok(Captured {
payment: payment.id,
amount_cents: payment.amount_cents,
}),
// Past the rate limit: the server holds the payment and delivers it after the delay.
Err(Refusal::Busy) => Err(HandlerOutcome::retry_after(BACKOFF)),
// No answer: the server delivers the payment again at once.
Err(Refusal::Unavailable) => Err(HandlerOutcome::retry()),
// The server never delivers a terminated payment again.
Err(Refusal::Declined) => {
tracing::warn!(payment.id, "card declined, payment terminated");
Err(HandlerOutcome::drop())
}
}
}
/// How long a busy bank asks its callers to wait.
const BACKOFF: Duration = Duration::from_secs(30);
#[derive(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[schemars(crate = "ruststream::schemars")]
struct Payment {
id: u64,
card: String,
amount_cents: u64,
}
impl OutgoingDestination for Payment {
type Form = FixedName;
const DESTINATION: &'static str = "payments.authorized";
}
impl MessageHeaders for Payment {
type Contract = NoHeaders;
}
impl MessageInfo for Payment {
const NAME: &'static str = "Payment";
}
#[derive(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[schemars(crate = "ruststream::schemars")]
struct Captured {
payment: u64,
amount_cents: u64,
}
impl OutgoingDestination for Captured {
type Form = FixedName;
const DESTINATION: &'static str = "payments.captured";
}
impl MessageHeaders for Captured {
type Contract = NoHeaders;
}
impl MessageInfo for Captured {
const NAME: &'static str = "Captured";
}
struct Capture;
impl Handle<Payment, Captured, (), JetStreamContext, Clients> for Capture {
async fn handle(
&self,
payment: &Payment,
_outs: &(),
ctx: &mut Context<'_, JetStreamContext, Clients>,
) -> Result<Captured, HandlerOutcome> {
let delivered = ctx.context(DELIVERED);
if delivered.is_some_and(|count| count > 1) {
tracing::info!(payment.id, ?delivered, "payment delivered again");
}
match ctx.state().bank.capture(payment).await {
Ok(()) => Ok(Captured {
payment: payment.id,
amount_cents: payment.amount_cents,
}),
// Past the rate limit: the server holds the payment and delivers it after the delay.
Err(Refusal::Busy) => Err(HandlerOutcome::retry_after(BACKOFF)),
// No answer: the server delivers the payment again at once.
Err(Refusal::Unavailable) => Err(HandlerOutcome::retry()),
// The server never delivers a terminated payment again.
Err(Refusal::Declined) => {
tracing::warn!(payment.id, "card declined, payment terminated");
Err(HandlerOutcome::drop())
}
}
}
}
银行有四种答复,每种需要不同的结果。
扣款成功时返回 Captured,运行时把它作为回复发布,然后确认这笔付款。
Busy 表示银行的速率限额已经用完,所以付款在服务器上等待 30 秒。
Unavailable 表示银行没有应答,所以付款立即回来。
Declined 表示这张卡无法扣款,再试一次也是同样的答复,所以这笔付款被终止。
不设上限的 retry 会无休止地回来;重试与死信加上上限和死信 subject。
投递了两次的消息
JetStream 至少投递一次。
在 retry 之后、ack_wait 耗尽之后,以及进程在结算前停止之后,付款都会回来。
所以处理器会两次看到某些付款,它的工作必须能接受重复。
这里银行把付款 id 当作幂等键,重复的扣款只扣一次。
Ctx<Delivered> 是服务器统计的投递次数,第一次投递时为 1;capture 会记录它再次看到的付款。
慢处理器与 ack_wait
服务器等待结算的时间是 ack_wait,描述符未另行指定时为 30 秒。
超时后它再次投递消息,而第一个处理器可能仍在运行。
capture 设为 60 秒,高于银行答复的最长时间。
把 ack_wait 设得高于处理器的最坏耗时,两次投递就永远不会重叠。
耗尽的 ack_wait 也计入尝试上限的投递次数。
Core NATS 上的看板
- 宏
- 手写
#[subscriber("payments.captured")]
async fn refresh(captured: &Captured) -> HandlerOutcome {
tracing::info!(captured.payment, "dashboard refreshed");
HandlerOutcome::ack()
}
struct Refresh;
impl Handle<Captured, (), (), (), Clients> for Refresh {
fn handle(
&self,
captured: &Captured,
_outs: &(),
_ctx: &mut Context<'_, (), Clients>,
) -> impl Future<Output = Result<(), HandlerOutcome>> {
tracing::info!(captured.payment, "dashboard refreshed");
ready(Ok(()))
}
}
refresh 从一个 Core NATS subject 读取扣款。
Core NATS 至多投递一次,并且没有确认。
结算 Core 投递会报告 AckError::Unsupported,运行时把它记录为警告。
进程丢失的 Core 消息就此消失,没有任何东西能把它找回来。
看板可以丢失一次更新,因为下一次扣款会再次刷新它。
付款不能丢失,所以付款经由流传递。
应用
- 宏
- 手写
#[ruststream::app]
fn app() -> impl App<State = Clients> {
RustStream::new(AppInfo::new("payments", "0.1.0"))
.on_startup(async move |()| Ok::<_, Infallible>(Clients::default()))
.with_broker(NatsBroker::new("nats://localhost:4222"), |b| {
b.include(capture);
b.include(refresh);
})
}
fn app() -> impl App<State = Clients> {
RustStream::new(AppInfo::new("payments", "0.1.0"))
.on_startup(async move |()| Ok::<_, Infallible>(Clients::default()))
.with_broker(NatsBroker::new("nats://localhost:4222"), |b| {
b.include(
subscriber(
JetStreamSubject::new("payments.authorized", "PAYMENTS")
.durable("capture")
.ack_wait(NonZeroDuration::from_secs(nonzero!(60))),
Capture,
)
.reply()
.build(),
);
b.include(subscriber("payments.captured", Refresh).build());
})
}
fn main() -> ExitCode {
run_main(app)
}
银行客户端是应用的状态,在启动时打开一次。
批处理器用一个结果结算整批;返回 Vec<HandlerOutcome> 时,则逐条结算每条消息。
示例文件中的测试在暂停的时钟上于进程内运行应用,所以 Busy 的 30 秒瞬间就过去了。