Подтверждение доставки сообщений — один из ключевых механизмов протокола STOMP, позволяющий контролировать факт успешной обработки сообщений клиентом. Без подтверждений брокер сообщений не знает, было ли сообщение действительно обработано приложением или потеряно из-за ошибки, разрыва соединения или аварийного завершения процесса.
В STOMP подтверждение доставки реализуется через ACK-фреймы и специальные режимы подтверждения, задаваемые при подписке.
При подписке через client.subscribe() используется
заголовок ack, определяющий способ подтверждения
доставки.
Поддерживаются три основных режима:
| Режим | Описание |
|---|---|
auto |
сообщения подтверждаются автоматически |
client |
подтверждение выполняется вручную |
client-individual |
каждое сообщение подтверждается отдельно |
auto)Режим auto используется по умолчанию. После отправки
сообщения клиенту брокер считает его успешно доставленным без
дополнительных действий со стороны приложения.
client.subscribe('/queue/tasks', (message) => {
console.log(message.body);
});
Эквивалентная запись:
client.subscribe(
'/queue/tasks',
(message) => {
console.log(message.body);
},
{
ack: 'auto'
}
);
autoРежим подходит для:
client)В режиме client приложение самостоятельно сообщает
брокеру о завершении обработки сообщения.
client.subscribe(
'/queue/orders',
(message) => {
console.log('Заказ:', message.body);
message.ack();
},
{
ack: 'client'
}
);
После вызова:
message.ack();
STOMP.js отправляет ACK-фрейм брокеру.
Если подтверждение не отправлено:
Подтверждение доставки в STOMP основано на фрейме
ACK.
Пример ACK-фрейма:
ACK
id:ack-7
^@
STOMP.js формирует такой фрейм автоматически после вызова:
message.ack();
NACK)Если обработка завершилась ошибкой, можно явно сообщить брокеру о неуспешной доставке.
client.subscribe(
'/queue/orders',
(message) => {
try {
processOrder(message.body);
message.ack();
} catch (error) {
console.error(error);
message.nack();
}
},
{
ack: 'client'
}
);
Вызов:
message.nack();
отправляет NACK-фрейм.
В зависимости от конфигурации брокера сообщение может:
client и групповое подтверждениеОсобенность режима client заключается в каскадном
подтверждении.
Если клиент получил несколько сообщений подряд:
msg-1
msg-2
msg-3
и подтвердил только третье:
message3.ack();
то брокер может считать подтверждёнными:
msg-1
msg-2
msg-3
Это связано с тем, что ACK подтверждает весь поток сообщений до указанного идентификатора.
Такое поведение удобно:
Но может быть опасным:
client-individualРежим client-individual устраняет каскадное
подтверждение.
Каждое сообщение подтверждается отдельно.
client.subscribe(
'/queue/tasks',
(message) => {
doWork(message.body);
message.ack();
},
{
ack: 'client-individual'
}
);
Подтверждение:
message.ack();
затрагивает только текущее сообщение.
Ручные подтверждения особенно важны при асинхронной логике.
client.subscribe(
'/queue/images',
async (message) => {
try {
const data = JSON.parse(message.body);
await resizeImage(data.path);
message.ack();
} catch (error) {
message.nack();
}
},
{
ack: 'client-individual'
}
);
awaitЕсли подтвердить сообщение раньше:
message.ack();
await resizeImage();
то при ошибке обработки сообщение уже будет потеряно.
ACK и NACK могут участвовать в STOMP-транзакциях.
const tx = client.begin();
client.subscribe(
'/queue/payments',
(message) => {
try {
processPayment(message.body);
message.ack({
transaction: tx.id
});
tx.commit();
} catch (error) {
tx.abort();
}
},
{
ack: 'client'
}
);
Позволяет:
Если сообщение не подтверждено:
message.ack();
брокер может повторно отправить его.
Причины повторной доставки:
Каждое сообщение содержит специальный идентификатор подтверждения.
Пример:
console.log(message.headers);
Результат:
{
ack: 'ack-id-42',
subscription: 'sub-1',
destination: '/queue/tasks'
}
Этот идентификатор используется внутри ACK/NACK-фреймов.
При повторной доставке приложение может получить одно и то же сообщение несколько раз.
Для защиты используют:
Пример:
const processed = new Set();
client.subscribe(
'/queue/orders',
(message) => {
const order = JSON.parse(message.body);
if (processed.has(order.id)) {
message.ack();
return;
}
processed.add(order.id);
processOrder(order);
message.ack();
},
{
ack: 'client-individual'
}
);
Подтверждение должно происходить только после полного завершения обработки.
Правильный порядок:
await saveToDatabase();
await sendEmail();
message.ack();
Опасный порядок:
message.ack();
await saveToDatabase();
Надёжная схема обработки:
client.subscribe(
'/queue/jobs',
async (message) => {
try {
const payload = JSON.parse(message.body);
await executeJob(payload);
message.ack();
} catch (error) {
console.error(error);
message.nack();
}
},
{
ack: 'client-individual'
}
);
Многие брокеры поддерживают DLQ — очередь сообщений с ошибками.
Сообщение может попасть туда:
В RabbitMQ подтверждения STOMP преобразуются во внутренние AMQP acknowledgements.
Особенности RabbitMQ:
В ActiveMQ подтверждения влияют на:
Поддерживаются:
client;client-individual;Многие брокеры заранее отправляют несколько сообщений клиенту.
Это называется prefetch.
Если ACK отсутствуют, сообщения накапливаются у клиента.
Брокер отправил:
1000 сообщений
Клиент не отправляет ACK.
Результат:
В RabbitMQ используется prefetch count.
Пример:
client.subscribe(
'/queue/tasks',
onMessage,
{
ack: 'client-individual',
prefetch-count: '10'
}
);
Теперь клиент одновременно получает не более 10 неподтверждённых сообщений.
| Характеристика | auto | client | client-individual |
|---|---|---|---|
| Автоматическое подтверждение | Да | Нет | Нет |
| Гарантия обработки | Низкая | Высокая | Очень высокая |
| Каскадный ACK | Нет | Да | Нет |
| Поддержка параллельной обработки | Ограничена | Опасна | Хорошая |
| Производительность | Максимальная | Высокая | Ниже |
| Надёжность | Низкая | Высокая | Максимальная |
autoПодходит для:
clientПодходит для:
client-individualПодходит для:
client.subscribe(
'/queue/orders',
async (message) => {
try {
const order = JSON.parse(message.body);
await validate(order);
await reserveItems(order);
await createInvoice(order);
await saveOrder(order);
message.ack();
} catch (error) {
console.error(error);
message.nack();
}
},
{
ack: 'client-individual'
}
);
Такая схема обеспечивает: