@platform-x/hep-message-broker-client 1.1.14 → 1.1.16
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/README.md +1 -1
- package/dist/src/Util/commonUtil.js.map +1 -0
- package/dist/src/Util/constants.js.map +1 -0
- package/dist/src/Util/logger.js.map +1 -0
- package/dist/src/Util/requestTracer.js.map +1 -0
- package/dist/src/config/ConfigManager.js.map +1 -0
- package/dist/src/config/index.js.map +1 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/messageBroker/BaseRabbitMQClient.js.map +1 -0
- package/dist/src/messageBroker/ConnectionManager.js.map +1 -0
- package/dist/src/messageBroker/MessageBrokerClient.js.map +1 -0
- package/dist/src/messageBroker/MessageConsumer.js.map +1 -0
- package/dist/src/messageBroker/MessageProducer.js.map +1 -0
- package/dist/src/messageBroker/RabbitMQClient.js.map +1 -0
- package/dist/src/messageBroker/RetryManager.js.map +1 -0
- package/dist/src/messageBroker/interface/ConnectionWrapper.js.map +1 -0
- package/dist/src/messageBroker/interface/IMessageBrokerClient.js.map +1 -0
- package/dist/src/messageBroker/rabbitmq/MessageBroker.js +31 -29
- package/dist/src/messageBroker/rabbitmq/MessageBroker.js.map +1 -0
- package/dist/src/messageBroker/rabbitmq/MessageBrokerClient.js +33 -31
- package/dist/src/messageBroker/rabbitmq/MessageBrokerClient.js.map +1 -0
- package/dist/src/messageBroker/types/ActionType.js.map +1 -0
- package/dist/src/messageBroker/types/PublishMessageInputType.js.map +1 -0
- package/dist/src/models/MessageModel.js.map +1 -0
- package/dist/src/models/NotificationMessageModel.js.map +1 -0
- package/package.json +43 -43
- package/src/Util/commonUtil.ts +41 -0
- package/src/Util/constants.ts +9 -0
- package/src/Util/logger.ts +219 -0
- package/src/Util/requestTracer.ts +28 -0
- package/src/config/ConfigManager.ts +35 -0
- package/src/config/index.ts +38 -0
- package/src/index.ts +74 -0
- package/src/messageBroker/BaseRabbitMQClient.ts +30 -0
- package/src/messageBroker/ConnectionManager.ts +182 -0
- package/src/messageBroker/MessageBrokerClient.ts +88 -0
- package/src/messageBroker/MessageConsumer.ts +85 -0
- package/src/messageBroker/MessageProducer.ts +142 -0
- package/src/messageBroker/RabbitMQClient.ts +47 -0
- package/src/messageBroker/RetryManager.ts +64 -0
- package/src/messageBroker/interface/ConnectionWrapper.ts +7 -0
- package/src/messageBroker/interface/IMessageBrokerClient.ts +11 -0
- package/src/messageBroker/rabbitmq/MessageBroker.ts +525 -0
- package/src/messageBroker/rabbitmq/MessageBrokerClient.ts +681 -0
- package/src/messageBroker/types/ActionType.ts +1 -0
- package/src/messageBroker/types/PublishMessageInputType.ts +8 -0
- package/src/models/MessageModel.ts +14 -0
- package/src/models/NotificationMessageModel.ts +0 -0
- package/tsconfig.json +73 -0
- package/rabbitMQConfig.json +0 -344
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import * as amqp from 'amqplib';
|
|
2
|
+
import config from '../../config';
|
|
3
|
+
import ConfigManager from '../../config/ConfigManager';
|
|
4
|
+
import { Logger } from '../../Util/logger';
|
|
5
|
+
import { RABBITMQ_FEED_TYPE, SECRET_KEYS } from '../../Util/constants';
|
|
6
|
+
import { getIAMSecrets } from '../..';
|
|
7
|
+
|
|
8
|
+
const { RABBITMQ } = config;
|
|
9
|
+
|
|
10
|
+
let configManager: ConfigManager = ConfigManager.getInstance();
|
|
11
|
+
let maxRetries = RABBITMQ.MAX_RETRIES;
|
|
12
|
+
let retryDelay: any = RABBITMQ?.TTL?.split("|");
|
|
13
|
+
let prefetchCount = RABBITMQ.PREFETCH_COUNT;
|
|
14
|
+
let configData: any = {};
|
|
15
|
+
|
|
16
|
+
export class MessageBroker {
|
|
17
|
+
|
|
18
|
+
private connection: amqp.Connection | null = null;
|
|
19
|
+
private channel: amqp.Channel | null = null;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Description: Checks if the RabbitMQ connection and channel are established.
|
|
23
|
+
* @returns boolean indicating the connection status
|
|
24
|
+
*/
|
|
25
|
+
public isConnected(): boolean {
|
|
26
|
+
return this.connection !== null && this.channel !== null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Description: Checks if the connection and channel are alive. If not, recreates them.
|
|
31
|
+
* Also recreates the channel if its underlying socket/stream is destroyed.
|
|
32
|
+
*/
|
|
33
|
+
public async checkConnectionAndChannel(): Promise<void> {
|
|
34
|
+
Logger.info('Reached: checkConnectionAndChannel', 'checkConnectionAndChannel');
|
|
35
|
+
if (!this.isConnected()) {
|
|
36
|
+
Logger.warn('No active connection found. Recreating connection...', 'checkConnectionAndChannel');
|
|
37
|
+
await this.createConnection();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
// Check if the channel's underlying stream is still alive
|
|
41
|
+
const channelConn: any = (this.channel as any)?.connection;
|
|
42
|
+
if (!this.channel || !channelConn || channelConn.stream?.destroyed) {
|
|
43
|
+
Logger.warn('Channel is dead. Recreating channel...', 'checkConnectionAndChannel');
|
|
44
|
+
await this.recreateChannel();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Description: Recreates the channel on the existing connection.
|
|
50
|
+
*/
|
|
51
|
+
private async recreateChannel(): Promise<void> {
|
|
52
|
+
try {
|
|
53
|
+
this.channel = await this.connection!.createConfirmChannel();
|
|
54
|
+
await this.channel.prefetch(prefetchCount);
|
|
55
|
+
Logger.info('Channel recreated successfully', 'recreateChannel');
|
|
56
|
+
} catch (error) {
|
|
57
|
+
Logger.error(`Failed to recreate channel: ${(error as Error).message}`, 'recreateChannel');
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Initialize RabbitMQ connection and channel with configurable retry attempts.
|
|
64
|
+
* Waits for a configurable delay between each retry.
|
|
65
|
+
*/
|
|
66
|
+
public async initialize(): Promise<{ connection: amqp.Connection; channel: amqp.Channel }> {
|
|
67
|
+
try {
|
|
68
|
+
configData = configManager.getConfig();
|
|
69
|
+
Logger.info('Reached to createConnection', 'createConnection');
|
|
70
|
+
let attempt = 0;
|
|
71
|
+
while (attempt < maxRetries) {
|
|
72
|
+
try {
|
|
73
|
+
if(RABBITMQ.CONNECTION_RETRY_ERROR === 'true' || RABBITMQ.CONNECTION_RETRY_ERROR === true){
|
|
74
|
+
throw new Error("Error in connection");
|
|
75
|
+
}
|
|
76
|
+
return await this.createConnection();
|
|
77
|
+
} catch (error) {
|
|
78
|
+
attempt++;
|
|
79
|
+
Logger.error(`Attempt ${attempt} - Failed to connect to RabbitMQ: ${(error as Error).message}`, 'initialize');
|
|
80
|
+
if (attempt < maxRetries - 1) {
|
|
81
|
+
Logger.info(`Retrying to connect to RabbitMQ in ${retryDelay / 1000} seconds...`, 'initialize');
|
|
82
|
+
await new Promise(res => setTimeout(res, retryDelay));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
throw new Error(`Failed to connect to RabbitMQ after ${maxRetries} attempts`);
|
|
87
|
+
} catch (error: any) {
|
|
88
|
+
Logger.error("Connection to RabbitMQ failed.", 'createConnection');
|
|
89
|
+
Logger.error(`Failed to connect to RabbitMQ: ${(error as Error).message}`, 'createConnection');
|
|
90
|
+
throw error; // Rethrow the error after logging
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Description: Establishes a connection to RabbitMQ and creates a channel. Also registers exchanges, queues, and bindings based on the configuration data.
|
|
96
|
+
* @returns An object containing the RabbitMQ connection and channel.
|
|
97
|
+
*/
|
|
98
|
+
public async createConnection():Promise<{ connection: amqp.Connection; channel: amqp.Channel }>{
|
|
99
|
+
try {
|
|
100
|
+
Logger.info('Attempting to connect to RabbitMQ', 'createConnection');
|
|
101
|
+
configManager.loadConfig(RABBITMQ?.CONFIG_PATH);
|
|
102
|
+
const secret = await getIAMSecrets();
|
|
103
|
+
let url: string = `amqp://${secret?.[SECRET_KEYS.RABBITMQ_USER]}:${secret?.[SECRET_KEYS.RABBITMQ_PASS]}@${RABBITMQ?.HOST}`;
|
|
104
|
+
// console.log(SECRET_KEYS.RABBITMQ_USER, SECRET_KEYS.RABBITMQ_PASS);
|
|
105
|
+
// const url = `amqp://${RABBITMQ.USER}:${RABBITMQ.PASS}@${RABBITMQ.HOST}:${RABBITMQ.PORT}`;
|
|
106
|
+
|
|
107
|
+
this.connection = await amqp.connect(url, { heartbeat: RABBITMQ.HEARTBEAT });
|
|
108
|
+
this.channel = await this.connection.createConfirmChannel();
|
|
109
|
+
|
|
110
|
+
this.connectionEventHandler(); // Event handlers for connection 'error' and 'close'
|
|
111
|
+
|
|
112
|
+
await this.channel.prefetch(prefetchCount);
|
|
113
|
+
await this.registerQueueAndExchange();
|
|
114
|
+
|
|
115
|
+
Logger.info(`Connected to RabbitMQ`, 'createConnection');
|
|
116
|
+
return { connection: this.connection, channel: this.channel };
|
|
117
|
+
} catch (error) {
|
|
118
|
+
Logger.error(`Failed to connect to RabbitMQ: ${(error as Error).message}`, 'createConnection');
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Description: Registers 'close' and 'error' event handlers on the current connection.
|
|
125
|
+
* - 'error' only logs (amqplib always fires 'close' after 'error', so reconnect is handled there).
|
|
126
|
+
* - 'close' nullifies connection/channel, then attempts reconnect with exponential backoff.
|
|
127
|
+
* - connectionRetry resets on each successful connection via createConnection().
|
|
128
|
+
*/
|
|
129
|
+
private connectionEventHandler(): void {
|
|
130
|
+
try {
|
|
131
|
+
if (!this.connection) return;
|
|
132
|
+
this.connection.on('error', (err: Error) => {
|
|
133
|
+
Logger.error(`RabbitMQ connection error: ${err.message}`, 'connectionEventHandler');
|
|
134
|
+
});
|
|
135
|
+
this.connection.on('close', async () => {
|
|
136
|
+
Logger.warn('RabbitMQ connection closed', 'connectionEventHandler');
|
|
137
|
+
this.connection = null;
|
|
138
|
+
this.channel = null;
|
|
139
|
+
await this.handleReconnect();
|
|
140
|
+
});
|
|
141
|
+
} catch(error) {
|
|
142
|
+
Logger.error(`Error in connectionEventHandler: ${(error as Error).message}`, 'connectionEventHandler');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Description: Attempts to reconnect to RabbitMQ with exponential backoff.
|
|
148
|
+
* Resets connectionRetry on success so future disconnections can recover.
|
|
149
|
+
*/
|
|
150
|
+
private async handleReconnect(): Promise<void> {
|
|
151
|
+
let connectionRetry:number = 1;
|
|
152
|
+
const retryDelays = RABBITMQ.TTL.split('|').map(Number);
|
|
153
|
+
|
|
154
|
+
while (connectionRetry <= maxRetries) {
|
|
155
|
+
const delay = retryDelays[connectionRetry - 1] ?? retryDelays[retryDelays.length - 1];
|
|
156
|
+
Logger.info(`Reconnect attempt ${connectionRetry} of ${maxRetries} in ${delay / 1000}s...`, 'handleReconnect');
|
|
157
|
+
await new Promise(res => setTimeout(res, delay));
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
await this.createConnection();
|
|
161
|
+
connectionRetry = 1;
|
|
162
|
+
Logger.info('Reconnected to RabbitMQ successfully', 'handleReconnect');
|
|
163
|
+
return;
|
|
164
|
+
} catch (error) {
|
|
165
|
+
Logger.error(`Reconnect attempt ${connectionRetry} failed: ${(error as Error).message}`, 'handleReconnect');
|
|
166
|
+
connectionRetry++;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
Logger.error('Max reconnection attempts reached. No longer trying to reconnect.', 'handleReconnect');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Description: Registers exchanges, queues, and bindings in RabbitMQ based on the provided configuration data.
|
|
174
|
+
*/
|
|
175
|
+
public async registerQueueAndExchange(){
|
|
176
|
+
Logger.info('Reached to registerQueueAndExchange', 'registerQueueAndExchange');
|
|
177
|
+
try {
|
|
178
|
+
const { exchanges = [], queues = [], bindings = {} } = configData;
|
|
179
|
+
|
|
180
|
+
// Create all exchanges
|
|
181
|
+
for (const exchange of exchanges) {
|
|
182
|
+
await this.createExchange(exchange);
|
|
183
|
+
Logger.info(`${exchange.name} exchange registered`, 'registerQueueAndExchange');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Create all queues
|
|
187
|
+
for (const queue of queues) {
|
|
188
|
+
await this.createQueue(queue);
|
|
189
|
+
Logger.info(`${queue.name} queue registered`, 'registerQueueAndExchange');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Bind all queues to exchanges with routing keys
|
|
193
|
+
for (const exchangeName in bindings) {
|
|
194
|
+
for (const binding of bindings[exchangeName]) {
|
|
195
|
+
await this.bindQueueAndExchanges(exchangeName, binding.queue, binding.routingKey, binding.options);
|
|
196
|
+
Logger.info(`${binding.queue} bound to exchange ${exchangeName} with routing key ${binding.routingKey}`, 'registerQueueAndExchange');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} catch (error) {
|
|
200
|
+
Logger.error("Error on registering queue, exchange and binding", 'registerQueueAndExchange', (error as Error).message);
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Description: Binds a RabbitMQ queue to an exchange with a specified routing key.
|
|
208
|
+
* @param exchangeName
|
|
209
|
+
* @param queueName
|
|
210
|
+
* @param routingKey
|
|
211
|
+
*/
|
|
212
|
+
private async bindQueueAndExchanges(exchangeName: string, queueName: string, routingKey: string, options: any) {
|
|
213
|
+
Logger.info('Reached to bindQueueAndExchanges', 'bindQueueAndExchanges');
|
|
214
|
+
try {
|
|
215
|
+
if(options) {
|
|
216
|
+
await this.channel!.bindQueue(queueName, exchangeName, routingKey, options);
|
|
217
|
+
Logger.info(`Queue ${queueName} bound to exchange ${exchangeName} with routing key ${routingKey} and options ${JSON.stringify(options)}`, 'bindQueueAndExchanges');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
await this.channel!.bindQueue(queueName, exchangeName, routingKey);
|
|
221
|
+
Logger.info(`Queue ${queueName} bound to exchange ${exchangeName} with routing key ${routingKey}`, 'bindQueueAndExchanges');
|
|
222
|
+
} catch (error) {
|
|
223
|
+
Logger.error(`Failed to bind queue ${queueName} to exchange ${exchangeName}: ${(error as Error).message}`, 'bindQueueAndExchanges');
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Description: Creates a RabbitMQ exchange with the specified configuration.
|
|
230
|
+
* @param exchangeData
|
|
231
|
+
*/
|
|
232
|
+
private async createExchange(exchangeData: any) {
|
|
233
|
+
Logger.info('Reached to createExchange', 'createExchange');
|
|
234
|
+
try {
|
|
235
|
+
await this.channel!.assertExchange(exchangeData?.name, exchangeData?.type, { durable: exchangeData?.durable, autoDelete: exchangeData?.autoDelete, arguments: exchangeData?.arguments });
|
|
236
|
+
Logger.info(`${exchangeData?.name} exchange created`, 'createExchange');
|
|
237
|
+
} catch (error) {
|
|
238
|
+
Logger.error(`Failed to create ${exchangeData?.name} exchange: ${(error as Error).message}`, 'createExchange');
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Description: Creates a RabbitMQ queue with the specified configuration.
|
|
245
|
+
* @param queueData
|
|
246
|
+
*/
|
|
247
|
+
private async createQueue(queueData: any) {
|
|
248
|
+
Logger.info('Reached to createQueue', 'createQueue');
|
|
249
|
+
try {
|
|
250
|
+
await this.channel!.assertQueue(queueData?.name, { durable: queueData?.durable, exclusive: queueData?.exclusive, autoDelete: queueData?.autoDelete, arguments: queueData?.arguments });
|
|
251
|
+
Logger.info(`${queueData?.name} queue created`, 'createQueue');
|
|
252
|
+
} catch (error) {
|
|
253
|
+
Logger.error(`Failed to create ${queueData?.name} queue: ${(error as Error).message}`, 'createQueue');
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Description: Publishes a message to a RabbitMQ exchange with configurable retry attempts.
|
|
260
|
+
* On each failed attempt, waits using exponential backoff based on TTL config.
|
|
261
|
+
* After all retries are exhausted, sends the message to the dead letter queue.
|
|
262
|
+
* @param exchangeName - The target exchange name
|
|
263
|
+
* @param queueName - The routing key / queue name
|
|
264
|
+
* @param message - The message payload to publish
|
|
265
|
+
* @returns true if published successfully
|
|
266
|
+
*/
|
|
267
|
+
public async publishMessageToExchange(request: { exchangeName: string; queueName: string; message: any }): Promise<boolean> {
|
|
268
|
+
Logger.info('Reached to publishMessageToExchange', 'publishMessageToExchange');
|
|
269
|
+
const { exchangeName, queueName, message } = request;
|
|
270
|
+
const retryDelayConfig = message?.message?.feed_type === RABBITMQ_FEED_TYPE.OVR ? RABBITMQ.OVR_TTL : RABBITMQ.ETX_TTL;
|
|
271
|
+
const retryDelay = retryDelayConfig.split('|').map(Number);
|
|
272
|
+
|
|
273
|
+
let attempt = 0;
|
|
274
|
+
while (attempt < maxRetries) {
|
|
275
|
+
try {
|
|
276
|
+
await this.checkConnectionAndChannel();
|
|
277
|
+
const messageBuffer = Buffer.from(JSON.stringify(message));
|
|
278
|
+
const sent = this.channel!.publish(exchangeName, queueName, messageBuffer, { persistent: true });
|
|
279
|
+
//if(attempt < 1){ sent = false}else{sent = true} // --- IGNORE ---
|
|
280
|
+
if (sent && (RABBITMQ.PUBLISHER_RETRY_ERROR !== 'true' && RABBITMQ.PUBLISHER_RETRY_ERROR !== true)) {
|
|
281
|
+
Logger.info(`Message published successfully on attempt ${attempt + 1}`, 'publishMessageToExchange');
|
|
282
|
+
return true;
|
|
283
|
+
} else {
|
|
284
|
+
throw new Error('Channel publish returned false — buffer full');
|
|
285
|
+
}
|
|
286
|
+
} catch (error) {
|
|
287
|
+
attempt++;
|
|
288
|
+
Logger.error(`Attempt ${attempt} of ${maxRetries} failed in ${retryDelay[attempt - 1] / 1000}s...: ${(error as Error).message}`, 'publishMessageToExchange');
|
|
289
|
+
|
|
290
|
+
if (attempt >= maxRetries) {
|
|
291
|
+
Logger.error('Max retries reached. Sending message to Dead Letter Queue...', 'publishMessageToExchange');
|
|
292
|
+
await this.sendToDeadLetterQueue({ message, attempt });
|
|
293
|
+
Logger.error(`Failed to publish message to exchange ${exchangeName} after ${maxRetries} attempts`, 'publishMessageToExchange');
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Exponential backoff using TTL array
|
|
298
|
+
const delay = retryDelay[attempt - 1] ?? retryDelay[retryDelay.length - 1];
|
|
299
|
+
Logger.info(`Retrying in ${delay / 1000}s...`, 'publishMessageToExchange');
|
|
300
|
+
await new Promise(res => setTimeout(res, delay));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Description: Publishes a message directly to a RabbitMQ queue with configurable retry attempts.
|
|
309
|
+
* On each failed attempt, waits using exponential backoff based on TTL config.
|
|
310
|
+
* After all retries are exhausted, sends the message to the dead letter queue.
|
|
311
|
+
* @param request - The request object containing the queue name and the message payload
|
|
312
|
+
* @returns true if the message was published successfully, false otherwise
|
|
313
|
+
*/
|
|
314
|
+
public async publishMessage(request: { queueName: string; message: any }): Promise<boolean> {
|
|
315
|
+
Logger.info('Reached to publishMessage', 'publishMessage');
|
|
316
|
+
const { queueName, message } = request;
|
|
317
|
+
const retryDelayConfig = message?.message?.feed_type === RABBITMQ_FEED_TYPE.OVR ? RABBITMQ.OVR_TTL : RABBITMQ.ETX_TTL;
|
|
318
|
+
const retryDelay = retryDelayConfig.split('|').map(Number);
|
|
319
|
+
|
|
320
|
+
let attempt = 0;
|
|
321
|
+
while (attempt < maxRetries) {
|
|
322
|
+
try {
|
|
323
|
+
await this.checkConnectionAndChannel();
|
|
324
|
+
const messageBuffer = Buffer.from(JSON.stringify(message));
|
|
325
|
+
const sent = this.channel!.sendToQueue(queueName, messageBuffer, { persistent: true });
|
|
326
|
+
//if(attempt < 1){ sent = false}else{sent = true} // --- IGNORE ---
|
|
327
|
+
if (sent && (RABBITMQ.PUBLISHER_RETRY_ERROR !== 'true' && RABBITMQ.PUBLISHER_RETRY_ERROR !== true)) {
|
|
328
|
+
Logger.info(`Message published successfully on attempt ${attempt + 1}`, 'publishMessage');
|
|
329
|
+
return true;
|
|
330
|
+
} else {
|
|
331
|
+
throw new Error('Channel publish returned false — buffer full');
|
|
332
|
+
}
|
|
333
|
+
} catch (error) {
|
|
334
|
+
attempt++;
|
|
335
|
+
Logger.error(`Attempt ${attempt} of ${maxRetries} failed in ${retryDelay[attempt - 1] / 1000}s...: ${(error as Error).message}`, 'publishMessage');
|
|
336
|
+
|
|
337
|
+
if (attempt >= maxRetries) {
|
|
338
|
+
Logger.error('Max retries reached. Sending message to Dead Letter Queue...', 'publishMessage');
|
|
339
|
+
await this.sendToDeadLetterQueue({ message, attempt });
|
|
340
|
+
Logger.error(`Failed to publish message to queue ${queueName} after ${maxRetries} attempts`, 'publishMessage');
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Exponential backoff using TTL array
|
|
345
|
+
const delay = retryDelay[attempt - 1] ?? retryDelay[retryDelay.length - 1];
|
|
346
|
+
Logger.info(`Retrying in ${delay / 1000}s...`, 'publishMessage');
|
|
347
|
+
await new Promise(res => setTimeout(res, delay));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Description: Sends a failed message to the dead letter queue after all retries are exhausted.
|
|
356
|
+
* @param request - The request object containing the message payload and the number of attempts made
|
|
357
|
+
*/
|
|
358
|
+
private async sendToDeadLetterQueue(request: { message: any; attempt: number }): Promise<void> {
|
|
359
|
+
const { message, attempt } = request;
|
|
360
|
+
try {
|
|
361
|
+
await this.checkConnectionAndChannel(); // Ensure connection and channel are alive before sending to DLQ
|
|
362
|
+
const dlqQueue = RABBITMQ.DLX_QUEUE_ETG;
|
|
363
|
+
const messageBuffer = Buffer.from(JSON.stringify(message));
|
|
364
|
+
const sent = this.channel!.sendToQueue(dlqQueue, messageBuffer, {
|
|
365
|
+
persistent: true,
|
|
366
|
+
headers: { 'x-retry-count': attempt },
|
|
367
|
+
});
|
|
368
|
+
if (sent) {
|
|
369
|
+
Logger.info(`Message sent to DLQ: ${dlqQueue} (attempts: ${attempt})`, 'sendToDeadLetterQueue');
|
|
370
|
+
} else {
|
|
371
|
+
Logger.error(`Failed to send message to DLQ: ${dlqQueue} — buffer full`, 'sendToDeadLetterQueue');
|
|
372
|
+
}
|
|
373
|
+
} catch (error) {
|
|
374
|
+
Logger.error(`Error sending message to DLQ: ${(error as Error).message}`, 'sendToDeadLetterQueue');
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Description: Initializes consumers for all queues listed in `consume_queues` config.
|
|
380
|
+
* @param classInstance - Object whose methods will be called as handlers
|
|
381
|
+
* @param consumerHandler - Method name on classInstance to process each message
|
|
382
|
+
* @param consumerErrorHandler - Method name on classInstance called when all retries fail
|
|
383
|
+
*/
|
|
384
|
+
public async initializeConsumers(classInstance: any, consumerHandler: string, consumerErrorHandler: string): Promise<void> {
|
|
385
|
+
try {
|
|
386
|
+
Logger.info('Reached in initialize consumers', 'initializeConsumers');
|
|
387
|
+
const consumeQueues = configData?.consume_queues;
|
|
388
|
+
for (const queueName in consumeQueues) {
|
|
389
|
+
if (Object.prototype.hasOwnProperty.call(consumeQueues, queueName)) {
|
|
390
|
+
await this.consumeMessage(queueName, classInstance, consumerHandler, consumerErrorHandler);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
} catch (error: unknown) {
|
|
394
|
+
Logger.error('Error initializing consumers:', 'initializeConsumers', (error as Error).message);
|
|
395
|
+
throw new Error((error as Error).message);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Description: Consumes messages from a queue with retry logic.
|
|
401
|
+
* - Success → ack
|
|
402
|
+
* - Failure + retries left → send to retry queue with incremented x-retry-count & TTL backoff, ack original
|
|
403
|
+
* - Failure + retries exhausted → DLQ + error handler + nack (no requeue)
|
|
404
|
+
* - Unhandled exception → DLQ + error handler + nack
|
|
405
|
+
* @param queueName - Queue to consume from
|
|
406
|
+
* @param classInstance - Object whose methods will be called as handlers
|
|
407
|
+
* @param consumerHandler - Method name invoked to process each message
|
|
408
|
+
* @param consumerErrorHandler - Method name invoked when all retries fail
|
|
409
|
+
*/
|
|
410
|
+
public async consumeMessage(
|
|
411
|
+
queueName: string,
|
|
412
|
+
classInstance: any,
|
|
413
|
+
consumerHandler: string,
|
|
414
|
+
consumerErrorHandler: string
|
|
415
|
+
): Promise<void> {
|
|
416
|
+
Logger.info(`Reached: Consuming messages from queue ${queueName}`, 'consumeMessage');
|
|
417
|
+
|
|
418
|
+
await this.checkConnectionAndChannel();
|
|
419
|
+
|
|
420
|
+
await this.channel!.consume(queueName, async (message: any) => {
|
|
421
|
+
if (!message) return;
|
|
422
|
+
|
|
423
|
+
const msgData: any = JSON.parse(message.content.toString());
|
|
424
|
+
const headers = message.properties.headers || {};
|
|
425
|
+
const retryCount: number = headers['x-retry-count'] || 0;
|
|
426
|
+
|
|
427
|
+
const feedType = msgData?.message?.feed_type ?? msgData?.feed_type;
|
|
428
|
+
const retryDelayTTL = feedType === RABBITMQ_FEED_TYPE.OVR ? RABBITMQ.OVR_TTL : RABBITMQ.ETX_TTL;
|
|
429
|
+
const retryDelay:any = retryDelayTTL.split('|').map(Number);
|
|
430
|
+
|
|
431
|
+
const data: any = {
|
|
432
|
+
queue_name: queueName,
|
|
433
|
+
body: msgData?.message?.body ?? msgData?.body,
|
|
434
|
+
trace: msgData?.message?.trace ?? msgData?.trace,
|
|
435
|
+
retries: msgData?.message?.retries ?? msgData?.retries,
|
|
436
|
+
version: msgData?.message?.version ?? msgData?.version,
|
|
437
|
+
feed_type: msgData?.message?.feed_type ?? msgData?.feed_type,
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
let success: any = {};
|
|
442
|
+
|
|
443
|
+
if (RABBITMQ.CONSUMER_RETRY_ERROR !== 'true' && RABBITMQ.CONSUMER_RETRY_ERROR !== true) {
|
|
444
|
+
if (typeof classInstance[consumerHandler] === 'function') {
|
|
445
|
+
Logger.info('Consumer handler call for processing the request', 'consumeMessage');
|
|
446
|
+
success = await classInstance[consumerHandler]({ queueName, message: msgData });
|
|
447
|
+
} else {
|
|
448
|
+
Logger.error(`Method ${consumerHandler} not found on classInstance`, 'consumeMessage');
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if(success?.isMatch === true){
|
|
452
|
+
if (success?.status) {
|
|
453
|
+
Logger.info('Acknowledge the successful processing', 'consumeMessage');
|
|
454
|
+
this.channel!.ack(message);
|
|
455
|
+
} else {
|
|
456
|
+
if (retryCount >= maxRetries) {
|
|
457
|
+
Logger.error('Max retries reached. Sending message to Dead Letter Queue.', 'consumeMessage');
|
|
458
|
+
await this.sendToDeadLetterQueue({ message: data, attempt: retryCount });
|
|
459
|
+
|
|
460
|
+
if (typeof classInstance[consumerErrorHandler] === 'function') {
|
|
461
|
+
classInstance[consumerErrorHandler](data);
|
|
462
|
+
} else {
|
|
463
|
+
Logger.error(`Method ${consumerErrorHandler} not found on classInstance`, 'consumeMessage');
|
|
464
|
+
}
|
|
465
|
+
this.channel!.nack(message, false, false);
|
|
466
|
+
} else {
|
|
467
|
+
const nextAttempt = retryCount + 1;
|
|
468
|
+
if (data?.retries >= 0) {
|
|
469
|
+
data.retries = nextAttempt;
|
|
470
|
+
}
|
|
471
|
+
const delayIndex = nextAttempt - 1 < retryDelay.length ? nextAttempt - 1 : retryDelay.length - 1;
|
|
472
|
+
const delay = Number(retryDelay[delayIndex]);
|
|
473
|
+
Logger.info(`Retrying in ${delay / 1000} seconds... (attempt ${nextAttempt} of ${maxRetries} ${data.feed_type})`, 'consumeMessage');
|
|
474
|
+
await this.sendMessageToRetryQueue({ attempt: nextAttempt, data, retryDelayMs: delay, queueName });
|
|
475
|
+
this.channel!.ack(message);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
} catch (error) {
|
|
480
|
+
Logger.error('Error processing message', 'consumeMessage', { message: (error as Error).message });
|
|
481
|
+
await this.sendToDeadLetterQueue({ message: data, attempt: retryCount });
|
|
482
|
+
|
|
483
|
+
if (typeof classInstance[consumerErrorHandler] === 'function') {
|
|
484
|
+
classInstance[consumerErrorHandler](data);
|
|
485
|
+
} else {
|
|
486
|
+
Logger.error(`Method ${consumerErrorHandler} not found on classInstance`, 'consumeMessage');
|
|
487
|
+
}
|
|
488
|
+
this.channel!.nack(message, false, false);
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Description: Sends a message to a retry queue with an incremented retry count and appropriate TTL for delayed reprocessing.
|
|
495
|
+
* @param request
|
|
496
|
+
* @returns boolean indicating if the message was sent to the retry queue successfully
|
|
497
|
+
*/
|
|
498
|
+
private async sendMessageToRetryQueue(request: { attempt: number; data: any; retryDelayMs: number; queueName: string }): Promise<boolean> {
|
|
499
|
+
const { attempt, data, retryDelayMs, queueName } = request;
|
|
500
|
+
Logger.info(`Sending message to retry queue: ${queueName} (attempt: ${attempt}, delay: ${retryDelayMs}ms)`, 'sendMessageToRetryQueue');
|
|
501
|
+
try {
|
|
502
|
+
await this.checkConnectionAndChannel();
|
|
503
|
+
const retryQueueName = RABBITMQ.DLX_QUEUE_ETG_RETRY;
|
|
504
|
+
const messageBuffer = Buffer.from(JSON.stringify(data));
|
|
505
|
+
const sent = this.channel!.sendToQueue(retryQueueName, messageBuffer, {
|
|
506
|
+
headers: {
|
|
507
|
+
'x-retry-count': attempt,
|
|
508
|
+
toQueue: queueName,
|
|
509
|
+
},
|
|
510
|
+
expiration: retryDelayMs.toString(),
|
|
511
|
+
persistent: true,
|
|
512
|
+
mandatory: true
|
|
513
|
+
});
|
|
514
|
+
if (sent) {
|
|
515
|
+
Logger.info(`Message sent to retry queue: ${queueName} (attempt: ${attempt})`, 'sendMessageToRetryQueue');
|
|
516
|
+
} else {
|
|
517
|
+
Logger.error(`Failed to send message to retry queue: ${queueName} — buffer full`, 'sendMessageToRetryQueue');
|
|
518
|
+
}
|
|
519
|
+
return sent;
|
|
520
|
+
} catch (error) {
|
|
521
|
+
Logger.error(`Error sending message to retry queue: ${(error as Error).message}`, 'sendMessageToRetryQueue');
|
|
522
|
+
throw error;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|