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

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

В STOMP 1.2 введена расширенная модель подтверждений, которая управляется через заголовки подписки и методы подтверждения на стороне клиента. STOMP.js реализует этот функционал через объект подписки и объект входящего сообщения.

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


Базовая модель доставки сообщений

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

client.subscribe('/queue/orders', onMessage, {
  ack: 'auto'
});

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

  • автоматического подтверждения (auto),
  • явного подтверждения (client),
  • подтверждения каждого сообщения отдельно (client-individual).

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

  • messageId
  • subscription
  • ack (если поддерживается брокером)
  • заголовки

В STOMP.js объект сообщения содержит методы:

  • message.ack()
  • message.nack()

Режим auto

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

client.subscribe('/queue/orders', (message) => {
  const body = JSON.parse(message.body);
  processOrder(body);
}, {
  ack: 'auto'
});

Особенности режима auto

  • Сообщение считается обработанным сразу после передачи в callback
  • Брокер не ожидает явного подтверждения
  • Повторная доставка невозможна (даже при ошибке обработки на клиенте)
  • Подходит для некритичных данных

Поведение при ошибках

Если внутри обработчика возникает исключение:

client.subscribe('/queue/orders', (message) => {
  throw new Error('fail');
}, {
  ack: 'auto'
});

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

Когда использовать auto

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

Режим client

Режим client переводит ответственность за подтверждение сообщений на клиента. Брокер считает сообщение неподтвержденным, пока не будет вызван ack().

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

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

Поведение режима client

  • каждое сообщение требует явного подтверждения
  • при отсутствии ack сообщение может быть переотправлено
  • ack применяется к всей подписке (в зависимости от брокера)
  • обеспечивает at-least-once доставку

ack()

message.ack();

Подтверждает успешную обработку сообщения.

nack()

message.nack();

Означает отказ от обработки. Брокер может:

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

Режим client-individual

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

client.subscribe('/queue/orders', (message) => {
  if (isValid(message.body)) {
    message.ack();
  } else {
    message.nack();
  }
}, {
  ack: 'client-individual'
});

Отличия от client

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

Поведение в брокерах

Некоторые брокеры трактуют client и client-individual одинаково, но спецификация STOMP 1.2 разделяет их поведение.


Заголовок ack при подписке

Режим подтверждения задается в момент подписки:

client.subscribe('/queue/tasks', handler, {
  ack: 'client'
});

Допустимые значения:

  • auto
  • client
  • client-individual

Если параметр не указан, большинство реализаций используют auto по умолчанию.


Связь ack с subscription-id

Каждая подписка имеет идентификатор:

client.subscribe('/queue/tasks', handler, {
  id: 'sub-001',
  ack: 'client'
});

Этот id используется брокером для связывания сообщений с конкретной подпиской. При вызове ack() брокер понимает, к какой очереди и какой подписке относится подтверждение.


Структура сообщения и подтверждение

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

{
  command: 'MESSAGE',
  headers: {
    destination: '/queue/tasks',
    message-id: '12345',
    subscription: 'sub-001'
  },
  body: '{"task":"build"}'
}

Метод ack() использует эти заголовки:

message.ack();

Эквивалентно низкоуровневому STOMP-фрейму:

ACK
id:12345

Низкоуровневый протокол ACK/NACK

STOMP позволяет отправлять отдельные команды:

ACK frame

ACK
id:message-123
subscription:sub-001

NACK frame

NACK
id:message-123
subscription:sub-001

STOMP.js абстрагирует эти детали, но логика остается той же.


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

При использовании client или client-individual брокер может повторно доставить сообщение, если:

  • соединение разорвано до ack
  • клиент не вызвал ack в установленный таймаут
  • произошел nack

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

  • RabbitMQ может пометить сообщение как unacked и вернуть в очередь
  • ActiveMQ может использовать redelivery policy
  • Artemis применяет exponential backoff

Таймауты и нестабильные соединения

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

MESSAGE -> клиент
(нет ACK)
disconnect

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

Это важная гарантия режима client.


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

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

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

При ack: 'client' такое поведение приводит к зависанию сообщений в unacked состоянии.


Двойной ack

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

В большинстве брокеров это игнорируется или вызывает ошибку протокола.


ack после асинхронной операции без обработки ошибок

client.subscribe('/queue/tasks', async (message) => {
  await processAsync(message.body);
  message.ack();
}, { ack: 'client' });

Если processAsync падает, ack не будет вызван, и сообщение вернется в очередь.


Использование ack вместе с транзакциями

ACK может быть объединен с транзакциями STOMP:

const tx = client.begin();

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

  message.ack({ transaction: tx.id });
  tx.commit();
}, {
  ack: 'client'
});

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


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

auto

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

client

  • явное подтверждение
  • at-least-once доставка
  • высокая надежность

client-individual

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

Практика управления нагрузкой через ack

ACK режим напрямую влияет на backpressure:

  • auto увеличивает скорость обработки, но снижает надежность
  • client позволяет ограничивать поток необработанных сообщений
  • отсутствие ack приводит к накоплению unacked сообщений

Брокеры часто ограничивают количество unacked сообщений на подписку, что предотвращает перегрузку клиента.


Обработка ошибок и стратегия повторов

Типичная стратегия:

client.subscribe('/queue/tasks', (message) => {
  try {
    const task = JSON.parse(message.body);
    execute(task);
    message.ack();
  } catch (e) {
    message.nack();
  }
}, {
  ack: 'client-individual'
});

Поведение:

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

Особенности реализации в STOMP.js

STOMP.js реализует ack как метод объекта сообщения, который инкапсулирует:

  • subscription id
  • message id
  • destination
  • frame generation

Методы:

  • ack(headers?)
  • nack(headers?)

Дополнительные headers позволяют указывать:

message.ack({
  id: message.headers['message-id']
});

Влияние брокера на поведение ack

Хотя STOMP задает единый интерфейс, реальное поведение зависит от реализации:

  • RabbitMQ STOMP plugin: строгая поддержка ack/nack
  • ActiveMQ: расширенные политики redelivery
  • HornetQ / Artemis: сложные транзакционные сценарии
  • Spring WebSocket broker relay: ограниченная поддержка в зависимости от конфигурации

Различия проявляются в:

  • повторной доставке
  • очередности сообщений
  • обработке nack
  • таймаутах unacked сообщений

Контроль надежности через ack стратегию

ACK режим формирует базовую модель надежности системы доставки:

  • отсутствие ack → best effort
  • client ack → гарантированная доставка минимум один раз
  • индивидуальный ack → строгий контроль обработки

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

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