Зависшие подписки в STOMP.js возникают в ситуациях, когда клиент больше не использует подписку, но сервер продолжает считать её активной. Это приводит к накоплению неосвобождённых ресурсов, дублированию сообщений, росту сетевой нагрузки и постепенному ухудшению производительности приложения.
Подобные проблемы особенно часто появляются в:
Наиболее распространённые причины:
client.subscribe('/topic/orders', message => {
console.log(message.body);
});
При повторном выполнении этого кода создаётся новая подписка, а старая остаётся активной.
Если компонент перерисовывается многократно, количество подписок начинает быстро расти.
Метод subscribe() возвращает объект подписки:
const subscription = client.subscribe('/topic/orders', callback);
Если ссылка потеряна, вызвать unsubscribe() становится
невозможно.
Ошибочный вариант:
function init() {
client.subscribe('/topic/orders', callback);
}
Правильный вариант:
let subscription;
function init() {
subscription = client.subscribe('/topic/orders', callback);
}
function destroy() {
subscription.unsubscribe();
}
После автоматического переподключения многие разработчики повторно вызывают регистрацию подписок:
client.onConn ect = () => {
client.subscribe('/topic/orders', callback);
};
Если предыдущие подписки не были очищены корректно, сервер может сохранить старые соединения и подписки.
В результате одно сообщение начинает приходить несколько раз.
При аварийном закрытии вкладки браузера или потере сети STOMP-сессия может завершиться не полностью.
Сервер продолжает считать подписку активной до истечения таймаута heartbeat.
Критическая ошибка:
client.subscribe('/topic/main', message => {
client.subscribe('/topic/inner', inner => {
console.log(inner.body);
});
});
Каждое сообщение создаёт новую вложенную подписку.
Через несколько минут приложение может иметь сотни активных подписчиков.
Каждая подписка получает уникальный идентификатор:
const sub = client.subscribe(destination, callback);
Внутри STOMP формируется кадр:
SUBSCRIBE
id:sub-0
destination:/topic/orders
При отмене подписки отправляется:
UNSUBSCRIBE
id:sub-0
Если UNSUBSCRIBE не отправлен, сервер продолжает хранить
подписку.
Одно сообщение приходит несколько раз:
Order upd ated
Order upd ated
Order upd ated
Чем дольше работает приложение, тем больше дублей появляется.
Каждая подписка хранит:
При утечке подписок память браузера постепенно увеличивается.
Сервер продолжает отправлять сообщения всем неактивным подписчикам.
Это особенно критично при:
Broker хранит:
Тысячи «мертвых» подписок могут перегрузить сервер.
let subscription = null;
function subscribe() {
if (subscription) {
subscription.unsubscribe();
}
subscription = client.subscribe(
'/topic/orders',
message => {
console.log(message.body);
}
);
}
function unsubscribe() {
if (subscription) {
subscription.unsubscribe();
subscription = null;
}
}
В крупных приложениях подписок может быть десятки или сотни.
const subscriptions = new Map();
function addSubscription(name, destination, callback) {
removeSubscription(name);
const sub = client.subscribe(destination, callback);
subscriptions.se t(name, sub);
}
function removeSubscription(name) {
const sub = subscriptions.get(name);
if (sub) {
sub.unsubscribe();
subscriptions.delete(name);
}
}
function clearSubscriptions() {
subscriptions.forEach(sub => {
sub.unsubscribe();
});
subscriptions.clear();
}
if (!subscriptions.has('orders')) {
const sub = client.subscribe(
'/topic/orders',
callback
);
subscriptions.se t('orders', sub);
}
let subscribed = false;
function init() {
if (subscribed) {
return;
}
subscribed = true;
client.subscribe('/topic/orders', callback);
}
client.onConn ect = () => {
client.subscribe('/topic/orders', callback);
};
При каждом reconnect создаётся новая подписка.
let orderSubscription = null;
client.onConn ect = () => {
if (orderSubscription) {
orderSubscription.unsubscribe();
}
orderSubscription = client.subscribe(
'/topic/orders',
callback
);
};
В сложных приложениях удобно хранить конфигурацию подписок отдельно.
const subscriptionDefinitions = [
{
name: 'orders',
destination: '/topic/orders',
callback: onOrders
},
{
name: 'users',
destination: '/topic/users',
callback: onUsers
}
];
Восстановление:
const activeSubscriptions = new Map();
function restoreSubscriptions() {
subscriptionDefinitions.forEach(def => {
const sub = client.subscribe(
def.destination,
def.callback
);
activeSubscriptions.se t(def.name, sub);
});
}
function shutdown() {
clearSubscriptions();
client.deactivate();
}
client.deactivate();
clearSubscriptions();
После закрытия соединения UNSUBSCRIBE может не
отправиться.
useEffect(() => {
client.subscribe('/topic/orders', callback);
}, []);
Подписка создаётся, но не удаляется.
useEffect(() => {
const sub = client.subscribe(
'/topic/orders',
callback
);
return () => {
sub.unsubscribe();
};
}, []);
export default {
mounted() {
this.subscription = client.subscribe(
'/topic/orders',
this.onMessage
);
},
beforeUnmount() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
};
private subscription: StompSubscription;
ngOnInit(): void {
this.subscription = this.client.subscribe(
'/topic/orders',
this.onMessage
);
}
ngOnDestroy(): void {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
Подписка может удерживать ссылки на:
Даже если компонент удалён, зависшая подписка мешает сборщику мусора освободить память.
function trackedSubscribe(name, destination, callback) {
console.log('SUBSCRIBE:', name);
const sub = client.subscribe(destination, callback);
return {
unsubscribe() {
console.log('UNSUBSCRIBE:', name);
sub.unsubscribe();
}
};
}
console.log(
'Subscriptions:',
subscriptions.size
);
client.onConn ect = () => {
console.log('CONNECTED');
};
client.onDisconn ect = () => {
console.log('DISCONNECTED');
};
При использовании STOMP поверх RabbitMQ подписки могут создавать:
Неочищенные подписки приводят к росту потребления памяти broker’ом.
В ActiveMQ зависшие подписчики способны:
В Spring-приложениях зависшие подписки могут оставаться в:
Heartbeat помогает серверу обнаруживать неактивных клиентов.
Настройка:
client.heartbeatIncoming = 4000;
client.heartbeatOutgoing = 4000;
Если heartbeat отсутствует слишком долго, сервер закрывает соединение и удаляет подписки.
Некоторые брокеры поддерживают durable-подписки.
Они сохраняются даже после отключения клиента.
Ошибка управления durable subscriptions может привести к:
class SubscriptionManager {
constructor(client) {
this.client = client;
this.subscriptions = new Map();
}
subscribe(name, destination, callback) {
this.unsubscribe(name);
const sub = this.client.subscribe(
destination,
callback
);
this.subscriptions.set(name, sub);
return sub;
}
unsubscribe(name) {
const sub = this.subscriptions.get(name);
if (sub) {
sub.unsubscribe();
this.subscriptions.delete(name);
}
}
clear() {
this.subscriptions.forEach(sub => {
sub.unsubscribe();
});
this.subscriptions.clear();
}
}
Перед созданием подписки желательно проверять соединение:
if (client.connected) {
client.subscribe(
'/topic/orders',
callback
);
}
STOMP поддерживает собственные id подписок:
client.subscribe(
'/topic/orders',
callback,
{
id: 'orders-subscription'
}
);
Это облегчает диагностику на сервере.
window.addEventListener('beforeunload', () => {
clearSubscriptions();
client.deactivate();
});
Опасный сценарий:
client.deactivate();
client.activate();
Если предыдущие подписки ещё не удалены, возможно дублирование.
await client.deactivate();
client.activate();
Полезно отслеживать:
Если приложение постоянно создаёт и уничтожает подписки, это может означать:
unsubscribe;class RealtimeService {
constructor(client) {
this.client = client;
this.subscriptions = new Map();
}
subscribe(name, destination, callback) {
this.unsubscribe(name);
const sub = this.client.subscribe(
destination,
callback
);
this.subscriptions.set(name, sub);
}
unsubscribe(name) {
const sub = this.subscriptions.get(name);
if (sub) {
sub.unsubscribe();
this.subscriptions.delete(name);
}
}
disconnect() {
this.subscriptions.forEach(sub => {
sub.unsubscribe();
});
this.subscriptions.clear();
this.client.deactivate();
}
}