Индивидуальное квитирование

Индивидуальное квитирование в контексте STOMP.js представляет собой режим подтверждения доставки, при котором каждое сообщение подтверждается отдельно, независимо от других сообщений в очереди или подписке. Такой подход используется в сценариях, где требуется строгий контроль обработки каждого события, высокая надёжность и возможность точечного повторного получения сообщений при сбоях.

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


Режимы квитирования в STOMP

Протокол STOMP 1.2 и его реализация в STOMP.js поддерживают несколько режимов подтверждения:

auto

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

  • Не требуется вызов ack
  • Нет повторной доставки при падении клиента после получения
  • Минимальная надёжность
  • Максимальная производительность

client

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

  • Клиент обязан вызвать ack
  • Подтверждение может быть пакетным (в зависимости от брокера)
  • Возможна групповая обработка сообщений

client-individual

Индивидуальное подтверждение каждого сообщения.

  • Каждое сообщение подтверждается отдельно
  • Нельзя объединять несколько сообщений в один ack
  • Максимальная точность контроля доставки
  • Повышенная надёжность при обработке очередей

Именно режим client-individual является основой индивидуального квитирования.


Принцип работы индивидуального квитирования

При использовании client-individual брокер назначает каждому сообщению уникальный идентификатор доставки. Клиент обязан подтвердить обработку каждого сообщения отдельно.

Процесс выглядит следующим образом:

  1. Клиент подписывается на очередь с режимом ack: 'client-individual'
  2. Брокер отправляет сообщение с заголовками доставки
  3. Клиент получает сообщение и обрабатывает его
  4. После успешной обработки вызывается ack() для конкретного сообщения
  5. Брокер удаляет сообщение из очереди только после подтверждения

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

  • сообщение остаётся неподтверждённым
  • при разрыве соединения оно может быть повторно доставлено

Подписка с индивидуальным квитированием

В STOMP.js режим задаётся при подписке:

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

  // обработка
  processOrder(payload);

  // подтверждение
  message.ack();
}, {
  ack: 'client-individual'
});

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


Структура сообщения и ack-id

Каждое доставленное сообщение содержит метаданные, необходимые для подтверждения:

  • message-id — идентификатор сообщения
  • subscription — идентификатор подписки
  • ack — токен подтверждения (используется библиотекой)

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


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

Подтверждение (ack)

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

    handleTask(task);

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

Отрицательное подтверждение (nack)

nack сообщает брокеру, что сообщение не обработано и может быть отправлено повторно или перенаправлено в DLQ (dead letter queue), в зависимости от конфигурации брокера.

message.nack();

Отличие client и client-individual

Разница между режимами часто становится критической при высокой нагрузке.

client

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

client-individual

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

Поведение при сбоях

Индивидуальное квитирование напрямую влияет на повторную доставку сообщений.

Сценарий падения клиента

  1. Сообщение доставлено
  2. Клиент начал обработку
  3. Соединение разорвано до вызова ack
  4. Брокер считает сообщение неподтверждённым
  5. Сообщение возвращается в очередь

Такой механизм обеспечивает at-least-once delivery.


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

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

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

  • проверка уникального message-id
  • запись состояния обработки в БД
  • использование дедупликационных ключей
  • контроль статуса задачи на стороне сервера

Асинхронная обработка и задержка ack

Часто подтверждение откладывается до завершения асинхронных операций:

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

  try {
    await resizeImage(data.url);
    await saveResult(data.id);

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

Важно учитывать, что пока ack не вызван, сообщение остаётся “в обработке” с точки зрения брокера.


Поведение при параллельной обработке

Индивидуальное квитирование часто используется в воркерных системах.

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

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

При высокой параллельности важно контролировать:

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

Повторная доставка и redelivery

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

  • брокер увеличивает счётчик redelivery
  • сообщение может быть отправлено повторно
  • возможно попадание в DLQ после превышения лимита попыток

Это особенно характерно для брокеров типа RabbitMQ или ActiveMQ, которые строго учитывают ack-состояние.


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

Некоторые реализации STOMP позволяют запросить подтверждение самого факта ack:

message.ack({ receipt: 'ack-123' });

В ответ брокер отправляет frame RECEIPT, подтверждающий получение ack-команды. Это используется в критичных системах, где важно гарантировать не только обработку, но и фиксацию подтверждения на сервере.


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

Индивидуальное квитирование применяется в системах:

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

В этих сценариях потеря или двойная обработка сообщений недопустимы, поэтому требуется строгий контроль ack на уровне каждого сообщения.


Типичные ошибки при использовании

Отсутствие ack

Сообщения накапливаются в состоянии unacknowledged и повторно доставляются.

Слишком ранний ack

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

Игнорирование nack

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

Отсутствие контроля параллелизма

Слишком большое количество одновременно неподтверждённых сообщений может перегрузить брокер.


Внутреннее поведение STOMP.js

В STOMP.js каждый объект message инкапсулирует контекст доставки, включая идентификаторы, необходимые для ack/nack операций.

При вызове:

message.ack();

библиотека формирует STOMP frame:

ACK
id: <ack-id>
subscription: <subscription-id>

и отправляет его через WebSocket соединение.


Сравнение надёжности режимов

  • auto — минимальная надёжность, высокая скорость
  • client — средняя надёжность, гибкость подтверждения
  • client-individual — максимальная надёжность, строгий контроль

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