Восстановление после сбоев

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

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


Обнаружение разрыва соединения

Первый элемент восстановления — корректное определение факта сбоя. В STOMP.js это достигается через обработчики событий транспортного уровня и состояния клиента:

  • onConnect — успешное установление соединения
  • onDisconnect — штатное закрытие
  • onStompError — ошибка протокола STOMP
  • onWebSocketClose — разрыв транспортного канала
  • onWebSocketError — ошибка сокета

Ключевое значение имеет именно событие закрытия WebSocket, так как оно фиксирует потерю канала независимо от состояния STOMP-сессии.

Типичная проблема заключается в том, что разрыв может быть «тихим» — без явной ошибки. В таких случаях используется heartbeat-механизм.


Heartbeat как механизм раннего обнаружения

STOMP поддерживает heartbeat — периодическую отправку контрольных кадров между клиентом и брокером. В STOMP.js он задаётся при подключении:

heartbeatIncoming: 10000,
heartbeatOutgoing: 10000

Если в течение заданного интервала не приходит ожидаемый heartbeat, клиент интерпретирует это как разрыв соединения.

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

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

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


Стратегия переподключения

Переподключение не должно выполняться мгновенно и без ограничений. Типовая стратегия включает экспоненциальную задержку:

  • первая попытка — сразу или через 1–2 секунды
  • последующие — увеличение интервала (2s → 4s → 8s → 16s)
  • ограничение максимальной задержки (например, 30–60 секунд)

Такая модель предотвращает:

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

Пример логики:

let retryCount = 0;

function getDelay() {
  return Math.min(30000, Math.pow(2, retryCount) * 1000);
}

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


Восстановление подписок после реконнекта

Самая критичная часть восстановления — повторная регистрация подписок. После разрыва соединения брокер «забывает» все ранее созданные subscription.

Поэтому клиент обязан хранить локальный список активных подписок:

  • destination (топик или очередь)
  • callback обработки сообщений
  • параметры ack (auto/client/client-individual)

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

subscriptions.forEach(sub => {
  client.subscribe(sub.destination, sub.callback, { ack: sub.ackMode });
});

Особое внимание требуется к порядку подписки: некоторые брокеры начинают доставку сообщений сразу после ACK handshake, поэтому важно восстановить подписки до начала активной обработки бизнес-логики.


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

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

STOMP сам по себе не обеспечивает exactly-once delivery. Поэтому используются дополнительные механизмы:

Ack-режимы

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

Для устойчивых систем предпочтителен client-individual, так как он минимизирует риск потери сообщений при сбое обработки.

client.ack(message);
client.nack(message);

Повторная доставка брокером

Многие брокеры (RabbitMQ, ActiveMQ) поддерживают повторную доставку неподтверждённых сообщений после реконнекта. Это приводит к возможным дубликатам.

Следовательно, клиентская логика должна быть идемпотентной:

  • обработка сообщений не должна зависеть от уникальности доставки
  • используется messageId или businessId для дедупликации

Сохранение состояния сессии

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

  • частично обработанные сообщения
  • очереди ожидания ACK
  • локальные буферы

Для восстановления используются следующие подходы:

Локальный буфер неподтверждённых сообщений

const pending = new Map();

При получении сообщения оно добавляется в буфер до подтверждения. После ACK удаляется.

При реконнекте возможна повторная проверка состояния и очистка устаревших записей.


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

Если брокер поддерживает durable subscription, он сохраняет состояние подписки между сессиями. В этом случае клиент получает сообщения, накопленные за время отсутствия.

Однако STOMP.js не управляет этим поведением — оно задаётся на уровне брокера и заголовков подписки.


Повторная аутентификация

После реконнекта требуется повторная отправка CONNECT с теми же параметрами:

  • login
  • passcode
  • custom headers (tokens, session id)

Если используется JWT или OAuth-токен, возможны дополнительные сценарии:

  • истечение токена во время реконнекта
  • необходимость обновления токена до повторного подключения
  • раздельная логика refresh-token

Ошибки аутентификации обрабатываются через onStompError, после чего реконнект может быть заблокирован до обновления credentials.


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

После успешного восстановления соединения возникает задача синхронизации:

  • проверка актуальности данных
  • запрос пропущенных сообщений
  • восстановление UI-состояния (если применяется)

Часто используется комбинация:

  • STOMP для realtime
  • REST API для догрузки пропущенного состояния

Типовой паттерн:

  1. восстановление соединения
  2. повторная подписка
  3. запрос последнего offset / timestamp
  4. догрузка изменений через HTTP
  5. переход в realtime-режим

Защита от лавины реконнектов

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

Для предотвращения используется jitter:

delay = baseDelay + Math.random() * 1000;

Также применяется глобальный backoff на уровне приложения, чтобы разные компоненты не инициировали реконнект синхронно.


Обработка частичных сбоев

Не все ошибки приводят к полному разрыву соединения. Возможны ситуации:

  • WebSocket жив, но STOMP-сессия потеряна
  • heartbeat работает, но сообщения не доставляются
  • брокер принудительно закрыл subscription

В таких случаях применяется принудительная ресинхронизация:

  • повторный SUBSCRIBE без полного disconnect
  • проверка состояния через тестовое сообщение
  • пересоздание клиента при нестабильности

Поведение при множественных реконнектах

При длительных сбоях система должна различать:

  • кратковременную нестабильность сети
  • недоступность брокера
  • неправильную конфигурацию

Стратегия может включать:

  • ограничение числа попыток
  • переход в degraded mode
  • отключение realtime-части с переходом на polling

Консистентность после восстановления

Основная проблема после сбоя — расхождение состояния клиента и сервера. Даже при успешном переподключении возможны:

  • пропущенные события
  • дублирование сообщений
  • несогласованные состояния подписок

Решается через:

  • идемпотентные обработчики
  • серверные sequence-id
  • периодическую сверку состояния
  • использование event sourcing подхода

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

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

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

Это делает поведение гибким, но переносит всю ответственность за устойчивость на прикладной слой.


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

При мобильных и нестабильных сетях типичны сценарии:

  • смена IP
  • кратковременные disconnect/reconnect циклы
  • переход между Wi-Fi и LTE

В таких условиях важно:

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

Часто вводится state machine:

  • CONNECTING
  • CONNECTED
  • RECONNECTING
  • DISCONNECTED

что позволяет исключить гонки состояний.


Итоговая модель устойчивости

Поведение системы после сбоев формируется комбинацией нескольких уровней:

  • транспортный уровень (WebSocket)
  • протокол STOMP (heartbeat, ACK)
  • клиентская логика (retry, resubscribe)
  • бизнес-уровень (идемпотентность, дедупликация)
  • серверный брокер (durable queues, redelivery)

Только совместное использование этих механизмов обеспечивает восстановление без потери данных и неконтролируемых повторов сообщений.