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

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

Публикация в стримы

Это руководство публикует ответы и сообщения в стримы JetStream, сохраняет повторное сообщение один раз и отклоняет запись, сделанную по устаревшей версии.

Обычная публикация уходит через Core NATS, и никто её не подтверждает. JetStreamPublish ждёт, пока стрим подтвердит каждое сообщение. Поэтому сообщение, которое стрим отклонил, - это ошибка, которую видит обработчик, а не тихая потеря.

Пример - сервис заказов. Он подтверждает каждый заказ, архивирует его и хранит историю каждой учётной записи клиента. Стримы, в которые он пишет, уже есть на сервере:

nats stream add ORDERS --subjects 'orders.placed' --defaults
nats stream add CONFIRMATIONS --subjects 'confirmations.orders' --defaults
nats stream add ARCHIVE --subjects 'archive.orders' --defaults
nats stream add ACCOUNTS --subjects 'accounts.*' --defaults

Ответ в стрим​

src/main.rs
#[derive(Debug, PartialEq, Serialize, Deserialize, Outgoing)]
#[outgoing(name = "confirmations.orders")]
struct Confirmation {
order: u64,
}

#[subscriber(
JetStreamSubject::new("orders.placed", "ORDERS").durable("confirmer"),
reply
)]
async fn confirm(order: &Order) -> Confirmation {
tracing::info!(order.id, "order confirmed");
Confirmation { order: order.id }
}

confirm отвечает на каждый заказ сообщением Confirmation в confirmations.orders. Приложение ниже отправляет этот ответ через JetStreamPublish::default().expect_stream("CONFIRMATIONS"). expect_stream называет стрим, который должен хранить субъект, и публикация, которую принял бы другой стрим, отклоняется. Заказ подтверждается только после того, как стрим сохранил подтверждение. Если подтверждение отклонено, доставка заказа завершается ошибкой, и заказ возвращается.

Субъект ответа лежит вне субъектов ORDERS. Ответ, сохранённый в стриме, который читает обработчик, снова пришёл бы к этому обработчику.

Однократное сохранение​

src/main.rs
#[derive(Debug, PartialEq, Serialize, Deserialize, Outgoing)]
#[outgoing(name = "archive.orders")]
struct Archived {
order: u64,
amount_cents: u64,
}

#[derive(OutSlot)]
#[publishes(Archived)]
struct Archive;

#[subscriber(JetStreamSubject::new("orders.placed", "ORDERS").durable("archiver"))]
async fn archive(
order: &Order,
Out(out): Out<impl Publisher<Options = JetStreamOptions>, Archive>,
) -> HandlerOutcome {
let archived = Archived {
order: order.id,
amount_cents: order.amount_cents,
};
// The same id on every delivery of this order: the stream stores the first copy only.
if out
.message(&archived)
.message_id(format!("order-{}", order.id))
.publish()
.await
.is_err()
{
return HandlerOutcome::retry();
}
HandlerOutcome::ack()
}

#[subscriber(JetStreamSubject::new("archive.orders", "ARCHIVE").durable("indexer"))]
async fn index(archived: &Archived) -> HandlerOutcome {
tracing::info!(archived.order, "archived order indexed");
HandlerOutcome::ack()
}

Заказ может быть доставлен дважды, и тогда archive публикует его копию дважды. message_id даёт каждой копии одного заказа один и тот же идентификатор, который передаётся в заголовке Nats-Msg-Id. Стрим сохраняет первую копию и подтверждает повтор, не сохраняя его, поэтому index читает каждый заказ один раз. Стрим помнит идентификаторы в течение своего окна дубликатов: две минуты, если стрим не задаёт другое значение.

message_id описывает одно сообщение, поэтому это шаг публикации, а не настройка точки монтирования. Шаги даёт JetStreamPublishSteps из прелюдии крейта. Тело обработчика, которое вызывает такой шаг, ограничивает свой слот типом Out<impl Publisher<Options = JetStreamOptions>, _>. Это ограничение привязывает обработчик к JetStream. Только здесь обработчик называет брокер.

Отказ в устаревшей записи​

src/main.rs
/// A change of an account's e-mail, made from the version of the account the client last read:
/// the stream sequence of its last event, or 0 for an account with no events yet.
#[derive(Debug, PartialEq, Serialize, Deserialize, Outgoing)]
#[outgoing(name = "commands.change-email")]
struct ChangeEmail {
account: u64,
version: u64,
email: String,
}

/// An event of an account's history, stored under the account's own subject.
#[derive(Debug, PartialEq, Serialize, Deserialize, Outgoing)]
struct EmailChanged {
account: u64,
email: String,
}

#[derive(OutSlot)]
#[publishes(EmailChanged)]
struct History;

#[subscriber("commands.change-email")]
async fn change_email(
change: &ChangeEmail,
Out(history): Out<impl Publisher<Options = JetStreamOptions>, History>,
) -> HandlerOutcome {
let event = EmailChanged {
account: change.account,
email: change.email.clone(),
};
let stored = history
.message(&event)
.to(format!("accounts.{}", change.account))
.expect_last_subject_sequence(change.version)
.publish()
.await;
if stored.is_err() {
tracing::warn!(
change.account,
change.version,
"change made from a stale version refused"
);
return HandlerOutcome::drop();
}
HandlerOutcome::ack()
}

История учётной записи 42 - это субъект стрима accounts.42. Клиент меняет адрес электронной почты учётной записи, опираясь на версию, которую прочитал последней: порядковый номер последнего события учётной записи в стриме. expect_last_subject_sequence заставляет стрим сохранить событие, только если последнее сообщение субъекта всё ещё стоит под этим номером. Два клиента, прочитавшие одну версию, не могут записать оба: вторая запись отклоняется, и change_email её отбрасывает. expect_last_sequence проверяет весь стрим, а не один субъект, а expect_last_message_id - идентификатор последнего сообщения стрима.

Приложение​

src/main.rs
#[ruststream::app]
fn app() -> impl App<State = ()> {
RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
NatsBroker::new("nats://localhost:4222"),
|b| {
b.include(confirm)
.out_reply(JetStreamPublish::default().expect_stream("CONFIRMATIONS"));
b.include(archive)
.out(
Archive,
JetStreamPublish::default().expect_stream("ARCHIVE"),
)
.build();
b.include(index);
b.include(change_email)
.out(
History,
JetStreamPublish::default().expect_stream("ACCOUNTS"),
)
.build();
},
)
}

Каждая публикация JetStream ждёт подтверждения от стрима. Вне доставки хук запуска принимает политику, и publish_ack возвращает это подтверждение: стрим, порядковый номер и то, видело ли сообщение окно дубликатов. Публикация Core NATS подходит там, где потеря сообщения допустима, например для панели мониторинга из руководства Подтверждение и гарантии доставки.

Тесты в файле примера запускают приложение внутри процесса: стрим внутри процесса проверяет идентификаторы сообщений и ожидания так же, как сервер.