Таймауты соединения

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

Процесс установления соединения начинается с создания WebSocket-инстанса. На этом уровне таймаут зависит не от STOMP.js, а от реализации WebSocket в браузере или Node.js-окружении. В классическом браузерном API отсутствует явный параметр connectTimeout, поэтому задержка подключения выражается через косвенные механизмы: события onerror, onclose и отсутствие onopen в заданный интервал времени.

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

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

const client = new Client({
  brokerURL: 'wss://example.com/ws',
  connectHeaders: {
    login: 'user',
    passcode: 'password'
  },
  debug: (msg) => console.log(msg)
});

client.activate();

Отсутствие явного подключения в течение заданного времени требует внешнего контроля. Обычно используется обёртка с Promise.race, где параллельно запускается таймер:

function connectWithTimeout(client, timeoutMs) {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => {
      client.deactivate();
      reject(new Error('Connection timeout'));
    }, timeoutMs);

    client.onConn ect = () => {
      clearTimeout(timer);
      resolve();
    };

    client.onStompEr ror = () => {
      clearTimeout(timer);
      reject(new Error('STOMP error during connection'));
    };

    client.activate();
  });
}

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

Таймаут транспортного уровня WebSocket

WebSocket соединение не предоставляет прямого управления временем ожидания handshake на уровне API. Однако сетевой стек браузера и TCP-соединение могут зависать на стадии DNS-резолвинга, TLS-handshake или ожидания ответа сервера.

На практике таймаут транспортного уровня проявляется через отсутствие событий open или close. Для серверных реализаций Node.js с использованием ws возможно частичное управление:

const WebSocket = require('ws');

const ws = new WebSocket('wss://example.com/ws', {
  handshakeTimeout: 5000
});

Параметр handshakeTimeout ограничивает время установки соединения до завершения HTTP Upgrade. STOMP.js при этом не участвует в управлении данным уровнем, но его поведение зависит от результата WebSocket-соединения.

Heartbeat как механизм контроля жизнеспособности соединения

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

В STOMP.js heartbeat задаётся в конфигурации клиента:

const client = new Client({
  brokerURL: 'wss://example.com/ws',
  heartbeatIncoming: 10000,
  heartbeatOutgoing: 10000
});

Значения задаются в миллисекундах. heartbeatOutgoing определяет интервал отправки сигналов клиентом, а heartbeatIncoming — допустимый интервал ожидания сигналов от брокера.

Отсутствие heartbeat в течение заданного времени приводит к закрытию соединения. Внутренне STOMP.js инициирует разрыв WebSocket-сессии, что позволяет трактовать ситуацию как таймаут неактивного соединения.

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

Таймауты подписок и доставки сообщений

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

Подписка создаётся через:

const subscription = client.subscribe('/topic/messages', (message) => {
  console.log(message.body);
});

Отсутствие сообщений в течение определённого времени не приводит к автоматическому разрыву подписки. Таймаут реализуется внешней логикой, например через таймер ожидания первого сообщения:

let received = false;

const timer = setTimeout(() => {
  if (!received) {
    subscription.unsubscribe();
  }
}, 15000);

client.subscribe('/topic/messages', (message) => {
  received = true;
  clearTimeout(timer);
});

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

Поведение reconnect и таймаут восстановления

STOMP.js включает встроенную поддержку повторного подключения через параметр reconnectDelay. Этот параметр определяет задержку между попытками восстановления соединения, но не ограничивает общее время восстановления.

const client = new Client({
  brokerURL: 'wss://example.com/ws',
  reconnectDelay: 5000
});

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

  1. Обнаружение разрыва WebSocket
  2. Ожидание reconnectDelay
  3. Повторный вызов activate()

Отсутствие лимита на количество попыток означает, что таймаут восстановления должен задаваться дополнительно:

let attempts = 0;
const maxAttempts = 5;

client.onWebSocketCl ose = () => {
  if (attempts++ >= maxAttempts) {
    client.deactivate();
  }
};

Таким образом формируется прикладной таймаут восстановления соединения, не предусмотренный напрямую библиотекой.

Таймауты обработки кадров STOMP

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

Проблема возникает при блокировке event loop, когда входящие сообщения продолжают поступать, но обработчик не выполняется своевременно. В таких случаях применяется внешнее измерение latency:

const start = Date.now();

client.subscribe('/topic/ping', (msg) => {
  const latency = Date.now() - start;
  console.log('Latency:', latency);
});

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

Взаимодействие таймаутов на разных уровнях

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

  • транспортный таймаут WebSocket handshake
  • heartbeat таймаут STOMP-протокола
  • прикладной таймаут подключения
  • логический таймаут подписок
  • таймаут восстановления соединения

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

Особое значение имеет синхронизация heartbeat с параметрами брокера, такими как ActiveMQ, RabbitMQ STOMP plugin или SockJS-серверы. Несовпадение интервалов приводит к эффекту «плавающих разрывов», когда соединение нестабильно без явных ошибок.

Особенности поведения в браузере и Node.js

В браузере контроль таймаутов ограничен отсутствием низкоуровневого доступа к TCP-соединению. Все механизмы реализуются через события WebSocket API и таймеры JavaScript.

В Node.js появляется дополнительная гибкость через библиотеку ws, где доступны параметры handshakeTimeout, maxPayload, а также возможность перехвата сокетного уровня.

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

Сценарии деградации соединения

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

  • задержка heartbeat приводит к подозрению на разрыв
  • WebSocket переходит в состояние закрытия
  • STOMP.js инициирует reconnect
  • прикладные таймеры фиксируют отсутствие данных

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