Перейти к основному содержимому
Нормативная версия документации - английская

Эта страница переведена с английского языковой моделью. При любом расхождении верен английский оригинал.

Сообщения: полезная нагрузка, заголовки, кодеки

Заказ получает строки, контракт заголовков, обязательный при каждой публикации, и кодек, выбранный для каждой темы.

Полезная нагрузка - это любой тип 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> и задаёт имя сообщения в AsyncAPI-документе реализацией MessageInfo.

Контракт заголовков​

Каждое поле контракта - один заголовок. 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-документ показывает схему заголовков.

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, которые читают другие команды. Темы между шагами самого магазина передают сообщения в 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 нужна фича cbor крейта.