Подтверждение получения

Модель доставки и необходимость подтверждения

Протокол STOMP поверх брокеров сообщений (RabbitMQ, ActiveMQ, Apollo и др.) реализует модель доставки, в которой клиент подписывается на очередь или топик и получает сообщения асинхронно. Важной частью этой модели является механизм подтверждения получения сообщения (acknowledgement), который определяет момент, когда брокер может считать сообщение успешно обработанным.

Подтверждение получения влияет на гарантию доставки:

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

В STOMP.js управление подтверждениями реализуется через заголовки подписки и методы объекта сообщения.


Режимы подтверждения ACK

auto (автоматическое подтверждение)

В режиме автоматического подтверждения сообщение считается успешно доставленным сразу после отправки клиенту. Брокер не ожидает явного подтверждения.

Характеристики:

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

Пример подписки:

client.subscribe('/queue/orders', (message) => {
  const body = JSON.parse(message.body);

  processOrder(body);
}, {
  ack: 'auto'
});

Особенность режима заключается в том, что даже если обработка processOrder завершится с ошибкой, брокер уже считает сообщение доставленным.


client (ручное подтверждение)

Режим client предполагает, что клиент обязан явно подтвердить сообщение. Брокер удерживает сообщение до получения ACK.

Основные свойства:

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

Пример:

client.subscribe('/queue/orders', (message) => {
  try {
    const data = JSON.parse(message.body);

    processOrder(data);

    message.ack();
  } catch (err) {
    message.nack();
  }
}, {
  ack: 'client'
});

Методы:

  • message.ack() — подтверждает успешную обработку;
  • message.nack() — сообщает о неуспешной обработке.

client-individual (индивидуальное подтверждение)

Режим client-individual расширяет модель client, добавляя строгое индивидуальное подтверждение каждого сообщения без группировки.

Свойства:

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

Пример:

client.subscribe('/queue/tasks', (message) => {
  const task = JSON.parse(message.body);

  try {
    executeTask(task);
    message.ack();
  } catch (e) {
    message.nack();
  }
}, {
  ack: 'client-individual'
});

В этом режиме невозможно подтвердить сразу несколько сообщений одним вызовом, что особенно важно при параллельной обработке.


Методы подтверждения сообщения

ack()

Метод ack() сообщает брокеру, что сообщение успешно обработано.

Внутренне он отправляет STOMP-команду:

ACK
id:message-id

Используется только при режимах client и client-individual.

Пример поведения:

message.ack();

При вызове:

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

nack()

Метод nack() используется для отрицательного подтверждения.

Он сообщает брокеру, что сообщение не может быть обработано и должно быть переотправлено или перенаправлено в DLQ (Dead Letter Queue), если она настроена.

Пример:

message.nack();

Поведение зависит от брокера:

  • RabbitMQ может вернуть сообщение в очередь;
  • ActiveMQ может отправить в DLQ;
  • возможна задержка перед повторной доставкой.

Стратегии обработки сообщений

Синхронная обработка

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

client.subscribe('/queue/invoices', (message) => {
  const invoice = JSON.parse(message.body);

  generateInvoice(invoice);

  message.ack();
}, {
  ack: 'client'
});

Преимущество — высокая надежность.

Недостаток — снижение throughput при долгих операциях.


Асинхронная обработка

ACK отправляется после завершения асинхронной операции:

client.subscribe('/queue/files', async (message) => {
  const file = JSON.parse(message.body);

  try {
    await uploadFile(file);
    message.ack();
  } catch (e) {
    message.nack();
  }
}, {
  ack: 'client'
});

Особенность заключается в необходимости корректного управления промисами, иначе ACK может быть отправлен до завершения операции.


Ошибочная модель (ACK до обработки)

Антипаттерн:

client.subscribe('/queue/bad', (message) => {
  message.ack();

  processSomething(message.body);
}, {
  ack: 'client'
});

Последствия:

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

Поведение при отключении клиента

При разрыве соединения:

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

Особенно важно в режимах client и client-individual, где ACK обязателен.


Prefetch и влияние на ACK

Параметр prefetch определяет, сколько сообщений может быть доставлено без подтверждения.

Пример:

client.subscribe('/queue/jobs', handler, {
  ack: 'client',
  'prefetch': 10
});

Поведение:

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

Неправильная настройка prefetch может привести к:

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

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

При использовании nack() или отсутствии ack() в течение таймаута:

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

Это важно учитывать при разработке:

  • обработчики должны быть идемпотентными;
  • операции не должны приводить к дублированию эффектов.

Идемпотентность обработки

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

Пример:

client.subscribe('/queue/payments', async (message) => {
  const payment = JSON.parse(message.body);

  const exists = await checkPayment(payment.id);

  if (exists) {
    message.ack();
    return;
  }

  await savePayment(payment);

  message.ack();
}, {
  ack: 'client'
});

Такая логика предотвращает повторное применение одной и той же операции.


Взаимодействие ACK и транзакций

ACK может использоваться вместе с транзакционными отправками сообщений. В этом случае:

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

Типичные ошибки при работе с ACK

Отсутствие ack в client режиме

client.subscribe('/queue/test', (message) => {
  process(message.body);
});

Результат:

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

Повторный ack

message.ack();
message.ack();

Последствия:

  • ошибка протокола;
  • поведение зависит от реализации брокера.

ACK в неправильном режиме

client.subscribe('/queue/test', (message) => {
  message.ack();
}, {
  ack: 'auto'
});

Результат:

  • вызов ack() игнорируется или вызывает ошибку;
  • логика становится неконсистентной.

Контроль доставки и надежность системы

Механизм подтверждения в STOMP.js является ключевым элементом построения надежных систем обработки сообщений. Он определяет:

  • гарантии доставки (at-most-once, at-least-once);
  • устойчивость к сбоям сети;
  • поведение при масштабировании потребителей;
  • корректность повторной обработки событий.

Выбор режима ACK напрямую влияет на архитектуру всей системы обработки сообщений.