@onlineapps/mq-client-core 1.0.79 → 1.0.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/config/queueConfig.js +68 -0
- package/src/config.js +3 -0
- package/src/defaults.js +3 -0
- package/src/monitoring-publish.js +5 -1
- package/src/transports/rabbitmqClient.js +82 -29
package/package.json
CHANGED
|
@@ -249,6 +249,7 @@ module.exports = {
|
|
|
249
249
|
monitoring: {
|
|
250
250
|
/**
|
|
251
251
|
* monitoring.workflow - Unified workflow lifecycle monitoring
|
|
252
|
+
* Bound to monitoring.workflow.fanout exchange (fanout).
|
|
252
253
|
* Delivery Dispatcher publishes 'completed'/'failed' events after processing workflow.completed/workflow.failed
|
|
253
254
|
* Business services publish 'progress' events during workflow step processing
|
|
254
255
|
* Message format includes event_type: 'completed' | 'failed' | 'progress'
|
|
@@ -277,6 +278,73 @@ module.exports = {
|
|
|
277
278
|
'x-message-ttl': 600000, // 10 minutes TTL (longer for service tracking)
|
|
278
279
|
'x-max-length': 20000
|
|
279
280
|
}
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* monitoring.workflow.fanout - Fanout exchange for workflow events
|
|
285
|
+
* Publishers send to this exchange; monitoring.workflow + delivery.workflow.events both receive copies.
|
|
286
|
+
*/
|
|
287
|
+
'workflow.fanout': {
|
|
288
|
+
type: 'exchange',
|
|
289
|
+
exchangeType: 'fanout',
|
|
290
|
+
durable: true
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Delivery endpoint event queue configurations
|
|
296
|
+
* These are queues consumed by api_delivery_endpoint for real-time WS push.
|
|
297
|
+
*/
|
|
298
|
+
deliveryEvents: {
|
|
299
|
+
/**
|
|
300
|
+
* delivery.workflow.events - Workflow progress events for WS clients
|
|
301
|
+
* Bound to monitoring.workflow.fanout exchange.
|
|
302
|
+
*/
|
|
303
|
+
'workflow.events': {
|
|
304
|
+
durable: true,
|
|
305
|
+
arguments: {
|
|
306
|
+
'x-message-ttl': 60000, // 60s TTL - real-time events, stale ones are useless
|
|
307
|
+
'x-max-length': 10000
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* delivery.health.events - Infrastructure health events for WS clients
|
|
313
|
+
* Bound to infrastructure.health.events exchange.
|
|
314
|
+
*/
|
|
315
|
+
'health.events': {
|
|
316
|
+
durable: true,
|
|
317
|
+
arguments: {
|
|
318
|
+
'x-message-ttl': 60000,
|
|
319
|
+
'x-max-length': 5000
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* delivery.alert.events - Alert events for WS clients
|
|
325
|
+
* Bound to alert.events.fanout exchange.
|
|
326
|
+
*/
|
|
327
|
+
'alert.events': {
|
|
328
|
+
durable: true,
|
|
329
|
+
arguments: {
|
|
330
|
+
'x-message-ttl': 300000, // 5 minutes - alerts are more important
|
|
331
|
+
'x-max-length': 5000
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Alert exchange configuration
|
|
338
|
+
*/
|
|
339
|
+
alerts: {
|
|
340
|
+
/**
|
|
341
|
+
* alert.events.fanout - Fanout exchange for alert events
|
|
342
|
+
* Monitoring watchdogs publish alerts here; delivery.alert.events consumes copies.
|
|
343
|
+
*/
|
|
344
|
+
'events.fanout': {
|
|
345
|
+
type: 'exchange',
|
|
346
|
+
exchangeType: 'fanout',
|
|
347
|
+
durable: true
|
|
280
348
|
}
|
|
281
349
|
},
|
|
282
350
|
|
package/src/config.js
CHANGED
package/src/defaults.js
CHANGED
|
@@ -103,7 +103,11 @@ async function publishToMonitoringResilient(mqClient, queueName, message, logger
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
try {
|
|
106
|
-
|
|
106
|
+
const WORKFLOW_FANOUT = 'monitoring.workflow.fanout';
|
|
107
|
+
const publishOptions = queueName === 'monitoring.workflow'
|
|
108
|
+
? { exchange: WORKFLOW_FANOUT, exchangeType: 'fanout', routingKey: '' }
|
|
109
|
+
: {};
|
|
110
|
+
await mqClient.publish(queueName, message, publishOptions);
|
|
107
111
|
|
|
108
112
|
if (logger && logger.debug) {
|
|
109
113
|
logger.debug(`Published to ${queueName}`, {
|
|
@@ -1258,7 +1258,8 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1258
1258
|
// Track operation for debugging
|
|
1259
1259
|
this._trackChannelOperation(this._channel, `publish to ${queue}`);
|
|
1260
1260
|
|
|
1261
|
-
const exchange = this._config.exchange || '';
|
|
1261
|
+
const exchange = options.exchange || this._config.exchange || '';
|
|
1262
|
+
const exchangeType = options.exchangeType || 'direct';
|
|
1262
1263
|
const routingKey = options.routingKey || queue;
|
|
1263
1264
|
const persistent = options.persistent !== undefined ? options.persistent : this._config.durable;
|
|
1264
1265
|
const headers = options.headers || {};
|
|
@@ -1473,7 +1474,7 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1473
1474
|
} else {
|
|
1474
1475
|
// If exchange is specified, assert exchange and publish to it
|
|
1475
1476
|
// Channel is guaranteed to be open (ensured above)
|
|
1476
|
-
await this._channel.assertExchange(exchange,
|
|
1477
|
+
await this._channel.assertExchange(exchange, exchangeType, { durable: this._config.durable });
|
|
1477
1478
|
|
|
1478
1479
|
// Use callback-based confirmation - kanály jsou spolehlivé
|
|
1479
1480
|
const confirmPromise = new Promise((resolve, reject) => {
|
|
@@ -1552,16 +1553,17 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1552
1553
|
|
|
1553
1554
|
if (queueConfig) {
|
|
1554
1555
|
if (isInfraQueue) {
|
|
1555
|
-
// Infrastructure queue - use central config
|
|
1556
|
+
// Infrastructure queue - use central config for expected arguments,
|
|
1557
|
+
// but DO NOT create it here. Ownership rule: infra queues are created by their owning infra service.
|
|
1556
1558
|
try {
|
|
1557
1559
|
const infraConfig = queueConfig.getInfrastructureQueueConfig(queue);
|
|
1558
1560
|
queueOptions = {
|
|
1559
1561
|
durable: infraConfig.durable !== false,
|
|
1560
1562
|
arguments: { ...infraConfig.arguments }
|
|
1561
1563
|
};
|
|
1562
|
-
console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER]
|
|
1564
|
+
console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER] Checking infrastructure queue ${queue} exists (no auto-create)`);
|
|
1563
1565
|
} catch (configErr) {
|
|
1564
|
-
console.warn(`[RabbitMQClient] [mq-client-core] [CONSUMER] Infrastructure queue config not found for ${queue},
|
|
1566
|
+
console.warn(`[RabbitMQClient] [mq-client-core] [CONSUMER] Infrastructure queue config not found for ${queue}, will still require it to exist:`, configErr.message);
|
|
1565
1567
|
}
|
|
1566
1568
|
} else if (isBusinessQueue) {
|
|
1567
1569
|
// Business queue - use central config
|
|
@@ -1600,28 +1602,45 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1600
1602
|
}
|
|
1601
1603
|
}
|
|
1602
1604
|
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1605
|
+
// Ensure queue channel is available
|
|
1606
|
+
await this._ensureQueueChannel();
|
|
1607
|
+
if (!this._queueChannel) {
|
|
1608
|
+
throw new Error('Queue channel is not available (connection may be closed)');
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
if (isInfraQueue) {
|
|
1612
|
+
// IMPORTANT: Do NOT auto-create infrastructure queues in consumers.
|
|
1613
|
+
// If missing, fail-fast. The owning infra service must recreate on startup.
|
|
1614
|
+
try {
|
|
1615
|
+
this._trackChannelOperation(this._queueChannel, `checkQueue ${queue}`);
|
|
1616
|
+
await this._queueChannel.checkQueue(queue);
|
|
1617
|
+
console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER] ✓ Infrastructure queue ${queue} exists (consumer will proceed)`);
|
|
1618
|
+
} catch (checkErr) {
|
|
1619
|
+
if (checkErr.code === 404) {
|
|
1620
|
+
throw new Error(
|
|
1621
|
+
`Infrastructure queue ${queue} is missing. ` +
|
|
1622
|
+
'Ownership rule: infrastructure queues must be created by their owning infrastructure service via initInfrastructureQueues().'
|
|
1623
|
+
);
|
|
1624
|
+
}
|
|
1625
|
+
throw checkErr;
|
|
1610
1626
|
}
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1627
|
+
} else {
|
|
1628
|
+
// Business queue (or unknown) - assert with canonical parameters to prevent 406 drift.
|
|
1629
|
+
try {
|
|
1630
|
+
console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER] About to call assertQueue(${queue}, ${JSON.stringify(queueOptions)})`);
|
|
1631
|
+
this._trackChannelOperation(this._queueChannel, `assertQueue ${queue}`);
|
|
1632
|
+
await this._queueChannel.assertQueue(queue, queueOptions);
|
|
1633
|
+
console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER] ✓ Queue ${queue} asserted successfully`);
|
|
1634
|
+
} catch (assertErr) {
|
|
1635
|
+
// If queue exists with different arguments (406), this is a CRITICAL ERROR
|
|
1636
|
+
// We should NOT proceed - the root cause must be fixed
|
|
1637
|
+
if (assertErr.code === 406) {
|
|
1638
|
+
console.error(`[RabbitMQClient] [mq-client-core] [CONSUMER] ✗ CRITICAL: Queue ${queue} exists with different arguments!`);
|
|
1639
|
+
console.error(`[RabbitMQClient] [mq-client-core] [CONSUMER] Error:`, assertErr.message);
|
|
1640
|
+
console.error(`[RabbitMQClient] [mq-client-core] [CONSUMER] Expected options:`, JSON.stringify(queueOptions, null, 2));
|
|
1641
|
+
console.error(`[RabbitMQClient] [mq-client-core] [CONSUMER] This means assertQueue() was called without parameters somewhere else. Root cause must be fixed!`);
|
|
1642
|
+
throw new Error(`Cannot assertQueue ${queue}: queue exists with different arguments. Root cause: assertQueue() was called without parameters. Fix the root cause instead of proceeding.`);
|
|
1643
|
+
}
|
|
1625
1644
|
// Other error - rethrow
|
|
1626
1645
|
throw assertErr;
|
|
1627
1646
|
}
|
|
@@ -1663,6 +1682,14 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1663
1682
|
if (msg === null) {
|
|
1664
1683
|
return; // Consumer cancellation
|
|
1665
1684
|
}
|
|
1685
|
+
|
|
1686
|
+
// IMPORTANT:
|
|
1687
|
+
// `msg` MUST be acked/nacked on the SAME channel instance that delivered it.
|
|
1688
|
+
// During reconnects, `this._consumerChannel` may be replaced while a handler is running.
|
|
1689
|
+
// If we ack/nack a message using a different (new) channel, RabbitMQ will close the channel with:
|
|
1690
|
+
// 406 (PRECONDITION-FAILED) unknown delivery tag
|
|
1691
|
+
// which cascades into "Channel closed" errors across the service.
|
|
1692
|
+
const channelForMsg = this._consumerChannel;
|
|
1666
1693
|
|
|
1667
1694
|
// Structured log: message received
|
|
1668
1695
|
const msgHeaders = msg.properties?.headers || {};
|
|
@@ -1690,7 +1717,23 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1690
1717
|
await onMessage(msg);
|
|
1691
1718
|
// Acknowledge message after successful processing
|
|
1692
1719
|
if (!noAck) {
|
|
1693
|
-
|
|
1720
|
+
// Only ack on the original channel instance; if it's gone/closed, do not attempt ack.
|
|
1721
|
+
if (!channelForMsg || channelForMsg.closed) {
|
|
1722
|
+
console.warn('[RabbitMQClient] [mq-client-core] [CONSUMER] Cannot ack - consumer channel is closed/recreated (message will be requeued by broker)');
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
try {
|
|
1726
|
+
channelForMsg.ack(msg);
|
|
1727
|
+
} catch (ackErr) {
|
|
1728
|
+
// If the channel was closed/recreated during processing, delivery tag becomes invalid.
|
|
1729
|
+
// Do NOT send further acks that would close the channel with 406.
|
|
1730
|
+
const m = ackErr && ackErr.message ? ackErr.message : '';
|
|
1731
|
+
if (m.includes('unknown delivery tag') || m.includes('PRECONDITION_FAILED') || m.includes('Channel closed')) {
|
|
1732
|
+
console.warn('[RabbitMQClient] [mq-client-core] [CONSUMER] Ack failed due to invalid delivery tag / closed channel (message delivery will be handled by broker)', { error: m });
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
throw ackErr;
|
|
1736
|
+
}
|
|
1694
1737
|
// Structured log: message acknowledged
|
|
1695
1738
|
this._log.output(wfId, 'MSG_ACKED', {
|
|
1696
1739
|
handler: 'consume',
|
|
@@ -1708,9 +1751,9 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1708
1751
|
} catch (handlerErr) {
|
|
1709
1752
|
// Negative acknowledge and requeue by default
|
|
1710
1753
|
// Check if channel is still valid before nacking
|
|
1711
|
-
if (
|
|
1754
|
+
if (channelForMsg && !channelForMsg.closed) {
|
|
1712
1755
|
try {
|
|
1713
|
-
|
|
1756
|
+
channelForMsg.nack(msg, false, true);
|
|
1714
1757
|
} catch (nackErr) {
|
|
1715
1758
|
// Channel may have closed during nack - ignore
|
|
1716
1759
|
console.warn(`[RabbitMQClient] [mq-client-core] Failed to nack message (channel may be closed): ${nackErr.message}`);
|
|
@@ -2182,6 +2225,11 @@ class RabbitMQClient extends EventEmitter {
|
|
|
2182
2225
|
try {
|
|
2183
2226
|
this._consumerChannel.ack(msg);
|
|
2184
2227
|
} catch (err) {
|
|
2228
|
+
const m = err && err.message ? err.message : '';
|
|
2229
|
+
if (m.includes('unknown delivery tag') || m.includes('PRECONDITION_FAILED') || m.includes('Channel closed')) {
|
|
2230
|
+
console.warn('[RabbitMQClient] [mq-client-core] Cannot ack - consumer channel is closed/recreated (delivery tag invalid)', { error: m });
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2185
2233
|
this.emit('error', err);
|
|
2186
2234
|
throw err;
|
|
2187
2235
|
}
|
|
@@ -2200,6 +2248,11 @@ class RabbitMQClient extends EventEmitter {
|
|
|
2200
2248
|
try {
|
|
2201
2249
|
this._consumerChannel.nack(msg, false, requeue);
|
|
2202
2250
|
} catch (err) {
|
|
2251
|
+
const m = err && err.message ? err.message : '';
|
|
2252
|
+
if (m.includes('unknown delivery tag') || m.includes('PRECONDITION_FAILED') || m.includes('Channel closed')) {
|
|
2253
|
+
console.warn('[RabbitMQClient] [mq-client-core] Cannot nack - consumer channel is closed/recreated (delivery tag invalid)', { error: m });
|
|
2254
|
+
return;
|
|
2255
|
+
}
|
|
2203
2256
|
this.emit('error', err);
|
|
2204
2257
|
throw err;
|
|
2205
2258
|
}
|