@onlineapps/mq-client-core 1.0.33 → 1.0.35
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/transports/rabbitmqClient.js +654 -145
package/package.json
CHANGED
|
@@ -35,6 +35,28 @@ class RabbitMQClient extends EventEmitter {
|
|
|
35
35
|
this._channel = null; // ConfirmChannel for publish operations
|
|
36
36
|
this._queueChannel = null; // Regular channel for queue operations (assertQueue, checkQueue)
|
|
37
37
|
this._consumerChannel = null; // Dedicated channel for consume operations
|
|
38
|
+
|
|
39
|
+
// Track active consumers for re-registration after channel recreation
|
|
40
|
+
this._activeConsumers = new Map(); // queue -> { handler, options, consumerTag }
|
|
41
|
+
|
|
42
|
+
// Auto-reconnect flags
|
|
43
|
+
this._reconnecting = false;
|
|
44
|
+
this._reconnectAttempts = 0;
|
|
45
|
+
|
|
46
|
+
// Channel close hooks - called when channel closes with detailed information
|
|
47
|
+
this._channelCloseHooks = []; // Array of { type: 'publisher'|'queue'|'consumer', callback: (details) => void }
|
|
48
|
+
|
|
49
|
+
// Health monitoring
|
|
50
|
+
this._healthCheckInterval = null;
|
|
51
|
+
this._healthCheckIntervalMs = this._config.healthCheckInterval || 30000; // Default 30s
|
|
52
|
+
this._healthCheckEnabled = this._config.healthCheckEnabled !== false;
|
|
53
|
+
|
|
54
|
+
// Health reporting callbacks
|
|
55
|
+
this._healthReportCallback = this._config.healthReportCallback || null; // (health) => Promise<void>
|
|
56
|
+
this._healthCriticalCallback = this._config.healthCriticalCallback || null; // (health) => Promise<void>
|
|
57
|
+
this._criticalHealthShutdown = this._config.criticalHealthShutdown !== false; // Default: true - shutdown on critical health
|
|
58
|
+
this._criticalHealthShutdownDelay = this._config.criticalHealthShutdownDelay || 60000; // Default: 60s delay before shutdown
|
|
59
|
+
this._criticalHealthStartTime = null; // Track when critical health started
|
|
38
60
|
}
|
|
39
61
|
|
|
40
62
|
/**
|
|
@@ -59,6 +81,50 @@ class RabbitMQClient extends EventEmitter {
|
|
|
59
81
|
get queueChannel() {
|
|
60
82
|
return this._queueChannel || this._channel; // Fallback to main channel if queueChannel not available
|
|
61
83
|
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Register a hook to be called when a channel closes
|
|
87
|
+
* @param {string} type - Channel type: 'publisher', 'queue', 'consumer', or 'all'
|
|
88
|
+
* @param {Function} callback - Callback function receiving close details: { type, channel, error, reason, timestamp, stack }
|
|
89
|
+
*/
|
|
90
|
+
onChannelClose(type, callback) {
|
|
91
|
+
this._channelCloseHooks.push({ type, callback });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Call all registered channel close hooks
|
|
96
|
+
* @private
|
|
97
|
+
* @param {string} channelType - 'publisher', 'queue', or 'consumer'
|
|
98
|
+
* @param {Object} channel - The closed channel
|
|
99
|
+
* @param {Error} error - Error that caused closure (if any)
|
|
100
|
+
* @param {string} reason - Human-readable reason
|
|
101
|
+
*/
|
|
102
|
+
_callChannelCloseHooks(channelType, channel, error, reason) {
|
|
103
|
+
const details = {
|
|
104
|
+
type: channelType,
|
|
105
|
+
channel: channel,
|
|
106
|
+
error: error,
|
|
107
|
+
reason: reason,
|
|
108
|
+
timestamp: new Date().toISOString(),
|
|
109
|
+
stack: error?.stack || new Error().stack,
|
|
110
|
+
connectionState: this._connection ? (this._connection.closed ? 'closed' : 'open') : 'null',
|
|
111
|
+
channelCreatedAt: channel?._createdAt || 'unknown',
|
|
112
|
+
lastOperation: channel?._lastOperation || 'unknown'
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
for (const hook of this._channelCloseHooks) {
|
|
116
|
+
if (hook.type === 'all' || hook.type === channelType) {
|
|
117
|
+
try {
|
|
118
|
+
hook.callback(details);
|
|
119
|
+
} catch (hookErr) {
|
|
120
|
+
console.error(`[RabbitMQClient] [mq-client-core] Channel close hook error:`, hookErr.message);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Emit event for external listeners
|
|
126
|
+
this.emit('channel:close', details);
|
|
127
|
+
}
|
|
62
128
|
|
|
63
129
|
/**
|
|
64
130
|
* Connects to RabbitMQ server and creates a confirm channel.
|
|
@@ -94,15 +160,39 @@ class RabbitMQClient extends EventEmitter {
|
|
|
94
160
|
|
|
95
161
|
// Use ConfirmChannel to enable publisher confirms for publish operations
|
|
96
162
|
this._channel = await this._connection.createConfirmChannel();
|
|
163
|
+
|
|
164
|
+
// Track channel metadata for debugging
|
|
165
|
+
this._channel._createdAt = new Date().toISOString();
|
|
166
|
+
this._channel._closeReason = null;
|
|
167
|
+
this._channel._lastOperation = 'Initial creation';
|
|
168
|
+
this._channel._type = 'publisher';
|
|
169
|
+
|
|
97
170
|
// Enable publisher confirms explicitly
|
|
98
|
-
this._channel.on('error', (err) => {
|
|
99
|
-
|
|
171
|
+
this._channel.on('error', async (err) => {
|
|
172
|
+
const reason = `Publisher channel error: ${err.message} (code: ${err.code || 'unknown'})`;
|
|
173
|
+
this._channel._closeReason = reason;
|
|
174
|
+
console.error(`[RabbitMQClient] [mq-client-core] ${reason}`);
|
|
175
|
+
|
|
176
|
+
// Call close hooks with detailed information
|
|
177
|
+
this._callChannelCloseHooks('publisher', this._channel, err, reason);
|
|
178
|
+
|
|
179
|
+
// Auto-recreate channel if connection is still alive
|
|
180
|
+
await this._ensurePublisherChannel();
|
|
100
181
|
this.emit('error', err);
|
|
101
182
|
});
|
|
102
|
-
this._channel.on('close', () => {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
183
|
+
this._channel.on('close', async () => {
|
|
184
|
+
const reason = this._channel._closeReason || 'Publisher channel closed unexpectedly';
|
|
185
|
+
console.warn(`[RabbitMQClient] [mq-client-core] ${reason} - will auto-recreate on next publish`);
|
|
186
|
+
|
|
187
|
+
// Call close hooks with detailed information
|
|
188
|
+
this._callChannelCloseHooks('publisher', this._channel, null, reason);
|
|
189
|
+
|
|
190
|
+
// Mark channel as null - will be recreated on next publish
|
|
191
|
+
const closedChannel = this._channel;
|
|
192
|
+
this._channel = null;
|
|
193
|
+
|
|
194
|
+
// Try to recreate immediately if connection is alive
|
|
195
|
+
await this._ensurePublisherChannel();
|
|
106
196
|
});
|
|
107
197
|
// Set up publisher confirms callback
|
|
108
198
|
this._channel.on('drain', () => {
|
|
@@ -112,25 +202,80 @@ class RabbitMQClient extends EventEmitter {
|
|
|
112
202
|
// Create a separate regular channel for queue operations (assertQueue, checkQueue)
|
|
113
203
|
// This avoids RPC reply queue issues with ConfirmChannel.assertQueue()
|
|
114
204
|
this._queueChannel = await this._connection.createChannel();
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
205
|
+
|
|
206
|
+
// Track channel metadata for debugging
|
|
207
|
+
this._queueChannel._createdAt = new Date().toISOString();
|
|
208
|
+
this._queueChannel._closeReason = null;
|
|
209
|
+
this._queueChannel._lastOperation = 'Initial creation';
|
|
210
|
+
this._queueChannel._type = 'queue';
|
|
211
|
+
|
|
212
|
+
this._queueChannel.on('error', async (err) => {
|
|
213
|
+
const reason = `Queue channel error: ${err.message} (code: ${err.code || 'unknown'})`;
|
|
214
|
+
this._queueChannel._closeReason = reason;
|
|
215
|
+
console.warn(`[RabbitMQClient] [mq-client-core] ${reason}`);
|
|
216
|
+
|
|
217
|
+
// Call close hooks with detailed information
|
|
218
|
+
this._callChannelCloseHooks('queue', this._queueChannel, err, reason);
|
|
219
|
+
|
|
220
|
+
// Auto-recreate channel if connection is still alive
|
|
221
|
+
await this._ensureQueueChannel();
|
|
118
222
|
});
|
|
119
|
-
this._queueChannel.on('close', () => {
|
|
120
|
-
|
|
223
|
+
this._queueChannel.on('close', async () => {
|
|
224
|
+
const reason = this._queueChannel._closeReason || 'Queue channel closed unexpectedly';
|
|
225
|
+
console.warn(`[RabbitMQClient] [mq-client-core] ${reason} - will auto-recreate on next operation`);
|
|
226
|
+
|
|
227
|
+
// Call close hooks with detailed information
|
|
228
|
+
this._callChannelCloseHooks('queue', this._queueChannel, null, reason);
|
|
229
|
+
|
|
230
|
+
// Mark channel as null - will be recreated on next queue operation
|
|
231
|
+
const closedChannel = this._queueChannel;
|
|
232
|
+
this._queueChannel = null;
|
|
233
|
+
|
|
234
|
+
// Try to recreate immediately if connection is alive
|
|
235
|
+
await this._ensureQueueChannel();
|
|
121
236
|
});
|
|
122
237
|
|
|
123
238
|
// Create a dedicated channel for consume operations
|
|
124
239
|
// This prevents channel conflicts between publish (ConfirmChannel) and consume operations
|
|
125
240
|
this._consumerChannel = await this._connection.createChannel();
|
|
126
|
-
|
|
127
|
-
|
|
241
|
+
|
|
242
|
+
// Track channel metadata for debugging
|
|
243
|
+
this._consumerChannel._createdAt = new Date().toISOString();
|
|
244
|
+
this._consumerChannel._closeReason = null;
|
|
245
|
+
this._consumerChannel._lastOperation = 'Initial creation';
|
|
246
|
+
this._consumerChannel._type = 'consumer';
|
|
247
|
+
|
|
248
|
+
this._consumerChannel.on('error', async (err) => {
|
|
249
|
+
const reason = `Consumer channel error: ${err.message} (code: ${err.code || 'unknown'})`;
|
|
250
|
+
this._consumerChannel._closeReason = reason;
|
|
251
|
+
console.warn(`[RabbitMQClient] [mq-client-core] ${reason}`);
|
|
252
|
+
|
|
253
|
+
// Call close hooks with detailed information
|
|
254
|
+
this._callChannelCloseHooks('consumer', this._consumerChannel, err, reason);
|
|
255
|
+
|
|
256
|
+
// Auto-recreate channel and re-register consumers
|
|
257
|
+
await this._ensureConsumerChannel();
|
|
128
258
|
this.emit('error', err);
|
|
129
259
|
});
|
|
130
|
-
this._consumerChannel.on('close', () => {
|
|
131
|
-
|
|
132
|
-
|
|
260
|
+
this._consumerChannel.on('close', async () => {
|
|
261
|
+
const reason = this._consumerChannel._closeReason || 'Consumer channel closed unexpectedly';
|
|
262
|
+
console.warn(`[RabbitMQClient] [mq-client-core] ${reason} - will auto-recreate and re-register consumers`);
|
|
263
|
+
|
|
264
|
+
// Call close hooks with detailed information
|
|
265
|
+
this._callChannelCloseHooks('consumer', this._consumerChannel, null, reason);
|
|
266
|
+
|
|
267
|
+
// Mark channel as null - will be recreated and consumers re-registered
|
|
268
|
+
const closedChannel = this._consumerChannel;
|
|
269
|
+
this._consumerChannel = null;
|
|
270
|
+
|
|
271
|
+
// Try to recreate and re-register consumers immediately if connection is alive
|
|
272
|
+
await this._ensureConsumerChannel();
|
|
133
273
|
});
|
|
274
|
+
|
|
275
|
+
// Start health monitoring if enabled
|
|
276
|
+
if (this._healthCheckEnabled) {
|
|
277
|
+
this._startHealthMonitoring();
|
|
278
|
+
}
|
|
134
279
|
} catch (err) {
|
|
135
280
|
// Cleanup partially created resources
|
|
136
281
|
if (this._connection) {
|
|
@@ -145,11 +290,446 @@ class RabbitMQClient extends EventEmitter {
|
|
|
145
290
|
}
|
|
146
291
|
}
|
|
147
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Ensure publisher channel exists and is open
|
|
295
|
+
* Auto-recreates if closed
|
|
296
|
+
* @private
|
|
297
|
+
* @returns {Promise<void>}
|
|
298
|
+
*/
|
|
299
|
+
async _ensurePublisherChannel() {
|
|
300
|
+
if (this._channel && !this._channel.closed) {
|
|
301
|
+
return; // Channel is good
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (!this._connection || this._connection.closed) {
|
|
305
|
+
throw new Error('Cannot recreate publisher channel: connection is closed');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
// Close old channel if exists
|
|
310
|
+
if (this._channel) {
|
|
311
|
+
try {
|
|
312
|
+
await this._channel.close();
|
|
313
|
+
} catch (_) {
|
|
314
|
+
// Ignore errors when closing already-closed channel
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Create new ConfirmChannel
|
|
319
|
+
this._channel = await this._connection.createConfirmChannel();
|
|
320
|
+
|
|
321
|
+
// Track channel metadata for debugging
|
|
322
|
+
this._channel._createdAt = new Date().toISOString();
|
|
323
|
+
this._channel._closeReason = null;
|
|
324
|
+
this._channel._lastOperation = 'Recreated via _ensurePublisherChannel()';
|
|
325
|
+
this._channel._type = 'publisher';
|
|
326
|
+
|
|
327
|
+
this._channel.on('error', async (err) => {
|
|
328
|
+
const reason = `Publisher channel error: ${err.message} (code: ${err.code || 'unknown'})`;
|
|
329
|
+
this._channel._closeReason = reason;
|
|
330
|
+
console.error(`[RabbitMQClient] [mq-client-core] ${reason}`);
|
|
331
|
+
|
|
332
|
+
// Call close hooks
|
|
333
|
+
this._callChannelCloseHooks('publisher', this._channel, err, reason);
|
|
334
|
+
|
|
335
|
+
await this._ensurePublisherChannel();
|
|
336
|
+
this.emit('error', err);
|
|
337
|
+
});
|
|
338
|
+
this._channel.on('close', async () => {
|
|
339
|
+
const reason = this._channel._closeReason || 'Publisher channel closed unexpectedly';
|
|
340
|
+
console.warn(`[RabbitMQClient] [mq-client-core] ${reason} - will auto-recreate on next publish`);
|
|
341
|
+
|
|
342
|
+
// Call close hooks
|
|
343
|
+
this._callChannelCloseHooks('publisher', this._channel, null, reason);
|
|
344
|
+
|
|
345
|
+
const closedChannel = this._channel;
|
|
346
|
+
this._channel = null;
|
|
347
|
+
await this._ensurePublisherChannel();
|
|
348
|
+
});
|
|
349
|
+
this._channel.on('drain', () => {
|
|
350
|
+
console.log('[RabbitMQClient] [mq-client-core] Publisher channel drained');
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
console.log('[RabbitMQClient] [mq-client-core] ✓ Publisher channel recreated');
|
|
354
|
+
} catch (err) {
|
|
355
|
+
this._channel = null;
|
|
356
|
+
throw new Error(`Failed to recreate publisher channel: ${err.message}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Ensure queue channel exists and is open
|
|
362
|
+
* Auto-recreates if closed
|
|
363
|
+
* @private
|
|
364
|
+
* @returns {Promise<void>}
|
|
365
|
+
*/
|
|
366
|
+
async _ensureQueueChannel() {
|
|
367
|
+
if (this._queueChannel && !this._queueChannel.closed) {
|
|
368
|
+
return; // Channel is good
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (!this._connection || this._connection.closed) {
|
|
372
|
+
throw new Error('Cannot recreate queue channel: connection is closed');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
// Close old channel if exists
|
|
377
|
+
if (this._queueChannel) {
|
|
378
|
+
try {
|
|
379
|
+
await this._queueChannel.close();
|
|
380
|
+
} catch (_) {
|
|
381
|
+
// Ignore errors when closing already-closed channel
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Create new regular channel
|
|
386
|
+
this._queueChannel = await this._connection.createChannel();
|
|
387
|
+
|
|
388
|
+
// Track channel metadata for debugging
|
|
389
|
+
this._queueChannel._createdAt = new Date().toISOString();
|
|
390
|
+
this._queueChannel._closeReason = null;
|
|
391
|
+
this._queueChannel._lastOperation = 'Recreated via _ensureQueueChannel()';
|
|
392
|
+
this._queueChannel._type = 'queue';
|
|
393
|
+
|
|
394
|
+
this._queueChannel.on('error', async (err) => {
|
|
395
|
+
const reason = `Queue channel error: ${err.message} (code: ${err.code || 'unknown'})`;
|
|
396
|
+
this._queueChannel._closeReason = reason;
|
|
397
|
+
console.warn(`[RabbitMQClient] [mq-client-core] ${reason}`);
|
|
398
|
+
|
|
399
|
+
// Call close hooks
|
|
400
|
+
this._callChannelCloseHooks('queue', this._queueChannel, err, reason);
|
|
401
|
+
|
|
402
|
+
await this._ensureQueueChannel();
|
|
403
|
+
});
|
|
404
|
+
this._queueChannel.on('close', async () => {
|
|
405
|
+
const reason = this._queueChannel._closeReason || 'Queue channel closed unexpectedly';
|
|
406
|
+
console.warn(`[RabbitMQClient] [mq-client-core] ${reason} - will auto-recreate on next operation`);
|
|
407
|
+
|
|
408
|
+
// Call close hooks
|
|
409
|
+
this._callChannelCloseHooks('queue', this._queueChannel, null, reason);
|
|
410
|
+
|
|
411
|
+
const closedChannel = this._queueChannel;
|
|
412
|
+
this._queueChannel = null;
|
|
413
|
+
await this._ensureQueueChannel();
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
console.log('[RabbitMQClient] [mq-client-core] ✓ Queue channel recreated');
|
|
417
|
+
} catch (err) {
|
|
418
|
+
this._queueChannel = null;
|
|
419
|
+
throw new Error(`Failed to recreate queue channel: ${err.message}`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Ensure consumer channel exists and is open
|
|
425
|
+
* Auto-recreates and re-registers all consumers if closed
|
|
426
|
+
* @private
|
|
427
|
+
* @returns {Promise<void>}
|
|
428
|
+
*/
|
|
429
|
+
async _ensureConsumerChannel() {
|
|
430
|
+
if (this._consumerChannel && !this._consumerChannel.closed) {
|
|
431
|
+
return; // Channel is good
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (!this._connection || this._connection.closed) {
|
|
435
|
+
throw new Error('Cannot recreate consumer channel: connection is closed');
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
try {
|
|
439
|
+
// Close old channel if exists
|
|
440
|
+
if (this._consumerChannel) {
|
|
441
|
+
try {
|
|
442
|
+
await this._consumerChannel.close();
|
|
443
|
+
} catch (_) {
|
|
444
|
+
// Ignore errors when closing already-closed channel
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Create new regular channel
|
|
449
|
+
this._consumerChannel = await this._connection.createChannel();
|
|
450
|
+
|
|
451
|
+
// Track channel metadata for debugging
|
|
452
|
+
this._consumerChannel._createdAt = new Date().toISOString();
|
|
453
|
+
this._consumerChannel._closeReason = null;
|
|
454
|
+
this._consumerChannel._lastOperation = 'Recreated via _ensureConsumerChannel()';
|
|
455
|
+
this._consumerChannel._type = 'consumer';
|
|
456
|
+
|
|
457
|
+
this._consumerChannel.on('error', async (err) => {
|
|
458
|
+
const reason = `Consumer channel error: ${err.message} (code: ${err.code || 'unknown'})`;
|
|
459
|
+
this._consumerChannel._closeReason = reason;
|
|
460
|
+
console.warn(`[RabbitMQClient] [mq-client-core] ${reason}`);
|
|
461
|
+
|
|
462
|
+
// Call close hooks
|
|
463
|
+
this._callChannelCloseHooks('consumer', this._consumerChannel, err, reason);
|
|
464
|
+
|
|
465
|
+
await this._ensureConsumerChannel();
|
|
466
|
+
this.emit('error', err);
|
|
467
|
+
});
|
|
468
|
+
this._consumerChannel.on('close', async () => {
|
|
469
|
+
const reason = this._consumerChannel._closeReason || 'Consumer channel closed unexpectedly';
|
|
470
|
+
console.warn(`[RabbitMQClient] [mq-client-core] ${reason} - will auto-recreate and re-register consumers`);
|
|
471
|
+
|
|
472
|
+
// Call close hooks
|
|
473
|
+
this._callChannelCloseHooks('consumer', this._consumerChannel, null, reason);
|
|
474
|
+
|
|
475
|
+
const closedChannel = this._consumerChannel;
|
|
476
|
+
this._consumerChannel = null;
|
|
477
|
+
await this._ensureConsumerChannel();
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// Re-register all active consumers
|
|
481
|
+
if (this._activeConsumers.size > 0) {
|
|
482
|
+
console.log(`[RabbitMQClient] [mq-client-core] Re-registering ${this._activeConsumers.size} consumers...`);
|
|
483
|
+
for (const [queue, consumerInfo] of this._activeConsumers.entries()) {
|
|
484
|
+
try {
|
|
485
|
+
// Re-assert queue with correct options before consuming (may have been deleted)
|
|
486
|
+
if (consumerInfo.options.queueOptions) {
|
|
487
|
+
try {
|
|
488
|
+
await this._ensureQueueChannel();
|
|
489
|
+
this._trackChannelOperation(this._queueChannel, `assertQueue ${queue} (re-register)`);
|
|
490
|
+
await this._queueChannel.assertQueue(queue, consumerInfo.options.queueOptions);
|
|
491
|
+
} catch (assertErr) {
|
|
492
|
+
if (assertErr.code === 404) {
|
|
493
|
+
console.warn(`[RabbitMQClient] [mq-client-core] Queue ${queue} does not exist, skipping consumer re-registration`);
|
|
494
|
+
this._activeConsumers.delete(queue);
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
// 406 error means queue exists with different args - skip this consumer
|
|
498
|
+
if (assertErr.code === 406) {
|
|
499
|
+
console.warn(`[RabbitMQClient] [mq-client-core] Queue ${queue} exists with different args, skipping consumer re-registration`);
|
|
500
|
+
this._activeConsumers.delete(queue);
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
throw assertErr;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const consumeResult = await this._consumerChannel.consume(
|
|
508
|
+
queue,
|
|
509
|
+
consumerInfo.handler,
|
|
510
|
+
{ noAck: consumerInfo.options.noAck }
|
|
511
|
+
);
|
|
512
|
+
consumerInfo.consumerTag = consumeResult.consumerTag;
|
|
513
|
+
console.log(`[RabbitMQClient] [mq-client-core] ✓ Re-registered consumer for queue: ${queue} (consumerTag: ${consumeResult.consumerTag})`);
|
|
514
|
+
} catch (err) {
|
|
515
|
+
console.error(`[RabbitMQClient] [mq-client-core] Failed to re-register consumer for queue ${queue}:`, err.message);
|
|
516
|
+
// Remove failed consumer from tracking
|
|
517
|
+
this._activeConsumers.delete(queue);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
console.log('[RabbitMQClient] [mq-client-core] ✓ Consumer channel recreated');
|
|
523
|
+
} catch (err) {
|
|
524
|
+
this._consumerChannel = null;
|
|
525
|
+
throw new Error(`Failed to recreate consumer channel: ${err.message}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
148
529
|
/**
|
|
149
530
|
* Disconnects: closes channel and connection.
|
|
150
531
|
* @returns {Promise<void>}
|
|
151
532
|
*/
|
|
533
|
+
/**
|
|
534
|
+
* Start health monitoring - periodically checks channels, queues, and consumers
|
|
535
|
+
* @private
|
|
536
|
+
*/
|
|
537
|
+
_startHealthMonitoring() {
|
|
538
|
+
if (this._healthCheckInterval) {
|
|
539
|
+
return; // Already started
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
this._healthCheckInterval = setInterval(async () => {
|
|
543
|
+
try {
|
|
544
|
+
await this._performHealthCheck();
|
|
545
|
+
} catch (err) {
|
|
546
|
+
console.error(`[RabbitMQClient] [mq-client-core] Health check error:`, err.message);
|
|
547
|
+
// Emit event for external monitoring
|
|
548
|
+
this.emit('health:check:error', err);
|
|
549
|
+
}
|
|
550
|
+
}, this._healthCheckIntervalMs);
|
|
551
|
+
|
|
552
|
+
console.log(`[RabbitMQClient] [mq-client-core] Health monitoring started (interval: ${this._healthCheckIntervalMs}ms)`);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Stop health monitoring
|
|
557
|
+
* @private
|
|
558
|
+
*/
|
|
559
|
+
_stopHealthMonitoring() {
|
|
560
|
+
if (this._healthCheckInterval) {
|
|
561
|
+
clearInterval(this._healthCheckInterval);
|
|
562
|
+
this._healthCheckInterval = null;
|
|
563
|
+
console.log('[RabbitMQClient] [mq-client-core] Health monitoring stopped');
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Perform health check - verify channels, queues, and consumers
|
|
569
|
+
* @private
|
|
570
|
+
* @returns {Promise<Object>} Health check results
|
|
571
|
+
*/
|
|
572
|
+
async _performHealthCheck() {
|
|
573
|
+
const health = {
|
|
574
|
+
timestamp: new Date().toISOString(),
|
|
575
|
+
connection: {
|
|
576
|
+
exists: !!this._connection,
|
|
577
|
+
closed: this._connection ? this._connection.closed : true
|
|
578
|
+
},
|
|
579
|
+
channels: {
|
|
580
|
+
publisher: {
|
|
581
|
+
exists: !!this._channel,
|
|
582
|
+
closed: this._channel ? this._channel.closed : true
|
|
583
|
+
},
|
|
584
|
+
queue: {
|
|
585
|
+
exists: !!this._queueChannel,
|
|
586
|
+
closed: this._queueChannel ? this._queueChannel.closed : true
|
|
587
|
+
},
|
|
588
|
+
consumer: {
|
|
589
|
+
exists: !!this._consumerChannel,
|
|
590
|
+
closed: this._consumerChannel ? this._consumerChannel.closed : true
|
|
591
|
+
}
|
|
592
|
+
},
|
|
593
|
+
consumers: {
|
|
594
|
+
tracked: this._activeConsumers.size,
|
|
595
|
+
active: 0,
|
|
596
|
+
inactive: []
|
|
597
|
+
},
|
|
598
|
+
queues: {
|
|
599
|
+
checked: 0,
|
|
600
|
+
missing: [],
|
|
601
|
+
noConsumer: []
|
|
602
|
+
},
|
|
603
|
+
healthy: true,
|
|
604
|
+
issues: []
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
// Check connection
|
|
608
|
+
if (!health.connection.exists || health.connection.closed) {
|
|
609
|
+
health.healthy = false;
|
|
610
|
+
health.issues.push('Connection is closed or missing');
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Check channels
|
|
614
|
+
if (health.channels.publisher.closed || !health.channels.publisher.exists) {
|
|
615
|
+
health.healthy = false;
|
|
616
|
+
health.issues.push('Publisher channel is closed or missing');
|
|
617
|
+
}
|
|
618
|
+
if (health.channels.queue.closed || !health.channels.queue.exists) {
|
|
619
|
+
health.healthy = false;
|
|
620
|
+
health.issues.push('Queue channel is closed or missing');
|
|
621
|
+
}
|
|
622
|
+
if (health.channels.consumer.closed || !health.channels.consumer.exists) {
|
|
623
|
+
health.healthy = false;
|
|
624
|
+
health.issues.push('Consumer channel is closed or missing');
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Check tracked consumers - verify queues exist and have active consumers
|
|
628
|
+
if (this._connection && !this._connection.closed && this._queueChannel && !this._queueChannel.closed) {
|
|
629
|
+
for (const [queue, consumerInfo] of this._activeConsumers.entries()) {
|
|
630
|
+
health.queues.checked++;
|
|
631
|
+
try {
|
|
632
|
+
// Check if queue exists
|
|
633
|
+
this._trackChannelOperation(this._queueChannel, `checkQueue ${queue}`);
|
|
634
|
+
const queueInfo = await this._queueChannel.checkQueue(queue);
|
|
635
|
+
if (queueInfo) {
|
|
636
|
+
// Queue exists - check if consumer is active
|
|
637
|
+
// Note: RabbitMQ doesn't provide direct API to check if consumer is active
|
|
638
|
+
// We can only verify that consumerTag is set
|
|
639
|
+
if (consumerInfo.consumerTag) {
|
|
640
|
+
health.consumers.active++;
|
|
641
|
+
} else {
|
|
642
|
+
health.consumers.inactive.push(queue);
|
|
643
|
+
health.queues.noConsumer.push(queue);
|
|
644
|
+
health.healthy = false;
|
|
645
|
+
health.issues.push(`Queue ${queue} has no active consumer (consumerTag missing)`);
|
|
646
|
+
}
|
|
647
|
+
} else {
|
|
648
|
+
health.queues.missing.push(queue);
|
|
649
|
+
health.healthy = false;
|
|
650
|
+
health.issues.push(`Queue ${queue} does not exist`);
|
|
651
|
+
}
|
|
652
|
+
} catch (err) {
|
|
653
|
+
if (err.code === 404) {
|
|
654
|
+
health.queues.missing.push(queue);
|
|
655
|
+
health.healthy = false;
|
|
656
|
+
health.issues.push(`Queue ${queue} does not exist (404)`);
|
|
657
|
+
} else {
|
|
658
|
+
health.healthy = false;
|
|
659
|
+
health.issues.push(`Error checking queue ${queue}: ${err.message}`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Emit health check result
|
|
666
|
+
this.emit('health:check', health);
|
|
667
|
+
|
|
668
|
+
// If unhealthy, log and potentially trigger shutdown
|
|
669
|
+
if (!health.healthy) {
|
|
670
|
+
console.error(`[RabbitMQClient] [mq-client-core] Health check FAILED:`, health.issues);
|
|
671
|
+
console.error(`[RabbitMQClient] [mq-client-core] Health details:`, JSON.stringify(health, null, 2));
|
|
672
|
+
|
|
673
|
+
// Track critical health start time
|
|
674
|
+
if (!this._criticalHealthStartTime) {
|
|
675
|
+
this._criticalHealthStartTime = Date.now();
|
|
676
|
+
console.error(`[RabbitMQClient] [mq-client-core] Critical health detected at ${new Date(this._criticalHealthStartTime).toISOString()}`);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// Report to monitoring if callback provided
|
|
680
|
+
if (this._healthReportCallback) {
|
|
681
|
+
try {
|
|
682
|
+
await this._healthReportCallback(health);
|
|
683
|
+
} catch (reportErr) {
|
|
684
|
+
console.error(`[RabbitMQClient] [mq-client-core] Health report callback failed:`, reportErr.message);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Call critical health callback if provided
|
|
689
|
+
if (this._healthCriticalCallback) {
|
|
690
|
+
try {
|
|
691
|
+
await this._healthCriticalCallback(health);
|
|
692
|
+
} catch (criticalErr) {
|
|
693
|
+
console.error(`[RabbitMQClient] [mq-client-core] Critical health callback failed:`, criticalErr.message);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Emit critical health event
|
|
698
|
+
this.emit('health:critical', health);
|
|
699
|
+
|
|
700
|
+
// Check if we should shutdown after delay
|
|
701
|
+
if (this._criticalHealthShutdown) {
|
|
702
|
+
const timeSinceCritical = Date.now() - this._criticalHealthStartTime;
|
|
703
|
+
if (timeSinceCritical >= this._criticalHealthShutdownDelay) {
|
|
704
|
+
console.error(`[RabbitMQClient] [mq-client-core] Critical health persisted for ${timeSinceCritical}ms (threshold: ${this._criticalHealthShutdownDelay}ms) - triggering shutdown`);
|
|
705
|
+
this.emit('health:shutdown', health);
|
|
706
|
+
// Note: Actual shutdown should be handled by the application using this event
|
|
707
|
+
} else {
|
|
708
|
+
const remaining = this._criticalHealthShutdownDelay - timeSinceCritical;
|
|
709
|
+
console.warn(`[RabbitMQClient] [mq-client-core] Critical health will trigger shutdown in ${remaining}ms if not resolved`);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
} else {
|
|
713
|
+
// Health is OK - reset critical health tracking
|
|
714
|
+
if (this._criticalHealthStartTime) {
|
|
715
|
+
const duration = Date.now() - this._criticalHealthStartTime;
|
|
716
|
+
console.log(`[RabbitMQClient] [mq-client-core] Health recovered after ${duration}ms of critical state`);
|
|
717
|
+
this._criticalHealthStartTime = null;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
console.log(`[RabbitMQClient] [mq-client-core] Health check OK: ${health.consumers.active}/${health.consumers.tracked} consumers active, ${health.queues.checked} queues checked`);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return health;
|
|
724
|
+
}
|
|
725
|
+
|
|
152
726
|
async disconnect() {
|
|
727
|
+
// Stop health monitoring
|
|
728
|
+
this._stopHealthMonitoring();
|
|
729
|
+
|
|
730
|
+
// Clear active consumers tracking
|
|
731
|
+
this._activeConsumers.clear();
|
|
732
|
+
|
|
153
733
|
try {
|
|
154
734
|
if (this._consumerChannel) {
|
|
155
735
|
await this._consumerChannel.close();
|
|
@@ -194,15 +774,14 @@ class RabbitMQClient extends EventEmitter {
|
|
|
194
774
|
* @throws {Error} If publish fails or channel is not available.
|
|
195
775
|
*/
|
|
196
776
|
async publish(queue, buffer, options = {}) {
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
777
|
+
// Ensure publisher channel exists and is open (auto-recreates if closed)
|
|
778
|
+
await this._ensurePublisherChannel();
|
|
779
|
+
|
|
780
|
+
// Track operation for debugging
|
|
781
|
+
this._trackChannelOperation(this._channel, `publish to ${queue}`);
|
|
203
782
|
|
|
204
783
|
// Log publish attempt for debugging
|
|
205
|
-
console.log(`[RabbitMQClient] [mq-client-core] [PUBLISH] Attempting to publish to queue "${queue}"
|
|
784
|
+
console.log(`[RabbitMQClient] [mq-client-core] [PUBLISH] Attempting to publish to queue "${queue}"`);
|
|
206
785
|
|
|
207
786
|
const exchange = this._config.exchange || '';
|
|
208
787
|
const routingKey = options.routingKey || queue;
|
|
@@ -217,21 +796,9 @@ class RabbitMQClient extends EventEmitter {
|
|
|
217
796
|
// If queue doesn't exist (404), we should NOT auto-create it - infrastructure queues must be created explicitly
|
|
218
797
|
// This prevents creating queues with wrong arguments (no TTL) which causes 406 errors later
|
|
219
798
|
try {
|
|
220
|
-
//
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (this._connection && !this._connection.closed) {
|
|
224
|
-
this._queueChannel = await this._connection.createChannel();
|
|
225
|
-
this._queueChannel.on('error', (err) => {
|
|
226
|
-
console.warn('[RabbitMQClient] Queue channel error:', err.message);
|
|
227
|
-
});
|
|
228
|
-
this._queueChannel.on('close', () => {
|
|
229
|
-
console.warn('[RabbitMQClient] Queue channel closed');
|
|
230
|
-
});
|
|
231
|
-
} else {
|
|
232
|
-
throw new Error('Cannot publish: connection is closed');
|
|
233
|
-
}
|
|
234
|
-
}
|
|
799
|
+
// Ensure queue channel exists and is open (auto-recreates if closed)
|
|
800
|
+
await this._ensureQueueChannel();
|
|
801
|
+
this._trackChannelOperation(this._queueChannel, `checkQueue ${queue}`);
|
|
235
802
|
await this._queueChannel.checkQueue(queue);
|
|
236
803
|
// Queue exists - proceed to publish
|
|
237
804
|
} catch (checkErr) {
|
|
@@ -259,6 +826,7 @@ class RabbitMQClient extends EventEmitter {
|
|
|
259
826
|
// For non-infrastructure queues, allow auto-creation with default options
|
|
260
827
|
const queueOptions = options.queueOptions || { durable: this._config.durable };
|
|
261
828
|
console.warn(`[RabbitMQClient] [mq-client-core] [PUBLISH] Auto-creating non-infrastructure queue ${queue} with default options (no TTL). This should be avoided for production.`);
|
|
829
|
+
this._trackChannelOperation(this._queueChannel, `assertQueue ${queue}`);
|
|
262
830
|
await this._queueChannel.assertQueue(queue, queueOptions);
|
|
263
831
|
} else {
|
|
264
832
|
// Other error - rethrow
|
|
@@ -266,23 +834,13 @@ class RabbitMQClient extends EventEmitter {
|
|
|
266
834
|
}
|
|
267
835
|
}
|
|
268
836
|
// Publish to queue using ConfirmChannel (for publisher confirms)
|
|
269
|
-
//
|
|
270
|
-
if (this._channel.closed) {
|
|
271
|
-
throw new Error(`Cannot publish to queue "${queue}": channel closed during operation (after queue check)`);
|
|
272
|
-
}
|
|
273
|
-
|
|
837
|
+
// Channel is guaranteed to be open (ensured above)
|
|
274
838
|
console.log(`[RabbitMQClient] [mq-client-core] [PUBLISH] Sending message to queue "${queue}" (size: ${buffer.length} bytes)`);
|
|
275
839
|
|
|
276
|
-
// Use callback-based confirmation
|
|
840
|
+
// Use callback-based confirmation - kanály jsou spolehlivé, takže callback vždy dorazí
|
|
277
841
|
const confirmPromise = new Promise((resolve, reject) => {
|
|
278
|
-
// Set up timeout
|
|
279
|
-
const timeout = setTimeout(() => {
|
|
280
|
-
reject(new Error(`Publisher confirm timeout after 5 seconds for queue "${queue}"`));
|
|
281
|
-
}, 5000);
|
|
282
|
-
|
|
283
842
|
// Send message with callback
|
|
284
|
-
|
|
285
|
-
clearTimeout(timeout);
|
|
843
|
+
this._channel.sendToQueue(queue, buffer, { persistent, headers }, (err, ok) => {
|
|
286
844
|
if (err) {
|
|
287
845
|
console.error(`[RabbitMQClient] [mq-client-core] [PUBLISH] Send callback error for queue "${queue}":`, err.message);
|
|
288
846
|
reject(err);
|
|
@@ -291,42 +849,17 @@ class RabbitMQClient extends EventEmitter {
|
|
|
291
849
|
resolve();
|
|
292
850
|
}
|
|
293
851
|
});
|
|
294
|
-
|
|
295
|
-
// If sendToQueue returns false, channel is full - wait for drain
|
|
296
|
-
if (!sent) {
|
|
297
|
-
console.log(`[RabbitMQClient] [mq-client-core] [PUBLISH] Channel full, waiting for drain for queue "${queue}"`);
|
|
298
|
-
this._channel.once('drain', () => {
|
|
299
|
-
console.log(`[RabbitMQClient] [mq-client-core] [PUBLISH] Channel drained for queue "${queue}"`);
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
852
|
});
|
|
303
853
|
|
|
304
|
-
|
|
305
|
-
await confirmPromise;
|
|
306
|
-
} catch (confirmErr) {
|
|
307
|
-
// Check if channel was closed during confirmation
|
|
308
|
-
if (this._channel.closed) {
|
|
309
|
-
const errorMsg = `Publisher confirm failed for queue "${queue}": channel closed during confirmation. Original error: ${confirmErr.message}`;
|
|
310
|
-
console.error('[RabbitMQClient] [mq-client-core] [PUBLISH]', errorMsg);
|
|
311
|
-
throw new Error(errorMsg);
|
|
312
|
-
}
|
|
313
|
-
throw confirmErr;
|
|
314
|
-
}
|
|
854
|
+
await confirmPromise;
|
|
315
855
|
} else {
|
|
316
856
|
// If exchange is specified, assert exchange and publish to it
|
|
317
|
-
|
|
318
|
-
throw new Error('Cannot publish: channel closed during operation');
|
|
319
|
-
}
|
|
857
|
+
// Channel is guaranteed to be open (ensured above)
|
|
320
858
|
await this._channel.assertExchange(exchange, 'direct', { durable: this._config.durable });
|
|
321
859
|
|
|
322
|
-
// Use callback-based confirmation
|
|
860
|
+
// Use callback-based confirmation - kanály jsou spolehlivé
|
|
323
861
|
const confirmPromise = new Promise((resolve, reject) => {
|
|
324
|
-
|
|
325
|
-
reject(new Error(`Publisher confirm timeout after 5 seconds for exchange "${exchange}"`));
|
|
326
|
-
}, 5000);
|
|
327
|
-
|
|
328
|
-
const sent = this._channel.publish(exchange, routingKey, buffer, { persistent, headers }, (err, ok) => {
|
|
329
|
-
clearTimeout(timeout);
|
|
862
|
+
this._channel.publish(exchange, routingKey, buffer, { persistent, headers }, (err, ok) => {
|
|
330
863
|
if (err) {
|
|
331
864
|
console.error(`[RabbitMQClient] [mq-client-core] [PUBLISH] Exchange publish callback error:`, err.message);
|
|
332
865
|
reject(err);
|
|
@@ -335,28 +868,22 @@ class RabbitMQClient extends EventEmitter {
|
|
|
335
868
|
resolve();
|
|
336
869
|
}
|
|
337
870
|
});
|
|
338
|
-
|
|
339
|
-
if (!sent) {
|
|
340
|
-
this._channel.once('drain', () => {
|
|
341
|
-
console.log(`[RabbitMQClient] [mq-client-core] [PUBLISH] Channel drained for exchange "${exchange}"`);
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
871
|
});
|
|
345
872
|
|
|
346
|
-
|
|
347
|
-
await confirmPromise;
|
|
348
|
-
} catch (confirmErr) {
|
|
349
|
-
if (this._channel.closed) {
|
|
350
|
-
throw new Error(`Publisher confirm failed for exchange "${exchange}": channel closed during confirmation. Original error: ${confirmErr.message}`);
|
|
351
|
-
}
|
|
352
|
-
throw confirmErr;
|
|
353
|
-
}
|
|
873
|
+
await confirmPromise;
|
|
354
874
|
}
|
|
355
875
|
} catch (err) {
|
|
356
|
-
// If channel was closed,
|
|
876
|
+
// If channel was closed, try to recreate and retry once
|
|
357
877
|
if (err.message && (err.message.includes('Channel closed') || err.message.includes('channel is closed') || this._channel?.closed)) {
|
|
358
|
-
console.warn('[RabbitMQClient] [mq-client-core] [PUBLISH] Channel closed during publish,
|
|
359
|
-
|
|
878
|
+
console.warn('[RabbitMQClient] [mq-client-core] [PUBLISH] Channel closed during publish, recreating and retrying...');
|
|
879
|
+
try {
|
|
880
|
+
await this._ensurePublisherChannel();
|
|
881
|
+
// Retry publish once
|
|
882
|
+
return await this.publish(queue, buffer, options);
|
|
883
|
+
} catch (retryErr) {
|
|
884
|
+
console.error('[RabbitMQClient] [mq-client-core] [PUBLISH] Retry failed:', retryErr.message);
|
|
885
|
+
throw retryErr;
|
|
886
|
+
}
|
|
360
887
|
}
|
|
361
888
|
this.emit('error', err);
|
|
362
889
|
throw err;
|
|
@@ -372,41 +899,11 @@ class RabbitMQClient extends EventEmitter {
|
|
|
372
899
|
* @throws {Error} If consume setup fails or channel is not available.
|
|
373
900
|
*/
|
|
374
901
|
async consume(queue, onMessage, options = {}) {
|
|
375
|
-
//
|
|
376
|
-
|
|
377
|
-
if (!this._consumerChannel) {
|
|
378
|
-
// Recreate consumer channel if closed
|
|
379
|
-
if (this._connection && !this._connection.closed) {
|
|
380
|
-
this._consumerChannel = await this._connection.createChannel();
|
|
381
|
-
this._consumerChannel.on('error', (err) => {
|
|
382
|
-
console.warn('[RabbitMQClient] Consumer channel error:', err.message);
|
|
383
|
-
this.emit('error', err);
|
|
384
|
-
});
|
|
385
|
-
this._consumerChannel.on('close', () => {
|
|
386
|
-
console.warn('[RabbitMQClient] Consumer channel closed');
|
|
387
|
-
this.emit('error', new Error('RabbitMQ consumer channel closed unexpectedly'));
|
|
388
|
-
});
|
|
389
|
-
} else {
|
|
390
|
-
throw new Error('Cannot consume: consumer channel is not initialized and connection is closed');
|
|
391
|
-
}
|
|
392
|
-
}
|
|
902
|
+
// Ensure consumer channel exists and is open (auto-recreates if closed and re-registers consumers)
|
|
903
|
+
await this._ensureConsumerChannel();
|
|
393
904
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
if (this._connection && !this._connection.closed) {
|
|
397
|
-
this._consumerChannel = await this._connection.createChannel();
|
|
398
|
-
this._consumerChannel.on('error', (err) => {
|
|
399
|
-
console.warn('[RabbitMQClient] Consumer channel error:', err.message);
|
|
400
|
-
this.emit('error', err);
|
|
401
|
-
});
|
|
402
|
-
this._consumerChannel.on('close', () => {
|
|
403
|
-
console.warn('[RabbitMQClient] Consumer channel closed');
|
|
404
|
-
this.emit('error', new Error('RabbitMQ consumer channel closed unexpectedly'));
|
|
405
|
-
});
|
|
406
|
-
} else {
|
|
407
|
-
throw new Error('Cannot consume: consumer channel is closed and connection is closed');
|
|
408
|
-
}
|
|
409
|
-
}
|
|
905
|
+
// Track operation for debugging
|
|
906
|
+
this._trackChannelOperation(this._consumerChannel, `consume from ${queue}`);
|
|
410
907
|
|
|
411
908
|
const durable = options.durable !== undefined ? options.durable : this._config.durable;
|
|
412
909
|
const prefetch = options.prefetch !== undefined ? options.prefetch : this._config.prefetch;
|
|
@@ -485,6 +982,7 @@ class RabbitMQClient extends EventEmitter {
|
|
|
485
982
|
|
|
486
983
|
try {
|
|
487
984
|
console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER] About to call assertQueue(${queue}, ${JSON.stringify(queueOptions)})`);
|
|
985
|
+
this._trackChannelOperation(this._queueChannel, `assertQueue ${queue}`);
|
|
488
986
|
await this._queueChannel.assertQueue(queue, queueOptions);
|
|
489
987
|
console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER] ✓ Queue ${queue} asserted successfully`);
|
|
490
988
|
} catch (assertErr) {
|
|
@@ -516,25 +1014,36 @@ class RabbitMQClient extends EventEmitter {
|
|
|
516
1014
|
|
|
517
1015
|
// Use dedicated consumer channel for consume operations
|
|
518
1016
|
// This prevents conflicts with ConfirmChannel used for publish operations
|
|
1017
|
+
const messageHandler = async (msg) => {
|
|
1018
|
+
if (msg === null) {
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
try {
|
|
1022
|
+
await onMessage(msg);
|
|
1023
|
+
if (!noAck) {
|
|
1024
|
+
this._consumerChannel.ack(msg);
|
|
1025
|
+
}
|
|
1026
|
+
} catch (handlerErr) {
|
|
1027
|
+
// Negative acknowledge and requeue by default
|
|
1028
|
+
this._consumerChannel.nack(msg, false, true);
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
|
|
519
1032
|
const consumeResult = await this._consumerChannel.consume(
|
|
520
1033
|
queue,
|
|
521
|
-
|
|
522
|
-
if (msg === null) {
|
|
523
|
-
return;
|
|
524
|
-
}
|
|
525
|
-
try {
|
|
526
|
-
await onMessage(msg);
|
|
527
|
-
if (!noAck) {
|
|
528
|
-
this._consumerChannel.ack(msg);
|
|
529
|
-
}
|
|
530
|
-
} catch (handlerErr) {
|
|
531
|
-
// Negative acknowledge and requeue by default
|
|
532
|
-
this._consumerChannel.nack(msg, false, true);
|
|
533
|
-
}
|
|
534
|
-
},
|
|
1034
|
+
messageHandler,
|
|
535
1035
|
{ noAck }
|
|
536
1036
|
);
|
|
537
1037
|
|
|
1038
|
+
// Track consumer for auto re-registration if channel closes
|
|
1039
|
+
this._activeConsumers.set(queue, {
|
|
1040
|
+
handler: messageHandler,
|
|
1041
|
+
options: { noAck, prefetch, durable, queueOptions },
|
|
1042
|
+
consumerTag: consumeResult.consumerTag
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
console.log(`[RabbitMQClient] [mq-client-core] [CONSUMER] ✓ Consumer registered for queue "${queue}" (consumerTag: ${consumeResult.consumerTag})`);
|
|
1046
|
+
|
|
538
1047
|
// Return consumer tag for cancellation
|
|
539
1048
|
return consumeResult.consumerTag;
|
|
540
1049
|
} catch (err) {
|