本页译自英文,由模型翻译。若与英文原文存在差异,以英文原文为准。
消息:载荷、消息头、编解码器
订单扩展为带订单行的载荷,加上每次发布都必须携带的消息头契约,以及按主题选定的编解码器。
载荷可以是任意 serde 类型。编解码器把它转换成字节,也把字节转换回来。
消息头与载荷一起传递。消息头契约是一个 serde 结构体,每个字段对应一个消息头。
如果消息类型指定了契约,不带契约就无法发布:这样的发布无法编译。
订阅者通过 Headers<T> 参数读取契约,解析发生在主体运行之前。
消息头解析失败的投递不会到达主体,而是被丢弃,与无法解码的载荷相同。
编解码器属于挂载点,而不属于类型。因此同一个类型可以在一个主题上以 JSON 传递,在另一个主题上以 CBOR 传递。
订单
从这里开始,商店的代码放在各个模块里。
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)?)
})
}
/// 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)]
pub(crate) struct Order {
pub(crate) id: u64,
pub(crate) customer: String,
pub(crate) lines: Vec<Line>,
}
// What `#[derive(Outgoing)]` with `#[outgoing(name = "orders.placed", headers = RequestMeta)]`
// writes: the destination, the headers a publish must carry, and the name in the document.
impl OutgoingDestination for Order {
type Form = FixedName;
const DESTINATION: &'static str = "orders.placed";
}
impl MessageHeaders for Order {
type Contract = WithHeaders<RequestMeta>;
}
impl MessageInfo for Order {
const NAME: &'static str = "Order";
const DESCRIPTION: Option<&'static str> = Some(
"An order placed in the storefront. It travels with the headers of the request that \
placed it.",
);
}
/// 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 指明下单的那次店面请求,这样每一步都能记录它。
- 宏
- 手写
/// 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,
}
/// 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。
- 宏
- 手写
/// 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()
}
/// Writes every placed order to the audit log, with the request that placed it.
struct Audit;
// The input is the pair of headers and payload: both decode before the body runs, and the
// document shows the header schema, as a `Headers<RequestMeta>` parameter does with the macros.
impl Handle<Message<RequestMeta, Order>, (), (), (), Shop> for Audit {
fn handle(
&self,
placed: &Message<RequestMeta, Order>,
_outs: &(),
_ctx: &mut Context<'_, (), Shop>,
) -> impl Future<Output = Result<(), HandlerOutcome>> {
let (meta, order) = (&placed.headers, &placed.body);
tracing::info!(
request = %meta.request_id,
order = order.id,
total_cents = total_cents(&order.lines),
"order placed",
);
ready(Ok(()))
}
}
商店内部主题的编解码器
JSON 是默认编解码器。orders.placed 和 notices 由其他团队读取,商店在这两个主题上保留 JSON。
商店各步之间的主题以 CBOR 传递:体积更小,而且没有别人读取。
一个常量指定这个编解码器,这样后面几章的路由器不会在这一点上出现分歧。
- 宏
- 手写
/// 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;
/// 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。