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

В STOMP.js устойчивость соединения строится вокруг повторного установления WebSocket-сессии и последующего восстановления STOMP-уровня поверх транспортного канала. Потеря соединения может происходить по множеству причин: сетевые разрывы, перезапуск брокера, таймауты прокси, переход устройства между сетями. В таких условиях логика переподключения становится частью прикладной архитектуры, а не вспомогательной функцией клиента.

Основной цикл восстановления включает три этапа:

  1. фиксация факта разрыва транспортного соединения
  2. выдержка паузы согласно выбранной стратегии
  3. повторное создание WebSocket и STOMP-клиента с восстановлением подписок

Ключевая сложность заключается в том, что STOMP-сессия не является самовосстанавливающейся. После переподключения требуется заново регистрировать все subscriptions и восстанавливать состояние приложения.


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

STOMP.js предоставляет события жизненного цикла, которые используются для детекции состояния транспорта. Основной сигнал потери соединения — callback ошибки или закрытия соединения WebSocket.

Типовая точка входа:

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

Разрыв не всегда означает окончательную потерю связи. Часто соединение может быть восстановлено в течение нескольких секунд, поэтому стратегия переподключения должна учитывать временные сбои.


Простейшая стратегия повторного подключения

Самая базовая модель — фиксированная задержка между попытками.

let stompClient;
let retryTimer;

function connect() {
  const socket = new WebSocket("wss://example.com/ws");

  stompClient = Stomp.over(socket);

  stompClient.connect({}, () => {
    clearTimeout(retryTimer);
    subscribeAll();
  }, () => {
    scheduleReconnect();
  });

  socket.oncl ose = scheduleReconnect;
}

function scheduleReconnect() {
  retryTimer = setTimeout(connect, 3000);
}

Данная модель проста, но обладает критическим недостатком: при длительных сбоях создаётся высокая нагрузка на сервер из-за одинакового интервала попыток.


Экспоненциальная задержка

Более устойчивая стратегия — exponential backoff. Она увеличивает интервал между попытками с каждой неудачей.

Математически:

[ delay_n = min(delay_{max}, delay_{base} ^n)]

Где:

  • n — номер попытки
  • delay_base — начальная задержка
  • delay_max — верхний предел

Пример реализации:

let retryCount = 0;
const BASE_DELAY = 1000;
const MAX_DELAY = 30000;

function scheduleReconnect() {
  const delay = Math.min(MAX_DELAY, BASE_DELAY * Math.pow(2, retryCount));
  retryCount++;

  setTimeout(connect, delay);
}

Такая стратегия снижает риск перегрузки брокера и сети при массовых сбоях клиентов.


Добавление jitter для распределения нагрузки

При массовом отключении клиентов экспоненциальная схема может привести к синхронным повторным подключениям. Это создаёт эффект «штормового восстановления».

Для устранения проблемы вводится jitter — случайная вариация задержки:

function getDelayWithJitter(baseDelay) {
  const jitter = Math.random() * 0.3 * baseDelay;
  return baseDelay + jitter;
}

Расширенная версия:

function scheduleReconnect() {
  const base = Math.min(MAX_DELAY, BASE_DELAY * Math.pow(2, retryCount));
  const delay = getDelayWithJitter(base);
  retryCount++;

  setTimeout(connect, delay);
}

Jitter распределяет нагрузку по времени и предотвращает синхронные пики запросов к брокеру.


Управление состояниями соединения

Для корректной реализации переподключения вводится конечный автомат состояний:

  • DISCONNECTED
  • CONNECTING
  • CONNECTED
  • RECONNECTING

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

Пример структуры:

const state = {
  value: "DISCONNECTED"
};

function setState(newState) {
  state.value = newState;
}

Использование состояния:

  • CONNECTING — блокирует повторный вызов connect
  • RECONNECTING — допускает только один активный таймер
  • CONNECTED — сбрасывает счётчики retry

Защита от множественных параллельных попыток

При нестабильной сети часто возникает ситуация, когда одновременно запускается несколько reconnect-циклов. Это приводит к множественным WebSocket-соединениям.

Решение — использование флага блокировки:

let isConnecting = false;

function connect() {
  if (isConnecting) return;
  isConnecting = true;

  const socket = new WebSocket("wss://example.com/ws");
  const client = Stomp.over(socket);

  client.connect({}, () => {
    isConnecting = false;
    retryCount = 0;
  }, () => {
    isConnecting = false;
    scheduleReconnect();
  });
}

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

STOMP не сохраняет подписки после переподключения. Следовательно, требуется внешний реестр подписок.

Подход:

const subscriptions = [];

function subscribeAll(client) {
  subscriptions.forEach((sub) => {
    client.subscribe(sub.destination, sub.callback);
  });
}

function addSubscription(destination, callback) {
  subscriptions.push({ destination, callback });

  if (stompClient && stompClient.connected) {
    stompClient.subscribe(destination, callback);
  }
}

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

client.connect({}, () => {
  subscribeAll(client);
});

Heartbeat и диагностика соединения

STOMP.js поддерживает heartbeat для проверки живости соединения. Он помогает обнаружить «тихие разрывы», когда TCP-соединение формально открыто, но данные не проходят.

Конфигурация:

const client = Stomp.over(socket);

client.heartbeat.outgoing = 10000;
client.heartbeat.incoming = 10000;

При отсутствии heartbeat-сигналов клиент инициирует переподключение, даже если WebSocket не был явно закрыт.


Backoff с учётом причин ошибки

Более зрелая стратегия учитывает тип сбоя:

  • сетевой разрыв → быстрый backoff
  • ошибка авторизации → остановка попыток
  • ошибка брокера → увеличенный backoff
  • rate limit → долгий backoff

Пример:

function getDelay(errorType) {
  switch (errorType) {
    case "NETWORK":
      return 1000;
    case "BROKER":
      return 10000;
    case "RATE_LIMIT":
      return 60000;
    default:
      return 5000;
  }
}

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

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

client.connect(
  { Authorization: "Bearer " + token },
  onConnect,
  onError
);

При истечении токена стратегия переподключения должна учитывать обновление credentials, иначе цикл reconnect будет бесконечным.


Очереди сообщений при оффлайне

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

const messageQueue = [];

function send(destination, body) {
  if (stompClient && stompClient.connected) {
    stompClient.send(destination, {}, JSON.stringify(body));
  } else {
    messageQueue.push({ destination, body });
  }
}

function flushQueue() {
  while (messageQueue.length && stompClient.connected) {
    const msg = messageQueue.shift();
    stompClient.send(msg.destination, {}, JSON.stringify(msg.body));
  }
}

Защита от reconnect storm

Reconnect storm возникает при массовом падении соединений и агрессивных retry-стратегиях. Основные меры защиты:

  • увеличение минимального интервала
  • jitter
  • глобальный lock на reconnect
  • лимит количества попыток
  • circuit breaker

Circuit breaker:

let failures = 0;
let circuitOpen = false;

function scheduleReconnect() {
  if (circuitOpen) return;

  failures++;

  if (failures > 10) {
    circuitOpen = true;
    setTimeout(() => {
      circuitOpen = false;
      failures = 0;
      connect();
    }, 60000);

    return;
  }

  const delay = Math.min(30000, 1000 * Math.pow(2, failures));
  setTimeout(connect, delay);
}

Мульти-вкладочная синхронизация

В браузере несколько вкладок могут одновременно пытаться восстановить соединение. Это приводит к дублированию подписок и нагрузке на брокер.

Решение — coordination через localStorage:

window.addEventListener("storage", (event) => {
  if (event.key === "stomp-master" && event.newValue) {
    isMaster = false;
  }
});

function becomeMaster() {
  localStorage.setItem("stomp-master", Date.now().toString());
  isMaster = true;
}

Только master-вкладка выполняет reconnect.


Итоговая архитектурная модель

Устойчивое переподключение в STOMP.js строится как комбинация нескольких уровней:

  • транспортный уровень WebSocket (детекция разрыва)
  • STOMP-уровень (ошибки протокола и heartbeat)
  • стратегия backoff (экспоненциальная задержка + jitter)
  • state machine соединения
  • реестр подписок
  • очередь сообщений
  • защита от параллельных reconnect
  • ограничение попыток через circuit breaker
  • синхронизация между вкладками

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