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

Транзакции в STOMP поверх WebSocket предоставляют механизм атомарной отправки сообщений: группа сообщений либо фиксируется целиком, либо откатывается без попадания в брокер. В контексте STOMP.js транзакции реализуются на уровне STOMP-протокола и поддерживаются брокером (например, ActiveMQ, RabbitMQ, Apollo, Artemis), а клиентская библиотека лишь управляет жизненным циклом транзакции и маркировкой кадров SEND.

Транзакция в STOMP представляет собой логическую обёртку над набором операций SEND. Сообщения, отправленные в рамках транзакции, не становятся доступными подписчикам до момента COMMIT. При ABORT все сообщения транзакции отбрасываются.


Инициализация транзакционного контекста в STOMP.js

В STOMP.js транзакция создаётся через объект соединения клиента. Базовая точка входа — метод begin, возвращающий объект транзакции.

const transaction = client.begin();

После вызова создаётся идентификатор транзакции (transaction.id), который автоматически используется при отправке сообщений в рамках этого контекста.

Транзакционный объект обычно содержит:

  • id — уникальный идентификатор транзакции
  • commit() — фиксация всех операций
  • abort() — откат всех операций
  • send() — отправка сообщений внутри транзакции

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

Каждое сообщение, отправленное в рамках транзакции, должно содержать заголовок transaction. STOMP.js автоматически добавляет этот заголовок при использовании метода transaction.send.

transaction.send({
  destination: "/queue/orders",
  body: JSON.stringify({
    orderId: 123,
    status: "created"
  })
});

Фактически клиент формирует STOMP FRAME следующего вида:

SEND
destination:/queue/orders
transaction:tx-1
content-type:application/json

{"orderId":123,"status":"created"}

До выполнения COMMIT брокер удерживает сообщения в изолированном состоянии.


Жизненный цикл транзакции

Транзакция в STOMP проходит строго определённые этапы:

  1. Инициализация через BEGIN
  2. Накопление сообщений SEND
  3. Завершение через COMMIT или ABORT

Начало транзакции

const tx = client.begin();

На этом этапе брокер фиксирует создание транзакции и резервирует её идентификатор.


Отправка сообщений внутри транзакции

tx.send({
  destination: "/queue/payments",
  body: JSON.stringify({
    userId: 42,
    amount: 1000
  }),
  headers: {
    persistent: "true"
  }
});

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

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

tx.send({ destination: "/queue/a", body: "A1" });
tx.send({ destination: "/queue/b", body: "B1" });
tx.send({ destination: "/queue/a", body: "A2" });

Завершение транзакции

Фиксация изменений
tx.commit();

После COMMIT брокер атомарно публикует все накопленные сообщения. Они становятся видимыми подписчикам одновременно.

Откат изменений
tx.abort();

При ABORT брокер удаляет все сообщения транзакции, как если бы они никогда не отправлялись.


Семантика атомарности

Транзакции в STOMP обеспечивают атомарность на уровне брокера, но не на уровне сети WebSocket. Это означает:

  • если commit дошёл до брокера — изменения фиксируются
  • если соединение оборвалось до commit — транзакция может быть отменена брокером
  • клиент не получает гарантии завершения операции без подтверждения от брокера

Таким образом, STOMP.js не реализует транзакции самостоятельно, а лишь передаёт управляющие кадры BEGIN, COMMIT, ABORT.


Внутренний формат STOMP кадров транзакции

STOMP использует специальные команды:

BEGIN

BEGIN
transaction:tx-123

SEND с транзакцией

SEND
destination:/queue/example
transaction:tx-123
content-type:text/plain

hello

COMMIT

COMMIT
transaction:tx-123

ABORT

ABORT
transaction:tx-123

STOMP.js абстрагирует формирование этих кадров, но логика остаётся идентичной.


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

STOMP.js реализует транзакции как объект-обёртку над клиентским соединением. Важные особенности:

  1. Транзакция привязана к конкретному соединению WebSocket
  2. Нельзя использовать транзакцию после разрыва соединения
  3. Повторный commit или abort игнорируется или вызывает ошибку в зависимости от реализации брокера
  4. Все send внутри транзакции используют один transaction-id

Пример полной цепочки:

const tx = client.begin();

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

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

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

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

Ошибки в транзакциях STOMP делятся на несколько типов:

Ошибки на клиенте

  • попытка отправки после commit
  • использование несуществующей транзакции
  • разрыв WebSocket до завершения транзакции

Ошибки брокера

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

В случае ошибок брокера обычно отправляется STOMP frame ERROR, после чего соединение может быть закрыто.


Работа с несколькими транзакциями

STOMP позволяет параллельно открывать несколько транзакций:

const tx1 = client.begin();
const tx2 = client.begin();

tx1.send({ destination: "/queue/a", body: "A" });
tx2.send({ destination: "/queue/b", body: "B" });

tx1.commit();
tx2.abort();

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


Влияние транзакций на производительность

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

  • увеличивается задержка доставки сообщений до момента COMMIT
  • растёт нагрузка на память брокера (буферизация сообщений)
  • увеличивается стоимость обработки при большом количестве транзакций

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


Совместимость с брокерами

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

  • ActiveMQ Artemis — полная поддержка транзакций STOMP
  • RabbitMQ (STOMP plugin) — ограниченная поддержка, зависит от конфигурации
  • Apollo — поддержка транзакций на уровне STOMP 1.2
  • Apache ActiveMQ Classic — зрелая реализация с расширенной семантикой

Некоторые брокеры могут эмулировать транзакции или ограничивать их использование только для SEND.


Использование заголовков внутри транзакций

Заголовки сообщений внутри транзакции не отличаются от обычных SEND, за исключением обязательного transaction:

tx.send({
  destination: "/queue/logs",
  headers: {
    priority: "9",
    persistent: "true",
    "content-type": "application/json"
  },
  body: JSON.stringify({ message: "critical event" })
});

Все заголовки фиксируются брокером только после COMMIT.


Отложенная публикация и консистентность

Транзакции создают модель отложенной публикации:

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

Это особенно важно для сценариев:

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

Ограничения транзакционной модели STOMP

Несмотря на удобство, модель имеет ограничения:

  • отсутствует распределённая транзакция между брокерами
  • нет поддержки двухфазного коммита на уровне STOMP.js
  • зависимость от возможностей конкретного брокера
  • невозможность гарантировать транзакцию при потере соединения до COMMIT

STOMP.js остаётся транспортным уровнем, не реализующим полноценную транзакционную систему, а лишь управляющим STOMP-командами, которые интерпретируются сервером.