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

Транзакции в STOMP.js строятся поверх семантики протокола STOMP и представляют собой механизм группировки нескольких кадров SEND/ACK/NACK в единый атомарный блок, который либо фиксируется целиком, либо откатывается без частичного применения. Подтверждение транзакции в этом контексте включает сразу несколько уровней: подтверждение со стороны клиента, подтверждение со стороны брокера и подтверждение доставки сообщений в рамках транзакционного контекста.

Транзакция в STOMP задаётся идентификатором и ограничивает область действия команд. Любое сообщение, отправленное с указанием transaction, не считается окончательно обработанным до получения команды COMMIT.

Базовая последовательность кадров:

  • BEGIN — открытие транзакции
  • SEND / ACK / NACK — операции внутри транзакции
  • COMMIT — фиксация изменений
  • ABORT — откат всех операций

В STOMP.js транзакции инкапсулируются объектом transaction, который создаётся через клиентское API и привязывается к идентификатору транзакции.

Создание транзакции и привязка сообщений

Транзакция создаётся на уровне клиента и получает уникальный идентификатор, который используется во всех последующих кадрах.

const tx = client.begin();

tx.send({
  destination: '/queue/orders',
  body: JSON.stringify({ id: 1, status: 'created' })
});

tx.commit();

В момент вызова begin клиент отправляет брокеру кадр BEGIN с заголовком transaction. Все последующие операции маркируются этим идентификатором.

Ключевой момент: сообщение, отправленное внутри транзакции, не покидает логическую область фиксации до COMMIT.

Подтверждение транзакции на уровне брокера

Подтверждение транзакции происходит только после получения COMMIT. До этого момента брокер:

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

После COMMIT брокер выполняет атомарную фиксацию:

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

Если вместо COMMIT приходит ABORT, брокер полностью откатывает изменения, как будто транзакции не существовало.

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

Подтверждение транзакции нельзя путать с подтверждением доставки сообщения (message acknowledgment). В STOMP существует два уровня:

  • транспортный уровень (TCP + STOMP frame delivery)
  • семантический уровень (ACK/NACK и транзакции)

Транзакция влияет на семантический уровень. Сообщение считается гарантированно обработанным только после COMMIT.

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

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

Receipt как механизм подтверждения команд

Отдельный механизм подтверждения в STOMP — заголовок receipt. Он позволяет получить явное подтверждение получения кадра брокером.

const tx = client.begin({
  receipt: 'tx-1'
});

tx.commit();

При использовании receipt брокер отправляет кадр RECEIPT после успешной обработки COMMIT. Это подтверждение отличается от логического подтверждения транзакции:

  • COMMIT подтверждает фиксацию данных
  • RECEIPT подтверждает получение и обработку команды COMMIT

Таким образом формируется двухуровневая модель надёжности.

Внутренний механизм STOMP.js

В STOMP.js транзакционный объект обычно реализует следующие функции:

  • send(headers, body) — отправка сообщений в рамках транзакции
  • commit() — завершение транзакции
  • abort() — отмена транзакции

При вызове send библиотека автоматически добавляет header:

transaction: <transaction-id>

Пример более полного сценария:

const tx = client.begin();

tx.send({
  destination: '/queue/payments',
  body: JSON.stringify({ amount: 500 })
});

tx.send({
  destination: '/queue/audit',
  body: JSON.stringify({ event: 'payment_created' })
});

tx.commit();

Обе операции отправки становятся атомарными с точки зрения брокера.

Откат транзакции и поведение ABORT

ABORT полностью отменяет все операции, выполненные внутри транзакции. Это особенно важно при ошибках бизнес-логики на клиенте.

const tx = client.begin();

try {
  tx.send({
    destination: '/queue/orders',
    body: JSON.stringify({ id: 10 })
  });

  throw new Error('validation failed');

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

После ABORT брокер не сохраняет никаких следов транзакционных операций.

Влияние транзакций на очереди и порядок сообщений

Транзакции напрямую влияют на порядок доставки сообщений. В рамках одного transaction-id соблюдается последовательность отправки:

  1. все SEND фиксируются в порядке вызова
  2. только после COMMIT они становятся видимыми подписчикам
  3. порядок внутри транзакции сохраняется как атомарный блок

Это критично для сценариев:

  • финансовых операций
  • событийной синхронизации
  • аудита действий пользователя

Подтверждение ACK внутри транзакции

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

const tx = client.begin();

subscription.ack(message, { transaction: tx });

tx.commit();

Здесь ACK не считается финальным до COMMIT. Если транзакция будет отменена, сообщение снова станет доступным для доставки.

Ошибки и повторная доставка

Если транзакция не была подтверждена из-за разрыва соединения или ABORT, брокер возвращает сообщения в очередь. Это приводит к повторной доставке, поэтому обработчики должны быть идемпотентными.

Ключевые сценарии:

  • потеря соединения до COMMIT → откат
  • ошибка сети во время COMMIT → неопределённое состояние до reconnection
  • повторное подключение → возможный redelivery

Согласованность и ограничения модели

Транзакции STOMP не являются распределёнными в классическом смысле. Они:

  • ограничены одним соединением
  • не охватывают несколько брокеров
  • не обеспечивают двухфазный commit (2PC)

Подтверждение транзакции всегда локально для конкретной сессии STOMP.

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

Разные брокеры реализуют транзакции с нюансами:

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

Однако для STOMP.js это прозрачно, так как библиотека работает на уровне кадров протокола.

Роль таймаутов и соединения

Таймауты транзакции обычно не задаются на уровне STOMP, но зависят от:

  • конфигурации брокера
  • keepalive механизма
  • heartbeat настроек

При длительном «висящем» транзакционном состоянии брокер может разорвать соединение, что автоматически приводит к ABORT.

Состояние транзакции в клиенте

Клиентская сторона хранит минимальное состояние:

  • transaction id
  • список отправленных кадров (логически)
  • состояние: active / committed / aborted

После commit или abort объект транзакции считается завершённым и больше не используется.

Особенность STOMP.js: повторный вызов операций на завершённой транзакции приводит к ошибке или игнорированию в зависимости от реализации клиента.

Связь с гарантиями доставки

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

  • at-least-once (при повторной доставке)
  • атомарной группировки сообщений

Но не обеспечивает exactly-once без внешней идемпотентности.

Основная ценность механизма заключается в том, что набор операций SEND/ACK/NACK становится неделимой единицей обработки в брокере.