跳到主要内容
英文版本为准

本页译自英文,由模型翻译。若与英文原文存在差异,以英文原文为准。

消息:载荷、消息头、编解码器

订单扩展为带订单行的载荷,加上每次发布都必须携带的消息头契约,以及按主题选定的编解码器。

载荷可以是任意 serde 类型。编解码器把它转换成字节,也把字节转换回来。 消息头与载荷一起传递。消息头契约是一个 serde 结构体,每个字段对应一个消息头。 如果消息类型指定了契约,不带契约就无法发布:这样的发布无法编译。 订阅者通过 Headers<T> 参数读取契约,解析发生在主体运行之前。 消息头解析失败的投递不会到达主体,而是被丢弃,与无法解码的载荷相同。 编解码器属于挂载点,而不属于类型。因此同一个类型可以在一个主题上以 JSON 传递,在另一个主题上以 CBOR 传递。

订单​

从这里开始,商店的代码放在各个模块里。 orders.rs 存放订单:订单行、总额,以及随订单一起传递的契约。 上限规定一个订单最多能买多少,总额用带溢出检查的算术累加,所以超出上限的订单没有总额。

orders.rs
/// How many lines an order may hold.
pub(crate) const MAX_LINES: usize = 100;
/// How many items one line may order.
pub(crate) const MAX_QUANTITY: u32 = 1_000;
/// The highest price of one item, in cents.
pub(crate) const MAX_PRICE_CENTS: u64 = 1_000_000;

/// One line of an order: what the customer buys, how many, and at what price.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
pub(crate) struct Line {
pub(crate) sku: String,
pub(crate) quantity: u32,
pub(crate) price_cents: u64,
}

/// An order placed in the storefront. It travels with the headers of the request that placed it.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema, Outgoing)]
#[outgoing(name = "orders.placed", headers = RequestMeta)]
pub(crate) struct Order {
pub(crate) id: u64,
pub(crate) customer: String,
pub(crate) lines: Vec<Line>,
}

/// Whether `lines` stay within the shop's bounds.
pub(crate) fn within_bounds(lines: &[Line]) -> bool {
lines.len() <= MAX_LINES
&& lines
.iter()
.all(|line| line.quantity <= MAX_QUANTITY && line.price_cents <= MAX_PRICE_CENTS)
}

/// What `lines` cost together, or `None` past the shop's bounds.
pub(crate) fn total_cents(lines: &[Line]) -> Option<u64> {
if !within_bounds(lines) {
return None;
}
lines.iter().try_fold(0_u64, |total, line| {
total.checked_add(u64::from(line.quantity).checked_mul(line.price_cents)?)
})
}

headers = RequestMeta 让契约成为类型的一部分。 手写形式用 type Contract = WithHeaders<RequestMeta> 表达同样的意思,并通过一个 MessageInfo impl 为消息在 AsyncAPI 文档中命名。

消息头契约​

契约的每个字段是一个消息头。 request_id 指明下单的那次店面请求,这样每一步都能记录它。

orders.rs
/// The headers of a placed order: each field is one header.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
pub(crate) struct RequestMeta {
pub(crate) request_id: String,
}

读取消息头​

audit 是 orders.placed 上的第二个订阅者:它把每个订单连同下单请求写进审计日志。 每个订阅都会收到其主题的每一条消息,因此 audit 和下一章的 accept 都能看到每个订单。 手写形式以 Message<RequestMeta, Order> 这一对作为输入:两部分都在主体运行之前解码,AsyncAPI 文档会展示消息头的 schema。

orders.rs
/// Writes every placed order to the audit log, with the request that placed it.
#[subscriber("orders.placed")]
async fn audit(order: &Order, Headers(meta): Headers<RequestMeta>) -> HandlerOutcome {
tracing::info!(
request = %meta.request_id,
order = order.id,
total_cents = total_cents(&order.lines),
"order placed",
);
HandlerOutcome::ack()
}

商店内部主题的编解码器​

JSON 是默认编解码器。orders.placed 和 notices 由其他团队读取,商店在这两个主题上保留 JSON。 商店各步之间的主题以 CBOR 传递:体积更小,而且没有别人读取。 一个常量指定这个编解码器,这样后面几章的路由器不会在这一点上出现分歧。

payments.rs
/// The codec of the topics between the shop's own steps: CBOR is smaller than JSON, and no
/// other team reads them.
pub(crate) const INTERNAL: CborCodec = CborCodec;

路由器用 with_codec 指定的编解码器解码,每个发布位置用 .codec(..) 指定自己的编解码器。 下一章会同时用到这两者。 CBOR 需要 crate 的 cbor feature。