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

Подтверждение доставки сообщений — один из ключевых механизмов протокола 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-фрейм брокеру.

Что происходит без ACK

Если подтверждение не отправлено:

  • сообщение считается необработанным;
  • брокер может отправить его повторно;
  • при отключении клиента сообщение возвращается в очередь.

Преимущества ручного подтверждения

  • защита от потери сообщений;
  • контроль завершения обработки;
  • возможность повторной доставки;
  • поддержка надёжных очередей.

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-фрейм.

Возможные действия брокера после NACK

В зависимости от конфигурации брокера сообщение может:

  • вернуться в очередь;
  • быть отправлено повторно;
  • попасть в dead-letter queue;
  • быть удалено.

Режим 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();

затрагивает только текущее сообщение.

Преимущества

  • безопасная параллельная обработка;
  • независимое подтверждение;
  • предсказуемое поведение;
  • отсутствие каскадного ACK.

Недостатки

  • больше 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'
    }
);

Почему ACK вызывается после 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'
    }
);

Назначение транзакционного ACK

Позволяет:

  • атомарно подтверждать сообщения;
  • объединять операции;
  • синхронизировать обработку;
  • предотвращать частично завершённые операции.

Повторная доставка сообщений

Если сообщение не подтверждено:

message.ack();

брокер может повторно отправить его.

Причины повторной доставки:

  • обрыв соединения;
  • падение приложения;
  • отсутствие ACK;
  • превышение timeout;
  • NACK.

Идентификатор подтверждения

Каждое сообщение содержит специальный идентификатор подтверждения.

Пример:

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'
    }
);

ACK после успешной бизнес-операции

Подтверждение должно происходить только после полного завершения обработки.

Правильный порядок:

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'
    }
);

Dead Letter Queue

Многие брокеры поддерживают DLQ — очередь сообщений с ошибками.

Сообщение может попасть туда:

  • после нескольких NACK;
  • после превышения лимита попыток;
  • после истечения TTL;
  • при невозможности маршрутизации.

RabbitMQ и подтверждение доставки

В RabbitMQ подтверждения STOMP преобразуются во внутренние AMQP acknowledgements.

Особенности RabbitMQ:

  • поддержка ACK/NACK;
  • повторная доставка;
  • dead-letter exchanges;
  • redelivery flag;
  • prefetch count.

ActiveMQ и подтверждения

В ActiveMQ подтверждения влияют на:

  • хранение сообщений;
  • повторную доставку;
  • persistent queues;
  • durable subscriptions.

Поддерживаются:

  • client;
  • client-individual;
  • транзакционные ACK.

Prefetch и подтверждения

Многие брокеры заранее отправляют несколько сообщений клиенту.

Это называется prefetch.

Если ACK отсутствуют, сообщения накапливаются у клиента.

Пример проблемы

Брокер отправил:

1000 сообщений

Клиент не отправляет ACK.

Результат:

  • рост памяти;
  • блокировка очереди;
  • замедление обработки;
  • повторные доставки.

Ограничение количества неподтверждённых сообщений

В RabbitMQ используется prefetch count.

Пример:

client.subscribe(
    '/queue/tasks',
    onMessage,
    {
        ack: 'client-individual',
        prefetch-count: '10'
    }
);

Теперь клиент одновременно получает не более 10 неподтверждённых сообщений.


Сравнение режимов ACK

Характеристика auto client client-individual
Автоматическое подтверждение Да Нет Нет
Гарантия обработки Низкая Высокая Очень высокая
Каскадный ACK Нет Да Нет
Поддержка параллельной обработки Ограничена Опасна Хорошая
Производительность Максимальная Высокая Ниже
Надёжность Низкая Высокая Максимальная

Выбор режима подтверждения

auto

Подходит для:

  • телеметрии;
  • логов;
  • мониторинга;
  • временных данных.

client

Подходит для:

  • последовательной обработки;
  • batch processing;
  • потоковых систем.

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'
    }
);

Такая схема обеспечивает:

  • отсутствие потери сообщений;
  • повторную доставку при ошибках;
  • согласованность обработки;
  • устойчивость к сбоям;
  • безопасную асинхронную обработку.