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

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

Подтверждение сообщений

Это руководство завершает сообщения Pulsar по исходу обработчика, ограничивает незавершённую доставку тайм-аутом подтверждения и завершает пакет целиком.

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

Исход и есть завершение​

src/main.rs
#[derive(Debug, PartialEq, Deserialize)]
struct Reservation {
order: u64,
sku: String,
quantity: u32,
}

/// Reserves stock for an order. A reservation left unsettled for a minute comes back.
#[subscriber(
PulsarSubscription::new("reservations", "inventory").ack_timeout(Duration::from_secs(60))
)]
async fn reserve(reservation: &Reservation, State(stock): State<Arc<Stock>>) -> HandlerOutcome {
match stock.reserve(reservation).await {
Ok(()) => {
tracing::info!(reservation.order, reservation.quantity, "stock reserved");
HandlerOutcome::ack()
}
// No answer: the subscription hands the reservation out again at once.
Err(Refusal::Unavailable) => HandlerOutcome::retry(),
Err(Refusal::UnknownSku) => {
tracing::warn!(reservation.order, "unknown SKU, reservation dropped");
HandlerOutcome::drop()
}
}
}

Значение, которое возвращает reserve, решает, что происходит с сообщением в Pulsar:

ИсходВ Pulsar
ackсообщение подтверждается и больше не возвращается
retryсообщение подтверждается отрицательно и сразу доставляется снова
retry_after(delay)сообщение удерживается на время delay, затем подтверждается отрицательно
dropсообщение подтверждается

Сервис остатков, который не ответил, может ответить на следующий вызов, поэтому reserve повторяет. Сообщение возвращается в подписку, и на подписке Shared его может взять другой консьюмер. Артикул, которого нет ни у одного товара, так и останется неизвестным, поэтому reserve отбрасывает резервирование. В Pulsar нет команды, которая отвергает сообщение окончательно, поэтому drop подтверждает его, и сообщение исчезает. Сообщение, которое раз за разом не обрабатывается, покидает подписку через топик dead-letter.

Клиент отправляет подтверждения в фоне. Завершение возвращает управление, когда подтверждение встало в очередь консьюмера, а брокер записывает его мгновением позже. Сервис, остановленный в этом промежутке, получит сообщение снова при следующем запуске, поэтому Pulsar доставляет каждое сообщение at-least-once. Обрабатывайте сообщение так, чтобы вторая доставка не навредила, например привязывайте работу к номеру заказа.

Тайм-аут подтверждения​

Доставка, которую никто не завершил, остаётся у своего консьюмера. ack_timeout в дескрипторе ограничивает её срок: когда срок истёк, клиент просит брокер доставить сообщение снова. Без тайм-аута такое сообщение возвращается, только когда его консьюмер отключается.

Задайте тайм-аут больше самого долгого времени работы обработчика. Обработчик, который ещё работает, когда тайм-аут истёк, получит вторую доставку того же сообщения. Каждая повторная доставка засчитывается в предел повторов, как retry.

Пакеты​

src/main.rs
#[derive(Debug, PartialEq, Deserialize)]
struct Count {
sku: String,
on_hand: u32,
}

/// Records the stock counts of a warehouse scan, many counts per call.
#[subscriber(
PulsarSubscription::new("stock-counts", "inventory").batch_wait(Duration::from_millis(250))
)]
async fn record_counts(counts: &[Count], State(stock): State<Arc<Stock>>) -> HandlerOutcome {
stock.record(counts).await;
tracing::info!(counts = counts.len(), "stock counts recorded");
HandlerOutcome::ack()
}

Обработчик, который принимает срез, - это пакетный обработчик. Клиент Pulsar выдаёт сообщения по одному, поэтому пакет собирает сервис. Пакет закрывается, когда он заполнен или когда с его первого сообщения прошло batch_wait, здесь 250 миллисекунд. Один исход завершает все сообщения пакета. В ручной форме Ok(()) подтверждает пакет, а Err(outcomes) завершает каждое сообщение его собственным исходом.

Приложение​

src/main.rs
#[ruststream::app]
fn app() -> impl App<State = Clients> {
RustStream::new(AppInfo::new("inventory", "0.1.0"))
.on_startup(async move |()| Ok::<_, Infallible>(Clients::default()))
.with_broker(PulsarBroker::new("pulsar://localhost:6650"), |b| {
b.include(reserve);
b.include(record_counts.batch(nonzero!(64)));
})
}

batch(n) в точке монтирования задаёт наибольший размер пакета, здесь 64 записи остатков. on_startup открывает клиенты один раз, а обработчики читают их как состояние приложения.

Тестирование исходов​

Тесты в файле примера запускают приложение внутри процесса, с TestApp::with_state вместо on_startup. Они намеренно заставляют сервис остатков отказывать и проверяют исход каждой доставки. Pulsar внутри процесса возвращает отрицательно подтверждённое сообщение в подписку, как это делает сервер. Повторная доставка по тайм-ауту подтверждения принадлежит серверу, и её проверяют живые тесты крейта на работающем Pulsar.