@autofleet/rabbit 3.1.2 → 3.2.2-2.beta-0

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/src/index.ts CHANGED
@@ -1,16 +1,19 @@
1
1
  /* eslint-disable no-empty,consistent-return,no-async-promise-executor,@typescript-eslint/no-unused-vars */
2
2
 
3
- import { EventEmitter } from 'events';
3
+ import { EventEmitter, once } from 'events';
4
4
  import { promisify } from 'util';
5
5
  import moment from 'moment';
6
6
  import RedisLock from 'redis-lock';
7
- import { AmqpConnectionManager, ChannelWrapper, connect } from 'amqp-connection-manager';
7
+ import {
8
+ AmqpConnectionManager, ChannelWrapper, connect, CreateChannelOpts,
9
+ } from 'amqp-connection-manager';
8
10
  import {
9
11
  ConfirmChannel, ConsumeMessage, Options, Replies,
10
12
  } from 'amqplib';
11
13
  import {
12
14
  getCurrentPayload, newTrace, traceTypes, createOrSetRabbitTrace, outbreak,
13
15
  } from '@autofleet/zehut';
16
+ import { randomUUID } from 'node:crypto';
14
17
  import logger from './logger';
15
18
  import RabbitError from './lib/rabbitError';
16
19
  import getRedisInstance, { RedisConfig } from './lib/redis';
@@ -32,12 +35,15 @@ import {
32
35
  CustomMessageHeaders,
33
36
  QueuesCache,
34
37
  RedisLockType,
35
- ExchangesCache, CONSUMER_DEFAULT_OPTIONS,
38
+ ExchangesCache, CONSUMER_DEFAULT_OPTIONS, QueueSetupPromisesDictionary,
39
+ ConnectionPurpose,
40
+ ConnectionData,
36
41
  } from './lib/types';
37
42
 
38
43
  // const debug = nodeDebug('af-rabbitmq')
39
- const debug = logger.info;
44
+ const debug = logger.debug.bind(logger);
40
45
 
46
+ const PUBLISH_TIMEOUT = 1000 * 10;
41
47
  export interface IAfRabbitMq {
42
48
  ack: any;
43
49
  nack: any;
@@ -51,6 +57,10 @@ export interface IAfRabbitMq {
51
57
  redisClient?: any;
52
58
  }
53
59
 
60
+ interface NackOptions {
61
+ skipRetry?: boolean;
62
+ }
63
+
54
64
  export interface AfRabbitOptions {
55
65
  disableReconnect?: boolean;
56
66
 
@@ -65,16 +75,21 @@ export interface AfRabbitOptions {
65
75
  * @default false
66
76
  */
67
77
  dontRetryAssert?: boolean;
78
+
79
+ rabbitHost?: string;
68
80
  }
69
81
 
70
82
  type newChannelOpts = {
71
83
  name?: string;
72
84
  onClose?: null | ((args: any | null) => void);
85
+ options?: CreateChannelOpts | undefined;
86
+ connectionPurpose: ConnectionPurpose,
73
87
  };
74
88
 
75
89
  type assertChannelOpts = {
76
90
  channelName?: string;
77
91
  force?: boolean;
92
+ connectionPurpose: ConnectionPurpose;
78
93
  }
79
94
 
80
95
  type AfConsumer = {
@@ -83,16 +98,10 @@ type AfConsumer = {
83
98
  options: ConsumeOptions | undefined;
84
99
  }
85
100
 
86
- const USERNAME: string = process.env.RABBITMQ_USERNAME || 'guest';
87
-
88
- const PASSWORD: string = process.env.RABBITMQ_PASSWORD || 'guest';
89
-
90
- const HOST: string = process.env.RABBITMQ_SERVICE_HOST || 'localhost';
91
-
92
101
  const HEARTBEAT = '60';
93
102
 
94
103
  class RabbitMq implements IAfRabbitMq {
95
- static parseMsg(msg: any) : any {
104
+ static parseMsg(msg: any): any {
96
105
  let { content } = msg;
97
106
  content = content.toString();
98
107
 
@@ -106,7 +115,7 @@ class RabbitMq implements IAfRabbitMq {
106
115
  };
107
116
  }
108
117
 
109
- static validateName(type: string, name: string) {
118
+ static validateName(type: string, name: string): void {
110
119
  if (!name || name === '') {
111
120
  throw new RabbitError(`error while using ${type} with no name`);
112
121
  }
@@ -119,6 +128,7 @@ class RabbitMq implements IAfRabbitMq {
119
128
  const outbreakTrace = outbreak.getCurrentContext();
120
129
  return {
121
130
  timestamp: moment().unix(),
131
+ timeout: PUBLISH_TIMEOUT,
122
132
  headers: {
123
133
  creationTimestamp: moment().valueOf(),
124
134
  ...customHeaders,
@@ -128,28 +138,29 @@ class RabbitMq implements IAfRabbitMq {
128
138
  };
129
139
  }
130
140
 
131
- // PUBLISH_TIMEOUT = Number(process.env.RABBITMQ_PUBLISH_TIMEOUT) || 60000;
132
-
133
- // PUBLISH_ERROR_MSG = `rabbit: publish timeout(${this.PUBLISH_TIMEOUT}ms) has pass, exchange: `;
134
-
135
141
  DISCONNECT_MSG = 'rabbit: connection disconnect';
136
142
 
137
143
  RECONNECT_MSG = 'rabbit: connection disconnect - reconnecting';
138
144
 
139
145
  channel: ChannelWrapper | null;
140
146
 
141
- blockReconnect: boolean | null | undefined
147
+ publishChannelSetupPromise: Promise<ChannelWrapper> | null;
142
148
 
143
- connection: AmqpConnectionManager | null | undefined
149
+ blockReconnect: boolean | null | undefined;
144
150
 
145
- em: EventEmitter;
151
+ connectionsMap: {
152
+ [ConnectionPurpose.Consume]: ConnectionData;
153
+ [ConnectionPurpose.Publish]: ConnectionData;
154
+ };
146
155
 
147
- creatingConnection: boolean;
156
+ em: EventEmitter;
148
157
 
149
158
  exchanges: ExchangesCache;
150
159
 
151
160
  queues: QueuesCache;
152
161
 
162
+ queueSetupPromises: QueueSetupPromisesDictionary;
163
+
153
164
  options: AfRabbitOptions | undefined;
154
165
 
155
166
  redisClient: any;
@@ -164,10 +175,15 @@ class RabbitMq implements IAfRabbitMq {
164
175
  constructor(options?: AfRabbitOptions, redisConfig?: RedisConfig) {
165
176
  this.em = new EventEmitter();
166
177
  this.channel = null;
167
- this.connection = null;
168
- this.creatingConnection = false;
178
+ this.publishChannelSetupPromise = null;
179
+ this.connectionsMap = {
180
+ [ConnectionPurpose.Consume]: { connection: null, creatingConnection: false },
181
+ [ConnectionPurpose.Publish]: { connection: null, creatingConnection: false },
182
+ };
169
183
  this.exchanges = {};
170
184
  this.queues = {};
185
+ this.queueSetupPromises = {};
186
+ this.consumers = [];
171
187
  this.options = options;
172
188
  this.redisClient = redisConfig && getRedisInstance(redisConfig);
173
189
  if (this.redisClient) {
@@ -199,8 +215,9 @@ class RabbitMq implements IAfRabbitMq {
199
215
  return false;
200
216
  }
201
217
 
202
- public ack = (channel: ChannelWrapper, msg: ConsumeMessageOrNull, shouldUpdateRedisTimestamp = false, releaseLock = null) => async (userMsg: ConsumeMessage) : Promise<any> => {
218
+ public ack = (channel: ConfirmChannel, msg: ConsumeMessageOrNull, shouldUpdateRedisTimestamp = false, releaseLock = null) => async (userMsg: ConsumeMessage): Promise<any> => {
203
219
  if (msg) {
220
+ debug('rabbit acking message', { deliveryTag: msg.fields.deliveryTag });
204
221
  await channel.ack(msg);
205
222
  const { properties: { headers } } = msg;
206
223
  const timestamp = headers?.creationTimestamp;
@@ -214,7 +231,7 @@ class RabbitMq implements IAfRabbitMq {
214
231
  }
215
232
 
216
233
  public nack = (
217
- channel: ChannelWrapper,
234
+ channel: ConfirmChannel,
218
235
  queue: string,
219
236
  options: any,
220
237
  deadQueueOptions: Options.AssertQueue,
@@ -224,8 +241,8 @@ class RabbitMq implements IAfRabbitMq {
224
241
  userMsg: ConsumeMessageOrNull,
225
242
  {
226
243
  skipRetry = false,
227
- }: any = { },
228
- ) : Promise<any> => {
244
+ }: NackOptions = {},
245
+ ): Promise<any> => {
229
246
  await this.unlockRedisIfNeeded(releaseLock);
230
247
  if (channel && msg) {
231
248
  if (
@@ -250,44 +267,68 @@ class RabbitMq implements IAfRabbitMq {
250
267
  : 1,
251
268
  });
252
269
  }
270
+ debug('rabbit nacking message', { deliveryTag: msg.fields.deliveryTag });
253
271
  await channel.ack(msg);
254
272
  } else {
255
273
  logger.error('no channel or msg', {
256
- channel: channel ? channel.name : '',
257
274
  msg,
258
275
  });
259
276
  }
260
277
  }
261
278
 
262
- async getConnection() {
263
- return new Promise<AmqpConnectionManager>(async (resolve, reject) => {
279
+ async getConnection(connectionPurpose: ConnectionPurpose) {
280
+ return new Promise<AmqpConnectionManager | undefined | null>(async (resolve, reject) => {
281
+ let { connection, creatingConnection } = this.connectionsMap[connectionPurpose];
264
282
  if (this.blockReconnect) {
265
283
  debug('rabbit: block reconnect');
266
284
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
267
285
  // @ts-ignore
268
286
  return resolve();
269
287
  }
270
- if (this.connection !== null) {
271
- if (this.options?.disableReconnect || this.connection?.isConnected()) {
288
+ if (connection !== null) {
289
+ if (this.options?.disableReconnect || connection?.isConnected()) {
272
290
  debug('rabbit: connection - is connected');
273
291
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
274
292
  // @ts-ignore
275
- return resolve(this.connection);
293
+ return resolve(connection);
276
294
  }
277
295
  debug('rabbit: connection - reconnecting');
278
296
  }
279
- if (this.creatingConnection) {
297
+ if (creatingConnection) {
280
298
  debug('rabbit: creating connection emi');
281
299
  this.em.once(CONNECTION_CREATED_CONST, resolve);
282
300
  this.em.once(CONNECTION_FAILED_CONST, reject);
283
301
  return;
284
302
  }
285
- this.creatingConnection = true;
303
+ this.connectionsMap[connectionPurpose].creatingConnection = true;
286
304
  let isResolved = false;
287
- debug('rabbit: creating connection', { HOST, USERNAME, HEARTBEAT });
288
- const connection: AmqpConnectionManager = await connect([`amqp://${USERNAME}:${PASSWORD}@${HOST}?heartbeat=${HEARTBEAT}}`]);
289
- this.connection = connection;
290
- this.connection.on('error', (err) => {
305
+
306
+ // It is import to use it as a function and not as a variable
307
+ // because of k8s changes the env variables
308
+ // and we want to use the new values
309
+ const findServers = () => {
310
+ const userName = process.env.RABBITMQ_USERNAME || 'guest';
311
+ const password = process.env.RABBITMQ_PASSWORD || 'guest';
312
+ const host = this.options?.rabbitHost || process.env.RABBITMQ_SERVICE_HOST || 'localhost';
313
+
314
+ debug('rabbit: creating connection', { host, userName, HEARTBEAT });
315
+
316
+ return [`amqp://${userName}:${password}@${host}?heartbeat=${HEARTBEAT}`];
317
+ };
318
+
319
+ const defaultUrls = findServers();
320
+ const newConnection: AmqpConnectionManager = await connect(defaultUrls, {
321
+ findServers,
322
+ });
323
+
324
+ if (!newConnection) {
325
+ logger.error('rabbit: couldnt create a connection');
326
+ return resolve(connection);
327
+ }
328
+
329
+ this.connectionsMap[connectionPurpose].connection = newConnection;
330
+
331
+ newConnection.on('error', (err) => {
291
332
  logger.error('rabbit: connection error', { err });
292
333
  if (!isResolved) {
293
334
  isResolved = true;
@@ -296,7 +337,8 @@ class RabbitMq implements IAfRabbitMq {
296
337
  }
297
338
  });
298
339
 
299
- this.connection.on('connectFailed', (err) => {
340
+ newConnection.on('connectFailed', (err) => {
341
+ this.consumersTags = [];
300
342
  logger.error('rabbit: connection connectFailed', { err });
301
343
  if (!isResolved) {
302
344
  isResolved = true;
@@ -305,12 +347,9 @@ class RabbitMq implements IAfRabbitMq {
305
347
  }
306
348
  });
307
349
 
308
- this.connection.on('disconnect', ({ err }) => {
350
+ newConnection.on('disconnect', ({ err }) => {
351
+ this.consumersTags = [];
309
352
  debug('rabbit: connection closed');
310
- this.exchanges = {};
311
- this.queues = {};
312
- this.connection = null;
313
- this.channel = null;
314
353
  if (this.options?.disableReconnect) {
315
354
  logger.error(`${this.DISCONNECT_MSG}${err && ` - ${err}`}`);
316
355
  this.blockReconnect = true;
@@ -318,66 +357,68 @@ class RabbitMq implements IAfRabbitMq {
318
357
  logger.error(`${this.RECONNECT_MSG}${err && ` - ${err}`}`);
319
358
  }
320
359
  });
321
- this.connection.once('connect', async () => {
360
+ newConnection.once('connect', async () => {
322
361
  debug('rabbit: connection established');
323
- await this.loadConsumers();
324
- this.creatingConnection = false;
325
- this.em.emit(CONNECTION_CREATED_CONST, connection);
362
+ this.connectionsMap[connectionPurpose].creatingConnection = false;
363
+ this.em.emit(CONNECTION_CREATED_CONST, newConnection);
326
364
  isResolved = true;
327
- resolve(connection);
365
+ resolve(newConnection);
328
366
  });
329
367
  });
330
368
  }
331
369
 
332
- async getNewChannel({ name = rand().toString(), onClose = null }: newChannelOpts = {}) {
333
- return new Promise<ChannelWrapper>(async (resolve, reject) => {
334
- const connection: AmqpConnectionManager = await this.getConnection();
335
- const channel = connection.createChannel({});
336
-
337
- let isResolved = false;
338
- channel.on('error', (err) => {
339
- logger.error(`rabbit: channel ${name} error`, { err });
340
- if (!isResolved) {
341
- isResolved = true;
342
- reject(err);
343
- }
344
- });
345
- channel.on('close', (...args) => {
346
- logger.error(`rabbit: channel ${name} closed`, { args });
347
- if (onClose) {
348
- onClose(args);
349
- }
350
- });
351
- channel.once('connect', () => {
352
- debug(`rabbit: channel ${name} CONNECTED`);
353
- isResolved = true;
354
- resolve(channel);
355
- });
370
+ async getNewChannel({
371
+ name = rand().toString(), onClose = null, options = {}, connectionPurpose = ConnectionPurpose.Consume,
372
+ }: newChannelOpts): Promise<ChannelWrapper> {
373
+ let connection!: AmqpConnectionManager | undefined | null;
374
+ try {
375
+ connection = await this.getConnection(connectionPurpose);
376
+ } catch (e) {
377
+ logger.error(`rabbit: error on get connection for new channel ${name} `, { e });
378
+ throw e;
379
+ }
380
+ if (!connection) {
381
+ throw new Error(`rabbit: couldnt get connection for new channel ${name}`);
382
+ }
383
+ const channel = connection.createChannel({ ...options });
384
+ once(channel, 'close').then((args) => {
385
+ logger.error(`rabbit: channel ${name} closed`);
386
+ onClose?.(args);
356
387
  });
388
+ try {
389
+ await once(channel, 'connect');
390
+ debug(`rabbit: channel ${name} CONNECTED`);
391
+ return channel;
392
+ } catch (err) {
393
+ logger.error(`rabbit: channel error ${name} error`, { err });
394
+ throw err;
395
+ }
357
396
  }
358
397
 
359
- async assertChannel({ force = false } : assertChannelOpts = {}): Promise<ChannelWrapper> {
360
- return new Promise<ChannelWrapper>(async (resolve, reject) => {
361
- if (this.channel && !force) {
362
- return resolve(this.channel);
363
- }
398
+ async assertChannel({ force = false, connectionPurpose = ConnectionPurpose.Consume }: assertChannelOpts): Promise<ChannelWrapper> {
399
+ if (!this.publishChannelSetupPromise) {
400
+ this.publishChannelSetupPromise = new Promise<ChannelWrapper>(async (resolve, reject) => {
401
+ if (this.channel && !force) {
402
+ return resolve(this.channel);
403
+ }
364
404
 
365
- try {
366
- const channel = await this.getNewChannel({
367
- onClose: () => {
368
- this.channel = null;
369
- },
370
- });
371
- this.channel = channel;
372
- resolve(channel);
373
- } catch (e) {
374
- reject(e);
375
- }
376
- });
405
+ try {
406
+ const channel = await this.getNewChannel({ connectionPurpose });
407
+ channel.on('error', (err) => {
408
+ logger.error('rabbit: channel error', { err });
409
+ });
410
+ this.channel = channel;
411
+ resolve(channel);
412
+ } catch (e) {
413
+ reject(e);
414
+ }
415
+ });
416
+ }
417
+ return this.publishChannelSetupPromise;
377
418
  }
378
419
 
379
420
  async assertExchange(exchangeName: string, options?: any) {
380
- const channel: ChannelWrapper = await this.assertChannel();
421
+ const channel: ChannelWrapper = await this.assertChannel({ connectionPurpose: options.connectionPurpose });
381
422
  if (this.exchanges[exchangeName]) {
382
423
  return this.exchanges[exchangeName];
383
424
  }
@@ -386,36 +427,36 @@ class RabbitMq implements IAfRabbitMq {
386
427
  return exchange;
387
428
  }
388
429
 
389
- async getQueueLength(queue: string) {
430
+ async getQueueLength(queue: string, connectionPurpose: ConnectionPurpose = ConnectionPurpose.Consume): Promise<Replies.AssertQueue> {
390
431
  RabbitMq.validateName('queue', queue);
391
- const channel: ChannelWrapper = await this.assertChannel();
392
- debug('rabbit: getting queue length', { queue, connected: this.connection?.isConnected() });
393
- return channel.checkQueue(queue);
432
+ let { connection } = this.connectionsMap[connectionPurpose];
433
+ const { channel } = this;
434
+ if (!channel) {
435
+ throw new Error('channel is not defined');
436
+ }
437
+ debug('rabbit: getting queue length', { queue, connected: connection?.isConnected() });
438
+ return channel?.checkQueue(queue);
394
439
  }
395
440
 
396
- private async deleteQueue(queue: string) {
441
+ private async deleteQueue(queue: string, connectionPurpose: ConnectionPurpose) {
397
442
  RabbitMq.validateName('queue', queue);
398
- const channel: ChannelWrapper = await this.assertChannel();
443
+ const channel: ChannelWrapper = await this.assertChannel({ connectionPurpose });
399
444
  logger.info('rabbit: deleting queue', { queue });
400
445
  const deleteQueueRes = await channel.deleteQueue(queue);
401
446
  debug('queue deleted', deleteQueueRes);
402
447
  return deleteQueueRes;
403
448
  }
404
449
 
405
- async bindQueue(queue: string, exchange: string) {
406
- const channel: ChannelWrapper = await this.assertChannel();
450
+ async bindQueue(queue: string, exchange: string, connectionPurpose: ConnectionPurpose) {
451
+ const channel: ChannelWrapper = await this.assertChannel({ connectionPurpose });
407
452
  await channel.addSetup((setupChannel: ConfirmChannel) => setupChannel.bindQueue(queue, exchange, ''));
408
453
  return channel.bindQueue(queue, exchange, '');
409
454
  }
410
455
 
411
- async assertQueue(queueName: string, options?: Options.AssertQueue) {
412
- let queue: Replies.AssertQueue | null = null;
413
- RabbitMq.validateName('queue', queueName);
414
- if (this.queues[queueName]) {
415
- return this.queues[queueName];
416
- }
456
+ async setupQueue(queueName: string, connectionPurpose: ConnectionPurpose, options?: Options.AssertQueue): Promise<Replies.AssertQueue> {
457
+ let queue: Replies.AssertQueue;
417
458
  try {
418
- const channel: ChannelWrapper = await this.assertChannel();
459
+ const channel: ChannelWrapper = await this.assertChannel({ connectionPurpose });
419
460
  debug('assertQueue->channel.addSetup', { queueName });
420
461
  await channel.addSetup((setupChannel: ConfirmChannel) => setupChannel.assertQueue(queueName, options));
421
462
  debug('assertQueue->channel.assertQueue', { queueName });
@@ -424,12 +465,12 @@ class RabbitMq implements IAfRabbitMq {
424
465
  logger.error('rabbit: assertQueue error', { queueName, options, error: e });
425
466
  if (!this.options?.dontRetryAssert) {
426
467
  debug('retrying assertQueue', { queueName });
427
- const channel = await this.assertChannel({ force: true });
428
- await this.deleteQueue(queueName);
468
+ const channel = await this.assertChannel({ force: true, connectionPurpose });
469
+ await this.deleteQueue(queueName, connectionPurpose);
429
470
 
430
- debug('1assertQueue->channel.addSetup', { queueName });
471
+ debug('retrying assertQueue->channel.addSetup', { queueName });
431
472
  await channel.addSetup((setupChannel: ConfirmChannel) => setupChannel.assertQueue(queueName, options));
432
- debug('1assertQueue->channel.assertQueue', { queueName });
473
+ debug('retrying assertQueue->channel.assertQueue', { queueName });
433
474
  queue = await channel.assertQueue(queueName, options);
434
475
  } else {
435
476
  throw e;
@@ -440,29 +481,35 @@ class RabbitMq implements IAfRabbitMq {
440
481
  return queue;
441
482
  }
442
483
 
443
- private saveConsumer(queue: string, callback: CallbackFunction, options: ConsumeOptions | undefined) {
444
- this.consumers.push({
445
- queue,
446
- callback,
447
- options,
448
- });
484
+ async assertQueue(queueName: string, connectionPurpose: ConnectionPurpose, options?: Options.AssertQueue) {
485
+ RabbitMq.validateName('queue', queueName);
486
+ if (this.queues[queueName]) {
487
+ delete this.queueSetupPromises[queueName];
488
+ return this.queues[queueName];
489
+ }
490
+
491
+ if (this.queueSetupPromises[queueName]) {
492
+ return this.queueSetupPromises[queueName];
493
+ }
494
+
495
+ this.queueSetupPromises[queueName] = this.setupQueue(queueName, connectionPurpose, options);
496
+ return this.queueSetupPromises[queueName];
449
497
  }
450
498
 
451
- private async loadConsumers() {
452
- debug('rabbit: loading consumers', { consumers: this.consumers.length });
453
- if (this.consumers.length > 0) {
454
- await Promise.all(
455
- this.consumers.map((consumer) => this
456
- .consumeFromRabbit(consumer.queue, consumer.callback, consumer.options)),
457
- );
499
+ private saveConsumer(queue: string, callback: CallbackFunction, options: ConsumeOptions | undefined) {
500
+ const isConsumerExist: boolean = this.consumers.some((consumer) => consumer.queue === queue);
501
+ if (!isConsumerExist) {
502
+ logger.info(`rabbit: consumer: ${queue} saved in consumer array`);
503
+ this.consumers.push({
504
+ queue,
505
+ callback,
506
+ options,
507
+ });
458
508
  }
459
509
  }
460
510
 
461
511
  async consume(queue: string, callback: CallbackFunction, options?: ConsumeOptions): Promise<any> {
462
- if (this.connection && !this.creatingConnection) {
463
- this.consumeFromRabbit(queue, callback, options);
464
- }
465
- return this.saveConsumer(queue, callback, options);
512
+ await this.consumeFromRabbit(queue, callback, options);
466
513
  }
467
514
 
468
515
  private async lockRedisIfNeeded(msg: any, options: any) {
@@ -488,6 +535,8 @@ class RabbitMq implements IAfRabbitMq {
488
535
  private async consumeFromRabbit(queue: string, callback: CallbackFunction, options?: ConsumeOptions): Promise<any> {
489
536
  const optionsWithDefaults = { ...DEFAULT_OPTIONS, ...options };
490
537
  RabbitMq.validateName('queue', queue);
538
+ this.saveConsumer(queue, callback, options);
539
+ const uniqueId = randomUUID();
491
540
  const {
492
541
  limit, deadMessageTtl, useConsumeWithLock, lockTimeout, auditContext, enableRabbitTrace,
493
542
  } = optionsWithDefaults;
@@ -497,11 +546,11 @@ class RabbitMq implements IAfRabbitMq {
497
546
  }
498
547
  logger.info(`rabbit: Consuming with lock from queue ${queue} with lockTimeout: ${lockTimeout}ms`);
499
548
  }
500
- const channel: ChannelWrapper = await this.getNewChannel({ name: `consume-queue-${queue}` });
501
- return channel.addSetup(async (c: ConfirmChannel) => {
502
- await c.assertQueue(queue, options ? { messageTtl: options.messageTtl } : undefined);
503
- await c.prefetch(limit, true);
504
- const { consumerTag } = await c.consume(
549
+ const channel = await this.getNewChannel({ connectionPurpose: ConnectionPurpose.Consume });
550
+ return channel.addSetup(async (confirmChannel: ConfirmChannel) => {
551
+ await confirmChannel.assertQueue(queue, options ? { messageTtl: options.messageTtl } : undefined);
552
+ await confirmChannel.prefetch(limit, true);
553
+ const { consumerTag } = await confirmChannel.consume(
505
554
  queue,
506
555
  async (msg: ConsumeMessageOrNull) => {
507
556
  if (!msg) {
@@ -525,7 +574,7 @@ class RabbitMq implements IAfRabbitMq {
525
574
  ]);
526
575
  } catch (e) {
527
576
  logger.error('rabbit: failed to setRabbitTrace', { userId, e });
528
- await this.nack(channel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg);
577
+ return this.nack(confirmChannel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg);
529
578
  }
530
579
  }
531
580
 
@@ -540,16 +589,37 @@ class RabbitMq implements IAfRabbitMq {
540
589
  const shouldConsume = await this.shouldConsumeMessageByTimestamp(parsedMessage);
541
590
  if (!shouldConsume) {
542
591
  await this.unlockRedisIfNeeded(releaseLock);
543
- return this.ack(channel, msg)(msg);
592
+ return this.ack(confirmChannel, msg)(msg);
544
593
  }
594
+
595
+ let messageAcked = false;
596
+ // setting the localAck function to be used in the callback
597
+
598
+ const localAck = async () => {
599
+ if (messageAcked) {
600
+ return;
601
+ }
602
+ messageAcked = true;
603
+ return this.ack(confirmChannel, msg, true, releaseLock)(msg);
604
+ };
605
+
606
+ const localNack = async (_: ConsumeMessageOrNull, nackOptions: NackOptions = {}) => {
607
+ if (messageAcked) {
608
+ return;
609
+ }
610
+ debug('rabbit localNack', { messageAcked, uniqueId, deliveryTag: msg.fields.deliveryTag });
611
+ messageAcked = true;
612
+ return this.nack(confirmChannel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg, nackOptions);
613
+ };
614
+
545
615
  try {
546
616
  await callback(
547
617
  parsedMessage,
548
- this.ack(channel, msg, true, releaseLock),
549
- this.nack(channel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock),
618
+ localAck,
619
+ localNack,
550
620
  );
551
621
  } catch (e) {
552
- await this.nack(channel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg);
622
+ await localNack(msg);
553
623
  }
554
624
  }, CONSUMER_DEFAULT_OPTIONS,
555
625
  );
@@ -557,17 +627,18 @@ class RabbitMq implements IAfRabbitMq {
557
627
  logger.error(`rabbit: failed to consume from queue ${queue}`);
558
628
  } else {
559
629
  logger.info(`rabbit: adding tag ${consumerTag} to the array.`);
560
- this.consumersTags.push([c, consumerTag]);
630
+ this.consumersTags.push([confirmChannel, consumerTag]);
561
631
  }
562
632
  });
563
633
  }
564
634
 
565
- async consumeFromExchange(queue: string, exchange: string, callback: CallbackFunction, options?: ConsumeOptions) : Promise<any> {
635
+ async consumeFromExchange(queue: string, exchange: string, callback: CallbackFunction, options?: ConsumeOptions): Promise<any> {
566
636
  const optionsWithDefaults = { ...DEFAULT_OPTIONS, ...options };
567
637
  RabbitMq.validateName('exchange', exchange);
568
638
  RabbitMq.validateName('queue', queue);
569
639
  const { limit, deadMessageTtl } = optionsWithDefaults;
570
- const channel: ChannelWrapper = await this.getNewChannel({ name: `consume-exchange-${exchange}-queue-${queue}` });
640
+ await this.saveConsumer(queue, callback, options);
641
+ const channel: ChannelWrapper = await this.getNewChannel({ name: `consume - exchange - ${exchange} -queue - ${queue}`, connectionPurpose: ConnectionPurpose.Consume });
571
642
 
572
643
  return channel.addSetup(async (c: ConfirmChannel) => {
573
644
  const assertExchange = await assertExchangeFanout(c, exchange);
@@ -585,11 +656,11 @@ class RabbitMq implements IAfRabbitMq {
585
656
  });
586
657
  }
587
658
 
588
- async publish(exchange: string, content: any, customHeaders?: any) : Promise<boolean> {
659
+ async publish(exchange: string, content: any, customHeaders?: any): Promise<boolean> {
589
660
  return wrapSetImmediate(async () => {
590
661
  RabbitMq.validateName('exchange', exchange);
591
- const channel: ChannelWrapper = await this.assertChannel();
592
- await this.assertExchange(exchange);
662
+ const channel: ChannelWrapper = await this.assertChannel({ connectionPurpose: ConnectionPurpose.Publish });
663
+ await this.assertExchange(exchange, { connectionPurpose: ConnectionPurpose.Publish });
593
664
  await channel.publish(exchange, '',
594
665
  Buffer.from(JSON.stringify(content)),
595
666
  RabbitMq.getPublishOptions(customHeaders));
@@ -601,30 +672,61 @@ class RabbitMq implements IAfRabbitMq {
601
672
  content: any,
602
673
  options?: any,
603
674
  customHeaders?: any,
604
- isBlocking?: boolean,
605
675
  ): Promise<boolean | undefined> {
606
- const callback = async (): Promise<boolean | undefined> => {
676
+ try {
677
+ await this.assertChannel({ connectionPurpose: ConnectionPurpose.Publish });
678
+ } catch (e) {
679
+ logger.error(`rabbit sendToQueue: failed to send assert channel when sending to queue ${queue}`, { e });
680
+ throw e;
681
+ }
682
+
683
+ try {
607
684
  RabbitMq.validateName('queue', queue);
608
- await this.assertChannel();
609
- await this.assertQueue(queue, options);
685
+ await this.assertQueue(queue, ConnectionPurpose.Publish, options);
686
+ } catch (e) {
687
+ logger.error(`rabbit sendToQueue: failed to assert queue ${queue}`, { e });
688
+ throw e;
689
+ }
690
+
691
+ try {
610
692
  const res = await this.channel?.sendToQueue(queue,
611
693
  Buffer.from(JSON.stringify(content)),
612
694
  RabbitMq.getPublishOptions(customHeaders));
613
695
  debug(`rabbit: sending to queue ${queue}`, { res });
614
696
  return res;
615
- };
616
- if (isBlocking) {
617
- return callback();
697
+ } catch (e) {
698
+ const isConnected = await this.isConnected(ConnectionPurpose.Publish);
699
+ logger.error(`rabbit sendToQueue: failed to send to queue ${queue}, isConnected: ${isConnected}`, { e });
700
+ throw e;
618
701
  }
619
- return wrapSetImmediate(callback);
620
702
  }
621
703
 
622
- async isConnected() : Promise<boolean> {
623
- const connection = await this.getConnection();
624
- return connection.isConnected();
704
+ async isConnected(connectionPurpose: ConnectionPurpose): Promise<boolean> {
705
+ const connection = await this.getConnection(connectionPurpose);
706
+ if (!connection) {
707
+ logger.error('rabbit: isConnected - false');
708
+ return false;
709
+ }
710
+ const isConnected = connection.isConnected();
711
+ if (!isConnected) {
712
+ logger.error('rabbit: isConnected - false');
713
+ return false;
714
+ }
715
+ const channel: any = await this.assertChannel({ connectionPurpose });
716
+ try {
717
+ await Promise.all([
718
+ channel.waitForConnect(),
719
+ ...this.consumers.map((c: AfConsumer) => channel.checkQueue(c.queue)),
720
+ ]);
721
+ } catch (e) {
722
+ logger.error('rabbit: isConnected - false');
723
+ return false;
724
+ }
725
+ logger.info('rabbit: isConnected - true');
726
+ return true;
625
727
  }
626
728
 
627
- async gracefulShutdown(signal: string) : Promise<void> {
729
+ async gracefulShutdown(signal: string): Promise<void> {
628
730
  const tagsNumber = this.consumersTags.length;
629
731
  logger.info(`rabbit: [gracefully-shutdown] received ${signal}! canceling #${tagsNumber} tags...`);
630
732
  const cancelTagPromises = this.consumersTags.map(([channel, tag]) => channel.cancel(tag));