Эта страница переведена с английского языковой моделью. При любом расхождении верен английский оригинал.
Публикация и ответы
Это руководство отвечает на сообщения Pulsar, публикует сообщения с ключом и заголовками через слот Out и публикует после запуска сервиса.
Пример - сервис оформления заказов. Он подтверждает заказы квитанциями, отвечает на запросы цен, отправляет на склады листы комплектации и объявляет о себе при запуске.
Ответ в объявленный топик
- Макросы
- Вручную
/// The receipt of an order; the type names the topic it goes to.
#[derive(Debug, PartialEq, Serialize, Deserialize, Outgoing)]
#[outgoing(name = "receipts")]
struct Receipt {
order: u64,
}
/// Confirms each order with a receipt.
#[subscriber(PulsarSubscription::new("orders", "receipts"), reply)]
async fn confirm(order: &Order) -> Receipt {
tracing::info!(order.id, "order confirmed");
Receipt { order: order.id }
}
/// The receipt of an order; the type names the topic it goes to.
#[derive(Debug, PartialEq, Serialize, Deserialize, Outgoing, JsonSchema)]
#[outgoing(name = "receipts")]
struct Receipt {
order: u64,
}
/// Confirms each order with a receipt.
struct Confirm;
impl Handle<Order, Receipt> for Confirm {
fn handle(
&self,
order: &Order,
_outs: &(),
_ctx: &mut Context<'_>,
) -> impl Future<Output = Result<Receipt, HandlerOutcome>> {
tracing::info!(order.id, "order confirmed");
ready(Ok(Receipt { order: order.id }))
}
}
confirm возвращает Receipt, и фреймворк его публикует.
#[outgoing(name = "receipts")] закрепляет топик за типом, поэтому подписчику достаточно аргумента reply макроса без имени. В ручной форме - .reply().
Политика публикации брокера, Publish из прелюдии, используется по умолчанию, поэтому простой include отвечает через неё.
Публикация возвращает управление, когда брокер сохранил сообщение.
Ответ в топик, который называет точка монтирования
- Макросы
- Вручную
#[derive(Debug, PartialEq, Deserialize)]
struct QuoteRequest {
request: u64,
items: u32,
}
/// A price quote; the mount site names where it goes.
#[derive(Debug, PartialEq, Serialize, Deserialize, Outgoing)]
struct Quote {
request: u64,
total_cents: u64,
}
/// Answers a quote request on `quotes`, the topic the requesting services read.
#[subscriber(PulsarSubscription::new("quote-requests", "quotes"), reply("quotes"))]
async fn quote(request: &QuoteRequest) -> Quote {
Quote {
request: request.request,
total_cents: u64::from(request.items) * 1_250,
}
}
#[derive(Debug, PartialEq, Deserialize, JsonSchema)]
struct QuoteRequest {
request: u64,
items: u32,
}
/// A price quote; the mount site names where it goes.
#[derive(Debug, PartialEq, Serialize, Deserialize, Outgoing, JsonSchema)]
struct Quote {
request: u64,
total_cents: u64,
}
/// Answers a quote request on `quotes`, the topic the requesting services read.
struct AnswerQuote;
impl Handle<QuoteRequest, Quote> for AnswerQuote {
fn handle(
&self,
request: &QuoteRequest,
_outs: &(),
_ctx: &mut Context<'_>,
) -> impl Future<Output = Result<Quote, HandlerOutcome>> {
ready(Ok(Quote {
request: request.request,
total_cents: u64::from(request.items) * 1_250,
}))
}
}
Quote не называет топик, поэтому его называет reply("quotes") в точке монтирования. В ручной форме - .reply().to("quotes").
Тогда тот же тип может отвечать в другой топик в другом сервисе.
Запрос и ответ на него проходят через два обычных топика.
Сервис, который запрашивает цены, публикует в quote-requests и читает quotes через собственную подписку.
Он сопоставляет каждый ответ с запросом по общему для двух сообщений номеру request.
Публикация с ключом и заголовками
- Макросы
- Вручную
/// A pick list for the warehouse that ships the order.
#[derive(Debug, PartialEq, Serialize, Deserialize, Outgoing)]
#[outgoing(name = "warehouse-picks")]
struct Pick {
order: u64,
}
#[derive(OutSlot)]
#[publishes(Pick)]
struct Picks;
/// Sends each order to its warehouse, keyed by the warehouse so its picks stay in order.
#[subscriber(PulsarSubscription::new("orders", "dispatch"))]
async fn dispatch(
order: &Order,
Out(picks): Out<impl Publisher<Options = PulsarPublishOptions>, Picks>,
) -> HandlerOutcome {
let mut headers = HeaderMap::new();
headers.insert("source", "checkout");
let sent = picks
.message(&Pick { order: order.id })
.with_headers(headers)
.partition_key(order.warehouse.clone())
.publish()
.await;
match sent {
Ok(()) => HandlerOutcome::ack(),
Err(error) => {
tracing::warn!(order.id, %error, "pick list not sent");
HandlerOutcome::retry()
}
}
}
/// A pick list for the warehouse that ships the order.
#[derive(Debug, PartialEq, Serialize, Deserialize, Outgoing, JsonSchema)]
#[outgoing(name = "warehouse-picks")]
struct Pick {
order: u64,
}
#[derive(OutSlot)]
#[publishes(Pick)]
struct Picks;
/// Sends each order to its warehouse, keyed by the warehouse so its picks stay in order.
struct Dispatch;
impl<Entry> Handle<Order, (), Outs<(Entry,)>> for Dispatch
where
Entry: OutEntry<Picks, Wire: Publisher<Options = PulsarPublishOptions>>,
{
async fn handle(
&self,
order: &Order,
outs: &Outs<(Entry,)>,
_ctx: &mut Context<'_>,
) -> Result<(), HandlerOutcome> {
let mut headers = HeaderMap::new();
headers.insert("source", "checkout");
outs.get(Picks)
.message(&Pick { order: order.id })
.with_headers(headers)
.partition_key(order.warehouse.clone())
.publish()
.await
.map_err(|error| {
tracing::warn!(order.id, %error, "pick list not sent");
HandlerOutcome::retry()
})
}
}
Слот Out публикует из тела обработчика, а Picks объявляет тип, который он передаёт.
partition_key задаёт ключом каждого листа комплектации его склад.
На партиционированном топике ключ выбирает партицию, а подписка KeyShared сохраняет порядок каждого ключа.
Этот шаг берётся из прелюдии брокера, поэтому слот ограничен Publisher<Options = PulsarPublishOptions>, чтобы шаг был доступен.
Каждый заголовок становится одноимённым свойством сообщения.
Консьюмер на другом языке читает source как обычное свойство Pulsar, без конверта вокруг полезной нагрузки.
Pulsar передаёт свойства текстом, поэтому публикация со значением заголовка не в UTF-8 возвращает ошибку.
Публикация, которая не удалась, возвращает ошибку, и dispatch повторяет заказ.
Полезная нагрузка больше max_message_size брокера, по умолчанию пять мебибайт, возвращает ошибку ещё до отправки.
Приложение
- Макросы
- Вручную
/// Tells the rest of the system that checkout is up.
#[derive(Debug, PartialEq, Serialize, Deserialize, Outgoing)]
#[outgoing(name = "service-events")]
struct Started {
service: String,
}
#[ruststream::app]
fn app() -> impl App {
RustStream::new(AppInfo::new("checkout", "0.1.0")).with_broker(
PulsarBroker::new("pulsar://localhost:6650"),
|b| {
b.include(confirm);
b.include(quote);
b.include(dispatch).out(Picks, Publish).build();
b.after_startup(Publish, async move |publisher| {
let started = Started {
service: "checkout".to_owned(),
};
publisher.message(&started).publish().await
});
},
)
}
/// Tells the rest of the system that checkout is up.
#[derive(Debug, PartialEq, Serialize, Deserialize, Outgoing, JsonSchema)]
#[outgoing(name = "service-events")]
struct Started {
service: String,
}
fn app() -> impl App {
RustStream::new(AppInfo::new("checkout", "0.1.0")).with_broker(
PulsarBroker::new("pulsar://localhost:6650"),
|b| {
b.include(
subscriber(PulsarSubscription::new("orders", "receipts"), Confirm)
.reply()
.build(),
);
b.include(
subscriber(
PulsarSubscription::new("quote-requests", "quotes"),
AnswerQuote,
)
.reply()
.to("quotes")
.build(),
);
b.include(subscriber(PulsarSubscription::new("orders", "dispatch"), Dispatch).build())
.out(Picks, Publish)
.build();
b.after_startup(Publish, async move |publisher| {
let started = Started {
service: "checkout".to_owned(),
};
publisher.message(&started).publish().await
});
},
)
}
fn main() -> ExitCode {
run_main(app)
}
.out(Picks, Publish) связывает слот с политикой публикации.
after_startup выполняется один раз, когда подписки открыты, с издателем названной в нём политики, и checkout объявляет о себе в service-events.
Хук, который возвращает ошибку, останавливает сервис при запуске.
Издатель открывает по одному продюсеру на топик при первой публикации в него, а shutdown закрывает их.
Код вне приложения берёт издателя у брокера: PulsarBroker::publisher до запуска сервиса и ConnectedPulsarBroker::publisher после.
Тестирование публикаций
Тесты в файле примера запускают приложение внутри процесса.
published проверяет, что дошло до топика, вместе с заголовками, а tb.out::<Picks>() проверяет ключ, с которым публиковал слот.
Pulsar внутри процесса упаковывает заголовки и ключ так же, как клиент, поэтому тест с заголовком, который отверг бы сервер, тоже не проходит.