Персистентность сообщений

Персистентность сообщений в системах STOMP определяется сочетанием поведения брокера сообщений и корректной работой клиента, который отправляет и подтверждает получение сообщений. Сам протокол STOMP сам по себе не гарантирует сохранность данных, он лишь задаёт формат взаимодействия, тогда как сохранение сообщений реализуется на стороне брокеров (RabbitMQ, ActiveMQ, Apollo, Artemis и других).

Персистентность начинается с конфигурации очередей и сообщений на сервере. Брокер определяет, будет ли сообщение сохранено на диск или останется в памяти.

Ключевые механизмы:

  • Durable queue (устойчивая очередь) — очередь сохраняется после перезапуска брокера
  • Persistent message (персистентное сообщение) — сообщение записывается на диск
  • Durable subscription (устойчивая подписка) — подписчик получает сообщения даже после временного отключения

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


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

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

client.publish({
  destination: "/queue/orders",
  body: JSON.stringify({ id: 1, status: "created" }),
  headers: {
    persistent: "true"
  }
});

Значение persistent: "true" интерпретируется брокером как необходимость сохранить сообщение. Без этого заголовка сообщение может быть обработано как volatile и потеряться при сбое.

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


Очереди и топики: влияние модели доставки

Персистентность различается в зависимости от модели маршрутизации:

Очереди (Queue) Сообщение хранится до тех пор, пока не будет доставлено одному потребителю. При включённой персистентности сообщение сохраняется на диске до подтверждения обработки.

Топики (Topic) Сообщение рассылается всем активным подписчикам. Персистентность здесь работает только в сочетании с durable subscription, иначе сообщения теряются при отсутствии подписчиков.


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

STOMP.js подписка создаётся через subscribe, и именно здесь определяется поведение доставки.

client.subscribe("/queue/orders", (message) => {
  const payload = JSON.parse(message.body);
  console.log(payload);
});

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


Режимы подтверждения (ACK) и их роль

Персистентность доставки тесно связана с механизмом подтверждений. STOMP поддерживает несколько режимов:

  • auto — подтверждение происходит автоматически при доставке
  • client — подтверждение выполняется вручную
  • client-individual — подтверждение каждого сообщения отдельно

Пример с ручным подтверждением:

const subscription = client.subscribe(
  "/queue/orders",
  (message) => {
    const data = JSON.parse(message.body);

    processOrder(data);

    message.ack();
  },
  { ack: "client" }
);

Если сообщение не подтверждено, брокер может повторно доставить его после восстановления соединения. Это создаёт механизм гарантированной доставки поверх персистентного хранения.


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

Персистентность не ограничивается сохранением сообщений. Важной частью является поведение при сбоях:

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

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


Durable subscription в топиках

Для топиков используется механизм устойчивых подписок. Он требует явного идентификатора подписки.

client.subscribe(
  "/topic/news",
  (message) => {
    console.log(JSON.parse(message.body));
  },
  {
    id: "news-subscription-1",
    persistent: "true"
  }
);

На стороне брокера создаётся сохранённая подписка, которая продолжает получать сообщения после восстановления соединения. При этом сообщения, отправленные во время отсутствия подписчика, могут быть накоплены, если брокер поддерживает store-and-forward модель.


Хранение сообщений в брокере

Жизненный цикл персистентного сообщения включает несколько этапов:

  1. Приём сообщения от STOMP клиента
  2. Запись в журнал (disk log / WAL)
  3. Помещение в очередь доставки
  4. Передача потребителю
  5. Удаление после подтверждения ACK

В брокерах с высокой надёжностью (например, RabbitMQ с persistent messages и durable queues) используется журналирование, позволяющее восстановить очередь после падения процесса.


Транзакции STOMP и влияние на сохранность

Некоторые брокеры поддерживают STOMP-транзакции, которые группируют отправку сообщений и подтверждения.

client.begin("tx1");

client.publish({
  destination: "/queue/orders",
  body: "order-1",
  headers: { transaction: "tx1" }
});

client.commit("tx1");

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


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

Персистентность в связке с STOMP.js проявляется при нестабильных соединениях:

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

STOMP.js обычно требует ручного восстановления подписок:

client.onConn ect = () => {
  client.subscribe("/queue/orders", handleMessage, { ack: "client" });
};

Дублирование сообщений и идемпотентность

Персистентная доставка не исключает повторов. Причины:

  • повторная доставка после таймаута ACK
  • реконнект клиента
  • сбои брокера до фиксации состояния доставки

Поэтому обработка сообщений должна учитывать уникальные идентификаторы:

const processed = new Set();

function handleMessage(message) {
  const data = JSON.parse(message.body);

  if (processed.has(data.id)) return;

  processed.add(data.id);
  process(data);
  message.ack();
}

Ограничения и особенности реализации

Персистентность в STOMP.js зависит от внешней инфраструктуры:

  • STOMP.js не хранит сообщения локально
  • гарантии доставки определяются брокером
  • заголовок persistent не является универсальным стандартом
  • durable subscriptions требуют поддержки на стороне сервера
  • поведение ACK может отличаться между брокерами

Эти различия делают поведение системы конфигурируемым, а не фиксированным на уровне клиента.