Мониторинг подписок

В STOMP-протоколе подписка представляет собой постоянное логическое соединение между клиентом и конкретным destination на брокере сообщений. В JavaScript-библиотеке STOMP.js каждая подписка создаётся как объект, связанный с активным соединением, и управляется через клиентский экземпляр.

Ключевая особенность модели заключается в том, что подписка не является статической: она может быть разорвана, пересоздана, потеряна при реконнекте или временно деактивирована при сбое транспортного уровня (WebSocket). Поэтому мониторинг состояния подписок становится критически важным элементом устойчивой архитектуры realtime-приложений.

Жизненный цикл подписки

Подписка в STOMP.js проходит несколько стадий:

  1. Инициализация через client.subscribe(destination, callback)
  2. Активное состояние при получении сообщений
  3. Потенциальное прерывание при разрыве соединения
  4. Восстановление при повторном подключении клиента
  5. Перерегистрация подписки после reconnect (если реализовано вручную)

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

Базовый механизм подписки в STOMP.js

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

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

Объект subscription содержит метод:

  • unsubscribe() — отмена подписки

Однако он не содержит встроенных инструментов для мониторинга состояния соединения или подтверждения доставки сообщений. Это формирует необходимость внешнего слоя наблюдения.

Проблема потери подписок при reconnect

При разрыве WebSocket соединения STOMP.js в большинстве конфигураций выполняет повторное подключение. Но:

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

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

Подходы к мониторингу подписок

Реестр подписок (Subscription Registry)

Основная стратегия — централизованное хранение всех активных подписок:

const subscriptions = new Map();

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

function removeSubscription(key) {
  const entry = subscriptions.get(key);
  if (entry) {
    entry.sub.unsubscribe();
    subscriptions.delete(key);
  }
}

Такой реестр позволяет:

  • отслеживать все активные подписки
  • управлять их жизненным циклом
  • пересоздавать их после reconnect

Автоматическое восстановление подписок

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

function restoreSubscriptions() {
  subscriptions.forEach((entry, key) => {
    const sub = client.subscribe(entry.destination, entry.handler);
    subscriptions.set(key, { ...entry, sub });
  });
}

Обычно это привязывается к событию onConnect:

client.onConn ect = () => {
  restoreSubscriptions();
};

Мониторинг состояния соединения

Подписки невозможно корректно мониторить без контроля состояния самого WebSocket/STOMP клиента.

STOMP.js предоставляет callback-и:

  • onConnect
  • onDisconnect
  • onStompError
  • onWebSocketClose

Пример централизованного наблюдения:

client.onConn ect = () => {
  console.log('connected');
  restoreSubscriptions();
};

client.onDisconn ect = () => {
  console.log('disconnected');
};

client.onStompEr ror = (frame) => {
  console.error('STOMP error', frame);
};

client.onWebSocketCl ose = () => {
  console.warn('socket closed');
};

Heartbeat как косвенный механизм контроля подписок

Heartbeat в STOMP используется для проверки живости соединения.

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

Отсутствие heartbeat-сообщений может указывать на:

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

Однако heartbeat не проверяет конкретные подписки, он лишь контролирует транспортный уровень.

Проверка активности подписки через трафик сообщений

Практический метод мониторинга — фиксация факта получения сообщений:

const lastMessageTime = new Map();

function subscribeWithMonitoring(key, destination) {
  const sub = client.subscribe(destination, (msg) => {
    lastMessageTime.set(key, Date.now());
    handleMessage(msg);
  });

  subscriptions.set(key, sub);
}

Далее можно периодически проверять “устаревшие” подписки:

setInterval(() => {
  const now = Date.now();

  lastMessageTime.forEach((time, key) => {
    if (now - time > 60000) {
      console.warn(`No messages for subscription ${key}`);
    }
  });
}, 30000);

Это позволяет обнаруживать:

  • “тихие” подписки
  • проблемы на сервере
  • отсутствие публикаций в топик

Диагностика через STOMP frame tracing

STOMP.js позволяет анализировать входящие и исходящие фреймы при включённом debug-режиме:

const client = new Client({
  brokerURL: 'ws://localhost:8080/ws',
  debug: (str) => {
    console.log(str);
  }
});

Мониторинг фреймов полезен для:

  • проверки факта SUBSCRIBE отправки
  • контроля UNSUBSCRIBE операций
  • анализа RECONNECT поведения

Управление подписками при масштабировании приложения

В крупных приложениях подписки часто привязаны к состоянию UI или бизнес-логике.

Типичные проблемы:

  • дублирование подписок при повторном рендере компонентов
  • утечки памяти из-за неотписанных каналов
  • конфликт одинаковых destination

Решение — строгая идентификация подписок:

const subscriptionKeys = {
  ORDERS: 'orders_subscription',
  NOTIFICATIONS: 'notifications_subscription'
};

И централизованное управление через менеджер:

class SubscriptionManager {
  constructor(client) {
    this.client = client;
    this.registry = new Map();
  }

  subscribe(key, destination, handler) {
    if (this.registry.has(key)) {
      this.registry.get(key).sub.unsubscribe();
    }

    const sub = this.client.subscribe(destination, handler);
    this.registry.set(key, { destination, handler, sub });
  }

  restore() {
    this.registry.forEach((entry, key) => {
      const sub = this.client.subscribe(entry.destination, entry.handler);
      this.registry.set(key, { ...entry, sub });
    });
  }
}

Обнаружение рассинхронизации состояния

Рассинхронизация возникает, когда:

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

Для выявления применяются комбинированные методы:

  • heartbeat
  • таймеры активности сообщений
  • периодический “ping” через отдельный topic
  • контроль reconnect циклов

Подписки и повторное подключение с backoff

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

Правильный подход:

  • очищать старые подписки перед восстановлением
  • избегать двойного восстановления
  • использовать флаг “reconnecting”
let reconnecting = false;

client.onDisconn ect = () => {
  reconnecting = true;
};

client.onConn ect = () => {
  if (reconnecting) {
    restoreSubscriptions();
    reconnecting = false;
  }
};

Метрики мониторинга подписок

Для production-уровня полезно собирать следующие показатели:

  • количество активных подписок
  • частота переподключений
  • время последнего сообщения по каждой подписке
  • количество ошибок STOMP
  • число пересозданий подписок

Эти метрики позволяют выявлять:

  • деградацию брокера
  • проблемы маршрутизации сообщений
  • утечки подписок в клиенте