@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
package/src/ConnectorMQClient.js
CHANGED
|
@@ -46,11 +46,43 @@ class ConnectorMQClient extends BaseClient {
|
|
|
46
46
|
* });
|
|
47
47
|
*/
|
|
48
48
|
constructor(config = {}) {
|
|
49
|
-
|
|
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
|
-
|
|
361
|
-
|
|
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
|
-
|