@onlineapps/conn-infra-mq 1.1.51 → 1.1.53
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,60 @@ 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
|
+
// Setup queue creation callback for RecoveryWorker (delegates to QueueManager)
|
|
84
|
+
// This ensures RecoveryWorker uses QueueManager for queue creation instead of direct creation
|
|
85
|
+
// Note: We create a closure that will access this.queues after it's initialized
|
|
86
|
+
let queueManagerRef = null;
|
|
87
|
+
const queueCreationCallback = async (queueName, options = {}) => {
|
|
88
|
+
if (!queueManagerRef) {
|
|
89
|
+
throw new Error('QueueManager not initialized - cannot create queue via RecoveryWorker');
|
|
90
|
+
}
|
|
91
|
+
// Use ensureQueue for individual queues (RecoveryWorker handles single queue creation)
|
|
92
|
+
// Note: setupServiceQueues creates multiple queues, but RecoveryWorker only needs one
|
|
93
|
+
// However, ensureQueue throws for business queues, so we need to handle that
|
|
94
|
+
try {
|
|
95
|
+
await queueManagerRef.ensureQueue(queueName, options);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
// If ensureQueue fails because it's a business queue, we can't create it via RecoveryWorker
|
|
98
|
+
// RecoveryWorker should only handle transient errors, not business queue creation
|
|
99
|
+
// Business queues must be created via setupServiceQueues() after registration
|
|
100
|
+
throw new Error(`Cannot create business queue '${queueName}' via RecoveryWorker. Business queues must be created via setupServiceQueues() after registration.`);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
54
103
|
this.retry = new RetryHandler(this, config);
|
|
55
104
|
this.forkJoin = new ForkJoinHandler(this, this.queues, config);
|
|
56
105
|
|
|
@@ -2,268 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* queueConfig.js
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
*
|
|
6
|
+
* Re-exports queueConfig from @onlineapps/mq-client-core.
|
|
7
|
+
*
|
|
8
|
+
* NOTE: queueConfig is now part of mq-client-core (spodní vrstva) because it's used by both:
|
|
9
|
+
* - Infrastructure services (via mq-client-core)
|
|
10
|
+
* - Business services (via conn-infra-mq connector)
|
|
11
|
+
*
|
|
12
|
+
* This file maintains backward compatibility for existing code that imports from conn-infra-mq.
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
* Workflow infrastructure queue configurations
|
|
16
|
-
* These are shared infrastructure queues used by all services
|
|
17
|
-
*/
|
|
18
|
-
workflow: {
|
|
19
|
-
/**
|
|
20
|
-
* workflow.init - Entry point for all workflows
|
|
21
|
-
* All services listen as competing consumers
|
|
22
|
-
*/
|
|
23
|
-
init: {
|
|
24
|
-
durable: true,
|
|
25
|
-
arguments: {
|
|
26
|
-
'x-message-ttl': 300000, // 5 minutes TTL
|
|
27
|
-
'x-max-length': 10000 // Max 10k messages
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* workflow.completed - Completed workflows
|
|
33
|
-
*/
|
|
34
|
-
completed: {
|
|
35
|
-
durable: true,
|
|
36
|
-
arguments: {
|
|
37
|
-
'x-message-ttl': 300000, // 5 minutes TTL
|
|
38
|
-
'x-max-length': 10000
|
|
39
|
-
}
|
|
40
|
-
},
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* workflow.failed - Failed workflows
|
|
44
|
-
*/
|
|
45
|
-
failed: {
|
|
46
|
-
durable: true,
|
|
47
|
-
arguments: {
|
|
48
|
-
'x-message-ttl': 300000, // 5 minutes TTL
|
|
49
|
-
'x-max-length': 10000
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* workflow.dlq - Dead letter queue for workflows
|
|
55
|
-
*/
|
|
56
|
-
dlq: {
|
|
57
|
-
durable: true,
|
|
58
|
-
arguments: {
|
|
59
|
-
// No TTL for DLQ - messages should persist
|
|
60
|
-
'x-max-length': 50000 // Higher limit for DLQ
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
},
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Business queue templates
|
|
67
|
-
* These are templates for service-specific queues ({service}.workflow, {service}.queue, {service}.dlq)
|
|
68
|
-
* Used by QueueManager.setupServiceQueues() to ensure consistent configuration
|
|
69
|
-
*/
|
|
70
|
-
business: {
|
|
71
|
-
/**
|
|
72
|
-
* {service}.workflow - Service-specific workflow queue
|
|
73
|
-
* Used for routing workflow steps to specific services
|
|
74
|
-
*/
|
|
75
|
-
workflow: {
|
|
76
|
-
durable: true,
|
|
77
|
-
arguments: {
|
|
78
|
-
'x-message-ttl': 300000, // 5 minutes TTL
|
|
79
|
-
'x-max-length': 10000, // Max 10k messages
|
|
80
|
-
'x-dead-letter-exchange': 'dlx',
|
|
81
|
-
'x-dead-letter-routing-key': '{service}.dlq' // Placeholder, replaced with actual service name
|
|
82
|
-
}
|
|
83
|
-
},
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* {service}.queue - Service main processing queue
|
|
87
|
-
* Used for direct service-to-service communication
|
|
88
|
-
*/
|
|
89
|
-
queue: {
|
|
90
|
-
durable: true,
|
|
91
|
-
arguments: {
|
|
92
|
-
'x-message-ttl': 30000, // 30 seconds TTL
|
|
93
|
-
'x-max-length': 10000, // Max 10k messages
|
|
94
|
-
'x-dead-letter-exchange': 'dlx',
|
|
95
|
-
'x-dead-letter-routing-key': '{service}.dlq' // Placeholder, replaced with actual service name
|
|
96
|
-
}
|
|
97
|
-
},
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* {service}.dlq - Service dead letter queue
|
|
101
|
-
* Receives failed messages from service.queue and service.workflow
|
|
102
|
-
*/
|
|
103
|
-
dlq: {
|
|
104
|
-
durable: true,
|
|
105
|
-
arguments: {
|
|
106
|
-
// No TTL for DLQ - messages should persist
|
|
107
|
-
'x-max-length': 50000 // Higher limit for DLQ
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
},
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Registry infrastructure queue configurations
|
|
114
|
-
*/
|
|
115
|
-
registry: {
|
|
116
|
-
/**
|
|
117
|
-
* registry.register - Registration requests
|
|
118
|
-
*/
|
|
119
|
-
register: {
|
|
120
|
-
durable: true,
|
|
121
|
-
arguments: {
|
|
122
|
-
'x-message-ttl': 60000, // 1 minute TTL
|
|
123
|
-
'x-max-length': 1000
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* registry.heartbeats - Service heartbeats
|
|
129
|
-
*/
|
|
130
|
-
heartbeats: {
|
|
131
|
-
durable: true,
|
|
132
|
-
arguments: {
|
|
133
|
-
'x-message-ttl': 120000, // 2 minutes TTL
|
|
134
|
-
'x-max-length': 5000
|
|
135
|
-
}
|
|
136
|
-
},
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* registry.changes - Registry event fanout exchange
|
|
140
|
-
* (This is an exchange, not a queue, but included for reference)
|
|
141
|
-
*/
|
|
142
|
-
changes: {
|
|
143
|
-
type: 'exchange',
|
|
144
|
-
durable: true
|
|
145
|
-
}
|
|
146
|
-
},
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Get infrastructure queue configuration by type and name
|
|
150
|
-
* @param {string} type - Queue type: 'workflow', 'registry'
|
|
151
|
-
* @param {string} name - Queue name: 'init', 'completed', 'register', etc.
|
|
152
|
-
* @returns {Object} Queue configuration
|
|
153
|
-
*/
|
|
154
|
-
getQueueConfig(type, name) {
|
|
155
|
-
const config = this[type]?.[name];
|
|
156
|
-
if (!config) {
|
|
157
|
-
throw new Error(`Infrastructure queue config not found: ${type}.${name}`);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Deep clone to avoid modifying original
|
|
161
|
-
return JSON.parse(JSON.stringify(config));
|
|
162
|
-
},
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Get workflow infrastructure queue configuration
|
|
166
|
-
* @param {string} queueName - Queue name (e.g., 'workflow.init', 'workflow.completed')
|
|
167
|
-
* @returns {Object} Queue configuration
|
|
168
|
-
*/
|
|
169
|
-
getWorkflowQueueConfig(queueName) {
|
|
170
|
-
const parts = queueName.split('.');
|
|
171
|
-
if (parts.length !== 2 || parts[0] !== 'workflow') {
|
|
172
|
-
throw new Error(`Invalid workflow queue name: ${queueName}. Expected format: workflow.{name}`);
|
|
173
|
-
}
|
|
174
|
-
return this.getQueueConfig('workflow', parts[1]);
|
|
175
|
-
},
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Get registry infrastructure queue configuration
|
|
179
|
-
* @param {string} queueName - Queue name (e.g., 'registry.register', 'registry.heartbeats')
|
|
180
|
-
* @returns {Object} Queue configuration
|
|
181
|
-
*/
|
|
182
|
-
getRegistryQueueConfig(queueName) {
|
|
183
|
-
const parts = queueName.split('.');
|
|
184
|
-
if (parts.length !== 2 || parts[0] !== 'registry') {
|
|
185
|
-
throw new Error(`Invalid registry queue name: ${queueName}. Expected format: registry.{name}`);
|
|
186
|
-
}
|
|
187
|
-
return this.getQueueConfig('registry', parts[1]);
|
|
188
|
-
},
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Check if queue name is an infrastructure queue
|
|
192
|
-
* @param {string} queueName - Queue name to check
|
|
193
|
-
* @returns {boolean} True if infrastructure queue
|
|
194
|
-
*/
|
|
195
|
-
isInfrastructureQueue(queueName) {
|
|
196
|
-
return queueName.startsWith('workflow.') || queueName.startsWith('registry.');
|
|
197
|
-
},
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Check if queue name is a business queue
|
|
201
|
-
* @param {string} queueName - Queue name to check
|
|
202
|
-
* @returns {boolean} True if business queue
|
|
203
|
-
*/
|
|
204
|
-
isBusinessQueue(queueName) {
|
|
205
|
-
// Business queues follow pattern: {service}.{type}
|
|
206
|
-
// where type is: workflow, queue, dlq
|
|
207
|
-
const parts = queueName.split('.');
|
|
208
|
-
if (parts.length !== 2) return false;
|
|
209
|
-
return ['workflow', 'queue', 'dlq'].includes(parts[1]);
|
|
210
|
-
},
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Parse business queue name to extract service name and queue type
|
|
214
|
-
* @param {string} queueName - Business queue name (e.g., 'hello-service.workflow')
|
|
215
|
-
* @returns {Object} { serviceName, queueType } or null if not a business queue
|
|
216
|
-
*/
|
|
217
|
-
parseBusinessQueue(queueName) {
|
|
218
|
-
if (!this.isBusinessQueue(queueName)) {
|
|
219
|
-
return null;
|
|
220
|
-
}
|
|
221
|
-
const parts = queueName.split('.');
|
|
222
|
-
return {
|
|
223
|
-
serviceName: parts[0],
|
|
224
|
-
queueType: parts[1]
|
|
225
|
-
};
|
|
226
|
-
},
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Get business queue configuration template
|
|
230
|
-
* @param {string} queueType - Queue type: 'workflow', 'queue', 'dlq'
|
|
231
|
-
* @param {string} serviceName - Service name (replaces {service} placeholder)
|
|
232
|
-
* @returns {Object} Queue configuration
|
|
233
|
-
*/
|
|
234
|
-
getBusinessQueueConfig(queueType, serviceName) {
|
|
235
|
-
const template = this.business?.[queueType];
|
|
236
|
-
if (!template) {
|
|
237
|
-
throw new Error(`Business queue template not found: ${queueType}`);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Deep clone to avoid modifying original
|
|
241
|
-
const config = JSON.parse(JSON.stringify(template));
|
|
242
|
-
|
|
243
|
-
// Replace {service} placeholder in routing keys
|
|
244
|
-
if (config.arguments && serviceName) {
|
|
245
|
-
const routingKey = config.arguments['x-dead-letter-routing-key'];
|
|
246
|
-
if (routingKey && typeof routingKey === 'string') {
|
|
247
|
-
config.arguments['x-dead-letter-routing-key'] = routingKey.replace('{service}', serviceName);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return config;
|
|
252
|
-
},
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Get infrastructure queue configuration by queue name (auto-detect type)
|
|
256
|
-
* @param {string} queueName - Full queue name (e.g., 'workflow.init', 'registry.register')
|
|
257
|
-
* @returns {Object} Queue configuration
|
|
258
|
-
*/
|
|
259
|
-
getInfrastructureQueueConfig(queueName) {
|
|
260
|
-
if (queueName.startsWith('workflow.')) {
|
|
261
|
-
return this.getWorkflowQueueConfig(queueName);
|
|
262
|
-
} else if (queueName.startsWith('registry.')) {
|
|
263
|
-
return this.getRegistryQueueConfig(queueName);
|
|
264
|
-
} else {
|
|
265
|
-
throw new Error(`Queue ${queueName} is not an infrastructure queue. Infrastructure queues must start with 'workflow.' or 'registry.'`);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
};
|
|
269
|
-
|
|
15
|
+
// Re-export from mq-client-core
|
|
16
|
+
module.exports = require('@onlineapps/mq-client-core/src/config/queueConfig');
|
|
@@ -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
|
}
|
|
@@ -336,44 +379,45 @@ class RabbitMQClient extends EventEmitter {
|
|
|
336
379
|
const assertStartTime = Date.now();
|
|
337
380
|
console.log(`[RabbitMQClient] [CONSUMER] Asserting queue ${queue} before consume() at ${new Date().toISOString()}`);
|
|
338
381
|
console.log(`[RabbitMQClient] [CONSUMER] Queue options:`, JSON.stringify(queueOptions, null, 2));
|
|
382
|
+
console.log(`[RabbitMQClient] [CONSUMER] _queueChannel state: exists=${!!this._queueChannel}, closed=${this._queueChannel ? this._queueChannel.closed : 'N/A'}`);
|
|
339
383
|
|
|
340
|
-
// CRITICAL:
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
console.error('[RabbitMQClient] Channel created at:', this._queueChannel._createdAt);
|
|
355
|
-
console.error('[RabbitMQClient] Last operation:', this._queueChannel._lastOperation);
|
|
356
|
-
this._queueChannel._closeReason = `Error: ${err.message} (code: ${err.code})`;
|
|
357
|
-
});
|
|
358
|
-
this._queueChannel.on('close', () => {
|
|
359
|
-
console.error('[RabbitMQClient] Queue channel closed');
|
|
360
|
-
console.error('[RabbitMQClient] Channel created at:', this._queueChannel._createdAt);
|
|
361
|
-
console.error('[RabbitMQClient] Close reason:', this._queueChannel._closeReason || 'Unknown');
|
|
362
|
-
console.error('[RabbitMQClient] Last operation:', this._queueChannel._lastOperation);
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
console.log(`[RabbitMQClient] [CONSUMER] ✓ _queueChannel recreated`);
|
|
366
|
-
} catch (recreateErr) {
|
|
367
|
-
console.error(`[RabbitMQClient] [CONSUMER] ✗ Failed to recreate _queueChannel:`, recreateErr.message);
|
|
368
|
-
throw new Error(`Cannot assertQueue: _queueChannel is closed and recreation failed: ${recreateErr.message}`);
|
|
369
|
-
}
|
|
384
|
+
// CRITICAL: Check if _queueChannel is closed - if so, this indicates a previous 406 error
|
|
385
|
+
// The root cause is that assertQueue() was called without parameters somewhere else
|
|
386
|
+
// We should NOT recreate the channel - we should fix the root cause
|
|
387
|
+
if (this._queueChannel && this._queueChannel.closed) {
|
|
388
|
+
const errorMsg = `[RabbitMQClient] [CONSUMER] ✗ CRITICAL: _queueChannel is CLOSED (likely due to 406 PRECONDITION-FAILED). This means assertQueue() was called without parameters somewhere else. Root cause must be fixed - do NOT recreate channel!`;
|
|
389
|
+
console.error(errorMsg);
|
|
390
|
+
console.error(`[RabbitMQClient] [CONSUMER] Channel was created at: ${this._queueChannel._createdAt}`);
|
|
391
|
+
console.error(`[RabbitMQClient] [CONSUMER] Close reason: ${this._queueChannel._closeReason || 'Unknown'}`);
|
|
392
|
+
console.error(`[RabbitMQClient] [CONSUMER] Last operation: ${this._queueChannel._lastOperation || 'Unknown'}`);
|
|
393
|
+
throw new Error(`Cannot assertQueue: _queueChannel is closed due to 406 error. Root cause: assertQueue() was called without parameters. Fix the root cause instead of recreating channel.`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (!this._queueChannel) {
|
|
397
|
+
throw new Error(`Cannot assertQueue: _queueChannel is null. Connection may not be established.`);
|
|
370
398
|
}
|
|
371
399
|
|
|
372
400
|
// Use queueChannel for assertQueue to avoid RPC reply queue issues
|
|
373
401
|
const channelForAssert = this._queueChannel;
|
|
374
|
-
this._queueChannel._lastOperation = `About to assertQueue ${queue}`;
|
|
375
|
-
|
|
376
|
-
|
|
402
|
+
this._queueChannel._lastOperation = `About to assertQueue ${queue} with options: ${JSON.stringify(queueOptions)}`;
|
|
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
|
+
}
|
|
377
421
|
|
|
378
422
|
const assertEndTime = Date.now();
|
|
379
423
|
console.log(`[RabbitMQClient] [CONSUMER] ✓ Queue ${queue} asserted (took ${assertEndTime - assertStartTime}ms)`);
|
|
@@ -1,72 +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
|
-
// Create queue with unified config
|
|
49
|
-
await channel.assertQueue(queueName, {
|
|
50
|
-
durable: config.durable !== false,
|
|
51
|
-
arguments: { ...config.arguments }
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
logger.log(`[QueueInit] ✓ Created infrastructure queue: ${queueName}`);
|
|
55
|
-
} catch (error) {
|
|
56
|
-
// If queue exists with different args (406), log warning but continue
|
|
57
|
-
if (error.code === 406) {
|
|
58
|
-
logger.warn(`[QueueInit] ⚠ Queue ${queueName} exists with different arguments - using as-is`);
|
|
59
|
-
} else {
|
|
60
|
-
logger.error(`[QueueInit] ✗ Failed to create ${queueName}:`, error.message);
|
|
61
|
-
throw error;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
logger.log(`[QueueInit] Infrastructure queues initialization complete`);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
module.exports = {
|
|
70
|
-
initInfrastructureQueues
|
|
71
|
-
};
|
|
72
|
-
|