При работе с брокерами сообщений отправка данных не всегда завершается мгновенно. Сетевые задержки, перегрузка брокера, нестабильное соединение, проблемы маршрутизации и ограничения TCP способны привести к зависанию операции передачи. В таких ситуациях приложение может бесконечно ожидать завершения отправки сообщения.
Таймауты отправки позволяют ограничить время ожидания операции публикации. Если сообщение не было передано за установленный промежуток времени, операция считается неуспешной.
В экосистеме STOMP поверх WebSocket тема таймаутов особенно важна, поскольку:
Метод publish() в STOMP.js не возвращает Promise и
обычно выполняется мгновенно:
client.publish({
destination: '/topic/chat',
body: 'Сообщение'
});
На первый взгляд может показаться, что сообщение успешно отправлено сразу после вызова метода. На практике это означает лишь передачу данных в WebSocket-слой браузера.
Фактическая доставка зависит от:
Из-за этого в STOMP.js таймауты реализуются не как встроенная функция
метода publish(), а через комбинацию дополнительных
механизмов контроля.
Типичная проблема возникает при частичном разрыве сети.
Например:
send;Такое состояние называется «half-open connection».
Без таймаутов приложение не способно обнаружить проблему своевременно.
Наиболее распространённый способ обнаружения проблем — heartbeat.
В STOMP.js heartbeat настраивается так:
import { Client } from '@stomp/stompjs';
const client = new Client({
brokerURL: 'ws://localhost:15674/ws',
heartbeatIncoming: 10000,
heartbeatOutgoing: 10000
});
Интервал отправки heartbeat-пакетов серверу.
Максимальное время ожидания heartbeat от брокера.
Heartbeat не контролирует отдельное сообщение напрямую, но позволяет определить потерю соединения.
Если heartbeat перестал приходить:
Таким образом heartbeat создаёт базовый механизм таймаута для всей транспортной сессии.
Для систем реального времени используются небольшие интервалы:
const client = new Client({
brokerURL: 'ws://localhost:15674/ws',
heartbeatOutgoing: 3000,
heartbeatIncoming: 3000
});
Преимущества:
Недостатки:
Поскольку publish() не поддерживает Promise, таймаут
обычно реализуется вручную.
Пример базового таймаута:
function publishWithTimeout(client, message, timeout = 5000) {
return new Promise((resolve, reject) => {
if (!client.connected) {
reject(new Error('Нет подключения'));
return;
}
const timer = setTimeout(() => {
reject(new Error('Таймаут отправки'));
}, timeout);
try {
client.publish(message);
clearTimeout(timer);
resolve();
} catch (error) {
clearTimeout(timer);
reject(error);
}
});
}
Этот механизм проверяет только успешность вызова
publish().
Он не гарантирует:
Поэтому для полноценного таймаута требуется подтверждение от сервера.
Надёжный способ контроля отправки — ожидание подтверждения.
Схема работы:
const requestId = crypto.randomUUID();
client.publish({
destination: '/app/send',
headers: {
'request-id': requestId
},
body: JSON.stringify({
text: 'Привет'
})
});
const pendingRequests = new Map();
client.subscribe('/topic/ack', message => {
const data = JSON.parse(message.body);
const callback = pendingRequests.get(data.requestId);
if (callback) {
callback();
pendingRequests.delete(data.requestId);
}
});
function sendWithAckTimeout(payload, timeout = 5000) {
return new Promise((resolve, reject) => {
const requestId = crypto.randomUUID();
const timer = setTimeout(() => {
pendingRequests.delete(requestId);
reject(new Error('ACK timeout'));
}, timeout);
pendingRequests.set(requestId, () => {
clearTimeout(timer);
resolve();
});
client.publish({
destination: '/app/send',
headers: {
'request-id': requestId
},
body: JSON.stringify(payload)
});
});
}
При большом количестве сообщений важно отслеживать зависшие операции.
Типичная структура:
const pendingMessages = new Map();
Каждое сообщение хранит:
Пример периодической проверки:
setInterval(() => {
const now = Date.now();
for (const [id, message] of pendingMessages.entries()) {
if (now - message.createdAt > 10000) {
console.error('Сообщение просрочено:', id);
pendingMessages.delete(id);
}
}
}, 1000);
В связке STOMP.js и RabbitMQ возможны ситуации, когда:
В таких условиях отправка может формально завершиться успешно, хотя сообщение будет потеряно.
Поэтому в RabbitMQ часто применяются:
Параметр reconnectDelay влияет на скорость
восстановления после таймаута.
const client = new Client({
brokerURL: 'ws://localhost:15674/ws',
reconnectDelay: 5000
});
После обнаружения проблемы:
На практике обычно используются:
const client = new Client({
brokerURL: 'ws://localhost:15674/ws',
heartbeatIncoming: 4000,
heartbeatOutgoing: 4000,
reconnectDelay: 3000
});
Такая конфигурация:
При временных сетевых ошибках используется retry-механизм.
Пример:
async function sendWithRetry(payload, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
await sendWithAckTimeout(payload);
return;
} catch (error) {
console.error('Ошибка отправки:', error);
if (i === retries - 1) {
throw error;
}
}
}
}
Фиксированная задержка создаёт нагрузку на брокер. Более устойчивым считается exponential backoff.
function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function sendWithBackoff(payload) {
let delay = 1000;
for (let i = 0; i < 5; i++) {
try {
await sendWithAckTimeout(payload);
return;
} catch (error) {
await wait(delay);
delay *= 2;
}
}
throw new Error('Не удалось отправить сообщение');
}
В offline-first архитектуре сообщения временно сохраняются локально.
Алгоритм:
Пример очереди:
const offlineQueue = [];
Добавление:
offlineQueue.push({
body: payload,
createdAt: Date.now()
});
Повторная отправка:
async function flushQueue() {
while (offlineQueue.length > 0) {
const item = offlineQueue.shift();
await sendWithAckTimeout(item.body);
}
}
Отсутствие ограничений может привести к:
Поэтому всегда ограничиваются:
Типичная ошибка:
setTimeout(() => {
reject(new Error());
}, 5000);
Без очистки:
clearTimeout(timer);
таймер продолжит существовать даже после успешной отправки.
При тысячах сообщений это становится серьёзной проблемой.
В крупных приложениях создаётся отдельный сервис управления отправкой.
Пример структуры:
class MessageTimeoutManager {
constructor() {
this.pending = new Map();
}
register(id, resolve, reject, timeout) {
const timer = setTimeout(() => {
this.pending.delete(id);
reject(new Error('Timeout'));
}, timeout);
this.pending.set(id, {
timer,
resolve,
reject
});
}
complete(id) {
const item = this.pending.get(id);
if (!item) {
return;
}
clearTimeout(item.timer);
item.resolve();
this.pending.delete(id);
}
}
В системах с большим количеством сообщений учитываются:
Неверно выбранный timeout способен:
2–5 секунд
1–3 секунды
10–30 секунд
15–60 секунд
Для анализа проблем обычно логируются:
async function monitoredSend(payload) {
const started = Date.now();
try {
await sendWithAckTimeout(payload);
console.log('Отправлено за', Date.now() - started, 'ms');
} catch (error) {
console.error('Таймаут отправки', {
duration: Date.now() - started,
error
});
throw error;
}
}
В production-приложениях таймауты отправки обычно строятся как комбинация нескольких механизмов: