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

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

Подтверждение и гарантии доставки

Это руководство урегулирует доставки, ограничивает число попыток, читает очередь в режиме at-most-once, выбирает кредит связи и обрабатывает пакеты.

В AMQP 1.0 получатель урегулирует каждую доставку исходом, и брокер действует по нему. Исход обработчика и есть эта диспозиция.

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

Три исхода​

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

/// The goods the warehouse has on its shelves.
fn in_stock(sku: &str) -> bool {
matches!(sku, "book" | "pen")
}

#[subscriber(AmqpAddress::queue("shipments").credit(nonzero!(32)))]
async fn pick(shipment: &Shipment) -> HandlerOutcome {
if shipment.quantity == 0 {
tracing::warn!(shipment.id, "an empty shipment rejected");
return HandlerOutcome::drop();
}
if !in_stock(&shipment.sku) {
tracing::info!(
shipment.id,
shipment.sku,
"out of stock, the shipment comes back"
);
return HandlerOutcome::retry();
}
tracing::info!(shipment.id, shipment.sku, "shipment picked");
HandlerOutcome::ack()
}
Исход обработчикаДиспозицияЧто делает брокер
HandlerOutcome::ack()acceptedудаляет сообщение
HandlerOutcome::retry()modified с delivery-failedучитывает попытку в delivery-count и доставляет сообщение повторно
HandlerOutcome::drop()rejectedприменяет свою политику недоставленных сообщений

pick принимает отгрузку, которую скомплектовал. Пустую отгрузку скомплектовать невозможно, поэтому pick её отклоняет. Artemis переносит отклонённое сообщение в адрес недоставленных сообщений, по умолчанию DLQ. Отгрузку товаров, которых нет на складе, можно скомплектовать позже, поэтому pick просит доставить её снова. Artemis прекращает доставлять сообщение после max-delivery-attempts попыток, по умолчанию 10, и переносит его в адрес недоставленных сообщений.

Предел попыток​

src/main.rs
fn service(broker: AmqpBroker) -> RustStream {
RustStream::new(AppInfo::new("warehouse", "0.1.0")).with_broker(broker, |b| {
b.include(pick).max_attempts(nonzero!(5u32));
b.include(log_pick);
b.include(restock.batch(nonzero!(20usize)));
})
}

#[ruststream::app]
fn app() -> impl App {
service(AmqpBroker::new("amqp://artemis:artemis@localhost:5672"))
}

max_attempts ограничивает доставки одной отгрузки пятью, включая первую. Пятая доставка отклоняется, а не повторяется. Число попыток - это delivery-count брокера, если у сообщения есть секция AMQP header. У сообщения, которое публикует этот крейт, её нет. Для такого сообщения фреймворк считает попытки сам: retry публикует копию обратно в shipments с номером повтора в заголовке. Руководство Повторы и недоставленные сообщения добавляет адрес недоставленных сообщений и задержку.

Сбой посреди обработки​

По умолчанию подписки работают в режиме at-least-once. Доставка, которую сервис не урегулировал к моменту остановки, возвращается в очередь, и её получает следующий потребитель. Поэтому обработчик может увидеть одно сообщение дважды, и его работа должна это выдерживать.

Не более одного раза​

src/main.rs
#[derive(Debug, PartialEq, Deserialize)]
struct Picked {
shipment: u64,
}

#[subscriber(AmqpAddress::queue("pick-log").settle(Settle::AtMostOnce))]
async fn log_pick(picked: &Picked) -> HandlerOutcome {
tracing::info!(picked.shipment, "pick logged");
HandlerOutcome::ack()
}

Settle::AtMostOnce урегулирует каждую доставку при получении. Брокер забывает сообщение в тот момент, когда отдаёт его, поэтому сбой до завершения обработчика его теряет. Это подходит журналу комплектаций: пропущенная строка обходится дешевле второго прохода по брокеру. Исходу обработчика урегулировать уже нечего: ack и nack возвращают AckError::Unsupported, и рантайм пишет предупреждение в журнал для каждого.

Кредит​

.credit(nonzero!(32)) на shipments - кредит связи: сколько доставок брокер может отправить вперёд, не дожидаясь урегулирования от сервиса. По умолчанию это DEFAULT_CREDIT, 256. Когда урегулирована половина кредита, клиент снова выдаёт полный кредит. Поэтому медленный обработчик сдерживает брокер, а не заполняет память сервиса, и остаток очереди остаётся на брокере для других реплик. Кредит имеет тип NonZeroU32, потому что связь без кредита ничего не получает: credit(nonzero!(0)) не компилируется.

Пакеты​

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

#[subscriber(AmqpAddress::queue("restock").batch_wait(Duration::from_millis(50)))]
async fn restock(items: &[Restock]) -> HandlerOutcome {
let units: u32 = items.iter().map(|item| item.quantity).sum();
tracing::info!(lines = items.len(), units, "shelves restocked");
HandlerOutcome::ack()
}

Обработчик, который принимает срез, получает пакет, а .batch(nonzero!(20usize)) в точке монтирования задаёт его наибольший размер. В AMQP 1.0 одна передача содержит одно сообщение, поэтому пакет собирает клиент. Пакет содержит не больше 20 доставок, а меньше - если больше не пришло вовремя. batch_wait - сколько неполный пакет ждёт новых доставок: здесь 50 миллисекунд, по умолчанию 10. Исход restock урегулирует все доставки пакета.

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

Тесты в файле примера запускают склад внутри процесса. Они проверяют каждый исход, пять доставок отгрузки товара, которого нет на складе, журнал комплектаций и пакеты. Кредит, политика недоставленных сообщений брокера и его предел доставок принадлежат Artemis, поэтому брокер внутри процесса их не моделирует. Руководство Тестирование сервиса на AMQP разбирает обвязку.