@onlineapps/mq-client-core 1.0.38 → 1.0.40
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
CHANGED
|
@@ -56,6 +56,9 @@ class RabbitMQClient extends EventEmitter {
|
|
|
56
56
|
this._reconnectMaxDelay = this._config.reconnectMaxDelay || 30000; // Max 30 seconds
|
|
57
57
|
this._reconnectEnabled = this._config.reconnectEnabled !== false; // Default: enabled
|
|
58
58
|
this._reconnectTimer = null;
|
|
59
|
+
// Reconnect wait timeout: at least maxReconnectAttempts × reconnectMaxDelay, minimum 60s
|
|
60
|
+
this._reconnectWaitTimeout = this._config.reconnectWaitTimeout ||
|
|
61
|
+
Math.max(60000, (this._maxReconnectAttempts * this._reconnectMaxDelay));
|
|
59
62
|
|
|
60
63
|
// Publisher retry configuration
|
|
61
64
|
this._publishRetryEnabled = this._config.publishRetryEnabled !== false; // Default: enabled
|
|
@@ -182,6 +185,53 @@ class RabbitMQClient extends EventEmitter {
|
|
|
182
185
|
return this._publishMonitor.getPrometheusMetrics();
|
|
183
186
|
}
|
|
184
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Get current channel state
|
|
190
|
+
* @returns {Object} Channel state information
|
|
191
|
+
*/
|
|
192
|
+
getChannelState() {
|
|
193
|
+
return {
|
|
194
|
+
publisher: {
|
|
195
|
+
exists: !!this._channel,
|
|
196
|
+
closed: this._channel ? this._channel.closed : true,
|
|
197
|
+
ready: this._channel && !this._channel.closed
|
|
198
|
+
},
|
|
199
|
+
queue: {
|
|
200
|
+
exists: !!this._queueChannel,
|
|
201
|
+
closed: this._queueChannel ? this._queueChannel.closed : true,
|
|
202
|
+
ready: this._queueChannel && !this._queueChannel.closed
|
|
203
|
+
},
|
|
204
|
+
consumer: {
|
|
205
|
+
exists: !!this._consumerChannel,
|
|
206
|
+
closed: this._consumerChannel ? this._consumerChannel.closed : true,
|
|
207
|
+
ready: this._consumerChannel && !this._consumerChannel.closed
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Get current consumer state
|
|
214
|
+
* @returns {Object} Consumer state information
|
|
215
|
+
*/
|
|
216
|
+
getConsumerState() {
|
|
217
|
+
return {
|
|
218
|
+
tracked: this._activeConsumers.size,
|
|
219
|
+
active: Array.from(this._activeConsumers.keys())
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Get current buffer state
|
|
225
|
+
* @returns {Object} Buffer state information
|
|
226
|
+
*/
|
|
227
|
+
getBufferState() {
|
|
228
|
+
return {
|
|
229
|
+
size: this._publishLayer._buffer.size(),
|
|
230
|
+
inMemory: this._publishLayer._buffer._inMemory?.size() || 0,
|
|
231
|
+
persistent: this._publishLayer._buffer._persistent ? (this._publishLayer._buffer._persistent.size?.() || 0) : 0
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
185
235
|
/**
|
|
186
236
|
* Getter for channel - provides compatibility with QueueManager
|
|
187
237
|
* Returns ConfirmChannel for publish operations
|
|
@@ -457,8 +507,8 @@ class RabbitMQClient extends EventEmitter {
|
|
|
457
507
|
const timeout = setTimeout(() => {
|
|
458
508
|
this.removeListener('reconnected', onReconnected);
|
|
459
509
|
this.removeListener('error', onError);
|
|
460
|
-
reject(new Error(
|
|
461
|
-
},
|
|
510
|
+
reject(new Error(`Reconnection timeout after ${this._reconnectWaitTimeout}ms`));
|
|
511
|
+
}, this._reconnectWaitTimeout);
|
|
462
512
|
|
|
463
513
|
const onReconnected = () => {
|
|
464
514
|
clearTimeout(timeout);
|
|
@@ -619,11 +669,12 @@ class RabbitMQClient extends EventEmitter {
|
|
|
619
669
|
* Ensure consumer channel exists and is open
|
|
620
670
|
* Auto-recreates and re-registers all consumers if closed
|
|
621
671
|
* @private
|
|
622
|
-
* @returns {Promise<
|
|
672
|
+
* @returns {Promise<{reRegistered: number, failed: number}>} - Number of consumers re-registered and failed
|
|
623
673
|
*/
|
|
624
674
|
async _ensureConsumerChannel() {
|
|
625
675
|
if (this._consumerChannel && !this._consumerChannel.closed) {
|
|
626
|
-
|
|
676
|
+
// Channel is good - return current consumer state
|
|
677
|
+
return { reRegistered: this._activeConsumers.size, failed: 0 };
|
|
627
678
|
}
|
|
628
679
|
|
|
629
680
|
if (!this._connection || this._connection.closed) {
|
|
@@ -631,7 +682,7 @@ class RabbitMQClient extends EventEmitter {
|
|
|
631
682
|
// Reconnection logic will handle this - don't throw, just return
|
|
632
683
|
// The channel will be recreated after reconnection completes
|
|
633
684
|
console.warn('[RabbitMQClient] [mq-client-core] Cannot recreate consumer channel: connection is closed (reconnection in progress)');
|
|
634
|
-
return;
|
|
685
|
+
return { reRegistered: 0, failed: this._activeConsumers.size };
|
|
635
686
|
}
|
|
636
687
|
|
|
637
688
|
try {
|
|
@@ -657,6 +708,9 @@ class RabbitMQClient extends EventEmitter {
|
|
|
657
708
|
this._attachConsumerChannelHandlers(this._consumerChannel);
|
|
658
709
|
|
|
659
710
|
// Re-register all active consumers
|
|
711
|
+
let reRegistered = 0;
|
|
712
|
+
let failed = 0;
|
|
713
|
+
|
|
660
714
|
if (this._activeConsumers.size > 0) {
|
|
661
715
|
console.log(`[RabbitMQClient] [mq-client-core] Re-registering ${this._activeConsumers.size} consumers...`);
|
|
662
716
|
for (const [queue, consumerInfo] of this._activeConsumers.entries()) {
|
|
@@ -665,18 +719,25 @@ class RabbitMQClient extends EventEmitter {
|
|
|
665
719
|
if (consumerInfo.options.queueOptions) {
|
|
666
720
|
try {
|
|
667
721
|
await this._ensureQueueChannel();
|
|
722
|
+
if (!this._queueChannel || this._queueChannel.closed) {
|
|
723
|
+
throw new Error('Queue channel is not available (connection may be closed)');
|
|
724
|
+
}
|
|
668
725
|
this._trackChannelOperation(this._queueChannel, `assertQueue ${queue} (re-register)`);
|
|
669
726
|
await this._queueChannel.assertQueue(queue, consumerInfo.options.queueOptions);
|
|
670
727
|
} catch (assertErr) {
|
|
671
728
|
if (assertErr.code === 404) {
|
|
672
729
|
console.warn(`[RabbitMQClient] [mq-client-core] Queue ${queue} does not exist, skipping consumer re-registration`);
|
|
673
730
|
this._activeConsumers.delete(queue);
|
|
731
|
+
failed++;
|
|
732
|
+
this.emit('consumer:re-registration:failed', { queue, error: 'Queue does not exist', code: 404 });
|
|
674
733
|
continue;
|
|
675
734
|
}
|
|
676
735
|
// 406 error means queue exists with different args - skip this consumer
|
|
677
736
|
if (assertErr.code === 406) {
|
|
678
737
|
console.warn(`[RabbitMQClient] [mq-client-core] Queue ${queue} exists with different args, skipping consumer re-registration`);
|
|
679
738
|
this._activeConsumers.delete(queue);
|
|
739
|
+
failed++;
|
|
740
|
+
this.emit('consumer:re-registration:failed', { queue, error: 'Queue exists with different arguments', code: 406 });
|
|
680
741
|
continue;
|
|
681
742
|
}
|
|
682
743
|
throw assertErr;
|
|
@@ -689,16 +750,21 @@ class RabbitMQClient extends EventEmitter {
|
|
|
689
750
|
{ noAck: consumerInfo.options.noAck }
|
|
690
751
|
);
|
|
691
752
|
consumerInfo.consumerTag = consumeResult.consumerTag;
|
|
753
|
+
reRegistered++;
|
|
692
754
|
console.log(`[RabbitMQClient] [mq-client-core] ✓ Re-registered consumer for queue: ${queue} (consumerTag: ${consumeResult.consumerTag})`);
|
|
693
755
|
} catch (err) {
|
|
756
|
+
failed++;
|
|
694
757
|
console.error(`[RabbitMQClient] [mq-client-core] Failed to re-register consumer for queue ${queue}:`, err.message);
|
|
695
758
|
// Remove failed consumer from tracking
|
|
696
759
|
this._activeConsumers.delete(queue);
|
|
760
|
+
// Emit event for re-registration failure
|
|
761
|
+
this.emit('consumer:re-registration:failed', { queue, error: err.message, code: err.code });
|
|
697
762
|
}
|
|
698
763
|
}
|
|
699
764
|
}
|
|
700
765
|
|
|
701
|
-
console.log(
|
|
766
|
+
console.log(`[RabbitMQClient] [mq-client-core] ✓ Consumer channel recreated (re-registered: ${reRegistered}, failed: ${failed})`);
|
|
767
|
+
return { reRegistered, failed };
|
|
702
768
|
} catch (err) {
|
|
703
769
|
this._consumerChannel = null;
|
|
704
770
|
throw new Error(`Failed to recreate consumer channel: ${err.message}`);
|
|
@@ -1010,6 +1076,13 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1010
1076
|
try {
|
|
1011
1077
|
// Ensure queue channel exists and is open (auto-recreates if closed)
|
|
1012
1078
|
await this._ensureQueueChannel();
|
|
1079
|
+
if (!this._queueChannel || this._queueChannel.closed) {
|
|
1080
|
+
// Connection is closed and not reconnecting - cannot check queue
|
|
1081
|
+
throw new TransientPublishError(
|
|
1082
|
+
`Cannot check queue ${queue}: queue channel is not available (connection may be closed)`,
|
|
1083
|
+
new Error('Queue channel unavailable')
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1013
1086
|
this._trackChannelOperation(this._queueChannel, `checkQueue ${queue}`);
|
|
1014
1087
|
await this._queueChannel.checkQueue(queue);
|
|
1015
1088
|
// Queue exists - proceed to publish
|
|
@@ -1037,11 +1110,20 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1037
1110
|
// Jasný, hlasitý signál pro infra služby – fronta chybí, je to programátorská chyba
|
|
1038
1111
|
throw new QueueNotFoundError(queue, true, checkErr);
|
|
1039
1112
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1113
|
+
// For non-infrastructure queues, allow auto-creation with default options
|
|
1114
|
+
const queueOptions = options.queueOptions || { durable: this._config.durable };
|
|
1115
|
+
console.warn(`[RabbitMQClient] [mq-client-core] [PUBLISH] Auto-creating non-infrastructure queue ${queue} with default options (no TTL). This should be avoided for production.`);
|
|
1116
|
+
// Channel could have been nulled out by close handlers; ensure it's available
|
|
1117
|
+
await this._ensureQueueChannel();
|
|
1118
|
+
if (!this._queueChannel || this._queueChannel.closed) {
|
|
1119
|
+
// Connection is closed and not reconnecting - cannot create queue
|
|
1120
|
+
throw new TransientPublishError(
|
|
1121
|
+
`Cannot create queue ${queue}: queue channel is not available (connection may be closed)`,
|
|
1122
|
+
checkErr
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
this._trackChannelOperation(this._queueChannel, `assertQueue ${queue}`);
|
|
1126
|
+
await this._queueChannel.assertQueue(queue, queueOptions);
|
|
1045
1127
|
} else {
|
|
1046
1128
|
// Other error - rethrow
|
|
1047
1129
|
throw checkErr;
|
|
@@ -1180,23 +1262,11 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1180
1262
|
await confirmPromise;
|
|
1181
1263
|
}
|
|
1182
1264
|
} catch (err) {
|
|
1183
|
-
//
|
|
1184
|
-
//
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
await this._ensurePublisherChannel();
|
|
1189
|
-
// Retry publish once (může znovu vyhodit – už dojde do retry/recovery vrstvy)
|
|
1190
|
-
return await this.publish(queue, buffer, options);
|
|
1191
|
-
} catch (retryErr) {
|
|
1192
|
-
console.error('[RabbitMQClient] [mq-client-core] [PUBLISH] Retry after channel recreation failed:', retryErr.message);
|
|
1193
|
-
const classifiedRetryErr = classifyPublishError(retryErr);
|
|
1194
|
-
this.emit('error', classifiedRetryErr);
|
|
1195
|
-
throw classifiedRetryErr;
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
// Všechny ostatní chyby projdou jednotnou klasifikací do Transient/Permanent/QueueNotFound.
|
|
1265
|
+
// CRITICAL: _publishOnce() je "single attempt" - žádný vnitřní retry
|
|
1266
|
+
// Retry logika je pouze v PublishLayer, aby byl počet pokusů předvídatelný
|
|
1267
|
+
// Pokud channel closed, vyhodíme TransientPublishError a necháme PublishLayer řešit retry
|
|
1268
|
+
|
|
1269
|
+
// Všechny chyby projdou jednotnou klasifikací do Transient/Permanent/QueueNotFound
|
|
1200
1270
|
const classifiedErr = classifyPublishError(err);
|
|
1201
1271
|
this.emit('error', classifiedErr);
|
|
1202
1272
|
throw classifiedErr;
|
|
@@ -1501,60 +1571,103 @@ class RabbitMQClient extends EventEmitter {
|
|
|
1501
1571
|
}
|
|
1502
1572
|
});
|
|
1503
1573
|
|
|
1504
|
-
// Recreate all channels
|
|
1574
|
+
// Recreate all channels with best effort - partial success is better than total failure
|
|
1505
1575
|
console.log('[RabbitMQClient] Recreating channels...');
|
|
1506
1576
|
|
|
1577
|
+
const channelState = { publisher: false, queue: false, consumer: false };
|
|
1578
|
+
let consumerReRegResult = { reRegistered: 0, failed: 0 };
|
|
1579
|
+
|
|
1507
1580
|
// Recreate publisher channel
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1581
|
+
try {
|
|
1582
|
+
this._channel = await this._connection.createConfirmChannel();
|
|
1583
|
+
this._channel._createdAt = new Date().toISOString();
|
|
1584
|
+
this._channel._closeReason = null;
|
|
1585
|
+
this._channel._lastOperation = 'Recreated via _reconnectWithBackoff()';
|
|
1586
|
+
this._channel._type = 'publisher';
|
|
1587
|
+
this._attachPublisherChannelHandlers(this._channel);
|
|
1588
|
+
channelState.publisher = true;
|
|
1589
|
+
console.log('[RabbitMQClient] ✓ Publisher channel recreated');
|
|
1590
|
+
} catch (err) {
|
|
1591
|
+
console.error('[RabbitMQClient] Failed to recreate publisher channel:', err.message);
|
|
1592
|
+
this._channel = null;
|
|
1593
|
+
// Continue - partial success is better than total failure
|
|
1594
|
+
}
|
|
1514
1595
|
|
|
1515
1596
|
// Recreate queue channel
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1597
|
+
try {
|
|
1598
|
+
this._queueChannel = await this._connection.createChannel();
|
|
1599
|
+
this._queueChannel._createdAt = new Date().toISOString();
|
|
1600
|
+
this._queueChannel._closeReason = null;
|
|
1601
|
+
this._queueChannel._lastOperation = 'Recreated via _reconnectWithBackoff()';
|
|
1602
|
+
this._queueChannel._type = 'queue';
|
|
1603
|
+
this._attachQueueChannelHandlers(this._queueChannel);
|
|
1604
|
+
channelState.queue = true;
|
|
1605
|
+
console.log('[RabbitMQClient] ✓ Queue channel recreated');
|
|
1606
|
+
} catch (err) {
|
|
1607
|
+
console.error('[RabbitMQClient] Failed to recreate queue channel:', err.message);
|
|
1608
|
+
this._queueChannel = null;
|
|
1609
|
+
// Continue - partial success is better than total failure
|
|
1610
|
+
}
|
|
1522
1611
|
|
|
1523
1612
|
// Recreate consumer channel and re-register all consumers
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1613
|
+
try {
|
|
1614
|
+
this._consumerChannel = await this._connection.createChannel();
|
|
1615
|
+
this._consumerChannel._createdAt = new Date().toISOString();
|
|
1616
|
+
this._consumerChannel._closeReason = null;
|
|
1617
|
+
this._consumerChannel._lastOperation = 'Recreated via _reconnectWithBackoff()';
|
|
1618
|
+
this._consumerChannel._type = 'consumer';
|
|
1619
|
+
this._attachConsumerChannelHandlers(this._consumerChannel);
|
|
1620
|
+
|
|
1621
|
+
// Re-register all consumers
|
|
1622
|
+
consumerReRegResult = await this._ensureConsumerChannel();
|
|
1623
|
+
channelState.consumer = true;
|
|
1624
|
+
console.log('[RabbitMQClient] ✓ Consumer channel recreated');
|
|
1625
|
+
} catch (err) {
|
|
1626
|
+
console.error('[RabbitMQClient] Failed to recreate consumer channel:', err.message);
|
|
1627
|
+
this._consumerChannel = null;
|
|
1628
|
+
// Continue - partial success is better than total failure
|
|
1629
|
+
}
|
|
1531
1630
|
|
|
1532
1631
|
// Reset reconnect state
|
|
1533
1632
|
this._reconnectAttempts = 0;
|
|
1534
1633
|
this._reconnecting = false;
|
|
1535
1634
|
|
|
1536
|
-
//
|
|
1537
|
-
//
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
}
|
|
1544
|
-
if (!this._consumerChannel || this._consumerChannel.closed) {
|
|
1545
|
-
throw new Error('Consumer channel not ready after reconnection');
|
|
1546
|
-
}
|
|
1635
|
+
// Emit reconnected event with state information
|
|
1636
|
+
// Partial success is acceptable - at least connection is re-established
|
|
1637
|
+
const reconnectState = {
|
|
1638
|
+
channels: channelState,
|
|
1639
|
+
consumers: consumerReRegResult,
|
|
1640
|
+
timestamp: new Date().toISOString()
|
|
1641
|
+
};
|
|
1547
1642
|
|
|
1548
|
-
console.log('[RabbitMQClient] ✓ Connection-level recovery completed
|
|
1549
|
-
this.emit('reconnected');
|
|
1643
|
+
console.log('[RabbitMQClient] ✓ Connection-level recovery completed', JSON.stringify(reconnectState, null, 2));
|
|
1644
|
+
this.emit('reconnected', reconnectState);
|
|
1550
1645
|
|
|
1551
1646
|
// Flush buffered messages po reconnectu
|
|
1647
|
+
let bufferFlushResult = { inMemory: 0, persistent: 0, flushed: false };
|
|
1552
1648
|
try {
|
|
1553
1649
|
const flushResult = await this._publishLayer.flushBuffered();
|
|
1554
|
-
|
|
1555
|
-
|
|
1650
|
+
bufferFlushResult = {
|
|
1651
|
+
inMemory: flushResult.inMemory || 0,
|
|
1652
|
+
persistent: flushResult.persistent || 0,
|
|
1653
|
+
flushed: true
|
|
1654
|
+
};
|
|
1655
|
+
this._publishMonitor.trackFlushed(bufferFlushResult.inMemory + bufferFlushResult.persistent);
|
|
1656
|
+
console.log(`[RabbitMQClient] ✓ Flushed ${bufferFlushResult.inMemory + bufferFlushResult.persistent} buffered messages after reconnection`);
|
|
1556
1657
|
} catch (flushErr) {
|
|
1557
1658
|
console.warn(`[RabbitMQClient] Failed to flush buffered messages after reconnection: ${flushErr.message}`);
|
|
1659
|
+
this.emit('buffer:flush:failed', { error: flushErr.message, bufferSize: this._publishLayer._buffer.size() });
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// If critical channels failed, log warning but don't throw - connection is re-established
|
|
1663
|
+
if (!channelState.publisher) {
|
|
1664
|
+
console.warn('[RabbitMQClient] ⚠ Publisher channel not ready after reconnection - publish operations may fail');
|
|
1665
|
+
}
|
|
1666
|
+
if (!channelState.queue) {
|
|
1667
|
+
console.warn('[RabbitMQClient] ⚠ Queue channel not ready after reconnection - queue operations may fail');
|
|
1668
|
+
}
|
|
1669
|
+
if (!channelState.consumer) {
|
|
1670
|
+
console.warn('[RabbitMQClient] ⚠ Consumer channel not ready after reconnection - consume operations may fail');
|
|
1558
1671
|
}
|
|
1559
1672
|
|
|
1560
1673
|
return; // Success - exit reconnection loop
|
|
@@ -90,6 +90,11 @@ class RecoveryWorker {
|
|
|
90
90
|
// Use queue channel to create queue
|
|
91
91
|
await this._client._ensureQueueChannel();
|
|
92
92
|
|
|
93
|
+
// Guard: ensure queue channel is available after ensure
|
|
94
|
+
if (!this._client._queueChannel || this._client._queueChannel.closed) {
|
|
95
|
+
throw new Error(`Cannot create queue ${queueName}: queue channel is not available (connection may be closed)`);
|
|
96
|
+
}
|
|
97
|
+
|
|
93
98
|
const queueOptions = options.queueOptions || { durable: this._client._config.durable };
|
|
94
99
|
await this._client._queueChannel.assertQueue(queueName, queueOptions);
|
|
95
100
|
}
|