Таймауты

В системах реального времени соединение между клиентом и брокером сообщений должно постоянно поддерживаться в активном состоянии. Однако WebSocket-соединения подвержены различным проблемам:

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

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

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

Основные типы таймаутов

В STOMP.js используются несколько категорий таймаутов.

Таймаут подключения

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

Если соединение не установлено за указанный период:

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

Таймаут heartbeat

Используется для проверки активности соединения.

Если heartbeat-сообщения перестают приходить:

  • клиент считает соединение мёртвым;
  • WebSocket закрывается;
  • запускается переподключение.

Таймаут ожидания сообщений

Применяется на уровне бизнес-логики.

Например:

  • ожидание ответа от сервера;
  • подтверждение доставки;
  • ответ RPC-запроса;
  • подтверждение авторизации.

Таймаут реконнекта

Определяет задержку между повторными попытками подключения.


Таймаут подписки

Контролирует длительность жизни подписки или ожидания сообщения внутри неё.


Настройка heartbeat в STOMP.js

Heartbeat — это механизм периодической отправки служебных пакетов для проверки активности соединения.

В STOMP.js используются два параметра:

heartbeatIncoming
heartbeatOutgoing

Пример настройки:

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

const client = new Client({
    brokerURL: 'ws://localhost:15674/ws',

    heartbeatIncoming: 10000,
    heartbeatOutgoing: 10000
});

Значения задаются в миллисекундах.

В данном примере:

  • клиент отправляет heartbeat каждые 10 секунд;
  • клиент ожидает heartbeat от сервера каждые 10 секунд.

Как работает heartbeat

Алгоритм работы выглядит следующим образом:

  1. Клиент устанавливает соединение.
  2. STOMP.js запускает heartbeat-таймеры.
  3. Каждые heartbeatOutgoing миллисекунд отправляется heartbeat.
  4. Если входящие heartbeat отсутствуют дольше heartbeatIncoming, соединение признаётся невалидным.
  5. Выполняется закрытие сокета.
  6. Запускается reconnect.

Пример heartbeat-трафика

Heartbeat представляет собой минимальный пакет:

\n

STOMP-протокол допускает использование пустой строки как сигнала активности.

Это снижает нагрузку на сеть и брокер.


Настройка агрессивных heartbeat

Для критичных realtime-систем heartbeat делают короткими.

Пример:

const client = new Client({
    brokerURL: 'ws://localhost:15674/ws',

    heartbeatIncoming: 3000,
    heartbeatOutgoing: 3000
});

Подобная конфигурация используется:

  • в трейдинговых системах;
  • игровых серверах;
  • системах мониторинга;
  • онлайн-редакторах;
  • VoIP-платформах.

Недостатки:

  • больше сетевого трафика;
  • выше нагрузка на CPU;
  • больше ложных реконнектов;
  • чувствительность к jitter.

Настройка медленных heartbeat

Для обычных корпоративных приложений heartbeat делают реже:

const client = new Client({
    brokerURL: 'ws://localhost:15674/ws',

    heartbeatIncoming: 30000,
    heartbeatOutgoing: 30000
});

Преимущества:

  • меньше сетевой нагрузки;
  • ниже потребление ресурсов;
  • стабильнее работа при нестабильном интернете.

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


Отключение heartbeat

Heartbeat можно полностью отключить:

const client = new Client({
    brokerURL: 'ws://localhost:15674/ws',

    heartbeatIncoming: 0,
    heartbeatOutgoing: 0
});

Подобный режим опасен, поскольку:

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

Таймауты реконнекта

STOMP.js поддерживает автоматическое переподключение.

Главный параметр:

reconnectDelay

Пример:

const client = new Client({
    brokerURL: 'ws://localhost:15674/ws',

    reconnectDelay: 5000
});

В этом случае:

  • после разрыва соединения;
  • либо ошибки подключения;

клиент подождёт 5 секунд и попытается подключиться снова.


Проблема бесконечных реконнектов

Без ограничений reconnect может создавать серьёзные проблемы:

  • DDoS на брокер;
  • сотни запросов в секунду;
  • бесконечная нагрузка на CPU;
  • утечки памяти;
  • рост очередей таймеров.

Плохой пример:

reconnectDelay: 10

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


Экспоненциальный backoff

Наиболее безопасный подход — постепенное увеличение таймаута реконнекта.

Пример:

let reconnectDelay = 1000;

const client = new Client({
    brokerURL: 'ws://localhost:15674/ws',

    reconnectDelay
});

client.onWebSocketCl ose = () => {
    reconnectDelay = Math.min(reconnectDelay * 2, 30000);

    client.reconnectDelay = reconnectDelay;
};

Алгоритм:

Попытка Задержка
1 1 сек
2 2 сек
3 4 сек
4 8 сек
5 16 сек
6 30 сек

Добавление jitter

Если тысячи клиентов одновременно переподключаются, возникает эффект thundering herd.

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

Пример:

function getReconnectDelay(baseDelay) {
    const jitter = Math.random() * 1000;

    return baseDelay + jitter;
}

client.reconnectDelay = getReconnectDelay(5000);

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


Таймаут ожидания ответа

В некоторых сценариях необходимо ограничивать время ожидания ответа.

Например:

  • RPC;
  • запрос данных;
  • подтверждение операции;
  • серверная обработка.

Пример:

function waitResponse(timeout = 5000) {
    return new Promise((resolve, reject) => {

        const timer = setTimeout(() => {
            reject(new Error('Timeout'));
        }, timeout);

        client.subscribe('/topic/response', message => {

            clearTimeout(timer);

            resolve(JSON.parse(message.body));
        });
    });
}

Проблема незавершённых таймеров

Типичная ошибка — забытый clearTimeout.

Плохой пример:

setTimeout(() => {
    console.log('timeout');
}, 5000);

Если операция завершилась раньше, таймер остаётся в памяти.

Это приводит к:

  • накоплению callback-функций;
  • росту памяти;
  • утечкам;
  • лишним wakeup CPU.

Безопасная очистка таймеров

Правильный вариант:

const timer = setTimeout(() => {
    reject(new Error('Timeout'));
}, 5000);

subscription.unsubscribe();

clearTimeout(timer);

Таймаут подписки

Иногда подписка должна существовать ограниченное время.

Пример:

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

setTimeout(() => {
    subscription.unsubscribe();
}, 10000);

Через 10 секунд подписка будет удалена.


Таймауты при ожидании ACK

В режиме manual acknowledgement клиент обязан подтверждать получение сообщений.

Пример:

client.subscribe(
    '/queue/tasks',
    message => {

        processTask(message);

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

Если ACK не отправлен вовремя:

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

Реализация ACK timeout

Пример:

client.subscribe(
    '/queue/tasks',
    async message => {

        const timer = setTimeout(() => {

            console.error('ACK timeout');

            message.nack();

        }, 5000);

        try {

            await processTask(message);

            clearTimeout(timer);

            message.ack();

        } catch (error) {

            clearTimeout(timer);

            message.nack();
        }
    },
    {
        ack: 'client'
    }
);

Таймаут подключения WebSocket

Иногда WebSocket зависает во время подключения.

Решение:

function connectWithTimeout(timeout = 5000) {

    return Promise.race([
        new Promise(resolve => {
            client.onConn ect = resolve;
            client.activate();
        }),

        new Promise((_, reject) => {
            setTimeout(() => {
                reject(new Error('Connect timeout'));
            }, timeout);
        })
    ]);
}

AbortController и таймауты

Современный подход — использование AbortController.

Пример:

function waitMessage(timeout = 5000) {

    const controller = new AbortController();

    return new Promise((resolve, reject) => {

        const timer = setTimeout(() => {

            controller.abort();

            reject(new Error('Timeout'));

        }, timeout);

        const subscription = client.subscribe(
            '/topic/data',
            message => {

                clearTimeout(timer);

                subscription.unsubscribe();

                resolve(message.body);
            }
        );

        controller.signal.addEventListener('abort', () => {

            subscription.unsubscribe();
        });
    });
}

Таймауты и браузерные вкладки

Во многих браузерах background-вкладки ограничивают работу таймеров.

Это влияет на:

  • heartbeat;
  • reconnect;
  • setTimeout;
  • setInterval.

Последствия:

  • ложные disconnect;
  • задержки reconnect;
  • пропуск heartbeat.

Visibility API

Для оптимизации таймаутов используют Visibility API.

Пример:

document.addEventListener('visibilitychange', () => {

    if (document.hidden) {

        client.deactivate();

    } else {

        client.activate();
    }
});

Таймауты в мобильных браузерах

Мобильные устройства агрессивно ограничивают фоновые процессы.

Особенности:

  • таймеры могут замораживаться;
  • WebSocket может отключаться ОС;
  • heartbeat может останавливаться;
  • reconnect может откладываться на минуты.

Поэтому мобильные приложения требуют:

  • увеличенных heartbeat;
  • tolerant reconnect;
  • повторной инициализации подписок.

Таймауты и прокси-серверы

Многие reverse proxy автоматически закрывают idle WebSocket-соединения.

Типичные значения:

Система Idle timeout
Nginx 60 сек
AWS ALB 60 сек
Cloudflare 100 сек
HAProxy 50 сек

Heartbeat должен быть меньше proxy-timeout.

Например:

heartbeatOutgoing: 30000

Таймауты и RabbitMQ

В RabbitMQ heartbeat координируется между клиентом и сервером.

Если значения несовместимы:

  • heartbeat может игнорироваться;
  • соединение может разрываться;
  • reconnect станет циклическим.

Диагностика heartbeat-проблем

Для анализа используют debug-режим:

const client = new Client({
    brokerURL: 'ws://localhost:15674/ws',

    debug(str) {
        console.log(str);
    }
});

В логах отображаются:

  • CONNECT;
  • CONNECTED;
  • SEND;
  • SUBSCRIBE;
  • heartbeat;
  • disconnect;
  • reconnect.

Типичные ошибки настройки таймаутов

Слишком короткий heartbeat

heartbeatIncoming: 500

Проблемы:

  • постоянные reconnect;
  • ложные disconnect;
  • нестабильность.

Слишком длинный reconnect

reconnectDelay: 600000

Недостаток:

  • клиент может оставаться офлайн десятки минут.

Отсутствие timeout cleanup

Плохой код:

setTimeout(() => {
    doSomething();
}, 5000);

Без:

clearTimeout(timer);

Множественные reconnect-циклы

Ошибка:

client.activate();
client.activate();
client.activate();

Это создаёт параллельные циклы подключения.


Централизованное управление таймаутами

Хорошая практика — вынесение всех значений в конфигурацию.

Пример:

const STOMP_TIMEOUTS = {
    heartbeatIncoming: 10000,
    heartbeatOutgoing: 10000,
    reconnectDelay: 5000,
    requestTimeout: 15000,
    ackTimeout: 30000
};

Адаптивные таймауты

В сложных системах таймауты меняются динамически.

Например:

  • mobile network → увеличенный heartbeat;
  • Wi-Fi → уменьшенный heartbeat;
  • высокая нагрузка → увеличенный reconnect;
  • background mode → отключение realtime.

Мониторинг таймаутов

В production-системах отслеживаются:

  • количество reconnect;
  • среднее время подключения;
  • heartbeat latency;
  • timeout rate;
  • ACK timeout count;
  • message processing timeout;
  • reconnect storm frequency.

Метрики timeout-систем

Типичные метрики:

Метрика Назначение
reconnect_total количество реконнектов
heartbeat_missed пропущенные heartbeat
websocket_disconnects разрывы соединения
ack_timeout_total timeout ACK
request_timeout_total timeout запросов
reconnect_duration длительность восстановления

Архитектура устойчивой timeout-системы

Надёжная реализация обычно включает:

  • heartbeat;
  • reconnect backoff;
  • jitter;
  • cleanup таймеров;
  • ACK timeout;
  • request timeout;
  • subscription timeout;
  • monitoring;
  • metrics;
  • reconnect protection;
  • visibility handling;
  • proxy compatibility.

Практическая production-конфигурация

const client = new Client({

    brokerURL: 'wss://broker.example/ws',

    reconnectDelay: 5000,

    heartbeatIncoming: 10000,

    heartbeatOutgoing: 10000,

    debug(str) {
        console.log(str);
    },

    onConnect() {

        console.log('Connected');
    },

    onDisconnect() {

        console.log('Disconnected');
    },

    onWebSocketClose() {

        console.log('Socket closed');
    },

    onStompError(frame) {

        console.error(frame.headers.message);
    }
});

Подобная конфигурация обеспечивает:

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