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

В STOMP.js работа поверх WebSocket подразумевает нестабильность соединения как нормальное состояние среды. Соединение может быть разорвано по множеству причин: сетевые сбои, перезапуск брокера сообщений, истечение сессии на сервере, таймауты heartbeat, переключение сети на клиенте (Wi-Fi → LTE), ограничения прокси или балансировщика нагрузки.

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


Базовая модель переподключения

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

  • потеря соединения (onWebSocketClose)
  • ошибка соединения (onStompError)
  • завершение сессии клиентом или сервером (deactivate)

Минимальная реализация обычно опирается на повторный вызов activate().

import { Client } from "@stomp/stompjs";

const client = new Client({
  brokerURL: "ws://localhost:8080/ws",
  reconnectDelay: 0,
  debug: (msg) => console.log(msg),
});

client.onWebSocketCl ose = () => {
  console.log("Соединение закрыто");
};

client.onStompEr ror = (frame) => {
  console.error("STOMP ошибка", frame.headers["message"]);
};

client.activate();

В таком виде переподключение отсутствует как механизм — оно управляется вручную.


Автоматическое переподключение через reconnectDelay

STOMP.js предоставляет встроенный механизм автоматического восстановления соединения через параметр reconnectDelay.

const client = new Client({
  brokerURL: "ws://localhost:8080/ws",
  reconnectDelay: 5000,
});

Механизм работает следующим образом:

  • при разрыве соединения клиент ждёт указанное количество миллисекунд
  • затем автоматически вызывает activate()
  • попытка повторяется бесконечно до успешного подключения

Особенность: стратегия линейная, без адаптации к состоянию сети.


Экспоненциальная стратегия переподключения

Линейный таймаут неэффективен при нестабильной сети или массовых сбоях сервиса. Более устойчивый подход — экспоненциальная задержка с ограничением максимального интервала.

Логика:

  • первая попытка: 1 секунда
  • вторая: 2 секунды
  • третья: 4 секунды
  • далее рост до максимума (например, 30 секунд)
import { Client } from "@stomp/stompjs";

const client = new Client({
  brokerURL: "ws://localhost:8080/ws",
  reconnectDelay: 0,
});

let attempt = 0;
const maxDelay = 30000;

function scheduleReconnect() {
  attempt += 1;

  const delay = Math.min(1000 * Math.pow(2, attempt), maxDelay);

  setTimeout(() => {
    console.log(`Попытка переподключения #${attempt}`);
    client.activate();
  }, delay);
}

client.onWebSocketCl ose = scheduleReconnect;
client.onStompEr ror = scheduleReconnect;

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


Джиттер (jitter) для распределения нагрузки

При одновременном обрыве соединений у тысяч клиентов возникает эффект «thundering herd» — все клиенты переподключаются одновременно. Для предотвращения этого добавляется случайная задержка.

function getDelay(attempt) {
  const base = Math.min(1000 * Math.pow(2, attempt), 30000);
  const jitter = Math.random() * 1000;
  return base + jitter;
}

Jitter особенно важен в системах реального времени (чаты, торговые терминалы, трекинг).


Сохранение состояния подписок

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

Подход: централизованное хранилище подписок.

const subscriptions = new Map();

function subscribe(destination, callback) {
  const sub = client.subscribe(destination, callback);
  subscriptions.set(destination, sub);
}

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

client.onConn ect = () => {
  console.log("Подключено");

  for (const [destination, _sub] of subscriptions) {
    client.subscribe(destination, (msg) => {
      console.log(msg.body);
    });
  }
};

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


Контроль состояния подключения

Для корректной логики приложения требуется отслеживание состояния клиента:

  • CONNECTING
  • OPEN
  • DISCONNECTED
  • RECONNECTING

В STOMP.js нет явного state machine API, поэтому состояние реализуется вручную:

let status = "DISCONNECTED";

client.onConn ect = () => {
  status = "OPEN";
};

client.onWebSocketCl ose = () => {
  status = "RECONNECTING";
};

client.onStompEr ror = () => {
  status = "RECONNECTING";
};

Это состояние используется для:

  • блокировки отправки сообщений
  • отображения статуса UI
  • предотвращения дублирующих reconnect-попыток

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

Одна из типичных ошибок — запуск нескольких activate() одновременно. Это приводит к гонке соединений.

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

let reconnecting = false;

function safeReconnect() {
  if (reconnecting) return;

  reconnecting = true;

  setTimeout(() => {
    client.activate();
    reconnecting = false;
  }, 3000);
}

Heartbeat как фактор переподключения

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

const client = new Client({
  brokerURL: "ws://localhost:8080/ws",
  heartbeatIncoming: 10000,
  heartbeatOutgoing: 10000,
});

Если heartbeat не подтверждается сервером:

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

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


Восстановление очередей сообщений

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

const queue = [];

function send(destination, body) {
  if (client.connected) {
    client.publish({ destination, body });
  } else {
    queue.push({ destination, body });
  }
}

client.onConn ect = () => {
  while (queue.length > 0) {
    const msg = queue.shift();
    client.publish(msg);
  }
};

Комбинированная стратегия устойчивого подключения

Практически применяемая модель включает несколько уровней:

  • встроенный reconnectDelay отключён
  • экспоненциальный backoff с jitter
  • защита от параллельных попыток
  • восстановление подписок
  • локальная очередь сообщений
  • контроль состояния соединения
  • heartbeat для раннего обнаружения разрыва

Такая композиция обеспечивает устойчивость при:

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

Поведение при флаппинге соединения

Флаппинг — частые циклы подключения/отключения. В этом случае важно вводить «карантин» соединения:

let failureCount = 0;

function onFailure() {
  failureCount++;

  if (failureCount > 5) {
    console.warn("Карантин соединения");
    setTimeout(() => {
      failureCount = 0;
      client.activate();
    }, 60000);
  } else {
    safeReconnect();
  }
}

Это защищает сервер от перегрузки при нестабильных клиентах.


Синхронизация после переподключения

После восстановления соединения часто требуется синхронизация состояния:

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

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

client.onConn ect = () => {
  client.publish({
    destination: "/app/sync",
    body: JSON.stringify({ lastMessageId: lastId }),
  });
};

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

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

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

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