@autofleet/rabbit 3.3.0-beta.0 → 3.3.0-beta.10

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
@@ -19,6 +19,7 @@ import RabbitError from './lib/rabbitError';
19
19
  import getRedisInstance, { RedisConfig } from './lib/redis';
20
20
  import { assertExchangeFanout, rand, wrapSetImmediate } from './lib/utils';
21
21
  import {
22
+ AUTOMATION_ID_HEADER,
22
23
  CONNECTION_CREATED_CONST,
23
24
  CONNECTION_FAILED_CONST,
24
25
  DEFAULT_LOCK_TIMEOUT,
@@ -35,7 +36,10 @@ import {
35
36
  CustomMessageHeaders,
36
37
  QueuesCache,
37
38
  RedisLockType,
38
- ExchangesCache, CONSUMER_DEFAULT_OPTIONS, QueueSetupPromisesDictionary,
39
+ ExchangesCache,
40
+ CONSUMER_DEFAULT_OPTIONS,
41
+ QueueSetupPromisesDictionary,
42
+ AssertExchangePromisesDictionary,
39
43
  } from './lib/types';
40
44
 
41
45
  // const debug = nodeDebug('af-rabbitmq')
@@ -76,10 +80,6 @@ export interface AfRabbitOptions {
76
80
  dontRetryAssert?: boolean;
77
81
 
78
82
  rabbitHost?: string;
79
-
80
- serviceName: string;
81
-
82
- podIp?: string;
83
83
  }
84
84
 
85
85
  type newChannelOpts = {
@@ -145,14 +145,14 @@ class RabbitMq implements IAfRabbitMq {
145
145
 
146
146
  channel: ChannelWrapper | null;
147
147
 
148
+ publishChannelSetupPromise: Promise<ChannelWrapper> | null;
149
+
148
150
  blockReconnect: boolean | null | undefined
149
151
 
150
152
  connection: AmqpConnectionManager | null | undefined
151
153
 
152
154
  em: EventEmitter;
153
155
 
154
- podId: string;
155
-
156
156
  creatingConnection: boolean;
157
157
 
158
158
  exchanges: ExchangesCache;
@@ -161,6 +161,8 @@ class RabbitMq implements IAfRabbitMq {
161
161
 
162
162
  queueSetupPromises: QueueSetupPromisesDictionary;
163
163
 
164
+ assertExchangePromises: AssertExchangePromisesDictionary;
165
+
164
166
  options: AfRabbitOptions | undefined;
165
167
 
166
168
  redisClient: any;
@@ -172,17 +174,48 @@ class RabbitMq implements IAfRabbitMq {
172
174
 
173
175
  private consumers: Array<AfConsumer> = [];
174
176
 
175
- constructor(options?: AfRabbitOptions, redisConfig?: RedisConfig) {
177
+ private doesVHostExist = false;
178
+
179
+ private vhost = 'quorum-vhost';
180
+
181
+ // TODO:[QUORUM-PHASE-3] Delete the old properties that we use for the old consumers and publishers
182
+ oldChannel: ChannelWrapper | null;
183
+
184
+ oldPublishChannelSetupPromise: Promise<ChannelWrapper> | null;
185
+
186
+ oldBlockReconnect: boolean | null | undefined
187
+
188
+ oldConnection: AmqpConnectionManager | null | undefined
189
+
190
+ oldEm: EventEmitter;
191
+
192
+ oldCreatingConnection: boolean;
193
+
194
+ oldExchanges: ExchangesCache;
195
+
196
+ oldQueues: QueuesCache;
197
+
198
+ oldQueueSetupPromises: QueueSetupPromisesDictionary;
199
+
200
+ oldAssertExchangePromises: AssertExchangePromisesDictionary;
201
+
202
+ oldConsumersTags: Array<[ConfirmChannel, string]>;
203
+
204
+ private oldConsumers: Array<AfConsumer> = [];
205
+
206
+ constructor(options: AfRabbitOptions = {}, redisConfig?: RedisConfig) {
176
207
  this.em = new EventEmitter();
177
208
  this.channel = null;
209
+ this.publishChannelSetupPromise = null;
178
210
  this.connection = null;
179
211
  this.creatingConnection = false;
180
212
  this.exchanges = {};
181
213
  this.queues = {};
182
214
  this.queueSetupPromises = {};
215
+ this.assertExchangePromises = {};
183
216
  this.consumers = [];
184
217
  this.options = options;
185
- this.podId = `${options?.serviceName}${options?.podIp ? `-${options.podIp}` : ''}`;
218
+
186
219
  this.redisClient = redisConfig && getRedisInstance(redisConfig);
187
220
  if (this.redisClient) {
188
221
  this.redisLock = promisify(RedisLock(this.redisClient)) as RedisLockType;
@@ -197,8 +230,74 @@ class RabbitMq implements IAfRabbitMq {
197
230
  await this.gracefulShutdown('SIGINT');
198
231
  });
199
232
  }
233
+
234
+ // TODO: [QUORUM-PHASE-3] Delete the old properties that we use for the old consumers and publishers
235
+ this.oldEm = new EventEmitter();
236
+ this.oldChannel = null;
237
+ this.oldPublishChannelSetupPromise = null;
238
+ this.oldConnection = null;
239
+ this.oldCreatingConnection = false;
240
+ this.oldExchanges = {};
241
+ this.oldQueues = {};
242
+ this.oldQueueSetupPromises = {};
243
+ this.oldAssertExchangePromises = {};
244
+ this.oldConsumers = [];
245
+ this.oldConsumersTags = [];
200
246
  }
201
247
 
248
+ private assertVHost = async () => {
249
+ if (this.doesVHostExist) {
250
+ return;
251
+ }
252
+
253
+ const username = process.env.RABBITMQ_USERNAME || 'guest';
254
+ const password = process.env.RABBITMQ_PASSWORD || 'guest';
255
+ const credentials = Buffer.from(`${username}:${password}`).toString('base64');
256
+ const headers = {
257
+ Authorization: `Basic ${credentials}`,
258
+ 'Content-Type': 'application/json',
259
+ };
260
+
261
+ const rabbitHost = `http://${(this.options?.rabbitHost || process.env.RABBITMQ_SERVICE_HOST || 'localhost').split(':')[0]}:15672`;
262
+
263
+ const url = `${rabbitHost}/api/vhosts/${encodeURIComponent(this.vhost)}`;
264
+
265
+ try {
266
+ const response = await fetch(url, {
267
+ method: 'GET',
268
+ headers,
269
+ });
270
+
271
+ if (response.status === 200) {
272
+ this.doesVHostExist = true;
273
+ logger.info('Vhost exists', { vhost: this.vhost });
274
+ return;
275
+ }
276
+
277
+ if (response.status !== 404) {
278
+ logger.error('Failed to check vhost', { response });
279
+ throw new RabbitError('Failed to check vhost');
280
+ }
281
+
282
+ const createResponse = await fetch(url, {
283
+ method: 'PUT',
284
+ headers,
285
+ body: JSON.stringify({ default_queue_type: 'quorum' }),
286
+ });
287
+
288
+ if (!createResponse.ok) {
289
+ logger.error('Failed to create vhost', { response: createResponse });
290
+ throw new RabbitError('Failed to create vhost');
291
+ }
292
+
293
+ this.doesVHostExist = true;
294
+ logger.info('Vhost created', { vhost: this.vhost });
295
+ } catch (error) {
296
+ logger.error('Failed to check or create vhost', { error });
297
+ throw error;
298
+ }
299
+ };
300
+
202
301
  private shouldConsumeMessageByTimestamp = async (msg: ConsumeMessageOrNull) => {
203
302
  if (msg) {
204
303
  const { properties: { headers } } = msg;
@@ -246,13 +345,13 @@ class RabbitMq implements IAfRabbitMq {
246
345
  if (
247
346
  !skipRetry
248
347
  && (
249
- !msg.properties.headers[RETRY_HEADER]
250
- || parseInt(msg.properties.headers[RETRY_HEADER], 10) < options.retries
348
+ !msg.properties.headers?.[RETRY_HEADER]
349
+ || parseInt(msg.properties.headers?.[RETRY_HEADER], 10) < options.retries
251
350
  )
252
351
  ) {
253
352
  await this.sendToQueue(queue, RabbitMq.parseMsg(msg).content, options, {
254
353
  ...msg.properties.headers,
255
- [RETRY_HEADER]: msg.properties.headers[RETRY_HEADER]
354
+ [RETRY_HEADER]: msg.properties.headers?.[RETRY_HEADER]
256
355
  ? msg.properties.headers[RETRY_HEADER] + 1
257
356
  : 1,
258
357
  });
@@ -260,7 +359,7 @@ class RabbitMq implements IAfRabbitMq {
260
359
  const deadQueue = `${queue}-dead`;
261
360
  await this.sendToQueue(deadQueue, RabbitMq.parseMsg(msg).content, deadQueueOptions, {
262
361
  ...msg.properties.headers,
263
- [RETRY_HEADER]: msg.properties.headers[RETRY_HEADER]
362
+ [RETRY_HEADER]: msg.properties.headers?.[RETRY_HEADER]
264
363
  ? msg.properties.headers[RETRY_HEADER] + 1
265
364
  : 1,
266
365
  });
@@ -309,8 +408,7 @@ class RabbitMq implements IAfRabbitMq {
309
408
  const host = this.options?.rabbitHost || process.env.RABBITMQ_SERVICE_HOST || 'localhost';
310
409
 
311
410
  debug('rabbit: creating connection', { host, userName, HEARTBEAT });
312
-
313
- return [`amqp://${userName}:${password}@${host}?heartbeat=${HEARTBEAT}`];
411
+ return [`amqp://${userName}:${password}@${host}/${this.vhost}?heartbeat=${HEARTBEAT}`];
314
412
  };
315
413
 
316
414
  const defaultUrls = findServers();
@@ -330,7 +428,7 @@ class RabbitMq implements IAfRabbitMq {
330
428
 
331
429
  this.connection.on('connectFailed', (err) => {
332
430
  this.consumersTags = [];
333
- logger.error('rabbit: connection connectFailed', { err });
431
+ logger.error('rabbit: connection connectFailed', { err, advice: 'Check if the vhost exist', vhost: this.vhost });
334
432
  if (!isResolved) {
335
433
  isResolved = true;
336
434
  reject(err);
@@ -339,6 +437,7 @@ class RabbitMq implements IAfRabbitMq {
339
437
  });
340
438
 
341
439
  this.connection.on('disconnect', ({ err }) => {
440
+ // this.channel = null;
342
441
  this.consumersTags = [];
343
442
  debug('rabbit: connection closed');
344
443
  if (this.options?.disableReconnect) {
@@ -348,6 +447,7 @@ class RabbitMq implements IAfRabbitMq {
348
447
  logger.error(`${this.RECONNECT_MSG}${err && ` - ${err}`}`);
349
448
  }
350
449
  });
450
+
351
451
  this.connection.once('connect', async () => {
352
452
  debug('rabbit: connection established');
353
453
  this.creatingConnection = false;
@@ -366,7 +466,7 @@ class RabbitMq implements IAfRabbitMq {
366
466
  logger.error(`rabbit: error on get connection for new channel ${name} `, { e });
367
467
  throw e;
368
468
  }
369
- const channel = connection.createChannel({ ...options, name });
469
+ const channel = connection.createChannel({ ...options });
370
470
  once(channel, 'close').then((args) => {
371
471
  logger.error(`rabbit: channel ${name} closed`);
372
472
  onClose?.(args);
@@ -382,39 +482,50 @@ class RabbitMq implements IAfRabbitMq {
382
482
  }
383
483
 
384
484
  async assertChannel({ force = false } : assertChannelOpts = {}): Promise<ChannelWrapper> {
385
- return new Promise<ChannelWrapper>(async (resolve, reject) => {
386
- if (this.channel && !force) {
387
- return resolve(this.channel);
388
- }
485
+ if (!this.publishChannelSetupPromise) {
486
+ this.publishChannelSetupPromise = new Promise<ChannelWrapper>(async (resolve, reject) => {
487
+ if (this.channel && !force) {
488
+ return resolve(this.channel);
489
+ }
389
490
 
390
- try {
391
- const channel = await this.getNewChannel({});
392
- channel.on('error', (err) => {
393
- logger.error('rabbit: channel error', { err });
394
- });
395
- this.channel = channel;
396
- resolve(channel);
397
- } catch (e) {
398
- reject(e);
399
- }
400
- });
491
+ try {
492
+ const channel = await this.getNewChannel({});
493
+ channel.on('error', (err) => {
494
+ logger.error('rabbit: channel error', { err });
495
+ });
496
+ this.channel = channel;
497
+ resolve(channel);
498
+ } catch (e) {
499
+ reject(e);
500
+ }
501
+ });
502
+ }
503
+ return this.publishChannelSetupPromise;
401
504
  }
402
505
 
403
506
  async assertExchange(exchangeName: string, options?: any) {
404
507
  const channel: ChannelWrapper = await this.assertChannel();
508
+
405
509
  if (this.exchanges[exchangeName]) {
510
+ delete this.assertExchangePromises[exchangeName];
406
511
  return this.exchanges[exchangeName];
407
512
  }
408
- const exchange = await assertExchangeFanout(channel, exchangeName);
409
- this.exchanges[exchangeName] = exchange;
410
- return exchange;
513
+
514
+ if (this.assertExchangePromises[exchangeName]) {
515
+ return this.assertExchangePromises[exchangeName];
516
+ }
517
+
518
+ this.assertExchangePromises[exchangeName] = assertExchangeFanout(channel, exchangeName);
519
+ this.exchanges[exchangeName] = await this.assertExchangePromises[exchangeName];
520
+ return this.exchanges[exchangeName];
411
521
  }
412
522
 
523
+ // TODO: [QUORUM-PHASE-2] Change the implementation to the new one
413
524
  async getQueueLength(queue: string) {
414
525
  RabbitMq.validateName('queue', queue);
415
- const { channel } = this;
526
+ const { oldChannel: channel } = this;
416
527
  if (!channel) {
417
- throw new Error('channel is not defined');
528
+ throw new RabbitError('channel is not defined');
418
529
  }
419
530
  debug('rabbit: getting queue length', { queue, connected: this.connection?.isConnected() });
420
531
  return channel?.checkQueue(queue);
@@ -429,20 +540,32 @@ class RabbitMq implements IAfRabbitMq {
429
540
  return deleteQueueRes;
430
541
  }
431
542
 
543
+ // TODO: [QUORUM-PHASE-2] Change the implementation to the new one
432
544
  async bindQueue(queue: string, exchange: string) {
433
- const channel: ChannelWrapper = await this.assertChannel();
545
+ const channel: ChannelWrapper = await this.assertChannelOld();
434
546
  await channel.addSetup((setupChannel: ConfirmChannel) => setupChannel.bindQueue(queue, exchange, ''));
435
547
  return channel.bindQueue(queue, exchange, '');
436
548
  }
437
549
 
438
550
  async setupQueue(queueName: string, options?: Options.AssertQueue): Promise<Replies.AssertQueue> {
439
551
  let queue: Replies.AssertQueue;
552
+ const localeOptions = {
553
+ ...options,
554
+ durable: true,
555
+ arguments: {
556
+ ...options?.arguments,
557
+ 'x-consumer-timeout': 1000 * 60 * 60 * 24,
558
+ 'x-queue-type': 'quorum',
559
+ },
560
+ };
440
561
  try {
441
562
  const channel: ChannelWrapper = await this.assertChannel();
442
563
  debug('assertQueue->channel.addSetup', { queueName });
443
- await channel.addSetup((setupChannel: ConfirmChannel) => setupChannel.assertQueue(queueName, options));
564
+ await channel.addSetup(async (setupChannel: ConfirmChannel) => {
565
+ await setupChannel.assertQueue(queueName, localeOptions);
566
+ });
444
567
  debug('assertQueue->channel.assertQueue', { queueName });
445
- queue = await channel.assertQueue(queueName, options);
568
+ queue = await channel.assertQueue(queueName, localeOptions);
446
569
  } catch (e) {
447
570
  logger.error('rabbit: assertQueue error', { queueName, options, error: e });
448
571
  if (!this.options?.dontRetryAssert) {
@@ -451,19 +574,34 @@ class RabbitMq implements IAfRabbitMq {
451
574
  await this.deleteQueue(queueName);
452
575
 
453
576
  debug('retrying assertQueue->channel.addSetup', { queueName });
454
- await channel.addSetup((setupChannel: ConfirmChannel) => setupChannel.assertQueue(queueName, options));
577
+ await channel.addSetup((setupChannel: ConfirmChannel) => setupChannel.assertQueue(queueName, localeOptions));
455
578
  debug('retrying assertQueue->channel.assertQueue', { queueName });
456
- queue = await channel.assertQueue(queueName, options);
579
+ queue = await channel.assertQueue(queueName, localeOptions);
457
580
  } else {
458
581
  throw e;
459
582
  }
460
583
  }
461
-
462
- this.queues[queueName] = queueName;
584
+ this.queues[queueName] = queue;
463
585
  return queue;
464
586
  }
465
587
 
466
- async assertQueue(queueName: string, options?: Options.AssertQueue) {
588
+ // TODO: [QUORUM-PHASE-3] Can be deleted after deleting the old consumers and publishers because all the queues are created us quorum queues
589
+ static shouldUseQuorum(queueName: string): boolean {
590
+ const envQuorumQueuesWhitelist = process.env.QUORUM_QUEUES_WHITELIST;
591
+
592
+ if (envQuorumQueuesWhitelist === '*') {
593
+ return true;
594
+ }
595
+
596
+ if (envQuorumQueuesWhitelist) {
597
+ const whitelist = envQuorumQueuesWhitelist.split(',');
598
+ return whitelist.includes(queueName);
599
+ }
600
+
601
+ return false;
602
+ }
603
+
604
+ async assertQueue(queueName: string, options?: Options.AssertQueue): Promise<any> {
467
605
  RabbitMq.validateName('queue', queueName);
468
606
  if (this.queues[queueName]) {
469
607
  delete this.queueSetupPromises[queueName];
@@ -490,10 +628,27 @@ class RabbitMq implements IAfRabbitMq {
490
628
  }
491
629
  }
492
630
 
631
+ // Used by the microservices to consume messages from the queue
493
632
  async consume(queue: string, callback: CallbackFunction, options?: ConsumeOptions): Promise<any> {
633
+ // TODO: [QUORUM-PHASE-3] Use only the implementation of consumeNew and delete consumeNew and consumeOld
634
+ if (options?.isQuorumQueue !== false) {
635
+ await this.assertVHost();
636
+ await this.consumeNew(queue, callback, options);
637
+ }
638
+
639
+ await this.consumeOld(queue, callback, options);
640
+ }
641
+
642
+ // TODO: [QUORUM-PHASE-3] Delete consumeNew we do not use it anymore
643
+ async consumeNew(queue: string, callback: CallbackFunction, options?: ConsumeOptions): Promise<any> {
494
644
  await this.consumeFromRabbit(queue, callback, options);
495
645
  }
496
646
 
647
+ // TODO: [QUORUM-PHASE-3] Delete consumeOld we do not use it anymore
648
+ async consumeOld(queue: string, callback: CallbackFunction, options?: ConsumeOptions): Promise<any> {
649
+ await this.consumeFromRabbitOld(queue, callback, options);
650
+ }
651
+
497
652
  private async lockRedisIfNeeded(msg: any, options: any) {
498
653
  const { properties: { headers } } = msg;
499
654
  const timestamp = headers?.creationTimestamp;
@@ -524,14 +679,15 @@ class RabbitMq implements IAfRabbitMq {
524
679
  } = optionsWithDefaults;
525
680
  if (useConsumeWithLock) {
526
681
  if (!this.redisLock) {
527
- throw new Error('Usage of consumeWithLock requires RedisInstance');
682
+ throw new RabbitError('Usage of consumeWithLock requires RedisInstance');
528
683
  }
529
684
  logger.info(`rabbit: Consuming with lock from queue ${queue} with lockTimeout: ${lockTimeout}ms`);
530
685
  }
531
- const channel = await this.getNewChannel({ name: `${this.podId}_queue_${queue}` });
686
+ const channel = await this.getNewChannel({});
532
687
  return channel.addSetup(async (confirmChannel: ConfirmChannel) => {
533
- await confirmChannel.assertQueue(queue, options ? { messageTtl: options.messageTtl } : undefined);
534
- await confirmChannel.prefetch(limit, true);
688
+ logger.info(`rabbit: channel.addSetup before ${queue} assertQueueOld`);
689
+ const q = await this.assertQueue(queue, optionsWithDefaults);
690
+ await confirmChannel.prefetch(limit, false);
535
691
  const { consumerTag } = await confirmChannel.consume(
536
692
  queue,
537
693
  async (msg: ConsumeMessageOrNull) => {
@@ -539,8 +695,9 @@ class RabbitMq implements IAfRabbitMq {
539
695
  return null;
540
696
  }
541
697
 
542
- const traceId = msg.properties.headers[TRACING_HEADER];
543
- const userId = msg.properties.headers[USER_TRACING_HEADER];
698
+ const traceId = msg.properties.headers?.[TRACING_HEADER];
699
+ const userId = msg.properties.headers?.[USER_TRACING_HEADER];
700
+ const automationId = msg.properties.headers?.[AUTOMATION_ID_HEADER];
544
701
  const parsedMessage = RabbitMq.parseMsg(msg);
545
702
  const releaseLock = await this.lockRedisIfNeeded(parsedMessage, optionsWithDefaults);
546
703
  const trace = newTrace(traceTypes.RABBIT);
@@ -566,7 +723,10 @@ class RabbitMq implements IAfRabbitMq {
566
723
  }
567
724
 
568
725
  if (auditContext) {
569
- await auditContext(queue);
726
+ await auditContext(queue, {
727
+ userId,
728
+ automationId,
729
+ });
570
730
  }
571
731
  const shouldConsume = await this.shouldConsumeMessageByTimestamp(parsedMessage);
572
732
  if (!shouldConsume) {
@@ -614,22 +774,45 @@ class RabbitMq implements IAfRabbitMq {
614
774
  });
615
775
  }
616
776
 
777
+ // Used by the microservices to consume messages from the exchange
617
778
  async consumeFromExchange(queue: string, exchange: string, callback: CallbackFunction, options?: ConsumeOptions) : Promise<any> {
618
779
  const optionsWithDefaults = { ...DEFAULT_OPTIONS, ...options };
619
780
  RabbitMq.validateName('exchange', exchange);
620
781
  RabbitMq.validateName('queue', queue);
621
782
  const { limit, deadMessageTtl } = optionsWithDefaults;
622
- await this.saveConsumer(queue, callback, options);
623
- const channel: ChannelWrapper = await this.getNewChannel({ name: `${this.podId}_exchange_${exchange}_queue_${queue}` });
783
+ // TODO: [QUORUM-PHASE-3] Delete the if statement after all the queues are created as quorum queues
784
+ if (options?.isQuorumQueue !== false) {
785
+ await this.assertVHost();
786
+ await this.saveConsumer(queue, callback, options);
787
+ const channel: ChannelWrapper = await this.getNewChannel({ name: `consume-exchange-${exchange}-queue-${queue}` });
788
+
789
+ await channel.addSetup(async (c: ConfirmChannel) => {
790
+ const assertExchange = await assertExchangeFanout(c, exchange);
791
+ await c.assertQueue(queue);
792
+ this.exchanges[exchange] = assertExchange;
793
+ await c.prefetch(limit, false);
794
+ return Promise.all([
795
+ c.bindQueue(queue, exchange, ''),
796
+ this.consumeNew(
797
+ queue,
798
+ callback,
799
+ options,
800
+ ),
801
+ ]);
802
+ });
803
+ }
624
804
 
625
- return channel.addSetup(async (c: ConfirmChannel) => {
805
+ // TODO: [QUORUM-PHASE-3] Delete the old implementation
806
+ await this.saveConsumerOld(queue, callback, options);
807
+ const channelOld: ChannelWrapper = await this.getNewChannelOld({ name: `consume-exchange-${exchange}-queue-${queue}-old` });
808
+ await channelOld.addSetup(async (c: ConfirmChannel) => {
626
809
  const assertExchange = await assertExchangeFanout(c, exchange);
627
- await c.assertQueue(queue);
628
- this.exchanges[exchange] = assertExchange;
629
- await c.prefetch(limit, true);
810
+ // await c.assertQueue(queue);
811
+ this.oldExchanges[exchange] = assertExchange;
812
+ await c.prefetch(limit, false);
630
813
  return Promise.all([
631
- c.bindQueue(queue, exchange, ''),
632
- this.consume(
814
+ // c.bindQueue(queue, exchange, ''),
815
+ this.consumeOld(
633
816
  queue,
634
817
  callback,
635
818
  options,
@@ -638,57 +821,68 @@ class RabbitMq implements IAfRabbitMq {
638
821
  });
639
822
  }
640
823
 
824
+ // Used by the microservices to publish messages to the exchange
825
+ // TODO: [QUORUM-PHASE-2] Change the implementation to the new one
641
826
  async publish(exchange: string, content: any, customHeaders?: any) : Promise<boolean> {
642
827
  return wrapSetImmediate(async () => {
643
828
  RabbitMq.validateName('exchange', exchange);
644
- const channel: ChannelWrapper = await this.assertChannel();
645
- await this.assertExchange(exchange);
829
+ const channel: ChannelWrapper = await this.assertChannelOld();
830
+ await this.assertExchangeOld(exchange);
646
831
  await channel.publish(exchange, '',
647
832
  Buffer.from(JSON.stringify(content)),
648
833
  RabbitMq.getPublishOptions(customHeaders));
649
834
  });
650
835
  }
651
836
 
837
+ // Used by the microservices to send messages to the queue
838
+ // TODO: [QUORUM-PHASE-2] Change the implementation to the new one
652
839
  async sendToQueue(
653
840
  queue: string,
654
841
  content: any,
655
842
  options?: any,
656
843
  customHeaders?: any,
657
- isBlocking?: boolean,
658
844
  ): Promise<boolean | undefined> {
659
- const callback = async (): Promise<boolean | undefined> => {
660
- try {
661
- RabbitMq.validateName('queue', queue);
662
- await this.assertChannel();
663
- await this.assertQueue(queue, options);
664
- const res = await this.channel?.sendToQueue(queue,
665
- Buffer.from(JSON.stringify(content)),
666
- RabbitMq.getPublishOptions(customHeaders));
667
- debug(`rabbit: sending to queue ${queue}`, { res });
668
- return res;
669
- } catch (e) {
670
- logger.error(`rabbit: failed to send to queue ${queue}`, { e });
671
- throw e;
672
- }
673
- };
674
- if (isBlocking) {
675
- return callback();
845
+ try {
846
+ await this.assertChannelOld();
847
+ } catch (e) {
848
+ logger.error(`rabbit sendToQueue: failed to send assert channel when sending to queue ${queue}`, { e });
849
+ throw e;
850
+ }
851
+
852
+ try {
853
+ RabbitMq.validateName('queue', queue);
854
+ await this.assertQueueOld(queue, options);
855
+ } catch (e) {
856
+ logger.error(`rabbit sendToQueue: failed to assert queue ${queue}`, { e });
857
+ throw e;
858
+ }
859
+
860
+ try {
861
+ const res = await this.oldChannel?.sendToQueue(queue,
862
+ Buffer.from(JSON.stringify(content)),
863
+ RabbitMq.getPublishOptions(customHeaders));
864
+ debug(`rabbit: sending to queue ${queue}`, { res });
865
+ return res;
866
+ } catch (e) {
867
+ const isConnected = await this.isConnected();
868
+ logger.error(`rabbit sendToQueue: failed to send to queue ${queue}, isConnected: ${isConnected}`, { e });
869
+ throw e;
676
870
  }
677
- return wrapSetImmediate(callback);
678
871
  }
679
872
 
873
+ // TODO: [QUORUM-PHASE-2] After changing the publish from old to new change from the old to the new implementation
680
874
  async isConnected() : Promise<boolean> {
681
- const connection = await this.getConnection();
875
+ const connection = await this.getConnectionOld();
682
876
  const isConnected = connection.isConnected();
683
877
  if (!isConnected) {
684
878
  logger.error('rabbit: isConnected - false');
685
879
  return false;
686
880
  }
687
- const channel: any = await this.assertChannel();
881
+ const channel: any = await this.assertChannelOld();
688
882
  try {
689
883
  await Promise.all([
690
884
  channel.waitForConnect(),
691
- ...this.consumers.map((c: AfConsumer) => channel.checkQueue(c.queue)),
885
+ ...this.oldConsumers.map((c: AfConsumer) => channel.checkQueue(c.queue)),
692
886
  ]);
693
887
  } catch (e) {
694
888
  logger.error('rabbit: isConnected - false');
@@ -699,12 +893,15 @@ class RabbitMq implements IAfRabbitMq {
699
893
  }
700
894
 
701
895
  async gracefulShutdown(signal: string) : Promise<void> {
702
- const tagsNumber = this.consumersTags.length;
896
+ // TODO: [QUORUM-PHASE-2] After changing the publish from old to new change from the old to the new implementation
897
+ const tagsNumber = this.consumersTags.length + this.oldConsumersTags.length;
703
898
  logger.info(`rabbit: [gracefully-shutdown] received ${signal}! canceling #${tagsNumber} tags...`);
704
899
  const cancelTagPromises = this.consumersTags.map(([channel, tag]) => channel.cancel(tag));
900
+ const cancelTagPromisesOld = this.oldConsumersTags.map(([channel, tag]) => channel.cancel(tag));
705
901
  // Clean the array to avoid race
706
902
  this.consumersTags = [];
707
- const results = await Promise.allSettled(cancelTagPromises);
903
+ this.oldConsumersTags = [];
904
+ const results = await Promise.allSettled([...cancelTagPromises, ...cancelTagPromisesOld]);
708
905
  const rejected = results.filter((p) => p.status === 'rejected');
709
906
  if (rejected.length > 0) {
710
907
  logger.warn(`rabbit: [gracefully-shutdown] #${rejected.length}/${tagsNumber} tags failed to cancel: ${rejected}`);
@@ -712,6 +909,342 @@ class RabbitMq implements IAfRabbitMq {
712
909
  logger.info('rabbit: [gracefully-shutdown] all tags successfully canceled.');
713
910
  }
714
911
  }
912
+
913
+ private async consumeFromRabbitOld(queue: string, callback: CallbackFunction, options?: ConsumeOptions): Promise<any> {
914
+ const optionsWithDefaults = { ...DEFAULT_OPTIONS, ...options };
915
+ RabbitMq.validateName('queue', queue);
916
+ this.saveConsumerOld(queue, callback, options);
917
+ const uniqueId = randomUUID();
918
+ const {
919
+ limit, deadMessageTtl, useConsumeWithLock, lockTimeout, auditContext, enableRabbitTrace,
920
+ } = optionsWithDefaults;
921
+ if (useConsumeWithLock) {
922
+ if (!this.redisLock) {
923
+ throw new RabbitError('Usage of consumeWithLock requires RedisInstance');
924
+ }
925
+ logger.info(`rabbit: Consuming with lock from queue ${queue} with lockTimeout: ${lockTimeout}ms`);
926
+ }
927
+ const channel = await this.getNewChannelOld({});
928
+ return channel.addSetup(async (confirmChannel: ConfirmChannel) => {
929
+ const q = await this.assertQueueOld(queue, optionsWithDefaults);
930
+ await confirmChannel.prefetch(limit, false);
931
+ const { consumerTag } = await confirmChannel.consume(
932
+ queue,
933
+ async (msg: ConsumeMessageOrNull) => {
934
+ if (!msg) {
935
+ return null;
936
+ }
937
+
938
+ const traceId = msg.properties.headers?.[TRACING_HEADER];
939
+ const userId = msg.properties.headers?.[USER_TRACING_HEADER];
940
+ const automationId = msg.properties.headers?.[AUTOMATION_ID_HEADER];
941
+ const parsedMessage = RabbitMq.parseMsg(msg);
942
+ const releaseLock = await this.lockRedisIfNeeded(parsedMessage, optionsWithDefaults);
943
+ const trace = newTrace(traceTypes.RABBIT);
944
+ // setting also outbreak trace as part of legacy code
945
+ const outbreakTrace = outbreak.newTrace(traceTypes.RABBIT);
946
+ // enableRabbitTrace is a flag to protect from different flows that doesn't work with permission
947
+ // and we don't want to fail the flow because of it
948
+ if (userId && enableRabbitTrace) {
949
+ try {
950
+ await Promise.all([
951
+ createOrSetRabbitTrace(trace, userId),
952
+ createOrSetRabbitTrace(outbreakTrace, userId),
953
+ ]);
954
+ } catch (e) {
955
+ logger.error('rabbit: failed to setRabbitTrace', { userId, e });
956
+ return this.nack(confirmChannel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg);
957
+ }
958
+ }
959
+
960
+ if (traceId) {
961
+ (trace as any)?.context?.set(TRACING_HEADER, traceId);
962
+ (outbreakTrace as any)?.context.set(TRACING_HEADER, traceId);
963
+ }
964
+
965
+ if (auditContext) {
966
+ await auditContext(queue, {
967
+ userId,
968
+ automationId,
969
+ });
970
+ }
971
+ const shouldConsume = await this.shouldConsumeMessageByTimestamp(parsedMessage);
972
+ if (!shouldConsume) {
973
+ await this.unlockRedisIfNeeded(releaseLock);
974
+ return this.ack(confirmChannel, msg)(msg);
975
+ }
976
+
977
+ let messageAcked = false;
978
+ // setting the localAck function to be used in the callback
979
+
980
+ const localAck = async () => {
981
+ if (messageAcked) {
982
+ return;
983
+ }
984
+ messageAcked = true;
985
+ return this.ack(confirmChannel, msg, true, releaseLock)(msg);
986
+ };
987
+
988
+ const localNack = async (_: ConsumeMessageOrNull, nackOptions: NackOptions = {}) => {
989
+ if (messageAcked) {
990
+ return;
991
+ }
992
+ debug('rabbit localNack', { messageAcked, uniqueId, deliveryTag: msg.fields.deliveryTag });
993
+ messageAcked = true;
994
+ return this.nack(confirmChannel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg, nackOptions);
995
+ };
996
+
997
+ try {
998
+ await callback(
999
+ parsedMessage,
1000
+ localAck,
1001
+ localNack,
1002
+ );
1003
+ } catch (e) {
1004
+ await localNack(msg);
1005
+ }
1006
+ }, CONSUMER_DEFAULT_OPTIONS,
1007
+ );
1008
+ if (!consumerTag) {
1009
+ logger.error(`rabbit: failed to consume from queue ${queue}`);
1010
+ } else {
1011
+ logger.info(`rabbit: adding tag ${consumerTag} to the array.`);
1012
+ this.oldConsumersTags.push([confirmChannel, consumerTag]);
1013
+ }
1014
+ });
1015
+ }
1016
+
1017
+ // TODO: [QUORUM-PHASE-3] Delete all the function under this line (getNewChannelOld, getConnectionOld, assertQueueOld, setupQueueOld, saveConsumerOld)
1018
+ async getNewChannelOld({ name = rand().toString(), onClose = null, options = {} }: newChannelOpts = {}) {
1019
+ let connection!: AmqpConnectionManager;
1020
+ try {
1021
+ connection = await this.getConnectionOld();
1022
+ } catch (e) {
1023
+ logger.error(`rabbit: error on get connection for new channel ${name} `, { e });
1024
+ throw e;
1025
+ }
1026
+ const channel = connection.createChannel({ ...options });
1027
+ once(channel, 'close').then((args) => {
1028
+ logger.error(`rabbit: channel ${name} closed`);
1029
+ onClose?.(args);
1030
+ });
1031
+ try {
1032
+ await once(channel, 'connect');
1033
+ debug(`rabbit: channel ${name} CONNECTED`);
1034
+ return channel;
1035
+ } catch (err) {
1036
+ logger.error(`rabbit: channel error ${name} error`, { err });
1037
+ throw err;
1038
+ }
1039
+ }
1040
+
1041
+ async getConnectionOld() {
1042
+ return new Promise<AmqpConnectionManager>(async (resolve, reject) => {
1043
+ if (this.oldBlockReconnect) {
1044
+ debug('rabbit: block reconnect');
1045
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1046
+ // @ts-ignore
1047
+ return resolve();
1048
+ }
1049
+ if (this.oldConnection !== null) {
1050
+ if (this.options?.disableReconnect || this.oldConnection?.isConnected()) {
1051
+ debug('rabbit: connection - is connected');
1052
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1053
+ // @ts-ignore
1054
+ return resolve(this.oldConnection);
1055
+ }
1056
+ debug('rabbit: connection - reconnecting');
1057
+ }
1058
+ if (this.oldCreatingConnection) {
1059
+ debug('rabbit: creating connection emi');
1060
+ this.oldEm.once(CONNECTION_CREATED_CONST, resolve);
1061
+ this.oldEm.once(CONNECTION_FAILED_CONST, reject);
1062
+ return;
1063
+ }
1064
+ this.oldCreatingConnection = true;
1065
+ let isResolved = false;
1066
+
1067
+ // It is import to use it as a function and not as a variable
1068
+ // because of k8s changes the env variables
1069
+ // and we want to use the new values
1070
+ const findServers = () => {
1071
+ const userName = process.env.RABBITMQ_USERNAME || 'guest';
1072
+ const password = process.env.RABBITMQ_PASSWORD || 'guest';
1073
+ const host = this.options?.rabbitHost || process.env.RABBITMQ_SERVICE_HOST || 'localhost';
1074
+
1075
+ debug('rabbit: creating connection', { host, userName, HEARTBEAT });
1076
+
1077
+ return [`amqp://${userName}:${password}@${host}?heartbeat=${HEARTBEAT}`];
1078
+ };
1079
+
1080
+ const defaultUrls = findServers();
1081
+ const connection: AmqpConnectionManager = await connect(defaultUrls, {
1082
+ findServers,
1083
+ });
1084
+
1085
+ this.oldConnection = connection;
1086
+ this.oldConnection.on('error', (err) => {
1087
+ logger.error('rabbit: connection error', { err });
1088
+ if (!isResolved) {
1089
+ isResolved = true;
1090
+ reject(err);
1091
+ this.oldEm.emit(CONNECTION_FAILED_CONST, err);
1092
+ }
1093
+ });
1094
+
1095
+ this.oldConnection.on('connectFailed', (err) => {
1096
+ this.oldConsumersTags = [];
1097
+ logger.error('rabbit: connection connectFailed', { err });
1098
+ if (!isResolved) {
1099
+ isResolved = true;
1100
+ reject(err);
1101
+ this.oldEm.emit(CONNECTION_FAILED_CONST, err);
1102
+ }
1103
+ });
1104
+
1105
+ this.oldConnection.on('disconnect', ({ err }) => {
1106
+ this.oldConsumersTags = [];
1107
+ debug('rabbit: connection closed');
1108
+ if (this.options?.disableReconnect) {
1109
+ logger.error(`${this.DISCONNECT_MSG}${err && ` - ${err}`}`);
1110
+ this.oldBlockReconnect = true;
1111
+ } else {
1112
+ logger.error(`${this.RECONNECT_MSG}${err && ` - ${err}`}`);
1113
+ }
1114
+ });
1115
+
1116
+ this.oldConnection.once('connect', async () => {
1117
+ debug('rabbit: connection established');
1118
+ this.oldCreatingConnection = false;
1119
+ this.oldEm.emit(CONNECTION_CREATED_CONST, connection);
1120
+ isResolved = true;
1121
+ resolve(connection);
1122
+ });
1123
+ });
1124
+ }
1125
+
1126
+ private saveConsumerOld(queue: string, callback: CallbackFunction, options: ConsumeOptions | undefined) {
1127
+ const isConsumerExist :boolean = this.oldConsumers.some((consumer) => consumer.queue === queue);
1128
+ if (!isConsumerExist) {
1129
+ logger.info(`rabbit: consumer: ${queue} saved in consumer array`);
1130
+ this.oldConsumers.push({
1131
+ queue,
1132
+ callback,
1133
+ options,
1134
+ });
1135
+ }
1136
+ }
1137
+
1138
+ async assertQueueOld(queueName: string, options?: Options.AssertQueue): Promise<any> {
1139
+ RabbitMq.validateName('queue', queueName);
1140
+ if (this.oldQueues[queueName]) {
1141
+ delete this.oldQueueSetupPromises[queueName];
1142
+ return this.oldQueues[queueName];
1143
+ }
1144
+
1145
+ if (this.oldQueueSetupPromises[queueName]) {
1146
+ return this.oldQueueSetupPromises[queueName];
1147
+ }
1148
+
1149
+ this.oldQueueSetupPromises[queueName] = this.setupQueueOld(queueName, options);
1150
+ return this.oldQueueSetupPromises[queueName];
1151
+ }
1152
+
1153
+ async setupQueueOld(queueName: string, options?: Options.AssertQueue): Promise<Replies.AssertQueue> {
1154
+ // let queue: Replies.AssertQueue;
1155
+ const shouldUseQuorum = RabbitMq.shouldUseQuorum(queueName);
1156
+ const localeOptions = {
1157
+ ...options,
1158
+ durable: true,
1159
+ arguments: {
1160
+ ...options?.arguments,
1161
+ 'x-consumer-timeout': 1000 * 60 * 60 * 24,
1162
+ 'x-queue-type': shouldUseQuorum ? 'quorum' : 'classic',
1163
+ },
1164
+ };
1165
+ try {
1166
+ const channel: ChannelWrapper = await this.assertChannelOld();
1167
+ debug('assertQueue->channel.addSetup', { queueName });
1168
+ // await channel.addSetup((setupChannel: ConfirmChannel) => setupChannel.assertQueue(queueName, localeOptions));
1169
+ debug('assertQueue->channel.assertQueue', { queueName });
1170
+ // queue = await channel.assertQueue(queueName, localeOptions);
1171
+ } catch (e) {
1172
+ logger.error('rabbit: assertQueue error', { queueName, options, error: e });
1173
+ if (!this.options?.dontRetryAssert) {
1174
+ debug('retrying assertQueue', { queueName });
1175
+ const channel = await this.assertChannelOld({ force: true });
1176
+ await this.deleteQueueOld(queueName);
1177
+
1178
+ debug('retrying assertQueue->channel.addSetup', { queueName });
1179
+ await channel.addSetup((setupChannel: ConfirmChannel) => setupChannel.assertQueue(queueName, localeOptions));
1180
+ debug('retrying assertQueue->channel.assertQueue', { queueName });
1181
+ // queue = await channel.assertQueue(queueName, localeOptions);
1182
+ } else {
1183
+ throw e;
1184
+ }
1185
+ }
1186
+
1187
+ const queue = {
1188
+ queue: 'bla bla queue',
1189
+ messageCount: 0,
1190
+ consumerCount: 2,
1191
+ };
1192
+
1193
+ this.oldQueues[queueName] = queue;
1194
+ return queue;
1195
+ }
1196
+
1197
+ async assertChannelOld({ force = false } : assertChannelOpts = {}): Promise<ChannelWrapper> {
1198
+ if (!this.oldPublishChannelSetupPromise) {
1199
+ this.oldPublishChannelSetupPromise = new Promise<ChannelWrapper>(async (resolve, reject) => {
1200
+ if (this.oldChannel && !force) {
1201
+ return resolve(this.oldChannel);
1202
+ }
1203
+
1204
+ try {
1205
+ const channel = await this.getNewChannelOld({});
1206
+ channel.on('error', (err) => {
1207
+ logger.error('rabbit: channel error', { err });
1208
+ });
1209
+ this.oldChannel = channel;
1210
+ resolve(channel);
1211
+ } catch (e) {
1212
+ reject(e);
1213
+ }
1214
+ });
1215
+ }
1216
+ return this.oldPublishChannelSetupPromise;
1217
+ }
1218
+
1219
+ private async deleteQueueOld(queue: string) {
1220
+ RabbitMq.validateName('queue', queue);
1221
+ const channel: ChannelWrapper = await this.assertChannelOld();
1222
+ logger.info('rabbit: deleting queue', { queue });
1223
+ const deleteQueueRes = await channel.deleteQueue(queue);
1224
+ debug('queue deleted', deleteQueueRes);
1225
+ return deleteQueueRes;
1226
+ }
1227
+
1228
+ async assertExchangeOld(exchangeName: string, options?: any) {
1229
+ const channel: ChannelWrapper = await this.assertChannelOld();
1230
+
1231
+ if (this.oldExchanges[exchangeName]) {
1232
+ delete this.oldAssertExchangePromises[exchangeName];
1233
+ return this.oldExchanges[exchangeName];
1234
+ }
1235
+
1236
+ if (this.oldAssertExchangePromises[exchangeName]) {
1237
+ return this.oldAssertExchangePromises[exchangeName];
1238
+ }
1239
+
1240
+ this.oldAssertExchangePromises[exchangeName] = assertExchangeFanout(channel, exchangeName);
1241
+ this.oldExchanges[exchangeName] = await this.oldAssertExchangePromises[exchangeName];
1242
+ return this.oldExchanges[exchangeName];
1243
+ }
715
1244
  }
716
1245
 
717
1246
  export default RabbitMq;
1247
+
1248
+ export {
1249
+ sendCeleryTaskViaHttp,
1250
+ } from './lib/celery';