@platform-x/hep-message-broker-client 1.1.28 → 1.1.29
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/dist/src/config/index.js +9 -8
- package/dist/src/messageBroker/rabbitmq/MessageBroker.js +76 -10
- package/dist/src/messageBroker/rabbitmq/MessageBrokerClient.js +18 -187
- package/package.json +1 -1
- package/rabbitMQConfig.json +344 -0
- package/dist/src/Util/commonUtil.js.map +0 -1
- package/dist/src/Util/constants.js.map +0 -1
- package/dist/src/Util/logger.js.map +0 -1
- package/dist/src/Util/requestTracer.js.map +0 -1
- package/dist/src/config/ConfigManager.js.map +0 -1
- package/dist/src/config/index.js.map +0 -1
- package/dist/src/index.js.map +0 -1
- package/dist/src/messageBroker/BaseRabbitMQClient.js.map +0 -1
- package/dist/src/messageBroker/ConnectionManager.js.map +0 -1
- package/dist/src/messageBroker/MessageBrokerClient.js.map +0 -1
- package/dist/src/messageBroker/MessageConsumer.js.map +0 -1
- package/dist/src/messageBroker/MessageProducer.js.map +0 -1
- package/dist/src/messageBroker/RabbitMQClient.js.map +0 -1
- package/dist/src/messageBroker/RetryManager.js.map +0 -1
- package/dist/src/messageBroker/interface/ConnectionWrapper.js.map +0 -1
- package/dist/src/messageBroker/interface/IMessageBrokerClient.js.map +0 -1
- package/dist/src/messageBroker/rabbitmq/MessageBroker.js.map +0 -1
- package/dist/src/messageBroker/rabbitmq/MessageBrokerClient.js.map +0 -1
- package/dist/src/messageBroker/types/ActionType.js.map +0 -1
- package/dist/src/messageBroker/types/PublishMessageInputType.js.map +0 -1
- package/dist/src/models/MessageModel.js.map +0 -1
- package/dist/src/models/NotificationMessageModel.js.map +0 -1
- package/src/Util/commonUtil.ts +0 -41
- package/src/Util/constants.ts +0 -9
- package/src/Util/logger.ts +0 -219
- package/src/Util/requestTracer.ts +0 -28
- package/src/config/ConfigManager.ts +0 -35
- package/src/config/index.ts +0 -38
- package/src/index.ts +0 -74
- package/src/messageBroker/BaseRabbitMQClient.ts +0 -30
- package/src/messageBroker/ConnectionManager.ts +0 -182
- package/src/messageBroker/MessageBrokerClient.ts +0 -88
- package/src/messageBroker/MessageConsumer.ts +0 -85
- package/src/messageBroker/MessageProducer.ts +0 -142
- package/src/messageBroker/RabbitMQClient.ts +0 -47
- package/src/messageBroker/RetryManager.ts +0 -64
- package/src/messageBroker/interface/ConnectionWrapper.ts +0 -7
- package/src/messageBroker/interface/IMessageBrokerClient.ts +0 -11
- package/src/messageBroker/rabbitmq/MessageBroker.ts +0 -547
- package/src/messageBroker/rabbitmq/MessageBrokerClient.ts +0 -855
- package/src/messageBroker/types/ActionType.ts +0 -1
- package/src/messageBroker/types/PublishMessageInputType.ts +0 -8
- package/src/models/MessageModel.ts +0 -14
- package/src/models/NotificationMessageModel.ts +0 -0
- package/tsconfig.json +0 -73
|
@@ -1,855 +0,0 @@
|
|
|
1
|
-
// Import the amqplib library for RabbitMQ
|
|
2
|
-
import * as amqp from 'amqplib';
|
|
3
|
-
import config from '../../config';
|
|
4
|
-
import { Logger } from '../../Util/logger';
|
|
5
|
-
import ConnectionWrapper from '../interface/ConnectionWrapper';
|
|
6
|
-
import ConfigManager from '../../config/ConfigManager';
|
|
7
|
-
import { PublishMessageInputType } from '../types/PublishMessageInputType';
|
|
8
|
-
import MessageModel from '../../models/MessageModel';
|
|
9
|
-
import { ActionType } from '../types/ActionType';
|
|
10
|
-
import { SECRET_KEYS } from '../../Util/constants';
|
|
11
|
-
import { getIAMSecrets } from '../..';
|
|
12
|
-
// Remove lodash delay import
|
|
13
|
-
let configManager: ConfigManager = ConfigManager.getInstance();
|
|
14
|
-
// Declare variables for the connection and channel
|
|
15
|
-
let connection: amqp.Connection;
|
|
16
|
-
let channel: amqp.Channel;
|
|
17
|
-
// Declare variable for check connection
|
|
18
|
-
let isConnectionOpen = false;
|
|
19
|
-
|
|
20
|
-
let configData: any = {};
|
|
21
|
-
const RABBITMQ = config?.RABBITMQ;
|
|
22
|
-
let maxRetries: number = RABBITMQ?.MAX_RETRIES;
|
|
23
|
-
let retry_delay: any = RABBITMQ?.TTL?.split("|");
|
|
24
|
-
// let dlx_exchange:string = RABBITMQ.DLX_EXCHANGE;
|
|
25
|
-
let dlx_queue:string = RABBITMQ.DLX_QUEUE;
|
|
26
|
-
// let retry_exchange:string = RABBITMQ.RETRY_EXCHANGE;
|
|
27
|
-
// let retry_queue:string = RABBITMQ.RETRY_QUEUE;
|
|
28
|
-
let connectionRetry:number = 1;
|
|
29
|
-
let retryCountHeader = 'x-retry-count';
|
|
30
|
-
|
|
31
|
-
// Registry of active consumers so they can be re-subscribed after any
|
|
32
|
-
// (re)connect. Without this, a connection blip brings the socket back up but
|
|
33
|
-
// leaves every queue with no consumer attached (the primary "consumer
|
|
34
|
-
// frequently detaches" bug). Keyed by queueName to avoid duplicate
|
|
35
|
-
// subscriptions on the same queue.
|
|
36
|
-
interface RegisteredConsumer {
|
|
37
|
-
queueName: string;
|
|
38
|
-
classInstance: any;
|
|
39
|
-
consumerHandler: string;
|
|
40
|
-
consumerErrorHandler: string;
|
|
41
|
-
}
|
|
42
|
-
let registeredConsumers: Map<string, RegisteredConsumer> = new Map();
|
|
43
|
-
// Guards handleReconnect() so overlapping close/error events don't spawn
|
|
44
|
-
// multiple concurrent reconnect loops.
|
|
45
|
-
let isReconnecting = false;
|
|
46
|
-
|
|
47
|
-
export class MessageBrokerClient {
|
|
48
|
-
|
|
49
|
-
// private readonly correlationId: string;
|
|
50
|
-
// constructor() {
|
|
51
|
-
// this.correlationId = generateCorrelationId();
|
|
52
|
-
// }
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Function to check the status of the connection
|
|
56
|
-
* @returns boolean
|
|
57
|
-
*/
|
|
58
|
-
public async isConnected(): Promise<boolean> {
|
|
59
|
-
return isConnectionOpen;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Function to initialize the connection, channel and setup the queues and exchanges
|
|
65
|
-
* @param reqData
|
|
66
|
-
* @returns
|
|
67
|
-
*/
|
|
68
|
-
public async initialize() {
|
|
69
|
-
Logger.info('Reached to initialize', 'initialize');
|
|
70
|
-
configData = configManager.getConfig();
|
|
71
|
-
// dlx_exchange = configData?.dlx_exchange;
|
|
72
|
-
dlx_queue = configData?.dlx_queue;
|
|
73
|
-
// retry_exchange = configData?.retry_exchange;
|
|
74
|
-
// retry_queue = configData?.retry_queue;
|
|
75
|
-
let retries: number = 0;
|
|
76
|
-
// Retrie times connection
|
|
77
|
-
while (retries < maxRetries) {
|
|
78
|
-
try {
|
|
79
|
-
if(RABBITMQ.CONNECTION_ERROR === 'true' || RABBITMQ.CONNECTION_ERROR === true){
|
|
80
|
-
throw new Error("Error in connection");
|
|
81
|
-
}
|
|
82
|
-
await this.createConnection();
|
|
83
|
-
return { connection: connection, channel: channel };
|
|
84
|
-
} catch (err) {
|
|
85
|
-
retries++;
|
|
86
|
-
Logger.error('initialize:Failed to connect to RabbitMQ', `Attempt ${retries} of ${maxRetries}.`);
|
|
87
|
-
if (retries >= maxRetries) {
|
|
88
|
-
Logger.error('Max retries reached, cannot connect to RabbitMQ.', 'initialize');
|
|
89
|
-
return { connection, channel };
|
|
90
|
-
}
|
|
91
|
-
// Wait before retrying (e.g., 10 seconds)
|
|
92
|
-
await new Promise(resolve => setTimeout(resolve, 10000));
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Function to publish a message with retry
|
|
101
|
-
* @param request
|
|
102
|
-
* @param classInstance
|
|
103
|
-
* @param messagePublisherErrorHandler
|
|
104
|
-
* @returns
|
|
105
|
-
*/
|
|
106
|
-
public async publishMessage(
|
|
107
|
-
request: PublishMessageInputType
|
|
108
|
-
// , classInstance:any, messagePublisherErrorHandler:string
|
|
109
|
-
){
|
|
110
|
-
Logger.info('Reached:publishMessage', 'publishMessage');
|
|
111
|
-
// Logger.debug('Reached:publishMessageToExchange', 'publishMessageToExchange',{classInstance, messagePublisherErrorHandler});
|
|
112
|
-
try{
|
|
113
|
-
if(isConnectionOpen){
|
|
114
|
-
Logger.info('Check connection open or not', 'publishMessage');
|
|
115
|
-
const queueName: string = request.queueName;
|
|
116
|
-
const data: MessageModel = request.message;
|
|
117
|
-
const messageBuffer = Buffer.from(JSON.stringify(data?.message));
|
|
118
|
-
// let attempt: number = 0;
|
|
119
|
-
// let retries: number = typeof data.message === 'object' && data.message !== null ? (data.message as { retries: number }).retries ?? 0 : 0;
|
|
120
|
-
// while (attempt < maxRetries ) {
|
|
121
|
-
Logger.info('Max retries check', 'publishMessage');
|
|
122
|
-
const isSent = await new Promise<boolean>((resolve, reject) => {
|
|
123
|
-
let sent = channel.sendToQueue(queueName, Buffer.from(messageBuffer), {
|
|
124
|
-
persistent: true, // Ensures the message is persisted
|
|
125
|
-
// correlationId: this.correlationId,
|
|
126
|
-
});
|
|
127
|
-
if (sent) {
|
|
128
|
-
Logger.debug(`Message sent successfully`, 'publishMessage', { data, queueName });
|
|
129
|
-
resolve(true);
|
|
130
|
-
} else {
|
|
131
|
-
Logger.error(`Failed to publish message.`, 'publishMessage', { message: messageBuffer.toString() });
|
|
132
|
-
reject(false);
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
if((isSent) && (RABBITMQ.PUBLISHER_ERROR !== 'true' && RABBITMQ.PUBLISHER_ERROR !== true)){
|
|
136
|
-
return isSent;
|
|
137
|
-
}
|
|
138
|
-
// else{
|
|
139
|
-
// let queueName = (request as PublishMessageInputType)?.queueName
|
|
140
|
-
// if (attempt >= maxRetries) {
|
|
141
|
-
// Logger.error('Max retries reached. Failed to publish message.', 'publishMessage', request);
|
|
142
|
-
// // Optionally: Log or persist the failed message to a DB, file, or other storage for manual intervention
|
|
143
|
-
// this.sendMessageToDeadLatter(attempt, data, queueName);
|
|
144
|
-
// this.messagePublisherErrorHandler(request); // we need to comment this line
|
|
145
|
-
// if (typeof classInstance[messagePublisherErrorHandler] === 'function') {
|
|
146
|
-
// classInstance[messagePublisherErrorHandler](data); // Dynamically call the method
|
|
147
|
-
// } else {
|
|
148
|
-
// Logger.info(`Method ${messagePublisherErrorHandler} not found in classInstance.`, 'publishMessage');
|
|
149
|
-
// }
|
|
150
|
-
// return false;
|
|
151
|
-
// }else{
|
|
152
|
-
// await this.publishMessageRetryQueue(request, attempt);
|
|
153
|
-
// attempt++;
|
|
154
|
-
// }
|
|
155
|
-
// }
|
|
156
|
-
// }
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
} catch (error) {
|
|
160
|
-
Logger.error(`Error sending message: ${error}`, 'sendMessage');
|
|
161
|
-
throw new Error("Failed to publish message to queue");
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Function to publish a message with retry
|
|
167
|
-
* @param request
|
|
168
|
-
* @param classInstance
|
|
169
|
-
* @param messagePublisherErrorHandler
|
|
170
|
-
* @returns
|
|
171
|
-
*/
|
|
172
|
-
public async publishMessageToExchange(request: PublishMessageInputType): Promise<boolean> {
|
|
173
|
-
Logger.info('Reached:publishMessageToExchange', 'publishMessageToExchange');
|
|
174
|
-
let attempt = 0;
|
|
175
|
-
const exchangeName: string | undefined = request?.exchangeName ?? '';
|
|
176
|
-
const queueName: string = request.queueName;
|
|
177
|
-
const data: MessageModel = request?.message;
|
|
178
|
-
const messageBuffer = Buffer.from(JSON.stringify(data.message));
|
|
179
|
-
try {
|
|
180
|
-
// 1. Ensure connection and channel exists
|
|
181
|
-
await this.checkConnectionAndChannel();
|
|
182
|
-
|
|
183
|
-
// 2. Retry loop
|
|
184
|
-
while (attempt < maxRetries) {
|
|
185
|
-
try {
|
|
186
|
-
Logger.info(`Attempting to publish message`, 'publishMessageToExchange');
|
|
187
|
-
let sent = channel.publish(exchangeName, queueName, messageBuffer, {
|
|
188
|
-
persistent: true,
|
|
189
|
-
});
|
|
190
|
-
// if(attempt <1){ sent = false}else{sent = true} // Simulate failure for testing retry logic
|
|
191
|
-
if (sent && (RABBITMQ.PUBLISHER_ERROR !== 'true' && RABBITMQ.PUBLISHER_ERROR !== true)) {
|
|
192
|
-
Logger.info(`Message published successfully on attempt ${attempt + 1}`, 'publishMessageToExchange');
|
|
193
|
-
Logger.info(`✅ Message sent successfully`, 'publishMessageToExchange');
|
|
194
|
-
break;
|
|
195
|
-
} else {
|
|
196
|
-
throw new Error('Publish returned false');
|
|
197
|
-
}
|
|
198
|
-
} catch (error) {
|
|
199
|
-
attempt++;
|
|
200
|
-
Logger.error(`Failed attempt ${attempt} to publish message`, 'publishMessageToExchange', error);
|
|
201
|
-
if (attempt < maxRetries) {
|
|
202
|
-
Logger.info(`Retrying in ${retry_delay}ms...`, 'publishMessageToExchange');
|
|
203
|
-
await new Promise(res => setTimeout(res, retry_delay));
|
|
204
|
-
}else {
|
|
205
|
-
Logger.error(`Error in publishMessageToExchange: ${error}`, 'publishMessageToExchange');
|
|
206
|
-
throw new Error('Max retries reached');
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
return true;
|
|
211
|
-
} catch (error) {
|
|
212
|
-
Logger.error('Max retries reached. Sending message to Dead Letter Queue...', 'publishMessageToExchange');
|
|
213
|
-
await this.sendMessageToDeadLatter(attempt, data);
|
|
214
|
-
const customError = new Error('Failed to publish message to exchange');
|
|
215
|
-
(customError as any).originalError = error; // optional
|
|
216
|
-
throw customError;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* Funciton to message handler
|
|
221
|
-
*/
|
|
222
|
-
// TO Do rename function name messagePublisherErrorHandler
|
|
223
|
-
public async messagePublisherErrorHandler(request: PublishMessageInputType): Promise<boolean> {
|
|
224
|
-
Logger.info(`${JSON.stringify(request)}`, 'messagePublisherErrorHandler');
|
|
225
|
-
return Promise.resolve(true);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Funvction for initialize consumer queue
|
|
230
|
-
* @param consumerQueues
|
|
231
|
-
*/
|
|
232
|
-
public async initializeConsumers(classInstance:any, consumerHandler:string, consumerErrorHandler:string,serviceConsumerQueues?: {}) {
|
|
233
|
-
try {
|
|
234
|
-
Logger.info('Reached in initialize consumer', `initializeConsumers`);
|
|
235
|
-
Logger.debug('Consumer queues serviceConsumerQueues : ', 'initializeConsumers', { serviceConsumerQueues });
|
|
236
|
-
|
|
237
|
-
let consume_queues = serviceConsumerQueues ?? configData?.consume_queues;
|
|
238
|
-
// Consume messages from all queues
|
|
239
|
-
for (let queueName in consume_queues) {
|
|
240
|
-
if (Object.prototype.hasOwnProperty.call(consume_queues, queueName)) {
|
|
241
|
-
// Call function Consume messages with retry
|
|
242
|
-
await this.consumeMessage(queueName, classInstance, consumerHandler, consumerErrorHandler,);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
} catch (error: unknown) {
|
|
246
|
-
Logger.error('Error initializing consumers:', 'initializeConsumers', (error as Error).message);
|
|
247
|
-
throw new Error((error as Error).message);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Function for perform action with retry
|
|
253
|
-
* @param actionHandler
|
|
254
|
-
* @param context
|
|
255
|
-
* @param failActionHandler
|
|
256
|
-
* @returns
|
|
257
|
-
*/
|
|
258
|
-
public async performActionWithRetry<T>(actionHandler: ActionType<T>, failActionHandler: ActionType<T>, context: T): Promise<boolean> {
|
|
259
|
-
Logger.info("Reached: in perform action with retry ", "performActionWithRetry")
|
|
260
|
-
let attempt: number = 0;
|
|
261
|
-
while (attempt < maxRetries) {
|
|
262
|
-
try {
|
|
263
|
-
actionHandler(context);
|
|
264
|
-
return true;
|
|
265
|
-
} catch (error) {
|
|
266
|
-
attempt++;
|
|
267
|
-
Logger.error(`Failed to publish message. Attempt ${attempt} of ${maxRetries}. Error:`, 'performActionWithRetry', { message: (error as Error).message });
|
|
268
|
-
let queueName = (context as PublishMessageInputType)?.queueName
|
|
269
|
-
const data: MessageModel = (context as PublishMessageInputType)?.message;
|
|
270
|
-
if (attempt >= maxRetries) {
|
|
271
|
-
Logger.error('Max retries reached. Failed to publish message.', 'performActionWithRetry');
|
|
272
|
-
// Optionally: Log or persist the failed message to a DB, file, or other storage for manual intervention
|
|
273
|
-
this.sendMessageToDeadLatter(attempt, data);
|
|
274
|
-
failActionHandler(context);
|
|
275
|
-
return false;
|
|
276
|
-
}
|
|
277
|
-
// Wait before retrying (Exponential Backoff)
|
|
278
|
-
let retryDelay: number;
|
|
279
|
-
retryDelay = Number(retry_delay[attempt- 1]);
|
|
280
|
-
let count:any = retry_delay.length - 1 === attempt?retry_delay.length - 1:retry_delay[attempt - 1];
|
|
281
|
-
retryDelay = Number(count);
|
|
282
|
-
this.sendMessageToRetryQueue(attempt, data, retryDelay, queueName);
|
|
283
|
-
Logger.info(`Retrying in ${retryDelay / 1000} seconds...`, 'performActionWithRetry');
|
|
284
|
-
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
return false;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Function to consume message with retry
|
|
294
|
-
* @param queueName
|
|
295
|
-
*/
|
|
296
|
-
public async consumeMessage(queueName: string, classInstance: any, consumerHandler: string , consumerErrorHandler:string) {
|
|
297
|
-
Logger.info("Reached: Consuming messages from the queue", 'consumeMessage')
|
|
298
|
-
// Remember this consumer so it can be re-attached after any reconnect or
|
|
299
|
-
// channel recreation. Keyed by queueName so repeated calls don't stack.
|
|
300
|
-
registeredConsumers.set(queueName, { queueName, classInstance, consumerHandler, consumerErrorHandler });
|
|
301
|
-
return await this.subscribeConsumer(queueName, classInstance, consumerHandler, consumerErrorHandler);
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Function to attach a single consumer to a queue. Performs the actual
|
|
306
|
-
* channel.consume() and owns the ack/retry/DLQ logic. Kept separate from
|
|
307
|
-
* consumeMessage() so re-subscription (on reconnect) does not re-register
|
|
308
|
-
* the consumer in the registry, which would grow it unboundedly.
|
|
309
|
-
* @param queueName
|
|
310
|
-
* @param classInstance
|
|
311
|
-
* @param consumerHandler
|
|
312
|
-
* @param consumerErrorHandler
|
|
313
|
-
*/
|
|
314
|
-
private async subscribeConsumer(queueName: string, classInstance: any, consumerHandler: string, consumerErrorHandler: string) {
|
|
315
|
-
let attempt:number = 0;
|
|
316
|
-
if(isConnectionOpen){
|
|
317
|
-
// Capture the channel this consumer is registered on. acks/nacks for
|
|
318
|
-
// messages delivered here MUST go back to this same channel object —
|
|
319
|
-
// if the module-level `channel` is later reassigned (recreateChannel /
|
|
320
|
-
// createConnection), acking against the new channel raises
|
|
321
|
-
// PRECONDITION_FAILED ("unknown delivery tag") and closes it, taking
|
|
322
|
-
// every consumer down with it.
|
|
323
|
-
const ch: amqp.Channel = channel;
|
|
324
|
-
return await ch.consume(queueName, async (message: any) => {
|
|
325
|
-
if(message){
|
|
326
|
-
Logger.info(`Message received from queue ${queueName}: ${JSON.stringify(message)}`, 'consumeMessage');
|
|
327
|
-
let msgData: any = JSON.parse(message.content.toString());
|
|
328
|
-
Logger.info(`Received message from queue ${queueName}: ${JSON.stringify(msgData)}`, 'consumeMessage');
|
|
329
|
-
const headers = message.properties.headers || {};
|
|
330
|
-
attempt = headers[retryCountHeader] || 0;
|
|
331
|
-
const data:any = {body:msgData?.result?.data, queue_name:queueName, language: msgData?.language, correlationId: msgData?.correlationId, retries: msgData?.retries, item_id: msgData?.item_id, metadatakey: msgData?.metadatakey};
|
|
332
|
-
try {
|
|
333
|
-
let success:any = {};
|
|
334
|
-
if(RABBITMQ.CONSUMER_ERROR !== 'true' && RABBITMQ.CONSUMER_ERROR !== true){
|
|
335
|
-
Logger.info("Consumer handler call for process the request", 'consumeMessage')
|
|
336
|
-
// success = await this.consumerHandler({ queueName, message: msgData });
|
|
337
|
-
if (typeof classInstance[consumerHandler] === 'function') {
|
|
338
|
-
success = await classInstance[consumerHandler]({ queueName, message: msgData }); // Dynamically call the method
|
|
339
|
-
} else {
|
|
340
|
-
Logger.info(`Method ${consumerHandler} not found in classInstance.`, 'consumerMessage');
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
if(success?.isMatch === true){
|
|
344
|
-
if (!success?.status) {
|
|
345
|
-
const retryCount = message.properties.headers['x-retry-count'] || 0;
|
|
346
|
-
// while (retryCount < maxRetries) {
|
|
347
|
-
attempt = retryCount?retryCount:attempt;
|
|
348
|
-
if (retryCount >= maxRetries) {
|
|
349
|
-
Logger.error('Max retries reached. Failed to publish message.', 'consumeMessage');
|
|
350
|
-
// Optionally: Log or persist the failed message to a DB, file, or other storage for manual intervention
|
|
351
|
-
this.sendMessageToDeadLatter(attempt, data);
|
|
352
|
-
// this.consumerErrorHandler(data);
|
|
353
|
-
if (typeof classInstance[consumerErrorHandler] === 'function') {
|
|
354
|
-
classInstance[consumerErrorHandler](data); // Dynamically call the method
|
|
355
|
-
} else {
|
|
356
|
-
Logger.info(`Method ${consumerErrorHandler} not found in classInstance.`, 'consumerMessage');
|
|
357
|
-
}
|
|
358
|
-
this.safeNack(ch, message); // Move message to DLQ after failure
|
|
359
|
-
return false;
|
|
360
|
-
}
|
|
361
|
-
attempt++;
|
|
362
|
-
if (data?.retries >=0) {
|
|
363
|
-
data.retries = attempt;
|
|
364
|
-
}
|
|
365
|
-
// Wait before retrying (Exponential Backoff)
|
|
366
|
-
let retryDelay: number;
|
|
367
|
-
let count:any = retry_delay.length === attempt?retry_delay[retry_delay.length - 1]:retry_delay[attempt-1];
|
|
368
|
-
retryDelay = Number(count);
|
|
369
|
-
await this.sendMessageToRetryQueue(attempt, data, retryDelay, queueName);
|
|
370
|
-
Logger.info(`Retrying in ${retryDelay / 1000} seconds...`, 'consumeMessage');
|
|
371
|
-
// Ack immediately once the message is safely on the retry
|
|
372
|
-
// queue. Do NOT block the delivery for the full TTL — the
|
|
373
|
-
// delay is enforced by the retry queue's own expiration.
|
|
374
|
-
// Holding it here risks breaching RabbitMQ's
|
|
375
|
-
// consumer_timeout and getting the channel cancelled.
|
|
376
|
-
this.safeAck(ch, message);
|
|
377
|
-
return;
|
|
378
|
-
} else {
|
|
379
|
-
Logger.info(`Acknowledge the successful processing`, 'consumeMessage');
|
|
380
|
-
}
|
|
381
|
-
// Acknowledge the successful processing
|
|
382
|
-
this.safeAck(ch, message);
|
|
383
|
-
}
|
|
384
|
-
} catch (error) {
|
|
385
|
-
let errorMessage:string = (error as Error).message;
|
|
386
|
-
Logger.error(`Error processing message`, 'consumeMessage', { message: errorMessage});
|
|
387
|
-
this.sendMessageToDeadLatter(attempt, data);
|
|
388
|
-
// this.consumerErrorHandler(data);
|
|
389
|
-
if (typeof classInstance[consumerErrorHandler] === 'function') {
|
|
390
|
-
classInstance[consumerErrorHandler](data); // Dynamically call the method
|
|
391
|
-
} else {
|
|
392
|
-
Logger.info(`Method ${consumerErrorHandler} not found in classInstance.`, 'consumerMessage');
|
|
393
|
-
}
|
|
394
|
-
this.safeNack(ch, message); // Move message to DLQ after failure
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* Function to re-attach every registered consumer. Called after each
|
|
403
|
-
* successful (re)connect and after a channel is recreated, so queues never
|
|
404
|
-
* end up with the connection alive but no consumer attached.
|
|
405
|
-
*/
|
|
406
|
-
private async reregisterConsumers(): Promise<void> {
|
|
407
|
-
if (registeredConsumers.size === 0) {
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
Logger.info(`Re-subscribing ${registeredConsumers.size} consumer(s) after (re)connect`, 'reregisterConsumers');
|
|
411
|
-
for (const c of registeredConsumers.values()) {
|
|
412
|
-
try {
|
|
413
|
-
await this.subscribeConsumer(c.queueName, c.classInstance, c.consumerHandler, c.consumerErrorHandler);
|
|
414
|
-
Logger.info(`Consumer re-subscribed on queue ${c.queueName}`, 'reregisterConsumers');
|
|
415
|
-
} catch (error) {
|
|
416
|
-
Logger.error(`Failed to re-subscribe consumer on queue ${c.queueName}`, 'reregisterConsumers', (error as Error).message);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Function to ack a message against the channel it was delivered on, guarded
|
|
423
|
-
* so a stale delivery tag (channel already replaced) cannot throw and take
|
|
424
|
-
* down the current channel.
|
|
425
|
-
* @param ch the channel the message was consumed on
|
|
426
|
-
* @param message
|
|
427
|
-
*/
|
|
428
|
-
private safeAck(ch: amqp.Channel, message: amqp.Message): void {
|
|
429
|
-
try {
|
|
430
|
-
ch.ack(message);
|
|
431
|
-
} catch (error) {
|
|
432
|
-
Logger.error('Failed to ack message (stale channel?)', 'safeAck', (error as Error).message);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* Function to nack a message against the channel it was delivered on, guarded
|
|
438
|
-
* against stale delivery tags. Never requeues (routes to DLQ).
|
|
439
|
-
* @param ch the channel the message was consumed on
|
|
440
|
-
* @param message
|
|
441
|
-
*/
|
|
442
|
-
private safeNack(ch: amqp.Channel, message: amqp.Message): void {
|
|
443
|
-
try {
|
|
444
|
-
ch.nack(message, false, false);
|
|
445
|
-
} catch (error) {
|
|
446
|
-
Logger.error('Failed to nack message (stale channel?)', 'safeNack', (error as Error).message);
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// /**
|
|
451
|
-
// * Function for consumer massage handler
|
|
452
|
-
// * @param request
|
|
453
|
-
// * @returns
|
|
454
|
-
// */
|
|
455
|
-
// public async consumerHandler(request: PublishMessageInputType): Promise<boolean> {
|
|
456
|
-
// Logger.info(`${JSON.stringify(request)}`, 'consumerHandler');
|
|
457
|
-
// return true;
|
|
458
|
-
// }
|
|
459
|
-
|
|
460
|
-
// /**
|
|
461
|
-
// * Function to consumer message error handler
|
|
462
|
-
// * @param request
|
|
463
|
-
// * @returns
|
|
464
|
-
// */
|
|
465
|
-
|
|
466
|
-
// public async consumerErrorHandler(request:any):Promise<boolean> {
|
|
467
|
-
// Logger.info(`${JSON.stringify(request)}`, 'consumerErrorHandler');
|
|
468
|
-
// return true;
|
|
469
|
-
// }
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
/**
|
|
473
|
-
* Function to send message to dead later queue
|
|
474
|
-
* @param attempt
|
|
475
|
-
* @param data
|
|
476
|
-
* @param queueName
|
|
477
|
-
* @retruns
|
|
478
|
-
*/
|
|
479
|
-
|
|
480
|
-
public async sendMessageToDeadLatter(attempt:number, data:any){
|
|
481
|
-
Logger.info("Reached: send message on dead letter queue", 'sendMessageToDeadLater')
|
|
482
|
-
try {
|
|
483
|
-
if(isConnectionOpen){
|
|
484
|
-
Logger.info(`Attempting to send message to DLX queue: ${dlx_queue} (attempt: ${attempt}, retry-count: ${attempt - 1}, size: ${JSON.stringify(data).length} bytes)`, 'sendMessageToDeadLater');
|
|
485
|
-
const sent = channel.sendToQueue(dlx_queue, Buffer.from(JSON.stringify(data)), {
|
|
486
|
-
persistent: true,
|
|
487
|
-
headers: {
|
|
488
|
-
'x-retry-count': attempt-1
|
|
489
|
-
},
|
|
490
|
-
});
|
|
491
|
-
if (sent) {
|
|
492
|
-
Logger.info(`Message successfully published to DLX queue: ${dlx_queue} (attempt: ${attempt}, retry-count: ${attempt - 1})`, 'sendMessageToDeadLater');
|
|
493
|
-
} else {
|
|
494
|
-
Logger.error(`Failed to publish message to DLX queue: ${dlx_queue} - Channel buffer full (attempt: ${attempt}, retry-count: ${attempt - 1})`, 'sendMessageToDeadLater');
|
|
495
|
-
}
|
|
496
|
-
return sent;
|
|
497
|
-
} else {
|
|
498
|
-
Logger.error(`Cannot send message to DLX - Connection is not open (queue: ${dlx_queue}, attempt: ${attempt})`, 'sendMessageToDeadLater');
|
|
499
|
-
return false;
|
|
500
|
-
}
|
|
501
|
-
} catch (error:unknown) {
|
|
502
|
-
Logger.error(`Error sending message to dead letter queue: ${(error as Error).message} (queue: ${dlx_queue}, attempt: ${attempt})`, 'sendMessageToDeadLater');
|
|
503
|
-
throw error;
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
/***
|
|
509
|
-
* Function to send message to retry queue
|
|
510
|
-
* @param attempt
|
|
511
|
-
* @param data
|
|
512
|
-
* @param retryDelay
|
|
513
|
-
* @param queueName
|
|
514
|
-
* @returns
|
|
515
|
-
*/
|
|
516
|
-
|
|
517
|
-
public async sendMessageToRetryQueue(attempt:number, data:any, retryDelay:any, queueName:string){
|
|
518
|
-
Logger.info("Reached: send message on retry queue", 'sendMessageToRetryQueue')
|
|
519
|
-
try {
|
|
520
|
-
if(isConnectionOpen){
|
|
521
|
-
const isSent = await new Promise<boolean>((resolve, reject) => {
|
|
522
|
-
const messageBuffer = Buffer.from(JSON.stringify(data));
|
|
523
|
-
let sent = channel.sendToQueue(queueName, messageBuffer, {
|
|
524
|
-
headers: {
|
|
525
|
-
'x-retry-count': attempt + 1,
|
|
526
|
-
'x-message-ttl': retryDelay,
|
|
527
|
-
}
|
|
528
|
-
});
|
|
529
|
-
if (sent) {
|
|
530
|
-
Logger.debug(`Message sent successfully`, 'publishMessage', { data, queueName });
|
|
531
|
-
resolve(true);
|
|
532
|
-
} else {
|
|
533
|
-
Logger.error(`Failed to publish message.`, 'publishMessage', { message: data.toString() });
|
|
534
|
-
reject(false);
|
|
535
|
-
}
|
|
536
|
-
});
|
|
537
|
-
return isSent;
|
|
538
|
-
}
|
|
539
|
-
} catch (error:unknown) {
|
|
540
|
-
Logger.error("Error in send message on retry queue", 'sendMessageToRetryQueue', {message: (error as Error ).message});
|
|
541
|
-
throw error;
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
/**
|
|
546
|
-
* Function to find the queue, exchange and add binding
|
|
547
|
-
* @param queueName
|
|
548
|
-
* @param exchangeName
|
|
549
|
-
* @returns
|
|
550
|
-
*/
|
|
551
|
-
public async findQueueAndExchange(queueName:string, exchangeName:string){
|
|
552
|
-
Logger.info('Reached to findQueueAndExchange', 'findQueueAndExchange');
|
|
553
|
-
try {
|
|
554
|
-
let exchanges: any = configData?.exchanges;
|
|
555
|
-
let queues: any = configData.queues;
|
|
556
|
-
let bindings: any = configData.bindings;
|
|
557
|
-
// Declare the queues with dlx
|
|
558
|
-
|
|
559
|
-
for (let data of queues) {
|
|
560
|
-
if(data?.name === queueName){
|
|
561
|
-
await this.createQueue(data);
|
|
562
|
-
}
|
|
563
|
-
Logger.info(`${data?.name} queue created `, 'findQueueAndExchange');
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
// Declare the exchange with dlx
|
|
567
|
-
for (let data of exchanges) {
|
|
568
|
-
if(data?.name === exchangeName){
|
|
569
|
-
await this.createExchange(data);
|
|
570
|
-
}
|
|
571
|
-
Logger.info(`${data?.name} exchange created`, 'findQueueAndExchange');
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// Declare the bindings for queues and exchanges with routing keys
|
|
575
|
-
for (let obj in bindings) {
|
|
576
|
-
bindings[obj]?.forEach(async (key: any) => {
|
|
577
|
-
if(obj === exchangeName && key?.queue === queueName){
|
|
578
|
-
await this.bindQueueAndExchanges(obj, key?.queue, key?.routingKey);
|
|
579
|
-
}
|
|
580
|
-
Logger.info(`${key?.queue} bind with exchange name ${obj} and routing key ${key?.routingKey}`, 'findQueueAndExchange');
|
|
581
|
-
})
|
|
582
|
-
}
|
|
583
|
-
} catch (error) {
|
|
584
|
-
Logger.error("Error on setup queues, exchanges and binding", 'findQueueAndExchange', (error as Error).message);
|
|
585
|
-
throw error;
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
/**
|
|
590
|
-
* Function to check connection and channel
|
|
591
|
-
*/
|
|
592
|
-
private async checkConnectionAndChannel() {
|
|
593
|
-
Logger.info('Reached: checkConnectionAndChannel', 'checkConnectionAndChannel');
|
|
594
|
-
// 🧪 1. Ensure connection exists
|
|
595
|
-
if (!isConnectionOpen) {
|
|
596
|
-
Logger.warn('No connection found. Creating new connection...', 'publishMessageToExchange');
|
|
597
|
-
await this.createConnection();
|
|
598
|
-
}
|
|
599
|
-
// 🧪 2. Ensure channel is alive
|
|
600
|
-
const channelConnection: any = channel?.connection;
|
|
601
|
-
if (!channel || !channelConnection || channelConnection.stream.destroyed) {
|
|
602
|
-
Logger.warn('Channel seems dead, recreating before publish...', 'publishMessageToExchange');
|
|
603
|
-
await this.recreateChannel();
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
/**
|
|
608
|
-
* Function to recreate channel
|
|
609
|
-
*/
|
|
610
|
-
private async recreateChannel() {
|
|
611
|
-
try {
|
|
612
|
-
channel = await connection.createConfirmChannel();
|
|
613
|
-
channel.prefetch(RABBITMQ?.PREFETCH_COUNT)
|
|
614
|
-
Logger.info(' Channel recreated successfully.', 'recreateChannel');
|
|
615
|
-
} catch (err: any) {
|
|
616
|
-
Logger.error(' Failed to recreate channel:', err);
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
/**
|
|
621
|
-
* Function to setup the queues and exchanges
|
|
622
|
-
* @params array
|
|
623
|
-
* @rtrun
|
|
624
|
-
*/
|
|
625
|
-
private async setupQueuesAndExchanges(): Promise<void> {
|
|
626
|
-
Logger.info('Reached to setupQueuesAndExchanges', 'setupQueuesAndExchanges');
|
|
627
|
-
try {
|
|
628
|
-
let exchanges: any = configData.exchanges;
|
|
629
|
-
let queues: any = configData.queues;
|
|
630
|
-
let bindings: any = configData.bindings;
|
|
631
|
-
// Declare the exchange with dlx
|
|
632
|
-
for (let data of exchanges) {
|
|
633
|
-
await this.createExchange(data);
|
|
634
|
-
Logger.info(`${data?.name} exchange created`, 'setupQueuesAndExchanges');
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
// Declare the queues with dlx
|
|
638
|
-
for (let data of queues) {
|
|
639
|
-
await this.createQueue(data);
|
|
640
|
-
Logger.info(`${data?.name} queue created `, 'setupQueuesAndExchanges');
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
// Declare the bindings for queues and exchanges with routing keys
|
|
644
|
-
for (let obj in bindings) {
|
|
645
|
-
bindings[obj]?.forEach(async (key: any) => {
|
|
646
|
-
await this.bindQueueAndExchanges( obj, key?.queue, key?.routingKey);
|
|
647
|
-
Logger.info(`${key?.queue} bind with exchange name ${obj} and routing key ${key?.routingKey}`, 'setupQueuesAndExchanges');
|
|
648
|
-
})
|
|
649
|
-
}
|
|
650
|
-
} catch (error) {
|
|
651
|
-
Logger.error("Error on setup queues, exchanges and binding", 'setupQueuesAndExchanges', (error as Error).message);
|
|
652
|
-
}
|
|
653
|
-
};
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
/**
|
|
657
|
-
* Function to binding queue and exchange
|
|
658
|
-
* @param exchangeName
|
|
659
|
-
* @param queueName
|
|
660
|
-
* @param routingKey
|
|
661
|
-
*/
|
|
662
|
-
|
|
663
|
-
private async bindQueueAndExchanges(exchangeName:string, queueName:string, routingKey:string):Promise<void> {
|
|
664
|
-
try {
|
|
665
|
-
await channel.bindQueue(queueName, exchangeName, routingKey, { 'toQueue':queueName, 'x-match': 'any' });
|
|
666
|
-
} catch (error:unknown) {
|
|
667
|
-
Logger.error('Error in bind Queue and exchange', 'bindQueueAndExchanges', {message:(error as Error).message});
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
/**
|
|
674
|
-
* Function to create the queue
|
|
675
|
-
* @param data
|
|
676
|
-
*/
|
|
677
|
-
|
|
678
|
-
private async createQueue(data:any ):Promise<void> {
|
|
679
|
-
try {
|
|
680
|
-
await channel.assertQueue(data?.name, { durable: data.durable, exclusive: data.exclusive, autoDelete: data.autoDelete, arguments: data.arguments });
|
|
681
|
-
} catch (error:unknown) {
|
|
682
|
-
Logger.error('Error in Create Queue', 'createQueue', {message:(error as Error).message});
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
/**
|
|
687
|
-
* Function to create the exchange
|
|
688
|
-
* @param data
|
|
689
|
-
*/
|
|
690
|
-
|
|
691
|
-
private async createExchange(data:any ):Promise<void> {
|
|
692
|
-
try {
|
|
693
|
-
await channel.assertExchange(data?.name, data.type, { durable: data.durable, autoDelete: data.autoDelete });
|
|
694
|
-
} catch (error:unknown) {
|
|
695
|
-
Logger.error('Error in Create exchange', 'createExchange', {message:(error as Error).message});
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
// private async publishMessageRetryQueue(request: PublishMessageInputType, attempt: number) {
|
|
700
|
-
// try {
|
|
701
|
-
// Logger.error(`Failed to publish message. Attempt ${attempt} of ${maxRetries}. Error:`, 'publishMessageRetryQueue', request);
|
|
702
|
-
// let queueName = (request as PublishMessageInputType)?.queueName
|
|
703
|
-
// const data: MessageModel = (request as PublishMessageInputType)?.message;
|
|
704
|
-
// if (typeof data.message === 'object' && data.message !== null) {
|
|
705
|
-
// (data.message as { retries: number }).retries = attempt;
|
|
706
|
-
// }
|
|
707
|
-
// // Wait before retrying (Exponential Backoff)
|
|
708
|
-
// let retryDelay: number;
|
|
709
|
-
// retryDelay = Number(retry_delay[attempt]);
|
|
710
|
-
// let count:any = retry_delay.length - 1 === attempt?retry_delay[retry_delay.length - 1]:retry_delay[attempt];
|
|
711
|
-
// retryDelay = Number(count);
|
|
712
|
-
// await this.sendMessageToRetryQueue(attempt, data, retryDelay, queueName);
|
|
713
|
-
// Logger.info(`Retrying in ${retryDelay / 1000}, seconds...`, 'publishMessageRetryQueue');
|
|
714
|
-
// await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
715
|
-
// } catch (error) {
|
|
716
|
-
// Logger.error(`Failed to publish message. Attempt ${attempt} of ${maxRetries}. Error:`, 'publishMessageRetryQueue', request);
|
|
717
|
-
// throw error;
|
|
718
|
-
// }
|
|
719
|
-
// }
|
|
720
|
-
|
|
721
|
-
/**
|
|
722
|
-
* Function to connect create to RabbitMQ
|
|
723
|
-
*/
|
|
724
|
-
public async createConnection(): Promise<ConnectionWrapper> {
|
|
725
|
-
try {
|
|
726
|
-
Logger.info('Reached to createConnection', 'createConnection');
|
|
727
|
-
configManager.loadConfig(RABBITMQ?.CONFIG_PATH);
|
|
728
|
-
const secret = await getIAMSecrets();
|
|
729
|
-
let url: string = `amqp://${secret?.[SECRET_KEYS.RABBITMQ_USER]}:${secret?.[SECRET_KEYS.RABBITMQ_PASS]}@${RABBITMQ?.HOST}`;
|
|
730
|
-
// Create a connection to RabbitMQ server
|
|
731
|
-
connection = await amqp.connect(url, { heartbeat: RABBITMQ?.HEARTBEAT });
|
|
732
|
-
isConnectionOpen = true;
|
|
733
|
-
// check the connection status
|
|
734
|
-
this.bindConnectionEventHandler();
|
|
735
|
-
// create channel
|
|
736
|
-
channel = await connection.createConfirmChannel();
|
|
737
|
-
channel.prefetch(RABBITMQ?.PREFETCH_COUNT);
|
|
738
|
-
// Watch the channel itself: a channel-level fault (e.g. PRECONDITION_FAILED
|
|
739
|
-
// from a stale ack) closes the channel and cancels its consumers without
|
|
740
|
-
// closing the connection, so the connection 'close' path never fires.
|
|
741
|
-
this.bindChannelEventHandler();
|
|
742
|
-
Logger.info('RabbitMQ connected successfully! ', 'createConnection');
|
|
743
|
-
// create exchange and queue and bind with exchange
|
|
744
|
-
await this.setupQueuesAndExchanges();
|
|
745
|
-
// Re-attach any consumers registered before this (re)connect. Without
|
|
746
|
-
// this, after a reconnect the connection is healthy but no consumer is
|
|
747
|
-
// bound to the queues — the root cause of consumers "disappearing".
|
|
748
|
-
await this.reregisterConsumers();
|
|
749
|
-
const connectionWrapper: ConnectionWrapper = { connection, channel }
|
|
750
|
-
return connectionWrapper;
|
|
751
|
-
} catch (error) {
|
|
752
|
-
// Log and rethrow any errors that occur during connection
|
|
753
|
-
Logger.error('Failed to connect to RabbitMQ', 'connection', (error as Error)?.message);
|
|
754
|
-
return { connection: null, channel: null } as unknown as ConnectionWrapper;
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
/**
|
|
759
|
-
* Function for bind connection event handler on close and error setupQueuesAndExchanges
|
|
760
|
-
* @returns
|
|
761
|
-
*/
|
|
762
|
-
private bindConnectionEventHandler(): boolean {
|
|
763
|
-
try {
|
|
764
|
-
// 'error' always precedes 'close' from amqplib, so only 'close' drives the
|
|
765
|
-
// reconnect to avoid triggering it twice.
|
|
766
|
-
connection.on('error', (err: any) => {
|
|
767
|
-
Logger.error(`RabbitMQ: bindConnectionEventHandler:error`, `Connection error`, err?.message ?? err);
|
|
768
|
-
});
|
|
769
|
-
connection.on('close', async(err: any) => {
|
|
770
|
-
Logger.error(`RabbitMQ: bindConnectionEventHandler:close`, `Connection closed`, err?.message ?? err);
|
|
771
|
-
isConnectionOpen = false;
|
|
772
|
-
await this.handleReconnect();
|
|
773
|
-
});
|
|
774
|
-
return isConnectionOpen;
|
|
775
|
-
} catch (error) {
|
|
776
|
-
isConnectionOpen = false;
|
|
777
|
-
return isConnectionOpen;
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
/**
|
|
783
|
-
* Function to bind channel-level event handlers. A channel can be cancelled
|
|
784
|
-
* (e.g. PRECONDITION_FAILED, consumer_timeout) while the connection stays up;
|
|
785
|
-
* in that case the connection 'close' handler never fires, so the channel is
|
|
786
|
-
* recreated and consumers re-attached here.
|
|
787
|
-
*/
|
|
788
|
-
private bindChannelEventHandler(): void {
|
|
789
|
-
try {
|
|
790
|
-
channel.on('error', (err: any) => {
|
|
791
|
-
Logger.error(`RabbitMQ: bindChannelEventHandler:error`, `Channel error`, err?.message ?? err);
|
|
792
|
-
});
|
|
793
|
-
channel.on('close', async () => {
|
|
794
|
-
Logger.error(`RabbitMQ: bindChannelEventHandler:close`, `Channel closed`);
|
|
795
|
-
// Only self-heal the channel while the connection is still open. If the
|
|
796
|
-
// connection is down, handleReconnect() owns recovery (and will recreate
|
|
797
|
-
// the channel + consumers), so doing it here too would double-subscribe.
|
|
798
|
-
if (isConnectionOpen && !isReconnecting) {
|
|
799
|
-
try {
|
|
800
|
-
await this.recreateChannel();
|
|
801
|
-
this.bindChannelEventHandler();
|
|
802
|
-
await this.reregisterConsumers();
|
|
803
|
-
Logger.info('Channel and consumers recovered after channel close', 'bindChannelEventHandler');
|
|
804
|
-
} catch (error) {
|
|
805
|
-
Logger.error('Failed to recover channel after close', 'bindChannelEventHandler', (error as Error).message);
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
});
|
|
809
|
-
} catch (error) {
|
|
810
|
-
Logger.error('Error in bindChannelEventHandler', 'bindChannelEventHandler', (error as Error).message);
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
/**
|
|
815
|
-
* Function to reconnect to RabbitMQ with capped exponential backoff. Unlike
|
|
816
|
-
* the old logic — which capped total lifetime reconnects at MAX_RETRIES and
|
|
817
|
-
* never reset the counter — this retries indefinitely and resets on success,
|
|
818
|
-
* so the service self-heals from every disconnect. createConnection()
|
|
819
|
-
* re-attaches all registered consumers on success.
|
|
820
|
-
*/
|
|
821
|
-
private async handleReconnect(): Promise<void> {
|
|
822
|
-
if (isReconnecting) {
|
|
823
|
-
Logger.info('Reconnect already in progress, skipping duplicate trigger', 'handleReconnect');
|
|
824
|
-
return;
|
|
825
|
-
}
|
|
826
|
-
isReconnecting = true;
|
|
827
|
-
connectionRetry = 1;
|
|
828
|
-
// Backoff schedule derived from the TTL config, capped at its last value.
|
|
829
|
-
const backoffs: number[] = (retry_delay ?? []).map(Number).filter((n: number) => !Number.isNaN(n) && n > 0);
|
|
830
|
-
const maxBackoff = backoffs.length ? backoffs[backoffs.length - 1] : 30000;
|
|
831
|
-
try {
|
|
832
|
-
while (!isConnectionOpen) {
|
|
833
|
-
const delay = backoffs[connectionRetry - 1] ?? maxBackoff;
|
|
834
|
-
Logger.info(`Reconnect attempt ${connectionRetry} in ${delay / 1000}s...`, 'handleReconnect');
|
|
835
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
836
|
-
try {
|
|
837
|
-
await this.createConnection();
|
|
838
|
-
if (isConnectionOpen) {
|
|
839
|
-
connectionRetry = 1;
|
|
840
|
-
Logger.info('Reconnected to RabbitMQ successfully', 'handleReconnect');
|
|
841
|
-
return;
|
|
842
|
-
}
|
|
843
|
-
throw new Error('createConnection did not establish an open connection');
|
|
844
|
-
} catch (error) {
|
|
845
|
-
Logger.error(`Reconnect attempt ${connectionRetry} failed`, 'handleReconnect', (error as Error).message);
|
|
846
|
-
connectionRetry++;
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
} finally {
|
|
850
|
-
isReconnecting = false;
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
}
|
|
855
|
-
|