@onlineapps/conn-infra-mq 1.1.52 → 1.1.54

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onlineapps/conn-infra-mq",
3
- "version": "1.1.52",
3
+ "version": "1.1.54",
4
4
  "description": "A promise-based, broker-agnostic client for sending and receiving messages via RabbitMQ",
5
5
  "main": "src/index.js",
6
6
  "repository": {
@@ -46,11 +46,43 @@ class ConnectorMQClient extends BaseClient {
46
46
  * });
47
47
  */
48
48
  constructor(config = {}) {
49
- super(config);
49
+ // Setup queue creation callback for RecoveryWorker (delegates to QueueManager)
50
+ // This ensures RecoveryWorker uses QueueManager for queue creation instead of direct creation
51
+ // Note: We create a closure that will access this.queues after it's initialized
52
+ let queueManagerRef = null;
53
+ const queueCreationCallback = async (queueName, options = {}) => {
54
+ if (!queueManagerRef) {
55
+ throw new Error('QueueManager not initialized - cannot create queue via RecoveryWorker');
56
+ }
57
+ // Use ensureQueue for individual queues (RecoveryWorker handles single queue creation)
58
+ // Note: setupServiceQueues creates multiple queues, but RecoveryWorker only needs one
59
+ // However, ensureQueue throws for business queues, so we need to handle that
60
+ try {
61
+ await queueManagerRef.ensureQueue(queueName, options);
62
+ } catch (err) {
63
+ // If ensureQueue fails because it's a business queue, we can't create it via RecoveryWorker
64
+ // RecoveryWorker should only handle transient errors, not business queue creation
65
+ // Business queues must be created via setupServiceQueues() after registration
66
+ throw new Error(`Cannot create business queue '${queueName}' via RecoveryWorker. Business queues must be created via setupServiceQueues() after registration.`);
67
+ }
68
+ };
69
+
70
+ // Pass queueCreationCallback to config for RecoveryWorker
71
+ const enhancedConfig = {
72
+ ...config,
73
+ queueCreationCallback,
74
+ recoveryScope: config.recoveryScope || 'business', // Business services can create queues
75
+ };
76
+
77
+ super(enhancedConfig);
50
78
 
51
79
  // Initialize layers
52
80
  this.workflow = new WorkflowRouter(this, config);
53
81
  this.queues = new QueueManager(this, config);
82
+
83
+ // Set reference for queueCreationCallback closure
84
+ queueManagerRef = this.queues;
85
+
54
86
  this.retry = new RetryHandler(this, config);
55
87
  this.forkJoin = new ForkJoinHandler(this, this.queues, config);
56
88
 
@@ -13,6 +13,7 @@ class RetryHandler {
13
13
  maxDelay: config.maxDelay || 30000,
14
14
  backoffMultiplier: config.backoffMultiplier || 2,
15
15
  dlqSuffix: config.dlqSuffix || '.dlq',
16
+ emitMonitoringEvents: config.emitMonitoringEvents !== false, // Enable by default
16
17
  ...config
17
18
  };
18
19
  this.retryStats = {
@@ -22,6 +23,36 @@ class RetryHandler {
22
23
  dlqMessages: 0
23
24
  };
24
25
  }
26
+
27
+ /**
28
+ * Emit retry event to monitoring (non-blocking)
29
+ * @private
30
+ */
31
+ async _emitRetryEvent(eventType, data) {
32
+ if (!this.config.emitMonitoringEvents) return;
33
+
34
+ try {
35
+ const { publishToMonitoringWorkflow } = require('@onlineapps/mq-client-core').monitoring;
36
+
37
+ await publishToMonitoringWorkflow(this.client, {
38
+ event_type: eventType,
39
+ workflow_id: data.workflow_id || data.message?.workflow_id || 'unknown',
40
+ timestamp: new Date().toISOString(),
41
+ retry_info: {
42
+ queue: data.queue,
43
+ retry_count: data.retryCount,
44
+ max_retries: data.maxRetries || this.config.maxRetries,
45
+ delay_ms: data.delay,
46
+ error_message: data.error?.message,
47
+ error_code: data.error?.code
48
+ }
49
+ }, null, { source: 'RetryHandler' });
50
+
51
+ } catch (e) {
52
+ // Silent fail - don't break retry logic due to monitoring
53
+ console.warn('[RetryHandler] Failed to emit monitoring event:', e.message);
54
+ }
55
+ }
25
56
 
26
57
  /**
27
58
  * Process message with retry logic
@@ -75,12 +106,23 @@ class RetryHandler {
75
106
 
76
107
  if (retryCount < maxRetries) {
77
108
  // Retry the message
109
+ const delay = this.calculateDelay(retryCount + 1);
78
110
  await this.retryMessage(enrichedMessage, queueName, retryCount + 1, options);
79
111
 
80
112
  // Acknowledge original to remove from queue
81
113
  await this.client.ack(rawMsg);
82
114
 
83
115
  this.retryStats.totalRetries++;
116
+
117
+ // Emit retry event to monitoring
118
+ this._emitRetryEvent('message_retry', {
119
+ workflow_id: message.workflow_id,
120
+ queue: queueName,
121
+ retryCount: retryCount + 1,
122
+ maxRetries,
123
+ delay,
124
+ error
125
+ });
84
126
 
85
127
  return { success: false, retried: true, retryCount: retryCount + 1 };
86
128
  } else {
@@ -92,6 +134,15 @@ class RetryHandler {
92
134
 
93
135
  this.retryStats.failedRetries++;
94
136
  this.retryStats.dlqMessages++;
137
+
138
+ // Emit DLQ event to monitoring
139
+ this._emitRetryEvent('message_dlq', {
140
+ workflow_id: message.workflow_id,
141
+ queue: queueName,
142
+ retryCount,
143
+ maxRetries,
144
+ error
145
+ });
95
146
 
96
147
  return { success: false, retried: false, dlq: true, retryCount };
97
148
  }
@@ -197,6 +197,36 @@ class RabbitMQClient extends EventEmitter {
197
197
  }
198
198
  }
199
199
 
200
+ /**
201
+ * Emits structured PRECONDITION_FAILED events and invokes optional hooks.
202
+ * @param {Object} context - Details about the failing operation
203
+ * @param {Error} error - Original amqplib error
204
+ * @private
205
+ */
206
+ _handlePreconditionFailed(context, error) {
207
+ const event = {
208
+ timestamp: new Date().toISOString(),
209
+ serviceName: this._config.serviceName || this._config.clientId || 'unknown',
210
+ host: this._config.host,
211
+ ...context,
212
+ error: {
213
+ message: error?.message,
214
+ code: error?.code,
215
+ stack: error?.stack
216
+ }
217
+ };
218
+
219
+ this.emit('preconditionFailed', event);
220
+
221
+ if (typeof this._config.onPreconditionFailed === 'function') {
222
+ try {
223
+ this._config.onPreconditionFailed(event);
224
+ } catch (hookError) {
225
+ console.warn('[RabbitMQClient] onPreconditionFailed hook threw error:', hookError.message);
226
+ }
227
+ }
228
+ }
229
+
200
230
  /**
201
231
  * Publishes a message buffer to the specified queue (or default queue) or exchange.
202
232
  * @param {string} queue - Target queue name.
@@ -265,6 +295,19 @@ class RabbitMQClient extends EventEmitter {
265
295
  // Wait for confirmation
266
296
  await this._channel.waitForConfirms();
267
297
  } catch (err) {
298
+ if (err?.code === 406) {
299
+ this._handlePreconditionFailed(
300
+ {
301
+ operation: 'publish',
302
+ queueName: queue,
303
+ routingKey,
304
+ payloadSample: buffer?.toString?.().slice(0, 512) || null,
305
+ persistent,
306
+ headers
307
+ },
308
+ err
309
+ );
310
+ }
268
311
  this.emit('error', err);
269
312
  throw err;
270
313
  }
@@ -357,8 +400,24 @@ class RabbitMQClient extends EventEmitter {
357
400
  // Use queueChannel for assertQueue to avoid RPC reply queue issues
358
401
  const channelForAssert = this._queueChannel;
359
402
  this._queueChannel._lastOperation = `About to assertQueue ${queue} with options: ${JSON.stringify(queueOptions)}`;
360
- await channelForAssert.assertQueue(queue, queueOptions);
361
- this._queueChannel._lastOperation = `assertQueue ${queue} succeeded`;
403
+ try {
404
+ await channelForAssert.assertQueue(queue, queueOptions);
405
+ this._queueChannel._lastOperation = `assertQueue ${queue} succeeded`;
406
+ } catch (assertErr) {
407
+ this._queueChannel._closeReason = `assertQueue failed: ${assertErr.message}`;
408
+ if (assertErr?.code === 406) {
409
+ this._handlePreconditionFailed(
410
+ {
411
+ operation: 'assertQueue',
412
+ phase: 'consume.setup',
413
+ queueName: queue,
414
+ queueOptions
415
+ },
416
+ assertErr
417
+ );
418
+ }
419
+ throw assertErr;
420
+ }
362
421
 
363
422
  const assertEndTime = Date.now();
364
423
  console.log(`[RabbitMQClient] [CONSUMER] ✓ Queue ${queue} asserted (took ${assertEndTime - assertStartTime}ms)`);
@@ -1,92 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * initInfrastructureQueues.js
5
- *
6
- * Unified tool for creating all infrastructure queues.
7
- * Used by ALL relevant services (infrastructure AND business) during startup.
8
- *
9
- * Ensures consistent queue parameters across all services.
10
- */
11
-
12
- const queueConfig = require('../config/queueConfig');
13
-
14
- /**
15
- * Initialize all infrastructure queues
16
- * @param {Object} channel - RabbitMQ channel
17
- * @param {Object} options - Options
18
- * @param {Array<string>} [options.queues] - Specific queues to create (default: all infrastructure queues)
19
- * @param {Object} [options.logger] - Logger instance (default: console)
20
- * @returns {Promise<void>}
21
- */
22
- async function initInfrastructureQueues(channel, options = {}) {
23
- const logger = options.logger || console;
24
- const queuesToCreate = options.queues || [
25
- // Workflow infrastructure queues
26
- 'workflow.init',
27
- 'workflow.completed',
28
- 'workflow.failed',
29
- 'workflow.dlq',
30
- // Registry infrastructure queues
31
- 'registry.register',
32
- 'registry.heartbeats'
33
- ];
34
-
35
- logger.log(`[QueueInit] Initializing ${queuesToCreate.length} infrastructure queues...`);
36
-
37
- for (const queueName of queuesToCreate) {
38
- try {
39
- // Verify it's an infrastructure queue
40
- if (!queueConfig.isInfrastructureQueue(queueName)) {
41
- logger.warn(`[QueueInit] Skipping ${queueName} - not an infrastructure queue`);
42
- continue;
43
- }
44
-
45
- // Get unified configuration
46
- const config = queueConfig.getInfrastructureQueueConfig(queueName);
47
-
48
- // CRITICAL: Check if queue exists with different arguments (406 error)
49
- // If so, delete it and recreate with correct parameters
50
- try {
51
- await channel.assertQueue(queueName, {
52
- durable: config.durable !== false,
53
- arguments: { ...config.arguments }
54
- });
55
- logger.log(`[QueueInit] ✓ Created/verified infrastructure queue: ${queueName}`);
56
- } catch (assertError) {
57
- if (assertError.code === 406) {
58
- // Queue exists with different arguments - delete and recreate
59
- logger.warn(`[QueueInit] ⚠ Queue ${queueName} exists with different arguments - deleting and recreating...`);
60
- try {
61
- await channel.deleteQueue(queueName);
62
- logger.log(`[QueueInit] ✓ Deleted queue ${queueName} with incorrect parameters`);
63
- } catch (deleteError) {
64
- logger.error(`[QueueInit] ✗ Failed to delete queue ${queueName}:`, deleteError.message);
65
- throw new Error(`Cannot delete queue ${queueName} with incorrect parameters: ${deleteError.message}`);
66
- }
67
-
68
- // Recreate with correct parameters
69
- await channel.assertQueue(queueName, {
70
- durable: config.durable !== false,
71
- arguments: { ...config.arguments }
72
- });
73
- logger.log(`[QueueInit] ✓ Recreated infrastructure queue: ${queueName} with correct parameters`);
74
- } else {
75
- // Other error - rethrow
76
- logger.error(`[QueueInit] ✗ Failed to create ${queueName}:`, assertError.message);
77
- throw assertError;
78
- }
79
- }
80
- } catch (error) {
81
- logger.error(`[QueueInit] ✗ Failed to initialize queue ${queueName}:`, error.message);
82
- throw error;
83
- }
84
- }
85
-
86
- logger.log(`[QueueInit] Infrastructure queues initialization complete`);
87
- }
88
-
89
- module.exports = {
90
- initInfrastructureQueues
91
- };
92
-