本页译自英文,由模型翻译。若与英文原文存在差异,以英文原文为准。
重试与死信
本指南立即或稍后重试一次投递,限定尝试次数,并把一直失败的消息存入死信流,适用于流消费者、Core subject 和模式。
NATS 没有死信机制。 JetStream 可以限定一条消息的投递次数,但不会把最后一次投递发往任何地方。 因此由挂载点声明上限和死信 subject,框架把它们应用到两种模型上。
示例是一个支付服务,它通过银行结清付款并发出退款。 它把死信保存在一个专用的流中:
nats stream add PAYMENTS --subjects 'payments.settle' --defaults
nats stream add DEAD --subjects 'dead.*' --defaults
在流上重试
- 宏
- 手写
/// How long a busy bank asks its callers to wait.
const BACKOFF: Duration = Duration::from_secs(30);
/// The outcome a refusal of the bank calls for.
fn outcome(refusal: Refusal) -> HandlerOutcome {
match refusal {
// Past the rate limit: a call right now would be refused again.
Refusal::Busy => HandlerOutcome::retry_after(BACKOFF),
// No answer: try again at once, up to the attempt cap.
Refusal::Unavailable => HandlerOutcome::retry(),
Refusal::Declined => HandlerOutcome::drop(),
}
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Outgoing)]
#[outgoing(name = "payments.settle")]
struct Payment {
id: u64,
card: String,
amount_cents: u64,
}
#[subscriber(JetStreamSubject::new("payments.settle", "PAYMENTS").durable("settler"))]
async fn settle(payment: &Payment, State(bank): State<Arc<Bank>>) -> HandlerOutcome {
match bank.charge(&payment.card).await {
Ok(()) => HandlerOutcome::ack(),
Err(refusal) => {
tracing::warn!(payment.id, ?refusal, "payment refused by the bank");
outcome(refusal)
}
}
}
/// How long a busy bank asks its callers to wait.
const BACKOFF: Duration = Duration::from_secs(30);
/// The outcome a refusal of the bank calls for.
fn outcome(refusal: Refusal) -> HandlerOutcome {
match refusal {
// Past the rate limit: a call right now would be refused again.
Refusal::Busy => HandlerOutcome::retry_after(BACKOFF),
// No answer: try again at once, up to the attempt cap.
Refusal::Unavailable => HandlerOutcome::retry(),
Refusal::Declined => HandlerOutcome::drop(),
}
}
#[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.settle";
}
impl MessageHeaders for Payment {
type Contract = NoHeaders;
}
impl MessageInfo for Payment {
const NAME: &'static str = "Payment";
}
struct Settle;
impl Handle<Payment, (), (), (), Clients> for Settle {
async fn handle(
&self,
payment: &Payment,
_outs: &(),
ctx: &mut Context<'_, (), Clients>,
) -> Result<(), HandlerOutcome> {
match ctx.state().bank.charge(&payment.card).await {
Ok(()) => Ok(()),
Err(refusal) => {
tracing::warn!(payment.id, ?refusal, "payment refused by the bank");
Err(outcome(refusal))
}
}
}
}
outcome 把银行的拒绝变成处理器的结果。
银行繁忙时,retry_after 在 30 秒后再问一次。
在 JetStream 消费者上,延迟放在否定确认里:服务器扣留这笔付款,然后在同一个消费者上以相同的流序号再次投递它。
不发布任何东西,在此期间停止的进程也不会丢失任何东西。
银行没有应答时,retry 立即再问一次;卡被拒时,付款被丢弃。
上限与死信
- 宏
- 手写
#[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| {
// Five deliveries of one payment, the first included; then the dead-letter subject.
b.include(settle)
.max_attempts(nonzero!(5u32))
.dead_letter("dead.payments");
b.include(review);
// A pattern is no subject to publish a copy to, so the mount site names one.
b.include(send_refund)
.max_attempts(nonzero!(3u32))
.dead_letter("dead.refunds")
.out_retry(Publish)
.to("refunds.retry");
})
}
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.settle", "PAYMENTS").durable("settler"),
Settle,
)
.build(),
)
// Five deliveries of one payment, the first included; then the dead-letter subject.
.max_attempts(nonzero!(5u32))
.dead_letter("dead.payments");
b.include(
subscriber(
JetStreamSubject::new("dead.payments", "DEAD").durable("review"),
Review,
)
.build(),
);
// A pattern is no subject to publish a copy to, so the mount site names one.
b.include(subscriber(CoreWildcard::new("refunds.*"), SendRefund).build())
.max_attempts(nonzero!(3u32))
.dead_letter("dead.refunds")
.out_retry(Publish)
.to("refunds.retry");
})
}
fn main() -> ExitCode {
run_main(app)
}
max_attempts 统计一笔付款的每次投递,第一次也算在内。
在 JetStream 消费者上,这个次数就是服务器自己统计的次数,所以耗尽的 ack_wait 也计算在内。
第五次投递不再重试:dead_letter 把它发布到 dead.payments,载荷和消息头保持到达时的样子。
被丢弃的付款完成结算,不会去任何地方,所以死信中只有用完了尝试次数的付款。
设了上限却没有死信 subject 的消费者,会终止它的最后一次投递。
dead.payments 是一次普通发布,而 Core NATS 不保存无人读取的消息。
DEAD 流保存它,所以死信会一直等到有人查看。
应用读取的任何 subject 都不与死信 subject 匹配,否则用完尝试次数的投递会回到放弃了它的处理器。
读取死信
- 宏
- 手写
#[subscriber(JetStreamSubject::new("dead.payments", "DEAD").durable("review"))]
async fn review(payment: &Payment) -> HandlerOutcome {
tracing::error!(payment.id, "payment needs a person to look at it");
HandlerOutcome::ack()
}
struct Review;
impl Handle<Payment, (), (), (), Clients> for Review {
fn handle(
&self,
payment: &Payment,
_outs: &(),
_ctx: &mut Context<'_, (), Clients>,
) -> impl Future<Output = Result<(), HandlerOutcome>> {
tracing::error!(payment.id, "payment needs a person to look at it");
ready(Ok(()))
}
}
review 像读取其他流一样从 DEAD 流读取失败的付款,并把每一笔记入日志,留给人来查看。
在 Core subject 上重试
- 宏
- 手写
#[derive(Debug, PartialEq, Serialize, Deserialize, Outgoing)]
struct Refund {
id: u64,
card: String,
amount_cents: u64,
}
#[subscriber(CoreWildcard::new("refunds.*"))]
async fn send_refund(refund: &Refund, State(bank): State<Arc<Bank>>) -> HandlerOutcome {
match bank.charge(&refund.card).await {
Ok(()) => HandlerOutcome::ack(),
Err(refusal) => {
tracing::warn!(refund.id, ?refusal, "refund refused by the bank");
outcome(refusal)
}
}
}
#[derive(Debug, PartialEq, Serialize, Deserialize, JsonSchema)]
#[schemars(crate = "ruststream::schemars")]
struct Refund {
id: u64,
card: String,
amount_cents: u64,
}
impl OutgoingDestination for Refund {
type Form = CallerName;
}
impl MessageHeaders for Refund {
type Contract = NoHeaders;
}
impl MessageInfo for Refund {
const NAME: &'static str = "Refund";
}
struct SendRefund;
impl Handle<Refund, (), (), (), Clients> for SendRefund {
async fn handle(
&self,
refund: &Refund,
_outs: &(),
ctx: &mut Context<'_, (), Clients>,
) -> Result<(), HandlerOutcome> {
match ctx.state().bank.charge(&refund.card).await {
Ok(()) => Ok(()),
Err(refusal) => {
tracing::warn!(refund.id, ?refusal, "refund refused by the bank");
Err(outcome(refusal))
}
}
}
}
Core NATS 无法扣留消息。
在 Core 订阅上,一次重试就是框架发布的投递副本:retry 立即发布它,retry_after 在延迟结束时发布它。
副本在 x-ruststream-retry-count 消息头中带着投递次数,上限读取的就是它。
延迟的副本是至多一次的:在延迟结束前停止的进程会丢失它。
CoreSubject 把副本发布到它自己的 subject。
send_refund 读取模式 refunds.*,这个模式没有唯一的 subject 可供发布。
所以应用用 out_retry(Publish).to("refunds.retry") 指定一个模式能匹配的 subject,副本就会回到 send_refund。
基于模式却没有这一设置的注册拒绝启动,不论它的处理器是否重试。
退款的第三次投递进入 dead.refunds,次数记在消息头里。
示例文件中的测试在暂停的时钟上于进程内运行应用,所以 tb.advance(BACKOFF) 让 30 秒瞬间过去。
测试 NATS 服务讲解了测试套件。