Эта страница переведена с английского языковой моделью. При любом расхождении верен английский оригинал.
Сообщения: полезная нагрузка, заголовки, кодеки
Заказ получает строки, контракт заголовков, обязательный при каждой публикации, и кодек, выбранный для каждой темы.
Полезная нагрузка - это любой тип 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> и задаёт имя сообщения в AsyncAPI-документе реализацией MessageInfo.
Контракт заголовков
Каждое поле контракта - один заголовок.
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-документ показывает схему заголовков.
- Макросы
- Вручную
/// 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, которые читают другие команды.
Темы между шагами самого магазина передают сообщения в 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 нужна фича cbor крейта.