Вложенные транзакции

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

= {, _1, _2, , }

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

Отсутствие истинной вложенности транзакций

STOMP не поддерживает вложенные транзакции в классическом смысле (как savepoints в SQL или nested transactions в некоторых брокерах JMS). Каждая транзакция является независимой сущностью, и брокер не хранит контекст родительских/дочерних связей.

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

Ключевое ограничение:

  • нет savepoint
  • нет rollback to nested tx
  • нет атомарного объединения нескольких transaction id

Логическая вложенность на уровне приложения

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

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

Такая модель ближе к концепции saga, чем к классическим ACID вложенным транзакциям.

Идентификаторы как основа псевдо-иерархии

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

  • tx-parent-1001
  • tx-parent-1001.step-1
  • tx-parent-1001.step-2

Брокер не интерпретирует эту структуру, но клиентская логика может восстанавливать связь между транзакциями.

Базовый API STOMP.js для транзакций

В STOMP.js транзакции создаются через объект соединения:

const tx = client.begin();

client.publish({
  destination: "/queue/orders",
  body: JSON.stringify({ id: 1 }),
  transaction: tx.id
});

tx.commit();

При необходимости отмены:

tx.abort();

Каждый publish внутри транзакции должен явно содержать ссылку на transaction.id.

Имитация вложенной транзакции

Вложенность достигается через каскадный контроль нескольких транзакций:

const parentTx = client.begin();

try {
  const step1Tx = client.begin();
  client.publish({
    destination: "/queue/step1",
    body: "A",
    transaction: step1Tx.id
  });
  step1Tx.commit();

  const step2Tx = client.begin();
  client.publish({
    destination: "/queue/step2",
    body: "B",
    transaction: step2Tx.id
  });
  step2Tx.commit();

  client.publish({
    destination: "/queue/finalize",
    body: "DONE",
    transaction: parentTx.id
  });

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

Здесь вложенность является логической, но не технической. Каждый шаг может быть независимо закоммичен или откатан до того, как завершится родительская транзакция.

Координация состояния между уровнями

Чтобы имитировать вложенность корректно, требуется внешнее состояние:

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

Пример структуры состояния:

const txState = {
  parentId: "tx-100",
  steps: [
    { id: "tx-100-1", status: "committed" },
    { id: "tx-100-2", status: "pending" }
  ]
};

Такой подход позволяет реализовать частичное восстановление при сбоях.

Обработка ошибок и компенсация

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

try {
  const tx1 = client.begin();

  client.publish({
    destination: "/queue/debit",
    body: JSON.stringify({ amount: 100 }),
    transaction: tx1.id
  });

  tx1.commit();
} catch (e) {
  client.publish({
    destination: "/queue/compensate-debit",
    body: JSON.stringify({ reason: e.message })
  });
}

Компенсация становится основным механизмом согласованности.

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

Реализация STOMP-транзакций зависит от брокера:

  • ActiveMQ поддерживает транзакции на уровне сессии
  • RabbitMQ STOMP plugin транслирует транзакции в AMQP-уровень
  • Artemis поддерживает расширенные сценарии, но без вложенности

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

Конкурентность и изоляция

Каждая транзакция в STOMP:

  • не видна другим клиентам до commit
  • не может быть объединена с другой транзакцией
  • не имеет уровней изоляции SQL-мира

Это означает, что «вложенность» не влияет на конкурентное поведение брокера.

Практические ограничения модели

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

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

Альтернативная модель: saga-подход

В системах с высокой распределённостью вложенные транзакции заменяются оркестрацией:

  • каждая операция — отдельное сообщение
  • состояние хранится вне брокера
  • компенсация вместо rollback
  • координация через event-driven архитектуру

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

Модель композиции транзакций

Логически вложенные транзакции можно представить как граф зависимостей:

G = (V, E), ; V = {tx_1, tx_2, }, ; E = {(tx_i tx_j)}

где вершины — транзакции, а рёбра — зависимости выполнения. Такая модель отражает реальное поведение клиентской логики лучше, чем попытки использовать «вложенность» напрямую.

Согласованность через прикладные контракты

Так как брокер не обеспечивает вложенную атомарность, согласованность достигается через:

  • уникальные correlation id
  • повторяемые операции (idempotency keys)
  • журналирование событий
  • контроль состояния шагов

Каждое сообщение становится частью более широкой бизнес-транзакции, а не технической единицей брокера.

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

Если одна из дочерних транзакций завершается с ошибкой:

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

Это приводит к модели eventual consistency, где вложенность существует только на уровне бизнес-логики.