@onlineapps/mq-client-core 1.0.78 → 1.0.80
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.js +3 -0
- package/src/defaults.js +3 -0
- package/src/transports/rabbitmqClient.js +140 -42
package/package.json
CHANGED
package/src/config.js
CHANGED
package/src/defaults.js
CHANGED
|
@@ -80,6 +80,7 @@ class RabbitMQClient extends EventEmitter {
|
|
|
80
80
|
this._reconnecting = false;
|
|
81
81
|
this._disconnecting = false;
|
|
82
82
|
this._reconnectAttempts = 0;
|
|
83
|
+
this._activeTimers = new Set();
|
|
83
84
|
this._maxReconnectAttempts = this._config.maxReconnectAttempts || 10; // Max 10 attempts
|
|
84
85
|
this._reconnectBaseDelay = this._config.reconnectBaseDelay || 1000; // Start with 1 second
|
|
85
86
|
this._reconnectMaxDelay = this._config.reconnectMaxDelay || 30000; // Max 30 seconds
|
|
@@ -418,6 +419,41 @@ class RabbitMQClient extends EventEmitter {
|
|
|
418
419
|
* @returns {Promise<void>}
|
|
419
420
|
* @throws {Error} If connection or channel creation fails.
|
|
420
421
|
*/
|
|
422
|
+
/**
|
|
423
|
+
* Helper to set a trackable timeout
|
|
424
|
+
* @private
|
|
425
|
+
*/
|
|
426
|
+
_setTimeout(callback, ms) {
|
|
427
|
+
const timer = setTimeout(() => {
|
|
428
|
+
this._activeTimers.delete(timer);
|
|
429
|
+
callback();
|
|
430
|
+
}, ms);
|
|
431
|
+
this._activeTimers.add(timer);
|
|
432
|
+
return timer;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Helper to clear a trackable timeout
|
|
437
|
+
* @private
|
|
438
|
+
*/
|
|
439
|
+
_clearTimeout(timer) {
|
|
440
|
+
if (timer) {
|
|
441
|
+
clearTimeout(timer);
|
|
442
|
+
this._activeTimers.delete(timer);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Clear all active timers
|
|
448
|
+
* @private
|
|
449
|
+
*/
|
|
450
|
+
_clearAllTimers() {
|
|
451
|
+
for (const timer of this._activeTimers) {
|
|
452
|
+
clearTimeout(timer);
|
|
453
|
+
}
|
|
454
|
+
this._activeTimers.clear();
|
|
455
|
+
}
|
|
456
|
+
|
|
421
457
|
async connect() {
|
|
422
458
|
let connectTimeoutTimer = null;
|
|
423
459
|
try {
|
|
@@ -435,7 +471,7 @@ class RabbitMQClient extends EventEmitter {
|
|
|
435
471
|
|
|
436
472
|
const connectPromise = amqp.connect(...connectArgs);
|
|
437
473
|
const timeoutPromise = new Promise((_, reject) => {
|
|
438
|
-
connectTimeoutTimer =
|
|
474
|
+
connectTimeoutTimer = this._setTimeout(() => reject(new Error('Connection timeout after 10 seconds')), 10000);
|
|
439
475
|
if (connectTimeoutTimer && typeof connectTimeoutTimer.unref === 'function') {
|
|
440
476
|
connectTimeoutTimer.unref();
|
|
441
477
|
}
|
|
@@ -443,7 +479,7 @@ class RabbitMQClient extends EventEmitter {
|
|
|
443
479
|
console.log('[RabbitMQClient] Starting connection race...');
|
|
444
480
|
this._connection = await Promise.race([connectPromise, timeoutPromise]);
|
|
445
481
|
if (connectTimeoutTimer) {
|
|
446
|
-
|
|
482
|
+
this._clearTimeout(connectTimeoutTimer);
|
|
447
483
|
connectTimeoutTimer = null;
|
|
448
484
|
}
|
|
449
485
|
console.log('[RabbitMQClient] Connection established');
|
|
@@ -562,18 +598,18 @@ class RabbitMQClient extends EventEmitter {
|
|
|
562
598
|
|
|
563
599
|
// Wait for 'reconnected' event
|
|
564
600
|
return new Promise((resolve, reject) => {
|
|
565
|
-
const timeout =
|
|
601
|
+
const timeout = this._setTimeout(() => {
|
|
566
602
|
this.removeListener('reconnected', onReconnected);
|
|
567
603
|
this.removeListener('error', onError);
|
|
568
604
|
reject(new Error(`Reconnection timeout after ${this._reconnectWaitTimeout}ms`));
|
|
569
605
|
}, this._reconnectWaitTimeout);
|
|
570
606
|
|
|
571
607
|
const onReconnected = () => {
|
|
572
|
-
|
|
608
|
+
this._clearTimeout(timeout);
|
|
573
609
|
this.removeListener('reconnected', onReconnected);
|
|
574
610
|
this.removeListener('error', onError);
|
|
575
611
|
// Wait a bit for channels to be fully recreated
|
|
576
|
-
|
|
612
|
+
this._setTimeout(resolve, 500);
|
|
577
613
|
};
|
|
578
614
|
|
|
579
615
|
const onError = (error) => {
|
|
@@ -581,7 +617,7 @@ class RabbitMQClient extends EventEmitter {
|
|
|
581
617
|
if (error.message && error.message.includes('Connection closed unexpectedly')) {
|
|
582
618
|
return; // Expected during reconnection
|
|
583
619
|
}
|
|
584
|
-
|
|
620
|
+
this._clearTimeout(timeout);
|
|
585
621
|
this.removeListener('reconnected', onReconnected);
|
|
586
622
|
this.removeListener('error', onError);
|
|
587
623
|
reject(error);
|
|
@@ -1052,6 +1088,10 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1052
1088
|
|
|
1053
1089
|
async disconnect() {
|
|
1054
1090
|
this._disconnecting = true;
|
|
1091
|
+
|
|
1092
|
+
// Clear all active timers (reconnection, waits, etc.)
|
|
1093
|
+
this._clearAllTimers();
|
|
1094
|
+
|
|
1055
1095
|
// Stop health monitoring
|
|
1056
1096
|
this._stopHealthMonitoring();
|
|
1057
1097
|
|
|
@@ -1324,13 +1364,13 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1324
1364
|
// Use callback-based confirmation - kanály jsou spolehlivé, takže callback vždy dorazí
|
|
1325
1365
|
const confirmPromise = new Promise((resolve, reject) => {
|
|
1326
1366
|
// Set timeout for publish confirmation (configurable)
|
|
1327
|
-
const timeout =
|
|
1367
|
+
const timeout = this._setTimeout(() => {
|
|
1328
1368
|
reject(new Error(`Publish confirmation timeout for queue "${queue}" after ${this._publishConfirmationTimeout}ms`));
|
|
1329
1369
|
}, this._publishConfirmationTimeout);
|
|
1330
1370
|
|
|
1331
1371
|
// Check if channel is still valid before sending
|
|
1332
1372
|
if (!this._channel || this._channel.closed) {
|
|
1333
|
-
|
|
1373
|
+
this._clearTimeout(timeout);
|
|
1334
1374
|
reject(new Error(`Cannot publish: channel is closed for queue "${queue}"`));
|
|
1335
1375
|
return;
|
|
1336
1376
|
}
|
|
@@ -1344,7 +1384,7 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1344
1384
|
try {
|
|
1345
1385
|
originalChannel.sendToQueue(queue, buffer, { persistent, headers }, (err, ok) => {
|
|
1346
1386
|
callbackInvoked = true;
|
|
1347
|
-
|
|
1387
|
+
this._clearTimeout(timeout);
|
|
1348
1388
|
|
|
1349
1389
|
// CRITICAL: Check if channel was closed or recreated during publish
|
|
1350
1390
|
// If channel was recreated, the delivery tag is invalid - ignore this callback
|
|
@@ -1398,11 +1438,11 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1398
1438
|
});
|
|
1399
1439
|
|
|
1400
1440
|
// Set a safety timeout - if callback wasn't invoked and channel closed, retry
|
|
1401
|
-
|
|
1441
|
+
this._setTimeout(() => {
|
|
1402
1442
|
if (!callbackInvoked && (!this._channel || this._channel.closed || this._channel !== originalChannel)) {
|
|
1403
1443
|
console.warn(`[RabbitMQClient] [mq-client-core] [PUBLISH] Callback timeout and channel closed for queue "${queue}", will retry after reconnection`);
|
|
1404
1444
|
if (this._reconnecting) {
|
|
1405
|
-
|
|
1445
|
+
this._clearTimeout(timeout);
|
|
1406
1446
|
this._waitForReconnection().then(() => {
|
|
1407
1447
|
return this.publish(queue, buffer, options);
|
|
1408
1448
|
}).then(resolve).catch(reject);
|
|
@@ -1438,12 +1478,12 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1438
1478
|
// Use callback-based confirmation - kanály jsou spolehlivé
|
|
1439
1479
|
const confirmPromise = new Promise((resolve, reject) => {
|
|
1440
1480
|
// Set timeout for exchange publish confirmation
|
|
1441
|
-
const timeout =
|
|
1481
|
+
const timeout = this._setTimeout(() => {
|
|
1442
1482
|
reject(new Error(`Exchange publish confirmation timeout for exchange "${exchange}" after ${this._publishConfirmationTimeout}ms`));
|
|
1443
1483
|
}, this._publishConfirmationTimeout);
|
|
1444
1484
|
|
|
1445
1485
|
this._channel.publish(exchange, routingKey, buffer, { persistent, headers }, (err, ok) => {
|
|
1446
|
-
|
|
1486
|
+
this._clearTimeout(timeout);
|
|
1447
1487
|
if (err) {
|
|
1448
1488
|
console.error(`[RabbitMQClient] [mq-client-core] [PUBLISH] Exchange publish callback error:`, err.message);
|
|
1449
1489
|
reject(err);
|
|
@@ -1512,16 +1552,17 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1512
1552
|
|
|
1513
1553
|
if (queueConfig) {
|
|
1514
1554
|
if (isInfraQueue) {
|
|
1515
|
-
// Infrastructure queue - use central config
|
|
1555
|
+
// Infrastructure queue - use central config for expected arguments,
|
|
1556
|
+
// but DO NOT create it here. Ownership rule: infra queues are created by their owning infra service.
|
|
1516
1557
|
try {
|
|
1517
1558
|
const infraConfig = queueConfig.getInfrastructureQueueConfig(queue);
|
|
1518
1559
|
queueOptions = {
|
|
1519
1560
|
durable: infraConfig.durable !== false,
|
|
1520
1561
|
arguments: { ...infraConfig.arguments }
|
|
1521
1562
|
};
|
|
1522
|
-
console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER]
|
|
1563
|
+
console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER] Checking infrastructure queue ${queue} exists (no auto-create)`);
|
|
1523
1564
|
} catch (configErr) {
|
|
1524
|
-
console.warn(`[RabbitMQClient] [mq-client-core] [CONSUMER] Infrastructure queue config not found for ${queue},
|
|
1565
|
+
console.warn(`[RabbitMQClient] [mq-client-core] [CONSUMER] Infrastructure queue config not found for ${queue}, will still require it to exist:`, configErr.message);
|
|
1525
1566
|
}
|
|
1526
1567
|
} else if (isBusinessQueue) {
|
|
1527
1568
|
// Business queue - use central config
|
|
@@ -1560,28 +1601,45 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1560
1601
|
}
|
|
1561
1602
|
}
|
|
1562
1603
|
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1604
|
+
// Ensure queue channel is available
|
|
1605
|
+
await this._ensureQueueChannel();
|
|
1606
|
+
if (!this._queueChannel) {
|
|
1607
|
+
throw new Error('Queue channel is not available (connection may be closed)');
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
if (isInfraQueue) {
|
|
1611
|
+
// IMPORTANT: Do NOT auto-create infrastructure queues in consumers.
|
|
1612
|
+
// If missing, fail-fast. The owning infra service must recreate on startup.
|
|
1613
|
+
try {
|
|
1614
|
+
this._trackChannelOperation(this._queueChannel, `checkQueue ${queue}`);
|
|
1615
|
+
await this._queueChannel.checkQueue(queue);
|
|
1616
|
+
console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER] ✓ Infrastructure queue ${queue} exists (consumer will proceed)`);
|
|
1617
|
+
} catch (checkErr) {
|
|
1618
|
+
if (checkErr.code === 404) {
|
|
1619
|
+
throw new Error(
|
|
1620
|
+
`Infrastructure queue ${queue} is missing. ` +
|
|
1621
|
+
'Ownership rule: infrastructure queues must be created by their owning infrastructure service via initInfrastructureQueues().'
|
|
1622
|
+
);
|
|
1623
|
+
}
|
|
1624
|
+
throw checkErr;
|
|
1570
1625
|
}
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1626
|
+
} else {
|
|
1627
|
+
// Business queue (or unknown) - assert with canonical parameters to prevent 406 drift.
|
|
1628
|
+
try {
|
|
1629
|
+
console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER] About to call assertQueue(${queue}, ${JSON.stringify(queueOptions)})`);
|
|
1630
|
+
this._trackChannelOperation(this._queueChannel, `assertQueue ${queue}`);
|
|
1631
|
+
await this._queueChannel.assertQueue(queue, queueOptions);
|
|
1632
|
+
console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER] ✓ Queue ${queue} asserted successfully`);
|
|
1633
|
+
} catch (assertErr) {
|
|
1634
|
+
// If queue exists with different arguments (406), this is a CRITICAL ERROR
|
|
1635
|
+
// We should NOT proceed - the root cause must be fixed
|
|
1636
|
+
if (assertErr.code === 406) {
|
|
1637
|
+
console.error(`[RabbitMQClient] [mq-client-core] [CONSUMER] ✗ CRITICAL: Queue ${queue} exists with different arguments!`);
|
|
1638
|
+
console.error(`[RabbitMQClient] [mq-client-core] [CONSUMER] Error:`, assertErr.message);
|
|
1639
|
+
console.error(`[RabbitMQClient] [mq-client-core] [CONSUMER] Expected options:`, JSON.stringify(queueOptions, null, 2));
|
|
1640
|
+
console.error(`[RabbitMQClient] [mq-client-core] [CONSUMER] This means assertQueue() was called without parameters somewhere else. Root cause must be fixed!`);
|
|
1641
|
+
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.`);
|
|
1642
|
+
}
|
|
1585
1643
|
// Other error - rethrow
|
|
1586
1644
|
throw assertErr;
|
|
1587
1645
|
}
|
|
@@ -1623,6 +1681,14 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1623
1681
|
if (msg === null) {
|
|
1624
1682
|
return; // Consumer cancellation
|
|
1625
1683
|
}
|
|
1684
|
+
|
|
1685
|
+
// IMPORTANT:
|
|
1686
|
+
// `msg` MUST be acked/nacked on the SAME channel instance that delivered it.
|
|
1687
|
+
// During reconnects, `this._consumerChannel` may be replaced while a handler is running.
|
|
1688
|
+
// If we ack/nack a message using a different (new) channel, RabbitMQ will close the channel with:
|
|
1689
|
+
// 406 (PRECONDITION-FAILED) unknown delivery tag
|
|
1690
|
+
// which cascades into "Channel closed" errors across the service.
|
|
1691
|
+
const channelForMsg = this._consumerChannel;
|
|
1626
1692
|
|
|
1627
1693
|
// Structured log: message received
|
|
1628
1694
|
const msgHeaders = msg.properties?.headers || {};
|
|
@@ -1650,7 +1716,23 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1650
1716
|
await onMessage(msg);
|
|
1651
1717
|
// Acknowledge message after successful processing
|
|
1652
1718
|
if (!noAck) {
|
|
1653
|
-
|
|
1719
|
+
// Only ack on the original channel instance; if it's gone/closed, do not attempt ack.
|
|
1720
|
+
if (!channelForMsg || channelForMsg.closed) {
|
|
1721
|
+
console.warn('[RabbitMQClient] [mq-client-core] [CONSUMER] Cannot ack - consumer channel is closed/recreated (message will be requeued by broker)');
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
try {
|
|
1725
|
+
channelForMsg.ack(msg);
|
|
1726
|
+
} catch (ackErr) {
|
|
1727
|
+
// If the channel was closed/recreated during processing, delivery tag becomes invalid.
|
|
1728
|
+
// Do NOT send further acks that would close the channel with 406.
|
|
1729
|
+
const m = ackErr && ackErr.message ? ackErr.message : '';
|
|
1730
|
+
if (m.includes('unknown delivery tag') || m.includes('PRECONDITION_FAILED') || m.includes('Channel closed')) {
|
|
1731
|
+
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 });
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
throw ackErr;
|
|
1735
|
+
}
|
|
1654
1736
|
// Structured log: message acknowledged
|
|
1655
1737
|
this._log.output(wfId, 'MSG_ACKED', {
|
|
1656
1738
|
handler: 'consume',
|
|
@@ -1668,9 +1750,9 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1668
1750
|
} catch (handlerErr) {
|
|
1669
1751
|
// Negative acknowledge and requeue by default
|
|
1670
1752
|
// Check if channel is still valid before nacking
|
|
1671
|
-
if (
|
|
1753
|
+
if (channelForMsg && !channelForMsg.closed) {
|
|
1672
1754
|
try {
|
|
1673
|
-
|
|
1755
|
+
channelForMsg.nack(msg, false, true);
|
|
1674
1756
|
} catch (nackErr) {
|
|
1675
1757
|
// Channel may have closed during nack - ignore
|
|
1676
1758
|
console.warn(`[RabbitMQClient] [mq-client-core] Failed to nack message (channel may be closed): ${nackErr.message}`);
|
|
@@ -1731,7 +1813,13 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1731
1813
|
);
|
|
1732
1814
|
|
|
1733
1815
|
console.log(`[RabbitMQClient] Waiting ${delay}ms before reconnection attempt ${this._reconnectAttempts + 1}...`);
|
|
1734
|
-
await new Promise(resolve =>
|
|
1816
|
+
await new Promise(resolve => this._setTimeout(resolve, delay));
|
|
1817
|
+
|
|
1818
|
+
// Check if we started disconnecting during the wait
|
|
1819
|
+
if (this._disconnecting) {
|
|
1820
|
+
console.log('[RabbitMQClient] Disconnecting during reconnection wait, aborting');
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1735
1823
|
|
|
1736
1824
|
// Attempt to reconnect
|
|
1737
1825
|
console.log(`[RabbitMQClient] Reconnection attempt ${this._reconnectAttempts + 1}...`);
|
|
@@ -1760,7 +1848,7 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1760
1848
|
|
|
1761
1849
|
const connectPromise = amqp.connect(...connectArgs);
|
|
1762
1850
|
const timeoutPromise = new Promise((_, reject) => {
|
|
1763
|
-
|
|
1851
|
+
this._setTimeout(() => reject(new Error('Connection timeout after 10 seconds')), 10000);
|
|
1764
1852
|
});
|
|
1765
1853
|
|
|
1766
1854
|
this._connection = await Promise.race([connectPromise, timeoutPromise]);
|
|
@@ -2136,6 +2224,11 @@ class RabbitMQClient extends EventEmitter {
|
|
|
2136
2224
|
try {
|
|
2137
2225
|
this._consumerChannel.ack(msg);
|
|
2138
2226
|
} catch (err) {
|
|
2227
|
+
const m = err && err.message ? err.message : '';
|
|
2228
|
+
if (m.includes('unknown delivery tag') || m.includes('PRECONDITION_FAILED') || m.includes('Channel closed')) {
|
|
2229
|
+
console.warn('[RabbitMQClient] [mq-client-core] Cannot ack - consumer channel is closed/recreated (delivery tag invalid)', { error: m });
|
|
2230
|
+
return;
|
|
2231
|
+
}
|
|
2139
2232
|
this.emit('error', err);
|
|
2140
2233
|
throw err;
|
|
2141
2234
|
}
|
|
@@ -2154,6 +2247,11 @@ class RabbitMQClient extends EventEmitter {
|
|
|
2154
2247
|
try {
|
|
2155
2248
|
this._consumerChannel.nack(msg, false, requeue);
|
|
2156
2249
|
} catch (err) {
|
|
2250
|
+
const m = err && err.message ? err.message : '';
|
|
2251
|
+
if (m.includes('unknown delivery tag') || m.includes('PRECONDITION_FAILED') || m.includes('Channel closed')) {
|
|
2252
|
+
console.warn('[RabbitMQClient] [mq-client-core] Cannot nack - consumer channel is closed/recreated (delivery tag invalid)', { error: m });
|
|
2253
|
+
return;
|
|
2254
|
+
}
|
|
2157
2255
|
this.emit('error', err);
|
|
2158
2256
|
throw err;
|
|
2159
2257
|
}
|