Клиентское квитирование

Модель доставки сообщений и роль квитирования

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

STOMP.js реализует три основных режима квитирования, задаваемых при подписке:

  • auto
  • client
  • client-individual

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


Режим auto: автоматическое квитирование

В режиме auto подтверждение отправляется библиотекой автоматически сразу после доставки сообщения в клиент. Разработчик не участвует в процессе подтверждения.

Подписка:

const subscription = client.subscribe('/queue/tasks', (message) => {
  console.log(message.body);
}, { ack: 'auto' });

Поведение:

  • сообщение считается обработанным сразу после вызова callback
  • брокер получает ACK автоматически
  • отсутствует контроль над успешностью обработки

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

  • минимальная задержка
  • отсутствие гарантий обработки
  • подходит для некритичных данных (логирование, телеметрия)

Режим client: ручное подтверждение батча сообщений

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

Подписка:

const subscription = client.subscribe('/queue/tasks', (message) => {
  console.log(message.body);

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

Механика работы:

  • каждое сообщение имеет идентификатор message-id
  • клиент вызывает message.ack()
  • подтверждаются все сообщения до данного message-id

Проблема модели:

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

Этот режим используется реже из-за своей «грубости» и ограниченной управляемости.


Режим client-individual: точечное квитирование сообщений

Наиболее гибкий режим — client-individual. Каждое сообщение подтверждается отдельно, независимо от других.

Подписка:

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

    processTask(data);

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

Особенности поведения:

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

ACK и NACK: управление состоянием обработки

В STOMP.js сообщение предоставляет два ключевых метода управления состоянием:

  • ack() — подтверждение успешной обработки
  • nack() — отклонение сообщения
Метод ack()

Используется для уведомления брокера об успешной обработке:

message.ack();

После вызова:

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

Метод nack()

Используется для индикации ошибки обработки:

message.nack();

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

  • сообщение может быть возвращено в очередь
  • может быть перенаправлено в DLQ (Dead Letter Queue)
  • может быть повторно доставлено другому потребителю

Транзакционное квитирование

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

Пример:

const tx = client.begin();

const subscription = client.subscribe('/queue/tasks', (message) => {
  try {
    processTask(message.body);

    message.ack({ transaction: tx.id });

    tx.commit();
  } catch (e) {
    tx.abort();
  }
});

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

  • ACK фиксируется только при commit
  • при abort сообщение возвращается в очередь
  • обеспечивает атомарность обработки

Поведение при потере соединения

Если соединение разрывается до отправки ACK:

  • в режиме client и client-individual сообщения считаются необработанными
  • брокер выполняет повторную доставку
  • возможны дубликаты сообщений

Это требует от клиента идемпотентной обработки:

  • проверка уникальности задач
  • использование correlation-id
  • дедупликация на уровне приложения

Сравнение режимов квитирования

Режим Управление ACK Точность Надежность Сложность
auto нет низкая низкая минимальная
client частичное средняя средняя средняя
client-individual полное высокая высокая высокая

Практические сценарии использования

Режим auto:

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

Режим client:

  • устаревшие системы
  • простые очереди без строгих требований

Режим client-individual:

  • обработка задач
  • платежные операции
  • распределенные системы с гарантией доставки

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

Некорректное использование ACK приводит к серьезным сбоям:

  • отсутствие ack() при client-individual → блокировка очереди
  • преждевременный ack() до завершения обработки → потеря данных
  • отсутствие обработки исключений → неконтролируемое повторное доставление
  • игнорирование nack() → зависание сообщений в состоянии in-flight

Поведение брокеров при ACK

Разные брокеры STOMP по-разному интерпретируют квитирование:

  • ActiveMQ: строгая поддержка всех режимов
  • RabbitMQ (STOMP plugin): частичная реализация, упор на client-individual
  • Apollo / HornetQ: различия в транзакционной модели

Это влияет на:

  • повторную доставку
  • порядок сообщений
  • поведение DLQ

Идемпотентность как обязательное условие

При использовании клиентского квитирования система должна быть устойчива к повторной доставке:

  • каждое сообщение может прийти более одного раза
  • ACK не является абсолютной гарантией «exactly once»
  • требуется логика защиты от дубликатов

Типичный подход:

if (processedMessages.has(message.id)) {
  message.ack();
  return;
}

ACK в асинхронной обработке

При асинхронных операциях ACK должен вызываться строго после завершения:

client.subscribe('/queue/tasks', async (message) => {
  try {
    await asyncProcess(message.body);
    message.ack();
  } catch (e) {
    message.nack();
  }
}, { ack: 'client-individual' });

Критично:

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