@autofleet/rabbit 3.2.0 → 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,11 +35,13 @@ 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
 
41
46
  const PUBLISH_TIMEOUT = 1000 * 10;
42
47
  export interface IAfRabbitMq {
@@ -52,6 +57,10 @@ export interface IAfRabbitMq {
52
57
  redisClient?: any;
53
58
  }
54
59
 
60
+ interface NackOptions {
61
+ skipRetry?: boolean;
62
+ }
63
+
55
64
  export interface AfRabbitOptions {
56
65
  disableReconnect?: boolean;
57
66
 
@@ -73,11 +82,14 @@ export interface AfRabbitOptions {
73
82
  type newChannelOpts = {
74
83
  name?: string;
75
84
  onClose?: null | ((args: any | null) => void);
85
+ options?: CreateChannelOpts | undefined;
86
+ connectionPurpose: ConnectionPurpose,
76
87
  };
77
88
 
78
89
  type assertChannelOpts = {
79
90
  channelName?: string;
80
91
  force?: boolean;
92
+ connectionPurpose: ConnectionPurpose;
81
93
  }
82
94
 
83
95
  type AfConsumer = {
@@ -89,7 +101,7 @@ type AfConsumer = {
89
101
  const HEARTBEAT = '60';
90
102
 
91
103
  class RabbitMq implements IAfRabbitMq {
92
- static parseMsg(msg: any) : any {
104
+ static parseMsg(msg: any): any {
93
105
  let { content } = msg;
94
106
  content = content.toString();
95
107
 
@@ -103,7 +115,7 @@ class RabbitMq implements IAfRabbitMq {
103
115
  };
104
116
  }
105
117
 
106
- static validateName(type: string, name: string) {
118
+ static validateName(type: string, name: string): void {
107
119
  if (!name || name === '') {
108
120
  throw new RabbitError(`error while using ${type} with no name`);
109
121
  }
@@ -132,48 +144,46 @@ class RabbitMq implements IAfRabbitMq {
132
144
 
133
145
  channel: ChannelWrapper | null;
134
146
 
135
- blockReconnect: boolean | null | undefined
147
+ publishChannelSetupPromise: Promise<ChannelWrapper> | null;
136
148
 
137
- connection: AmqpConnectionManager | null | undefined
149
+ blockReconnect: boolean | null | undefined;
138
150
 
139
- em: EventEmitter;
151
+ connectionsMap: {
152
+ [ConnectionPurpose.Consume]: ConnectionData;
153
+ [ConnectionPurpose.Publish]: ConnectionData;
154
+ };
140
155
 
141
- creatingConnection: boolean;
156
+ em: EventEmitter;
142
157
 
143
158
  exchanges: ExchangesCache;
144
159
 
145
160
  queues: QueuesCache;
146
161
 
162
+ queueSetupPromises: QueueSetupPromisesDictionary;
163
+
147
164
  options: AfRabbitOptions | undefined;
148
165
 
149
166
  redisClient: any;
150
167
 
151
168
  redisLock?: RedisLockType;
152
169
 
153
- userName: string;
154
-
155
- password: string;
156
-
157
- host: string;
158
-
159
170
  /** Array of consumers tags used for canceling consumption */
160
171
  consumersTags: Array<[ConfirmChannel, string]>;
161
172
 
162
173
  private consumers: Array<AfConsumer> = [];
163
174
 
164
175
  constructor(options?: AfRabbitOptions, redisConfig?: RedisConfig) {
165
- this.userName = process.env.RABBITMQ_USERNAME || 'guest';
166
-
167
- this.password = process.env.RABBITMQ_PASSWORD || 'guest';
168
-
169
- this.host = options?.rabbitHost || process.env.RABBITMQ_SERVICE_HOST || 'localhost';
170
-
171
176
  this.em = new EventEmitter();
172
177
  this.channel = null;
173
- this.connection = null;
174
- 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
+ };
175
183
  this.exchanges = {};
176
184
  this.queues = {};
185
+ this.queueSetupPromises = {};
186
+ this.consumers = [];
177
187
  this.options = options;
178
188
  this.redisClient = redisConfig && getRedisInstance(redisConfig);
179
189
  if (this.redisClient) {
@@ -205,8 +215,9 @@ class RabbitMq implements IAfRabbitMq {
205
215
  return false;
206
216
  }
207
217
 
208
- 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> => {
209
219
  if (msg) {
220
+ debug('rabbit acking message', { deliveryTag: msg.fields.deliveryTag });
210
221
  await channel.ack(msg);
211
222
  const { properties: { headers } } = msg;
212
223
  const timestamp = headers?.creationTimestamp;
@@ -220,7 +231,7 @@ class RabbitMq implements IAfRabbitMq {
220
231
  }
221
232
 
222
233
  public nack = (
223
- channel: ChannelWrapper,
234
+ channel: ConfirmChannel,
224
235
  queue: string,
225
236
  options: any,
226
237
  deadQueueOptions: Options.AssertQueue,
@@ -230,8 +241,8 @@ class RabbitMq implements IAfRabbitMq {
230
241
  userMsg: ConsumeMessageOrNull,
231
242
  {
232
243
  skipRetry = false,
233
- }: any = { },
234
- ) : Promise<any> => {
244
+ }: NackOptions = {},
245
+ ): Promise<any> => {
235
246
  await this.unlockRedisIfNeeded(releaseLock);
236
247
  if (channel && msg) {
237
248
  if (
@@ -256,44 +267,68 @@ class RabbitMq implements IAfRabbitMq {
256
267
  : 1,
257
268
  });
258
269
  }
270
+ debug('rabbit nacking message', { deliveryTag: msg.fields.deliveryTag });
259
271
  await channel.ack(msg);
260
272
  } else {
261
273
  logger.error('no channel or msg', {
262
- channel: channel ? channel.name : '',
263
274
  msg,
264
275
  });
265
276
  }
266
277
  }
267
278
 
268
- async getConnection() {
269
- 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];
270
282
  if (this.blockReconnect) {
271
283
  debug('rabbit: block reconnect');
272
284
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
273
285
  // @ts-ignore
274
286
  return resolve();
275
287
  }
276
- if (this.connection !== null) {
277
- if (this.options?.disableReconnect || this.connection?.isConnected()) {
288
+ if (connection !== null) {
289
+ if (this.options?.disableReconnect || connection?.isConnected()) {
278
290
  debug('rabbit: connection - is connected');
279
291
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
280
292
  // @ts-ignore
281
- return resolve(this.connection);
293
+ return resolve(connection);
282
294
  }
283
295
  debug('rabbit: connection - reconnecting');
284
296
  }
285
- if (this.creatingConnection) {
297
+ if (creatingConnection) {
286
298
  debug('rabbit: creating connection emi');
287
299
  this.em.once(CONNECTION_CREATED_CONST, resolve);
288
300
  this.em.once(CONNECTION_FAILED_CONST, reject);
289
301
  return;
290
302
  }
291
- this.creatingConnection = true;
303
+ this.connectionsMap[connectionPurpose].creatingConnection = true;
292
304
  let isResolved = false;
293
- debug('rabbit: creating connection', { host: this.host, userName: this.userName, HEARTBEAT });
294
- const connection: AmqpConnectionManager = await connect([`amqp://${this.userName}:${this.password}@${this.host}?heartbeat=${HEARTBEAT}}`]);
295
- this.connection = connection;
296
- 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) => {
297
332
  logger.error('rabbit: connection error', { err });
298
333
  if (!isResolved) {
299
334
  isResolved = true;
@@ -302,7 +337,8 @@ class RabbitMq implements IAfRabbitMq {
302
337
  }
303
338
  });
304
339
 
305
- this.connection.on('connectFailed', (err) => {
340
+ newConnection.on('connectFailed', (err) => {
341
+ this.consumersTags = [];
306
342
  logger.error('rabbit: connection connectFailed', { err });
307
343
  if (!isResolved) {
308
344
  isResolved = true;
@@ -311,10 +347,9 @@ class RabbitMq implements IAfRabbitMq {
311
347
  }
312
348
  });
313
349
 
314
- this.connection.on('disconnect', ({ err }) => {
350
+ newConnection.on('disconnect', ({ err }) => {
351
+ this.consumersTags = [];
315
352
  debug('rabbit: connection closed');
316
- this.exchanges = {};
317
- this.queues = {};
318
353
  if (this.options?.disableReconnect) {
319
354
  logger.error(`${this.DISCONNECT_MSG}${err && ` - ${err}`}`);
320
355
  this.blockReconnect = true;
@@ -322,67 +357,68 @@ class RabbitMq implements IAfRabbitMq {
322
357
  logger.error(`${this.RECONNECT_MSG}${err && ` - ${err}`}`);
323
358
  }
324
359
  });
325
- this.connection.once('connect', async () => {
360
+ newConnection.once('connect', async () => {
326
361
  debug('rabbit: connection established');
327
- await this.loadConsumers();
328
- this.creatingConnection = false;
329
- this.em.emit(CONNECTION_CREATED_CONST, connection);
362
+ this.connectionsMap[connectionPurpose].creatingConnection = false;
363
+ this.em.emit(CONNECTION_CREATED_CONST, newConnection);
330
364
  isResolved = true;
331
- resolve(connection);
365
+ resolve(newConnection);
332
366
  });
333
367
  });
334
368
  }
335
369
 
336
- async getNewChannel({ name = rand().toString(), onClose = null }: newChannelOpts = {}) {
337
- return new Promise<ChannelWrapper>(async (resolve, reject) => {
338
- const connection: AmqpConnectionManager = await this.getConnection();
339
- const channel = connection.createChannel({
340
- });
341
-
342
- let isResolved = false;
343
- channel.on('error', (err) => {
344
- logger.error(`rabbit: channel ${name} error`, { err });
345
- if (!isResolved) {
346
- isResolved = true;
347
- reject(err);
348
- }
349
- });
350
- channel.on('close', (...args) => {
351
- logger.error(`rabbit: channel ${name} closed`, { args });
352
- if (onClose) {
353
- onClose(args);
354
- }
355
- });
356
- channel.once('connect', () => {
357
- debug(`rabbit: channel ${name} CONNECTED`);
358
- isResolved = true;
359
- resolve(channel);
360
- });
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);
361
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
+ }
362
396
  }
363
397
 
364
- async assertChannel({ force = false } : assertChannelOpts = {}): Promise<ChannelWrapper> {
365
- return new Promise<ChannelWrapper>(async (resolve, reject) => {
366
- if (this.channel && !force) {
367
- return resolve(this.channel);
368
- }
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
+ }
369
404
 
370
- try {
371
- const channel = await this.getNewChannel({
372
- });
373
- channel.on('error', (err) => {
374
- logger.error('rabbit: channel error', { err });
375
- });
376
- this.channel = channel;
377
- resolve(channel);
378
- } catch (e) {
379
- reject(e);
380
- }
381
- });
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;
382
418
  }
383
419
 
384
420
  async assertExchange(exchangeName: string, options?: any) {
385
- const channel: ChannelWrapper = await this.assertChannel();
421
+ const channel: ChannelWrapper = await this.assertChannel({ connectionPurpose: options.connectionPurpose });
386
422
  if (this.exchanges[exchangeName]) {
387
423
  return this.exchanges[exchangeName];
388
424
  }
@@ -391,36 +427,36 @@ class RabbitMq implements IAfRabbitMq {
391
427
  return exchange;
392
428
  }
393
429
 
394
- async getQueueLength(queue: string) {
430
+ async getQueueLength(queue: string, connectionPurpose: ConnectionPurpose = ConnectionPurpose.Consume): Promise<Replies.AssertQueue> {
395
431
  RabbitMq.validateName('queue', queue);
396
- const channel: ChannelWrapper = await this.assertChannel();
397
- debug('rabbit: getting queue length', { queue, connected: this.connection?.isConnected() });
398
- 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);
399
439
  }
400
440
 
401
- private async deleteQueue(queue: string) {
441
+ private async deleteQueue(queue: string, connectionPurpose: ConnectionPurpose) {
402
442
  RabbitMq.validateName('queue', queue);
403
- const channel: ChannelWrapper = await this.assertChannel();
443
+ const channel: ChannelWrapper = await this.assertChannel({ connectionPurpose });
404
444
  logger.info('rabbit: deleting queue', { queue });
405
445
  const deleteQueueRes = await channel.deleteQueue(queue);
406
446
  debug('queue deleted', deleteQueueRes);
407
447
  return deleteQueueRes;
408
448
  }
409
449
 
410
- async bindQueue(queue: string, exchange: string) {
411
- const channel: ChannelWrapper = await this.assertChannel();
450
+ async bindQueue(queue: string, exchange: string, connectionPurpose: ConnectionPurpose) {
451
+ const channel: ChannelWrapper = await this.assertChannel({ connectionPurpose });
412
452
  await channel.addSetup((setupChannel: ConfirmChannel) => setupChannel.bindQueue(queue, exchange, ''));
413
453
  return channel.bindQueue(queue, exchange, '');
414
454
  }
415
455
 
416
- async assertQueue(queueName: string, options?: Options.AssertQueue) {
417
- let queue: Replies.AssertQueue | null = null;
418
- RabbitMq.validateName('queue', queueName);
419
- if (this.queues[queueName]) {
420
- return this.queues[queueName];
421
- }
456
+ async setupQueue(queueName: string, connectionPurpose: ConnectionPurpose, options?: Options.AssertQueue): Promise<Replies.AssertQueue> {
457
+ let queue: Replies.AssertQueue;
422
458
  try {
423
- const channel: ChannelWrapper = await this.assertChannel();
459
+ const channel: ChannelWrapper = await this.assertChannel({ connectionPurpose });
424
460
  debug('assertQueue->channel.addSetup', { queueName });
425
461
  await channel.addSetup((setupChannel: ConfirmChannel) => setupChannel.assertQueue(queueName, options));
426
462
  debug('assertQueue->channel.assertQueue', { queueName });
@@ -429,12 +465,12 @@ class RabbitMq implements IAfRabbitMq {
429
465
  logger.error('rabbit: assertQueue error', { queueName, options, error: e });
430
466
  if (!this.options?.dontRetryAssert) {
431
467
  debug('retrying assertQueue', { queueName });
432
- const channel = await this.assertChannel({ force: true });
433
- await this.deleteQueue(queueName);
468
+ const channel = await this.assertChannel({ force: true, connectionPurpose });
469
+ await this.deleteQueue(queueName, connectionPurpose);
434
470
 
435
- debug('1assertQueue->channel.addSetup', { queueName });
471
+ debug('retrying assertQueue->channel.addSetup', { queueName });
436
472
  await channel.addSetup((setupChannel: ConfirmChannel) => setupChannel.assertQueue(queueName, options));
437
- debug('1assertQueue->channel.assertQueue', { queueName });
473
+ debug('retrying assertQueue->channel.assertQueue', { queueName });
438
474
  queue = await channel.assertQueue(queueName, options);
439
475
  } else {
440
476
  throw e;
@@ -445,29 +481,35 @@ class RabbitMq implements IAfRabbitMq {
445
481
  return queue;
446
482
  }
447
483
 
448
- private saveConsumer(queue: string, callback: CallbackFunction, options: ConsumeOptions | undefined) {
449
- this.consumers.push({
450
- queue,
451
- callback,
452
- options,
453
- });
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];
454
497
  }
455
498
 
456
- private async loadConsumers() {
457
- debug('rabbit: loading consumers', { consumers: this.consumers.length });
458
- if (this.consumers.length > 0) {
459
- await Promise.all(
460
- this.consumers.map((consumer) => this
461
- .consumeFromRabbit(consumer.queue, consumer.callback, consumer.options)),
462
- );
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
+ });
463
508
  }
464
509
  }
465
510
 
466
511
  async consume(queue: string, callback: CallbackFunction, options?: ConsumeOptions): Promise<any> {
467
- if (this.connection && !this.creatingConnection) {
468
- this.consumeFromRabbit(queue, callback, options);
469
- }
470
- return this.saveConsumer(queue, callback, options);
512
+ await this.consumeFromRabbit(queue, callback, options);
471
513
  }
472
514
 
473
515
  private async lockRedisIfNeeded(msg: any, options: any) {
@@ -493,6 +535,8 @@ class RabbitMq implements IAfRabbitMq {
493
535
  private async consumeFromRabbit(queue: string, callback: CallbackFunction, options?: ConsumeOptions): Promise<any> {
494
536
  const optionsWithDefaults = { ...DEFAULT_OPTIONS, ...options };
495
537
  RabbitMq.validateName('queue', queue);
538
+ this.saveConsumer(queue, callback, options);
539
+ const uniqueId = randomUUID();
496
540
  const {
497
541
  limit, deadMessageTtl, useConsumeWithLock, lockTimeout, auditContext, enableRabbitTrace,
498
542
  } = optionsWithDefaults;
@@ -502,11 +546,11 @@ class RabbitMq implements IAfRabbitMq {
502
546
  }
503
547
  logger.info(`rabbit: Consuming with lock from queue ${queue} with lockTimeout: ${lockTimeout}ms`);
504
548
  }
505
- const channel: ChannelWrapper = await this.getNewChannel({ name: `consume-queue-${queue}` });
506
- return channel.addSetup(async (c: ConfirmChannel) => {
507
- await c.assertQueue(queue, options ? { messageTtl: options.messageTtl } : undefined);
508
- await c.prefetch(limit, true);
509
- 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(
510
554
  queue,
511
555
  async (msg: ConsumeMessageOrNull) => {
512
556
  if (!msg) {
@@ -530,7 +574,7 @@ class RabbitMq implements IAfRabbitMq {
530
574
  ]);
531
575
  } catch (e) {
532
576
  logger.error('rabbit: failed to setRabbitTrace', { userId, e });
533
- await this.nack(channel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg);
577
+ return this.nack(confirmChannel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg);
534
578
  }
535
579
  }
536
580
 
@@ -545,16 +589,37 @@ class RabbitMq implements IAfRabbitMq {
545
589
  const shouldConsume = await this.shouldConsumeMessageByTimestamp(parsedMessage);
546
590
  if (!shouldConsume) {
547
591
  await this.unlockRedisIfNeeded(releaseLock);
548
- return this.ack(channel, msg)(msg);
592
+ return this.ack(confirmChannel, msg)(msg);
549
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
+
550
615
  try {
551
616
  await callback(
552
617
  parsedMessage,
553
- this.ack(channel, msg, true, releaseLock),
554
- this.nack(channel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock),
618
+ localAck,
619
+ localNack,
555
620
  );
556
621
  } catch (e) {
557
- await this.nack(channel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg);
622
+ await localNack(msg);
558
623
  }
559
624
  }, CONSUMER_DEFAULT_OPTIONS,
560
625
  );
@@ -562,17 +627,18 @@ class RabbitMq implements IAfRabbitMq {
562
627
  logger.error(`rabbit: failed to consume from queue ${queue}`);
563
628
  } else {
564
629
  logger.info(`rabbit: adding tag ${consumerTag} to the array.`);
565
- this.consumersTags.push([c, consumerTag]);
630
+ this.consumersTags.push([confirmChannel, consumerTag]);
566
631
  }
567
632
  });
568
633
  }
569
634
 
570
- 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> {
571
636
  const optionsWithDefaults = { ...DEFAULT_OPTIONS, ...options };
572
637
  RabbitMq.validateName('exchange', exchange);
573
638
  RabbitMq.validateName('queue', queue);
574
639
  const { limit, deadMessageTtl } = optionsWithDefaults;
575
- 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 });
576
642
 
577
643
  return channel.addSetup(async (c: ConfirmChannel) => {
578
644
  const assertExchange = await assertExchangeFanout(c, exchange);
@@ -590,11 +656,11 @@ class RabbitMq implements IAfRabbitMq {
590
656
  });
591
657
  }
592
658
 
593
- async publish(exchange: string, content: any, customHeaders?: any) : Promise<boolean> {
659
+ async publish(exchange: string, content: any, customHeaders?: any): Promise<boolean> {
594
660
  return wrapSetImmediate(async () => {
595
661
  RabbitMq.validateName('exchange', exchange);
596
- const channel: ChannelWrapper = await this.assertChannel();
597
- await this.assertExchange(exchange);
662
+ const channel: ChannelWrapper = await this.assertChannel({ connectionPurpose: ConnectionPurpose.Publish });
663
+ await this.assertExchange(exchange, { connectionPurpose: ConnectionPurpose.Publish });
598
664
  await channel.publish(exchange, '',
599
665
  Buffer.from(JSON.stringify(content)),
600
666
  RabbitMq.getPublishOptions(customHeaders));
@@ -606,35 +672,61 @@ class RabbitMq implements IAfRabbitMq {
606
672
  content: any,
607
673
  options?: any,
608
674
  customHeaders?: any,
609
- isBlocking?: boolean,
610
675
  ): Promise<boolean | undefined> {
611
- const callback = async (): Promise<boolean | undefined> => {
612
- try {
613
- RabbitMq.validateName('queue', queue);
614
- await this.assertChannel();
615
- await this.assertQueue(queue, options);
616
- const res = await this.channel?.sendToQueue(queue,
617
- Buffer.from(JSON.stringify(content)),
618
- RabbitMq.getPublishOptions(customHeaders));
619
- debug(`rabbit: sending to queue ${queue}`, { res });
620
- return res;
621
- } catch (e) {
622
- logger.error(`rabbit: failed to send to queue ${queue}`, { e });
623
- throw e;
624
- }
625
- };
626
- if (isBlocking) {
627
- return callback();
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 {
684
+ RabbitMq.validateName('queue', queue);
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 {
692
+ const res = await this.channel?.sendToQueue(queue,
693
+ Buffer.from(JSON.stringify(content)),
694
+ RabbitMq.getPublishOptions(customHeaders));
695
+ debug(`rabbit: sending to queue ${queue}`, { res });
696
+ return res;
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;
628
701
  }
629
- return wrapSetImmediate(callback);
630
702
  }
631
703
 
632
- async isConnected() : Promise<boolean> {
633
- const connection = await this.getConnection();
634
- 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;
635
727
  }
636
728
 
637
- async gracefulShutdown(signal: string) : Promise<void> {
729
+ async gracefulShutdown(signal: string): Promise<void> {
638
730
  const tagsNumber = this.consumersTags.length;
639
731
  logger.info(`rabbit: [gracefully-shutdown] received ${signal}! canceling #${tagsNumber} tags...`);
640
732
  const cancelTagPromises = this.consumersTags.map(([channel, tag]) => channel.cancel(tag));