@autofleet/rabbit 3.3.2 → 3.3.22-connection-test-beta

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
@@ -100,6 +100,44 @@ type AfConsumer = {
100
100
  }
101
101
 
102
102
  const HEARTBEAT = '60';
103
+ const withTimeout = async<T>(promise: Promise<T>, timeoutMs: number): Promise<T> => new Promise((resolve, reject) => {
104
+ const timer = setTimeout(() => {
105
+ reject(new Error(`Promise timed out after ${timeoutMs} ms`));
106
+ }, timeoutMs);
107
+
108
+ promise
109
+ .then((value) => {
110
+ clearTimeout(timer);
111
+ resolve(value);
112
+ })
113
+ .catch((err) => {
114
+ clearTimeout(timer);
115
+ reject(err);
116
+ });
117
+ });
118
+
119
+ const withRetry = async <T>(
120
+ operation: () => Promise<T>,
121
+ retries: number,
122
+ timeoutMs: number,
123
+ ): Promise<T> => new Promise(async (resolve, reject) => {
124
+ let lastError: any;
125
+
126
+ for (let attempt = 1; attempt <= retries; attempt += 1) {
127
+ try {
128
+ // eslint-disable-next-line no-await-in-loop
129
+ const result = await withTimeout(operation(), timeoutMs);
130
+ return resolve(result);
131
+ } catch (err) {
132
+ lastError = err;
133
+ logger.error(`Attempt ${attempt} failed: ${err}`);
134
+ if (attempt < retries) {
135
+ logger.info(`Retrying (${attempt}/${retries})...`);
136
+ }
137
+ }
138
+ }
139
+ reject(lastError);
140
+ });
103
141
 
104
142
  class RabbitMq implements IAfRabbitMq {
105
143
  static parseMsg(msg: any) : any {
@@ -174,36 +212,7 @@ class RabbitMq implements IAfRabbitMq {
174
212
 
175
213
  private consumers: Array<AfConsumer> = [];
176
214
 
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) {
215
+ constructor(options?: AfRabbitOptions, redisConfig?: RedisConfig) {
207
216
  this.em = new EventEmitter();
208
217
  this.channel = null;
209
218
  this.publishChannelSetupPromise = null;
@@ -215,7 +224,6 @@ class RabbitMq implements IAfRabbitMq {
215
224
  this.assertExchangePromises = {};
216
225
  this.consumers = [];
217
226
  this.options = options;
218
-
219
227
  this.redisClient = redisConfig && getRedisInstance(redisConfig);
220
228
  if (this.redisClient) {
221
229
  this.redisLock = promisify(RedisLock(this.redisClient)) as RedisLockType;
@@ -230,74 +238,8 @@ class RabbitMq implements IAfRabbitMq {
230
238
  await this.gracefulShutdown('SIGINT');
231
239
  });
232
240
  }
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 = [];
246
241
  }
247
242
 
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
-
301
243
  private shouldConsumeMessageByTimestamp = async (msg: ConsumeMessageOrNull) => {
302
244
  if (msg) {
303
245
  const { properties: { headers } } = msg;
@@ -345,13 +287,13 @@ class RabbitMq implements IAfRabbitMq {
345
287
  if (
346
288
  !skipRetry
347
289
  && (
348
- !msg.properties.headers?.[RETRY_HEADER]
290
+ !msg.properties.headers[RETRY_HEADER]
349
291
  || parseInt(msg.properties.headers[RETRY_HEADER], 10) < options.retries
350
292
  )
351
293
  ) {
352
294
  await this.sendToQueue(queue, RabbitMq.parseMsg(msg).content, options, {
353
295
  ...msg.properties.headers,
354
- [RETRY_HEADER]: msg.properties.headers?.[RETRY_HEADER]
296
+ [RETRY_HEADER]: msg.properties.headers[RETRY_HEADER]
355
297
  ? msg.properties.headers[RETRY_HEADER] + 1
356
298
  : 1,
357
299
  });
@@ -359,7 +301,7 @@ class RabbitMq implements IAfRabbitMq {
359
301
  const deadQueue = `${queue}-dead`;
360
302
  await this.sendToQueue(deadQueue, RabbitMq.parseMsg(msg).content, deadQueueOptions, {
361
303
  ...msg.properties.headers,
362
- [RETRY_HEADER]: msg.properties.headers?.[RETRY_HEADER]
304
+ [RETRY_HEADER]: msg.properties.headers[RETRY_HEADER]
363
305
  ? msg.properties.headers[RETRY_HEADER] + 1
364
306
  : 1,
365
307
  });
@@ -408,7 +350,8 @@ class RabbitMq implements IAfRabbitMq {
408
350
  const host = this.options?.rabbitHost || process.env.RABBITMQ_SERVICE_HOST || 'localhost';
409
351
 
410
352
  debug('rabbit: creating connection', { host, userName, HEARTBEAT });
411
- return [`amqp://${userName}:${password}@${host}/${this.vhost}?heartbeat=${HEARTBEAT}`];
353
+
354
+ return [`amqp://${userName}:${password}@${host}?heartbeat=${HEARTBEAT}`];
412
355
  };
413
356
 
414
357
  const defaultUrls = findServers();
@@ -428,10 +371,7 @@ class RabbitMq implements IAfRabbitMq {
428
371
 
429
372
  this.connection.on('connectFailed', (err) => {
430
373
  this.consumersTags = [];
431
- if (typeof err.url === 'string') {
432
- err.url = this.maskURL(err.url);
433
- }
434
- logger.error('rabbit: connection connectFailed', { err, advice: 'Check if the vhost exist', vhost: this.vhost });
374
+ logger.error('rabbit: connection connectFailed', { err });
435
375
  if (!isResolved) {
436
376
  isResolved = true;
437
377
  reject(err);
@@ -523,12 +463,11 @@ class RabbitMq implements IAfRabbitMq {
523
463
  return this.exchanges[exchangeName];
524
464
  }
525
465
 
526
- // TODO: [QUORUM-PHASE-2] Change the implementation to the new one
527
466
  async getQueueLength(queue: string) {
528
467
  RabbitMq.validateName('queue', queue);
529
- const { oldChannel: channel } = this;
468
+ const { channel } = this;
530
469
  if (!channel) {
531
- throw new RabbitError('channel is not defined');
470
+ throw new Error('channel is not defined');
532
471
  }
533
472
  debug('rabbit: getting queue length', { queue, connected: this.connection?.isConnected() });
534
473
  return channel?.checkQueue(queue);
@@ -543,30 +482,28 @@ class RabbitMq implements IAfRabbitMq {
543
482
  return deleteQueueRes;
544
483
  }
545
484
 
546
- // TODO: [QUORUM-PHASE-2] Change the implementation to the new one
547
485
  async bindQueue(queue: string, exchange: string) {
548
- const channel: ChannelWrapper = await this.assertChannelOld();
486
+ const channel: ChannelWrapper = await this.assertChannel();
549
487
  await channel.addSetup((setupChannel: ConfirmChannel) => setupChannel.bindQueue(queue, exchange, ''));
550
488
  return channel.bindQueue(queue, exchange, '');
551
489
  }
552
490
 
553
491
  async setupQueue(queueName: string, options?: Options.AssertQueue): Promise<Replies.AssertQueue> {
554
492
  let queue: Replies.AssertQueue;
493
+ const shouldUseQuorum = RabbitMq.shouldUseQuorum(queueName);
555
494
  const localeOptions = {
556
495
  ...options,
557
496
  durable: true,
558
497
  arguments: {
559
498
  ...options?.arguments,
560
499
  'x-consumer-timeout': 1000 * 60 * 60 * 24,
561
- 'x-queue-type': 'quorum',
500
+ 'x-queue-type': shouldUseQuorum ? 'quorum' : 'classic',
562
501
  },
563
502
  };
564
503
  try {
565
504
  const channel: ChannelWrapper = await this.assertChannel();
566
505
  debug('assertQueue->channel.addSetup', { queueName });
567
- await channel.addSetup(async (setupChannel: ConfirmChannel) => {
568
- await setupChannel.assertQueue(queueName, localeOptions);
569
- });
506
+ await channel.addSetup((setupChannel: ConfirmChannel) => setupChannel.assertQueue(queueName, localeOptions));
570
507
  debug('assertQueue->channel.assertQueue', { queueName });
571
508
  queue = await channel.assertQueue(queueName, localeOptions);
572
509
  } catch (e) {
@@ -589,7 +526,6 @@ class RabbitMq implements IAfRabbitMq {
589
526
  return queue;
590
527
  }
591
528
 
592
- // TODO: [QUORUM-PHASE-3] Can be deleted after deleting the old consumers and publishers because all the queues are created us quorum queues
593
529
  static shouldUseQuorum(queueName: string): boolean {
594
530
  const envQuorumQueuesWhitelist = process.env.QUORUM_QUEUES_WHITELIST;
595
531
 
@@ -632,27 +568,10 @@ class RabbitMq implements IAfRabbitMq {
632
568
  }
633
569
  }
634
570
 
635
- // Used by the microservices to consume messages from the queue
636
571
  async consume(queue: string, callback: CallbackFunction, options?: ConsumeOptions): Promise<any> {
637
- // TODO: [QUORUM-PHASE-3] Use only the implementation of consumeNew and delete consumeNew and consumeOld
638
- if (options?.isQuorumQueue !== false) {
639
- await this.assertVHost();
640
- await this.consumeNew(queue, callback, options);
641
- }
642
-
643
- await this.consumeOld(queue, callback, options);
644
- }
645
-
646
- // TODO: [QUORUM-PHASE-3] Delete consumeNew we do not use it anymore
647
- async consumeNew(queue: string, callback: CallbackFunction, options?: ConsumeOptions): Promise<any> {
648
572
  await this.consumeFromRabbit(queue, callback, options);
649
573
  }
650
574
 
651
- // TODO: [QUORUM-PHASE-3] Delete consumeOld we do not use it anymore
652
- async consumeOld(queue: string, callback: CallbackFunction, options?: ConsumeOptions): Promise<any> {
653
- await this.consumeFromRabbitOld(queue, callback, options);
654
- }
655
-
656
575
  private async lockRedisIfNeeded(msg: any, options: any) {
657
576
  const { properties: { headers } } = msg;
658
577
  const timestamp = headers?.creationTimestamp;
@@ -683,7 +602,7 @@ class RabbitMq implements IAfRabbitMq {
683
602
  } = optionsWithDefaults;
684
603
  if (useConsumeWithLock) {
685
604
  if (!this.redisLock) {
686
- throw new RabbitError('Usage of consumeWithLock requires RedisInstance');
605
+ throw new Error('Usage of consumeWithLock requires RedisInstance');
687
606
  }
688
607
  logger.info(`rabbit: Consuming with lock from queue ${queue} with lockTimeout: ${lockTimeout}ms`);
689
608
  }
@@ -698,9 +617,9 @@ class RabbitMq implements IAfRabbitMq {
698
617
  return null;
699
618
  }
700
619
 
701
- const traceId = msg.properties.headers![TRACING_HEADER];
702
- const userId = msg.properties.headers![USER_TRACING_HEADER];
703
- const automationId = msg.properties.headers![AUTOMATION_ID_HEADER];
620
+ const traceId = msg.properties.headers[TRACING_HEADER];
621
+ const userId = msg.properties.headers[USER_TRACING_HEADER];
622
+ const automationId = msg.properties.headers[AUTOMATION_ID_HEADER];
704
623
  const parsedMessage = RabbitMq.parseMsg(msg);
705
624
  const releaseLock = await this.lockRedisIfNeeded(parsedMessage, optionsWithDefaults);
706
625
  const trace = newTrace(traceTypes.RABBIT);
@@ -777,45 +696,22 @@ class RabbitMq implements IAfRabbitMq {
777
696
  });
778
697
  }
779
698
 
780
- // Used by the microservices to consume messages from the exchange
781
699
  async consumeFromExchange(queue: string, exchange: string, callback: CallbackFunction, options?: ConsumeOptions) : Promise<any> {
782
700
  const optionsWithDefaults = { ...DEFAULT_OPTIONS, ...options };
783
701
  RabbitMq.validateName('exchange', exchange);
784
702
  RabbitMq.validateName('queue', queue);
785
703
  const { limit, deadMessageTtl } = optionsWithDefaults;
786
- // TODO: [QUORUM-PHASE-3] Delete the if statement after all the queues are created as quorum queues
787
- if (options?.isQuorumQueue !== false) {
788
- await this.assertVHost();
789
- await this.saveConsumer(queue, callback, options);
790
- const channel: ChannelWrapper = await this.getNewChannel({ name: `consume-exchange-${exchange}-queue-${queue}` });
791
-
792
- await channel.addSetup(async (c: ConfirmChannel) => {
793
- const assertExchange = await assertExchangeFanout(c, exchange);
794
- await c.assertQueue(queue);
795
- this.exchanges[exchange] = assertExchange;
796
- await c.prefetch(limit, false);
797
- return Promise.all([
798
- c.bindQueue(queue, exchange, ''),
799
- this.consumeNew(
800
- queue,
801
- callback,
802
- options,
803
- ),
804
- ]);
805
- });
806
- }
704
+ await this.saveConsumer(queue, callback, options);
705
+ const channel: ChannelWrapper = await this.getNewChannel({ name: `consume-exchange-${exchange}-queue-${queue}` });
807
706
 
808
- // TODO: [QUORUM-PHASE-3] Delete the old implementation
809
- await this.saveConsumerOld(queue, callback, options);
810
- const channelOld: ChannelWrapper = await this.getNewChannelOld({ name: `consume-exchange-${exchange}-queue-${queue}-old` });
811
- await channelOld.addSetup(async (c: ConfirmChannel) => {
707
+ return channel.addSetup(async (c: ConfirmChannel) => {
812
708
  const assertExchange = await assertExchangeFanout(c, exchange);
813
709
  await c.assertQueue(queue);
814
- this.oldExchanges[exchange] = assertExchange;
815
- await c.prefetch(limit, false);
710
+ this.exchanges[exchange] = assertExchange;
711
+ await c.prefetch(limit, true);
816
712
  return Promise.all([
817
713
  c.bindQueue(queue, exchange, ''),
818
- this.consumeOld(
714
+ this.consume(
819
715
  queue,
820
716
  callback,
821
717
  options,
@@ -824,21 +720,17 @@ class RabbitMq implements IAfRabbitMq {
824
720
  });
825
721
  }
826
722
 
827
- // Used by the microservices to publish messages to the exchange
828
- // TODO: [QUORUM-PHASE-2] Change the implementation to the new one
829
723
  async publish(exchange: string, content: any, customHeaders?: any) : Promise<boolean> {
830
724
  return wrapSetImmediate(async () => {
831
725
  RabbitMq.validateName('exchange', exchange);
832
- const channel: ChannelWrapper = await this.assertChannelOld();
833
- await this.assertExchangeOld(exchange);
726
+ const channel: ChannelWrapper = await this.assertChannel();
727
+ await this.assertExchange(exchange);
834
728
  await channel.publish(exchange, '',
835
729
  Buffer.from(JSON.stringify(content)),
836
730
  RabbitMq.getPublishOptions(customHeaders));
837
731
  });
838
732
  }
839
733
 
840
- // Used by the microservices to send messages to the queue
841
- // TODO: [QUORUM-PHASE-2] Change the implementation to the new one
842
734
  async sendToQueue(
843
735
  queue: string,
844
736
  content: any,
@@ -846,7 +738,7 @@ class RabbitMq implements IAfRabbitMq {
846
738
  customHeaders?: any,
847
739
  ): Promise<boolean | undefined> {
848
740
  try {
849
- await this.assertChannelOld();
741
+ await this.assertChannel();
850
742
  } catch (e) {
851
743
  logger.error(`rabbit sendToQueue: failed to send assert channel when sending to queue ${queue}`, { e });
852
744
  throw e;
@@ -854,14 +746,14 @@ class RabbitMq implements IAfRabbitMq {
854
746
 
855
747
  try {
856
748
  RabbitMq.validateName('queue', queue);
857
- await this.assertQueueOld(queue, options);
749
+ await this.assertQueue(queue, options);
858
750
  } catch (e) {
859
751
  logger.error(`rabbit sendToQueue: failed to assert queue ${queue}`, { e });
860
752
  throw e;
861
753
  }
862
754
 
863
755
  try {
864
- const res = await this.oldChannel?.sendToQueue(queue,
756
+ const res = await this.channel?.sendToQueue(queue,
865
757
  Buffer.from(JSON.stringify(content)),
866
758
  RabbitMq.getPublishOptions(customHeaders));
867
759
  debug(`rabbit: sending to queue ${queue}`, { res });
@@ -873,38 +765,67 @@ class RabbitMq implements IAfRabbitMq {
873
765
  }
874
766
  }
875
767
 
876
- // TODO: [QUORUM-PHASE-2] After changing the publish from old to new change from the old to the new implementation
877
768
  async isConnected() : Promise<boolean> {
878
- const connection = await this.getConnectionOld();
879
- const isConnected = connection.isConnected();
880
- if (!isConnected) {
881
- logger.error('rabbit: isConnected - false');
882
- return false;
883
- }
884
- const channel: any = await this.assertChannelOld();
885
769
  try {
886
- await Promise.all([
887
- channel.waitForConnect(),
888
- ...this.oldConsumers.map((c: AfConsumer) => channel.checkQueue(c.queue)),
889
- ]);
770
+ const connection = await this.getConnection();
771
+ if (!connection.isConnected()) {
772
+ logger.error('rabbit: isConnected - false');
773
+ return false;
774
+ }
775
+ const channel: any = await this.assertChannel();
776
+ const timeoutMs = 5000;
777
+ const retries = 2;
778
+ const chunkSize = 10;
779
+ const processChunks = async (consumers: AfConsumer[]): Promise<boolean> => {
780
+ for (let i = 0; i < consumers.length; i += chunkSize) {
781
+ const chunk = consumers.slice(i, i + chunkSize);
782
+ // eslint-disable-next-line no-await-in-loop
783
+ const chunkResults = await Promise.all(
784
+ chunk.map((c: AfConsumer) => withRetry(() => channel.checkQueue(c.queue), retries, timeoutMs).catch((err) => {
785
+ logger.error(`rabbit: Error in isConnected (checkQueue) - ${err.message}`);
786
+ return null;
787
+ })),
788
+ );
789
+ if (chunkResults.some((res) => res === null)) {
790
+ return false;
791
+ }
792
+ }
793
+ return true;
794
+ };
795
+ const waitForConnectResult = await withRetry(
796
+ () => channel.waitForConnect(),
797
+ retries,
798
+ timeoutMs,
799
+ ).catch((err) => {
800
+ logger.error(`rabbit: Error in isConnected (waitForConnect) - ${err.message}`);
801
+ return null;
802
+ });
803
+
804
+ if (waitForConnectResult === null) {
805
+ logger.error('rabbit: isConnected - false due to failed waitForConnect');
806
+ return false;
807
+ }
808
+ const allConsumersConnected = await processChunks(this.consumers);
809
+ if (!allConsumersConnected) {
810
+ logger.error('rabbit: isConnected - false due to failed consumer checks');
811
+ return false;
812
+ }
813
+
814
+ logger.info('rabbit: isConnected - true');
815
+ return true;
890
816
  } catch (e) {
891
- logger.error('rabbit: isConnected - false');
817
+ logger.error(`rabbit: isConnected - Exception occurred: ${e}`);
892
818
  return false;
893
819
  }
894
- logger.info('rabbit: isConnected - true');
895
- return true;
896
820
  }
897
821
 
898
822
  async gracefulShutdown(signal: string) : Promise<void> {
899
- // TODO: [QUORUM-PHASE-2] After changing the publish from old to new change from the old to the new implementation
900
- const tagsNumber = this.consumersTags.length + this.oldConsumersTags.length;
823
+ const tagsNumber = this.consumersTags.length;
901
824
  logger.info(`rabbit: [gracefully-shutdown] received ${signal}! canceling #${tagsNumber} tags...`);
902
825
  const cancelTagPromises = this.consumersTags.map(([channel, tag]) => channel.cancel(tag));
903
- const cancelTagPromisesOld = this.oldConsumersTags.map(([channel, tag]) => channel.cancel(tag));
904
826
  // Clean the array to avoid race
905
827
  this.consumersTags = [];
906
- this.oldConsumersTags = [];
907
- const results = await Promise.allSettled([...cancelTagPromises, ...cancelTagPromisesOld]);
828
+ const results = await Promise.allSettled(cancelTagPromises);
908
829
  const rejected = results.filter((p) => p.status === 'rejected');
909
830
  if (rejected.length > 0) {
910
831
  logger.warn(`rabbit: [gracefully-shutdown] #${rejected.length}/${tagsNumber} tags failed to cancel: ${rejected}`);
@@ -912,350 +833,6 @@ class RabbitMq implements IAfRabbitMq {
912
833
  logger.info('rabbit: [gracefully-shutdown] all tags successfully canceled.');
913
834
  }
914
835
  }
915
-
916
- private async consumeFromRabbitOld(queue: string, callback: CallbackFunction, options?: ConsumeOptions): Promise<any> {
917
- const optionsWithDefaults = { ...DEFAULT_OPTIONS, ...options };
918
- RabbitMq.validateName('queue', queue);
919
- this.saveConsumerOld(queue, callback, options);
920
- const uniqueId = randomUUID();
921
- const {
922
- limit, deadMessageTtl, useConsumeWithLock, lockTimeout, auditContext, enableRabbitTrace,
923
- } = optionsWithDefaults;
924
- if (useConsumeWithLock) {
925
- if (!this.redisLock) {
926
- throw new RabbitError('Usage of consumeWithLock requires RedisInstance');
927
- }
928
- logger.info(`rabbit: Consuming with lock from queue ${queue} with lockTimeout: ${lockTimeout}ms`);
929
- }
930
- const channel = await this.getNewChannelOld({});
931
- return channel.addSetup(async (confirmChannel: ConfirmChannel) => {
932
- const q = await this.assertQueueOld(queue, optionsWithDefaults);
933
- await confirmChannel.prefetch(limit, false);
934
- const { consumerTag } = await confirmChannel.consume(
935
- queue,
936
- async (msg: ConsumeMessageOrNull) => {
937
- if (!msg) {
938
- return null;
939
- }
940
-
941
- const traceId = msg.properties.headers![TRACING_HEADER];
942
- const userId = msg.properties.headers![USER_TRACING_HEADER];
943
- const automationId = msg.properties.headers![AUTOMATION_ID_HEADER];
944
- const parsedMessage = RabbitMq.parseMsg(msg);
945
- const releaseLock = await this.lockRedisIfNeeded(parsedMessage, optionsWithDefaults);
946
- const trace = newTrace(traceTypes.RABBIT);
947
- // setting also outbreak trace as part of legacy code
948
- const outbreakTrace = outbreak.newTrace(traceTypes.RABBIT);
949
- // enableRabbitTrace is a flag to protect from different flows that doesn't work with permission
950
- // and we don't want to fail the flow because of it
951
- if (userId && enableRabbitTrace) {
952
- try {
953
- await Promise.all([
954
- createOrSetRabbitTrace(trace, userId),
955
- createOrSetRabbitTrace(outbreakTrace, userId),
956
- ]);
957
- } catch (e) {
958
- logger.error('rabbit: failed to setRabbitTrace', { userId, e });
959
- return this.nack(confirmChannel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg);
960
- }
961
- }
962
-
963
- if (traceId) {
964
- (trace as any)?.context?.set(TRACING_HEADER, traceId);
965
- (outbreakTrace as any)?.context.set(TRACING_HEADER, traceId);
966
- }
967
-
968
- if (auditContext) {
969
- await auditContext(queue, {
970
- userId,
971
- automationId,
972
- });
973
- }
974
- const shouldConsume = await this.shouldConsumeMessageByTimestamp(parsedMessage);
975
- if (!shouldConsume) {
976
- await this.unlockRedisIfNeeded(releaseLock);
977
- return this.ack(confirmChannel, msg)(msg);
978
- }
979
-
980
- let messageAcked = false;
981
- // setting the localAck function to be used in the callback
982
-
983
- const localAck = async () => {
984
- if (messageAcked) {
985
- return;
986
- }
987
- messageAcked = true;
988
- return this.ack(confirmChannel, msg, true, releaseLock)(msg);
989
- };
990
-
991
- const localNack = async (_: ConsumeMessageOrNull, nackOptions: NackOptions = {}) => {
992
- if (messageAcked) {
993
- return;
994
- }
995
- debug('rabbit localNack', { messageAcked, uniqueId, deliveryTag: msg.fields.deliveryTag });
996
- messageAcked = true;
997
- return this.nack(confirmChannel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg, nackOptions);
998
- };
999
-
1000
- try {
1001
- await callback(
1002
- parsedMessage,
1003
- localAck,
1004
- localNack,
1005
- );
1006
- } catch (e) {
1007
- await localNack(msg);
1008
- }
1009
- }, CONSUMER_DEFAULT_OPTIONS,
1010
- );
1011
- if (!consumerTag) {
1012
- logger.error(`rabbit: failed to consume from queue ${queue}`);
1013
- } else {
1014
- logger.info(`rabbit: adding tag ${consumerTag} to the array.`);
1015
- this.oldConsumersTags.push([confirmChannel, consumerTag]);
1016
- }
1017
- });
1018
- }
1019
-
1020
- // TODO: [QUORUM-PHASE-3] Delete all the function under this line (getNewChannelOld, getConnectionOld, assertQueueOld, setupQueueOld, saveConsumerOld)
1021
- async getNewChannelOld({ name = rand().toString(), onClose = null, options = {} }: newChannelOpts = {}) {
1022
- let connection!: AmqpConnectionManager;
1023
- try {
1024
- connection = await this.getConnectionOld();
1025
- } catch (e) {
1026
- logger.error(`rabbit: error on get connection for new channel ${name} `, { e });
1027
- throw e;
1028
- }
1029
- const channel = connection.createChannel({ ...options });
1030
- once(channel, 'close').then((args) => {
1031
- logger.error(`rabbit: channel ${name} closed`);
1032
- onClose?.(args);
1033
- });
1034
- try {
1035
- await once(channel, 'connect');
1036
- debug(`rabbit: channel ${name} CONNECTED`);
1037
- return channel;
1038
- } catch (err) {
1039
- logger.error(`rabbit: channel error ${name} error`, { err });
1040
- throw err;
1041
- }
1042
- }
1043
-
1044
- async getConnectionOld() {
1045
- return new Promise<AmqpConnectionManager>(async (resolve, reject) => {
1046
- if (this.oldBlockReconnect) {
1047
- debug('rabbit: block reconnect');
1048
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1049
- // @ts-ignore
1050
- return resolve();
1051
- }
1052
- if (this.oldConnection !== null) {
1053
- if (this.options?.disableReconnect || this.oldConnection?.isConnected()) {
1054
- debug('rabbit: connection - is connected');
1055
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1056
- // @ts-ignore
1057
- return resolve(this.oldConnection);
1058
- }
1059
- debug('rabbit: connection - reconnecting');
1060
- }
1061
- if (this.oldCreatingConnection) {
1062
- debug('rabbit: creating connection emi');
1063
- this.oldEm.once(CONNECTION_CREATED_CONST, resolve);
1064
- this.oldEm.once(CONNECTION_FAILED_CONST, reject);
1065
- return;
1066
- }
1067
- this.oldCreatingConnection = true;
1068
- let isResolved = false;
1069
-
1070
- // It is import to use it as a function and not as a variable
1071
- // because of k8s changes the env variables
1072
- // and we want to use the new values
1073
- const findServers = () => {
1074
- const userName = process.env.RABBITMQ_USERNAME || 'guest';
1075
- const password = process.env.RABBITMQ_PASSWORD || 'guest';
1076
- const host = this.options?.rabbitHost || process.env.RABBITMQ_SERVICE_HOST || 'localhost';
1077
-
1078
- debug('rabbit: creating connection', { host, userName, HEARTBEAT });
1079
-
1080
- return [`amqp://${userName}:${password}@${host}?heartbeat=${HEARTBEAT}`];
1081
- };
1082
-
1083
- const defaultUrls = findServers();
1084
- const connection: AmqpConnectionManager = await connect(defaultUrls, {
1085
- findServers,
1086
- });
1087
-
1088
- this.oldConnection = connection;
1089
- this.oldConnection.on('error', (err) => {
1090
- logger.error('rabbit: connection error', { err });
1091
- if (!isResolved) {
1092
- isResolved = true;
1093
- reject(err);
1094
- this.oldEm.emit(CONNECTION_FAILED_CONST, err);
1095
- }
1096
- });
1097
-
1098
- this.oldConnection.on('connectFailed', (err) => {
1099
- this.oldConsumersTags = [];
1100
- if (typeof err.url === 'string') {
1101
- err.url = this.maskURL(err.url);
1102
- }
1103
- logger.error('rabbit: connection connectFailed', { err });
1104
- if (!isResolved) {
1105
- isResolved = true;
1106
- reject(err);
1107
- this.oldEm.emit(CONNECTION_FAILED_CONST, err);
1108
- }
1109
- });
1110
-
1111
- this.oldConnection.on('disconnect', ({ err }) => {
1112
- this.oldConsumersTags = [];
1113
- debug('rabbit: connection closed');
1114
- if (this.options?.disableReconnect) {
1115
- logger.error(`${this.DISCONNECT_MSG}${err && ` - ${err}`}`);
1116
- this.oldBlockReconnect = true;
1117
- } else {
1118
- logger.error(`${this.RECONNECT_MSG}${err && ` - ${err}`}`);
1119
- }
1120
- });
1121
-
1122
- this.oldConnection.once('connect', async () => {
1123
- debug('rabbit: connection established');
1124
- this.oldCreatingConnection = false;
1125
- this.oldEm.emit(CONNECTION_CREATED_CONST, connection);
1126
- isResolved = true;
1127
- resolve(connection);
1128
- });
1129
- });
1130
- }
1131
-
1132
- private saveConsumerOld(queue: string, callback: CallbackFunction, options: ConsumeOptions | undefined) {
1133
- const isConsumerExist :boolean = this.oldConsumers.some((consumer) => consumer.queue === queue);
1134
- if (!isConsumerExist) {
1135
- logger.info(`rabbit: consumer: ${queue} saved in consumer array`);
1136
- this.oldConsumers.push({
1137
- queue,
1138
- callback,
1139
- options,
1140
- });
1141
- }
1142
- }
1143
-
1144
- async assertQueueOld(queueName: string, options?: Options.AssertQueue): Promise<any> {
1145
- RabbitMq.validateName('queue', queueName);
1146
- if (this.oldQueues[queueName]) {
1147
- delete this.oldQueueSetupPromises[queueName];
1148
- return this.oldQueues[queueName];
1149
- }
1150
-
1151
- if (this.oldQueueSetupPromises[queueName]) {
1152
- return this.oldQueueSetupPromises[queueName];
1153
- }
1154
-
1155
- this.oldQueueSetupPromises[queueName] = this.setupQueueOld(queueName, options);
1156
- return this.oldQueueSetupPromises[queueName];
1157
- }
1158
-
1159
- async setupQueueOld(queueName: string, options?: Options.AssertQueue): Promise<Replies.AssertQueue> {
1160
- let queue: Replies.AssertQueue;
1161
- const shouldUseQuorum = RabbitMq.shouldUseQuorum(queueName);
1162
- const localeOptions = {
1163
- ...options,
1164
- durable: true,
1165
- arguments: {
1166
- ...options?.arguments,
1167
- 'x-consumer-timeout': 1000 * 60 * 60 * 24,
1168
- 'x-queue-type': shouldUseQuorum ? 'quorum' : 'classic',
1169
- },
1170
- };
1171
- try {
1172
- const channel: ChannelWrapper = await this.assertChannelOld();
1173
- debug('assertQueue->channel.addSetup', { queueName });
1174
- await channel.addSetup((setupChannel: ConfirmChannel) => setupChannel.assertQueue(queueName, localeOptions));
1175
- debug('assertQueue->channel.assertQueue', { queueName });
1176
- queue = await channel.assertQueue(queueName, localeOptions);
1177
- } catch (e) {
1178
- logger.error('rabbit: assertQueue error', { queueName, options, error: e });
1179
- if (!this.options?.dontRetryAssert) {
1180
- debug('retrying assertQueue', { queueName });
1181
- const channel = await this.assertChannelOld({ force: true });
1182
- await this.deleteQueueOld(queueName);
1183
-
1184
- debug('retrying assertQueue->channel.addSetup', { queueName });
1185
- await channel.addSetup((setupChannel: ConfirmChannel) => setupChannel.assertQueue(queueName, localeOptions));
1186
- debug('retrying assertQueue->channel.assertQueue', { queueName });
1187
- queue = await channel.assertQueue(queueName, localeOptions);
1188
- } else {
1189
- throw e;
1190
- }
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
- }
1244
-
1245
- private maskURL = (url: string): string => {
1246
- try {
1247
- const urlObj = new URL(url);
1248
- urlObj.username = '***';
1249
- urlObj.password = '***';
1250
- return urlObj.toString();
1251
- } catch {
1252
- return url;
1253
- }
1254
- }
1255
836
  }
1256
837
 
1257
838
  export default RabbitMq;
1258
-
1259
- export {
1260
- sendCeleryTaskViaHttp,
1261
- } from './lib/celery';