@autofleet/rabbit 3.3.0-beta.1 → 3.3.0-beta.11

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/dist/index.js CHANGED
@@ -56,10 +56,57 @@ class RabbitMq {
56
56
  },
57
57
  };
58
58
  }
59
- constructor(options, redisConfig) {
59
+ constructor(options = {}, redisConfig) {
60
60
  this.DISCONNECT_MSG = 'rabbit: connection disconnect';
61
61
  this.RECONNECT_MSG = 'rabbit: connection disconnect - reconnecting';
62
62
  this.consumers = [];
63
+ this.doesVHostExist = false;
64
+ this.vhost = 'quorum-vhost';
65
+ this.oldConsumers = [];
66
+ this.assertVHost = async () => {
67
+ if (this.doesVHostExist) {
68
+ return;
69
+ }
70
+ const username = process.env.RABBITMQ_USERNAME || 'guest';
71
+ const password = process.env.RABBITMQ_PASSWORD || 'guest';
72
+ const credentials = Buffer.from(`${username}:${password}`).toString('base64');
73
+ const headers = {
74
+ Authorization: `Basic ${credentials}`,
75
+ 'Content-Type': 'application/json',
76
+ };
77
+ const rabbitHost = `http://${(this.options?.rabbitHost || process.env.RABBITMQ_SERVICE_HOST || 'localhost').split(':')[0]}:15672`;
78
+ const url = `${rabbitHost}/api/vhosts/${encodeURIComponent(this.vhost)}`;
79
+ try {
80
+ const response = await fetch(url, {
81
+ method: 'GET',
82
+ headers,
83
+ });
84
+ if (response.status === 200) {
85
+ this.doesVHostExist = true;
86
+ logger_1.default.info('Vhost exists', { vhost: this.vhost });
87
+ return;
88
+ }
89
+ if (response.status !== 404) {
90
+ logger_1.default.error('Failed to check vhost', { response });
91
+ throw new rabbitError_1.default('Failed to check vhost');
92
+ }
93
+ const createResponse = await fetch(url, {
94
+ method: 'PUT',
95
+ headers,
96
+ body: JSON.stringify({ default_queue_type: 'quorum' }),
97
+ });
98
+ if (!createResponse.ok) {
99
+ logger_1.default.error('Failed to create vhost', { response: createResponse });
100
+ throw new rabbitError_1.default('Failed to create vhost');
101
+ }
102
+ this.doesVHostExist = true;
103
+ logger_1.default.info('Vhost created', { vhost: this.vhost });
104
+ }
105
+ catch (error) {
106
+ logger_1.default.error('Failed to check or create vhost', { error });
107
+ throw error;
108
+ }
109
+ };
63
110
  this.shouldConsumeMessageByTimestamp = async (msg) => {
64
111
  if (msg) {
65
112
  const { properties: { headers } } = msg;
@@ -117,22 +164,10 @@ class RabbitMq {
117
164
  }
118
165
  };
119
166
  this.em = new events_1.EventEmitter();
120
- this.publishChannel = null;
167
+ this.channel = null;
121
168
  this.publishChannelSetupPromise = null;
122
- this.connectionsMap = {
123
- [consts_1.ConnectionPurpose.Consume]: {
124
- connection: null,
125
- creatingConnection: false,
126
- connectionCreatedEventName: 'consumeConnectionCreated',
127
- connectionFailedEventName: 'consumeConnectionFailed',
128
- },
129
- [consts_1.ConnectionPurpose.Publish]: {
130
- connection: null,
131
- creatingConnection: false,
132
- connectionCreatedEventName: 'publishConnectionCreated',
133
- connectionFailedEventName: 'publishConnectionFailed',
134
- },
135
- };
169
+ this.connection = null;
170
+ this.creatingConnection = false;
136
171
  this.exchanges = {};
137
172
  this.queues = {};
138
173
  this.queueSetupPromises = {};
@@ -153,32 +188,43 @@ class RabbitMq {
153
188
  await this.gracefulShutdown('SIGINT');
154
189
  });
155
190
  }
191
+ // TODO: [QUORUM-PHASE-3] Delete the old properties that we use for the old consumers and publishers
192
+ this.oldEm = new events_1.EventEmitter();
193
+ this.oldChannel = null;
194
+ this.oldPublishChannelSetupPromise = null;
195
+ this.oldConnection = null;
196
+ this.oldCreatingConnection = false;
197
+ this.oldExchanges = {};
198
+ this.oldQueues = {};
199
+ this.oldQueueSetupPromises = {};
200
+ this.oldAssertExchangePromises = {};
201
+ this.oldConsumers = [];
202
+ this.oldConsumersTags = [];
156
203
  }
157
- async getConnection(connectionPurpose) {
204
+ async getConnection() {
158
205
  return new Promise(async (resolve, reject) => {
159
- const { connection, creatingConnection, connectionCreatedEventName, connectionFailedEventName, } = this.connectionsMap[connectionPurpose];
160
206
  if (this.blockReconnect) {
161
207
  debug('rabbit: block reconnect');
162
208
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
163
209
  // @ts-ignore
164
210
  return resolve();
165
211
  }
166
- if (connection !== null) {
167
- if (this.options?.disableReconnect || connection?.isConnected()) {
212
+ if (this.connection !== null) {
213
+ if (this.options?.disableReconnect || this.connection?.isConnected()) {
168
214
  debug('rabbit: connection - is connected');
169
215
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
170
216
  // @ts-ignore
171
- return resolve(connection);
217
+ return resolve(this.connection);
172
218
  }
173
219
  debug('rabbit: connection - reconnecting');
174
220
  }
175
- if (creatingConnection) {
221
+ if (this.creatingConnection) {
176
222
  debug('rabbit: creating connection emi');
177
- this.em.once(connectionCreatedEventName, resolve);
178
- this.em.once(connectionFailedEventName, reject);
223
+ this.em.once(consts_1.CONNECTION_CREATED_CONST, resolve);
224
+ this.em.once(consts_1.CONNECTION_FAILED_CONST, reject);
179
225
  return;
180
226
  }
181
- this.connectionsMap[connectionPurpose].creatingConnection = true;
227
+ this.creatingConnection = true;
182
228
  let isResolved = false;
183
229
  // It is import to use it as a function and not as a variable
184
230
  // because of k8s changes the env variables
@@ -188,32 +234,32 @@ class RabbitMq {
188
234
  const password = process.env.RABBITMQ_PASSWORD || 'guest';
189
235
  const host = this.options?.rabbitHost || process.env.RABBITMQ_SERVICE_HOST || 'localhost';
190
236
  debug('rabbit: creating connection', { host, userName, HEARTBEAT });
191
- return [`amqp://${userName}:${password}@${host}?heartbeat=${HEARTBEAT}`];
237
+ return [`amqp://${userName}:${password}@${host}/${this.vhost}?heartbeat=${HEARTBEAT}`];
192
238
  };
193
239
  const defaultUrls = findServers();
194
- const newConnection = await (0, amqp_connection_manager_1.connect)(defaultUrls, {
240
+ const connection = await (0, amqp_connection_manager_1.connect)(defaultUrls, {
195
241
  findServers,
196
242
  });
197
- this.connectionsMap[connectionPurpose].connection = newConnection;
198
- logger_1.default.info(`rabbit: created new connection ${connectionPurpose}`);
199
- newConnection.on('error', (err) => {
243
+ this.connection = connection;
244
+ this.connection.on('error', (err) => {
200
245
  logger_1.default.error('rabbit: connection error', { err });
201
246
  if (!isResolved) {
202
247
  isResolved = true;
203
248
  reject(err);
204
- this.em.emit(connectionFailedEventName, err);
249
+ this.em.emit(consts_1.CONNECTION_FAILED_CONST, err);
205
250
  }
206
251
  });
207
- newConnection.on('connectFailed', (err) => {
252
+ this.connection.on('connectFailed', (err) => {
208
253
  this.consumersTags = [];
209
- logger_1.default.error('rabbit: connection connectFailed', { err });
254
+ logger_1.default.error('rabbit: connection connectFailed', { err, advice: 'Check if the vhost exist', vhost: this.vhost });
210
255
  if (!isResolved) {
211
256
  isResolved = true;
212
257
  reject(err);
213
- this.em.emit(connectionFailedEventName, err);
258
+ this.em.emit(consts_1.CONNECTION_FAILED_CONST, err);
214
259
  }
215
260
  });
216
- newConnection.on('disconnect', ({ err }) => {
261
+ this.connection.on('disconnect', ({ err }) => {
262
+ // this.channel = null;
217
263
  this.consumersTags = [];
218
264
  debug('rabbit: connection closed');
219
265
  if (this.options?.disableReconnect) {
@@ -224,26 +270,24 @@ class RabbitMq {
224
270
  logger_1.default.error(`${this.RECONNECT_MSG}${err && ` - ${err}`}`);
225
271
  }
226
272
  });
227
- newConnection.once('connect', async () => {
228
- this.connectionsMap[connectionPurpose].creatingConnection = false;
229
- this.em.emit(connectionCreatedEventName, newConnection);
273
+ this.connection.once('connect', async () => {
274
+ debug('rabbit: connection established');
275
+ this.creatingConnection = false;
276
+ this.em.emit(consts_1.CONNECTION_CREATED_CONST, connection);
230
277
  isResolved = true;
231
- resolve(newConnection);
278
+ resolve(connection);
232
279
  });
233
280
  });
234
281
  }
235
- async getNewChannel({ name = (0, utils_1.rand)().toString(), onClose = null, options = {}, connectionPurpose = consts_1.ConnectionPurpose.Consume, }) {
282
+ async getNewChannel({ name = (0, utils_1.rand)().toString(), onClose = null, options = {} } = {}) {
236
283
  let connection;
237
284
  try {
238
- connection = await this.getConnection(connectionPurpose);
285
+ connection = await this.getConnection();
239
286
  }
240
287
  catch (e) {
241
288
  logger_1.default.error(`rabbit: error on get connection for new channel ${name} `, { e });
242
289
  throw e;
243
290
  }
244
- if (!connection) {
245
- throw new Error(`rabbit: couldnt get connection for new channel ${name}`);
246
- }
247
291
  const channel = connection.createChannel({ ...options });
248
292
  (0, events_1.once)(channel, 'close').then((args) => {
249
293
  logger_1.default.error(`rabbit: channel ${name} closed`);
@@ -259,22 +303,18 @@ class RabbitMq {
259
303
  throw err;
260
304
  }
261
305
  }
262
- async assertChannel({ force = false, connectionPurpose = consts_1.ConnectionPurpose.Consume }) {
263
- debug('rabbit: start assert channel', { connectionPurpose, publishPromise: this.publishChannelSetupPromise, channel: this.publishChannel });
306
+ async assertChannel({ force = false } = {}) {
264
307
  if (!this.publishChannelSetupPromise) {
265
308
  this.publishChannelSetupPromise = new Promise(async (resolve, reject) => {
266
- if (this.publishChannel && !force) {
267
- return resolve(this.publishChannel);
309
+ if (this.channel && !force) {
310
+ return resolve(this.channel);
268
311
  }
269
312
  try {
270
- const channel = await this.getNewChannel({ connectionPurpose });
271
- debug('rabbit: new channel got', { connectionPurpose, channel: this.publishChannel });
313
+ const channel = await this.getNewChannel({});
272
314
  channel.on('error', (err) => {
273
315
  logger_1.default.error('rabbit: channel error', { err });
274
316
  });
275
- if (connectionPurpose === consts_1.ConnectionPurpose.Publish) {
276
- this.publishChannel = channel;
277
- }
317
+ this.channel = channel;
278
318
  resolve(channel);
279
319
  }
280
320
  catch (e) {
@@ -284,8 +324,8 @@ class RabbitMq {
284
324
  }
285
325
  return this.publishChannelSetupPromise;
286
326
  }
287
- async assertExchange(exchangeName, options = { connectionPurpose: consts_1.ConnectionPurpose.Consume }) {
288
- const channel = await this.assertChannel({ connectionPurpose: options.connectionPurpose });
327
+ async assertExchange(exchangeName, options) {
328
+ const channel = await this.assertChannel();
289
329
  if (this.exchanges[exchangeName]) {
290
330
  delete this.assertExchangePromises[exchangeName];
291
331
  return this.exchanges[exchangeName];
@@ -297,46 +337,47 @@ class RabbitMq {
297
337
  this.exchanges[exchangeName] = await this.assertExchangePromises[exchangeName];
298
338
  return this.exchanges[exchangeName];
299
339
  }
300
- async getQueueLength(queue, connectionPurpose = consts_1.ConnectionPurpose.Consume) {
340
+ // TODO: [QUORUM-PHASE-2] Change the implementation to the new one
341
+ async getQueueLength(queue) {
301
342
  RabbitMq.validateName('queue', queue);
302
- const { connection } = this.connectionsMap[connectionPurpose];
303
- const { publishChannel } = this;
304
- if (!publishChannel) {
305
- throw new Error('channel is not defined');
343
+ const { oldChannel: channel } = this;
344
+ if (!channel) {
345
+ throw new rabbitError_1.default('channel is not defined');
306
346
  }
307
- debug('rabbit: getting queue length', { queue, connected: connection?.isConnected() });
308
- return publishChannel?.checkQueue(queue);
347
+ debug('rabbit: getting queue length', { queue, connected: this.connection?.isConnected() });
348
+ return channel?.checkQueue(queue);
309
349
  }
310
- async deleteQueue(queue, connectionPurpose) {
350
+ async deleteQueue(queue) {
311
351
  RabbitMq.validateName('queue', queue);
312
- const channel = await this.assertChannel({ connectionPurpose });
352
+ const channel = await this.assertChannel();
313
353
  logger_1.default.info('rabbit: deleting queue', { queue });
314
354
  const deleteQueueRes = await channel.deleteQueue(queue);
315
355
  debug('queue deleted', deleteQueueRes);
316
356
  return deleteQueueRes;
317
357
  }
358
+ // TODO: [QUORUM-PHASE-2] Change the implementation to the new one
318
359
  async bindQueue(queue, exchange) {
319
- const channel = await this.assertChannel({ connectionPurpose: consts_1.ConnectionPurpose.Publish });
360
+ const channel = await this.assertChannelOld();
320
361
  await channel.addSetup((setupChannel) => setupChannel.bindQueue(queue, exchange, ''));
321
362
  return channel.bindQueue(queue, exchange, '');
322
363
  }
323
364
  async setupQueue(queueName, options) {
324
365
  let queue;
325
- const connectionPurpose = consts_1.ConnectionPurpose.Publish;
326
- const shouldUseQuorum = RabbitMq.shouldUseQuorum(queueName);
327
366
  const localeOptions = {
328
367
  ...options,
329
368
  durable: true,
330
369
  arguments: {
331
370
  ...options?.arguments,
332
371
  'x-consumer-timeout': 1000 * 60 * 60 * 24,
333
- 'x-queue-type': shouldUseQuorum ? 'quorum' : 'classic',
372
+ 'x-queue-type': 'quorum',
334
373
  },
335
374
  };
336
375
  try {
337
- const channel = await this.assertChannel({ connectionPurpose });
376
+ const channel = await this.assertChannel();
338
377
  debug('assertQueue->channel.addSetup', { queueName });
339
- await channel.addSetup((setupChannel) => setupChannel.assertQueue(queueName, localeOptions));
378
+ await channel.addSetup(async (setupChannel) => {
379
+ await setupChannel.assertQueue(queueName, localeOptions);
380
+ });
340
381
  debug('assertQueue->channel.assertQueue', { queueName });
341
382
  queue = await channel.assertQueue(queueName, localeOptions);
342
383
  }
@@ -344,8 +385,8 @@ class RabbitMq {
344
385
  logger_1.default.error('rabbit: assertQueue error', { queueName, options, error: e });
345
386
  if (!this.options?.dontRetryAssert) {
346
387
  debug('retrying assertQueue', { queueName });
347
- const channel = await this.assertChannel({ force: true, connectionPurpose });
348
- await this.deleteQueue(queueName, connectionPurpose);
388
+ const channel = await this.assertChannel({ force: true });
389
+ await this.deleteQueue(queueName);
349
390
  debug('retrying assertQueue->channel.addSetup', { queueName });
350
391
  await channel.addSetup((setupChannel) => setupChannel.assertQueue(queueName, localeOptions));
351
392
  debug('retrying assertQueue->channel.assertQueue', { queueName });
@@ -358,6 +399,7 @@ class RabbitMq {
358
399
  this.queues[queueName] = queue;
359
400
  return queue;
360
401
  }
402
+ // TODO: [QUORUM-PHASE-3] Can be deleted after deleting the old consumers and publishers because all the queues are created us quorum queues
361
403
  static shouldUseQuorum(queueName) {
362
404
  const envQuorumQueuesWhitelist = process.env.QUORUM_QUEUES_WHITELIST;
363
405
  if (envQuorumQueuesWhitelist === '*') {
@@ -370,7 +412,6 @@ class RabbitMq {
370
412
  return false;
371
413
  }
372
414
  async assertQueue(queueName, options) {
373
- debug('rabbit: start assert queue', { queueName });
374
415
  RabbitMq.validateName('queue', queueName);
375
416
  if (this.queues[queueName]) {
376
417
  delete this.queueSetupPromises[queueName];
@@ -380,7 +421,6 @@ class RabbitMq {
380
421
  return this.queueSetupPromises[queueName];
381
422
  }
382
423
  this.queueSetupPromises[queueName] = this.setupQueue(queueName, options);
383
- debug('rabbit: done assert queue', { queueName });
384
424
  return this.queueSetupPromises[queueName];
385
425
  }
386
426
  saveConsumer(queue, callback, options) {
@@ -394,9 +434,23 @@ class RabbitMq {
394
434
  });
395
435
  }
396
436
  }
437
+ // Used by the microservices to consume messages from the queue
397
438
  async consume(queue, callback, options) {
439
+ // TODO: [QUORUM-PHASE-3] Use only the implementation of consumeNew and delete consumeNew and consumeOld
440
+ if (options?.isQuorumQueue !== false) {
441
+ await this.assertVHost();
442
+ await this.consumeNew(queue, callback, options);
443
+ }
444
+ await this.consumeOld(queue, callback, options);
445
+ }
446
+ // TODO: [QUORUM-PHASE-3] Delete consumeNew we do not use it anymore
447
+ async consumeNew(queue, callback, options) {
398
448
  await this.consumeFromRabbit(queue, callback, options);
399
449
  }
450
+ // TODO: [QUORUM-PHASE-3] Delete consumeOld we do not use it anymore
451
+ async consumeOld(queue, callback, options) {
452
+ await this.consumeFromRabbitOld(queue, callback, options);
453
+ }
400
454
  async lockRedisIfNeeded(msg, options) {
401
455
  const { properties: { headers } } = msg;
402
456
  const timestamp = headers?.creationTimestamp;
@@ -419,12 +473,13 @@ class RabbitMq {
419
473
  const { limit, deadMessageTtl, useConsumeWithLock, lockTimeout, auditContext, enableRabbitTrace, } = optionsWithDefaults;
420
474
  if (useConsumeWithLock) {
421
475
  if (!this.redisLock) {
422
- throw new Error('Usage of consumeWithLock requires RedisInstance');
476
+ throw new rabbitError_1.default('Usage of consumeWithLock requires RedisInstance');
423
477
  }
424
478
  logger_1.default.info(`rabbit: Consuming with lock from queue ${queue} with lockTimeout: ${lockTimeout}ms`);
425
479
  }
426
- const channel = await this.getNewChannel({ connectionPurpose: consts_1.ConnectionPurpose.Consume });
480
+ const channel = await this.getNewChannel({});
427
481
  return channel.addSetup(async (confirmChannel) => {
482
+ throw new Error('Dummy assertQueue error');
428
483
  const q = await this.assertQueue(queue, optionsWithDefaults);
429
484
  await confirmChannel.prefetch(limit, false);
430
485
  const { consumerTag } = await confirmChannel.consume(queue, async (msg) => {
@@ -501,36 +556,57 @@ class RabbitMq {
501
556
  }
502
557
  });
503
558
  }
559
+ // Used by the microservices to consume messages from the exchange
504
560
  async consumeFromExchange(queue, exchange, callback, options) {
505
561
  const optionsWithDefaults = { ...consts_1.DEFAULT_OPTIONS, ...options };
506
562
  RabbitMq.validateName('exchange', exchange);
507
563
  RabbitMq.validateName('queue', queue);
508
564
  const { limit, deadMessageTtl } = optionsWithDefaults;
509
- await this.saveConsumer(queue, callback, options);
510
- const channel = await this.getNewChannel({ name: `consume - exchange - ${exchange} -queue - ${queue}` });
511
- return channel.addSetup(async (c) => {
565
+ // TODO: [QUORUM-PHASE-3] Delete the if statement after all the queues are created as quorum queues
566
+ if (options?.isQuorumQueue !== false) {
567
+ await this.assertVHost();
568
+ await this.saveConsumer(queue, callback, options);
569
+ const channel = await this.getNewChannel({ name: `consume-exchange-${exchange}-queue-${queue}` });
570
+ await channel.addSetup(async (c) => {
571
+ const assertExchange = await (0, utils_1.assertExchangeFanout)(c, exchange);
572
+ await c.assertQueue(queue);
573
+ this.exchanges[exchange] = assertExchange;
574
+ await c.prefetch(limit, false);
575
+ return Promise.all([
576
+ c.bindQueue(queue, exchange, ''),
577
+ this.consumeNew(queue, callback, options),
578
+ ]);
579
+ });
580
+ }
581
+ // TODO: [QUORUM-PHASE-3] Delete the old implementation
582
+ await this.saveConsumerOld(queue, callback, options);
583
+ const channelOld = await this.getNewChannelOld({ name: `consume-exchange-${exchange}-queue-${queue}-old` });
584
+ await channelOld.addSetup(async (c) => {
512
585
  const assertExchange = await (0, utils_1.assertExchangeFanout)(c, exchange);
513
586
  await c.assertQueue(queue);
514
- this.exchanges[exchange] = assertExchange;
587
+ this.oldExchanges[exchange] = assertExchange;
515
588
  await c.prefetch(limit, false);
516
589
  return Promise.all([
517
590
  c.bindQueue(queue, exchange, ''),
518
- this.consume(queue, callback, options),
591
+ this.consumeOld(queue, callback, options),
519
592
  ]);
520
593
  });
521
594
  }
595
+ // Used by the microservices to publish messages to the exchange
596
+ // TODO: [QUORUM-PHASE-2] Change the implementation to the new one
522
597
  async publish(exchange, content, customHeaders) {
523
- debug('rabbit: start publish msg');
524
598
  return (0, utils_1.wrapSetImmediate)(async () => {
525
599
  RabbitMq.validateName('exchange', exchange);
526
- const channel = await this.assertChannel({ connectionPurpose: consts_1.ConnectionPurpose.Publish });
527
- await this.assertExchange(exchange, { connectionPurpose: consts_1.ConnectionPurpose.Publish });
600
+ const channel = await this.assertChannelOld();
601
+ await this.assertExchangeOld(exchange);
528
602
  await channel.publish(exchange, '', Buffer.from(JSON.stringify(content)), RabbitMq.getPublishOptions(customHeaders));
529
603
  });
530
604
  }
605
+ // Used by the microservices to send messages to the queue
606
+ // TODO: [QUORUM-PHASE-2] Change the implementation to the new one
531
607
  async sendToQueue(queue, content, options, customHeaders) {
532
608
  try {
533
- await this.assertChannel({ connectionPurpose: consts_1.ConnectionPurpose.Publish });
609
+ await this.assertChannelOld();
534
610
  }
535
611
  catch (e) {
536
612
  logger_1.default.error(`rabbit sendToQueue: failed to send assert channel when sending to queue ${queue}`, { e });
@@ -538,61 +614,55 @@ class RabbitMq {
538
614
  }
539
615
  try {
540
616
  RabbitMq.validateName('queue', queue);
541
- await this.assertQueue(queue, options);
617
+ await this.assertQueueOld(queue, options);
542
618
  }
543
619
  catch (e) {
544
620
  logger_1.default.error(`rabbit sendToQueue: failed to assert queue ${queue}`, { e });
545
621
  throw e;
546
622
  }
547
623
  try {
548
- const res = await this.publishChannel?.sendToQueue(queue, Buffer.from(JSON.stringify(content)), RabbitMq.getPublishOptions(customHeaders));
624
+ const res = await this.oldChannel?.sendToQueue(queue, Buffer.from(JSON.stringify(content)), RabbitMq.getPublishOptions(customHeaders));
549
625
  debug(`rabbit: sending to queue ${queue}`, { res });
550
626
  return res;
551
627
  }
552
628
  catch (e) {
553
- logger_1.default.error(`rabbit sendToQueue: failed to send to queue ${queue}`, { e });
629
+ const isConnected = await this.isConnected();
630
+ logger_1.default.error(`rabbit sendToQueue: failed to send to queue ${queue}, isConnected: ${isConnected}`, { e });
554
631
  throw e;
555
632
  }
556
633
  }
634
+ // TODO: [QUORUM-PHASE-2] After changing the publish from old to new change from the old to the new implementation
557
635
  async isConnected() {
558
- debug('rabbit: start is connected');
559
- const isEachConnectionConnected = await Promise.all(Object.entries(this.connectionsMap).map(async ([connectionPurpose, connectionData]) => {
560
- const { connection } = connectionData;
561
- if (connection) {
562
- const isConnected = connection.isConnected();
563
- if (!isConnected) {
564
- logger_1.default.error('rabbit: isConnected - false', { connectionPurpose });
565
- return false;
566
- }
567
- logger_1.default.info('rabbit: isConnected - true', { connectionPurpose });
568
- if (connectionPurpose === consts_1.ConnectionPurpose.Publish) {
569
- const channel = await this.assertChannel({ connectionPurpose: connectionPurpose });
570
- try {
571
- await Promise.all([
572
- channel.waitForConnect(),
573
- ...this.consumers.map((c) => channel.checkQueue(c.queue)),
574
- ]);
575
- }
576
- catch (e) {
577
- logger_1.default.error('rabbit: isConnected - false');
578
- return false;
579
- }
580
- }
581
- }
582
- else {
583
- logger_1.default.info('rabbit: connection hasnt initialized yet', { connectionPurpose });
584
- }
585
- return true;
586
- }));
587
- return isEachConnectionConnected.every((isConnected) => isConnected === true);
636
+ const connection = await this.getConnectionOld();
637
+ const isConnected = connection.isConnected();
638
+ if (!isConnected) {
639
+ logger_1.default.error('rabbit: isConnected - false');
640
+ return false;
641
+ }
642
+ const channel = await this.assertChannelOld();
643
+ try {
644
+ await Promise.all([
645
+ channel.waitForConnect(),
646
+ ...this.oldConsumers.map((c) => channel.checkQueue(c.queue)),
647
+ ]);
648
+ }
649
+ catch (e) {
650
+ logger_1.default.error('rabbit: isConnected - false');
651
+ return false;
652
+ }
653
+ logger_1.default.info('rabbit: isConnected - true');
654
+ return true;
588
655
  }
589
656
  async gracefulShutdown(signal) {
590
- const tagsNumber = this.consumersTags.length;
657
+ // TODO: [QUORUM-PHASE-2] After changing the publish from old to new change from the old to the new implementation
658
+ const tagsNumber = this.consumersTags.length + this.oldConsumersTags.length;
591
659
  logger_1.default.info(`rabbit: [gracefully-shutdown] received ${signal}! canceling #${tagsNumber} tags...`);
592
660
  const cancelTagPromises = this.consumersTags.map(([channel, tag]) => channel.cancel(tag));
661
+ const cancelTagPromisesOld = this.oldConsumersTags.map(([channel, tag]) => channel.cancel(tag));
593
662
  // Clean the array to avoid race
594
663
  this.consumersTags = [];
595
- const results = await Promise.allSettled(cancelTagPromises);
664
+ this.oldConsumersTags = [];
665
+ const results = await Promise.allSettled([...cancelTagPromises, ...cancelTagPromisesOld]);
596
666
  const rejected = results.filter((p) => p.status === 'rejected');
597
667
  if (rejected.length > 0) {
598
668
  logger_1.default.warn(`rabbit: [gracefully-shutdown] #${rejected.length}/${tagsNumber} tags failed to cancel: ${rejected}`);
@@ -601,6 +671,300 @@ class RabbitMq {
601
671
  logger_1.default.info('rabbit: [gracefully-shutdown] all tags successfully canceled.');
602
672
  }
603
673
  }
674
+ async consumeFromRabbitOld(queue, callback, options) {
675
+ const optionsWithDefaults = { ...consts_1.DEFAULT_OPTIONS, ...options };
676
+ RabbitMq.validateName('queue', queue);
677
+ this.saveConsumerOld(queue, callback, options);
678
+ const uniqueId = (0, node_crypto_1.randomUUID)();
679
+ const { limit, deadMessageTtl, useConsumeWithLock, lockTimeout, auditContext, enableRabbitTrace, } = optionsWithDefaults;
680
+ if (useConsumeWithLock) {
681
+ if (!this.redisLock) {
682
+ throw new rabbitError_1.default('Usage of consumeWithLock requires RedisInstance');
683
+ }
684
+ logger_1.default.info(`rabbit: Consuming with lock from queue ${queue} with lockTimeout: ${lockTimeout}ms`);
685
+ }
686
+ const channel = await this.getNewChannelOld({});
687
+ return channel.addSetup(async (confirmChannel) => {
688
+ const q = await this.assertQueueOld(queue, optionsWithDefaults);
689
+ await confirmChannel.prefetch(limit, false);
690
+ const { consumerTag } = await confirmChannel.consume(queue, async (msg) => {
691
+ if (!msg) {
692
+ return null;
693
+ }
694
+ const traceId = msg.properties.headers[consts_1.TRACING_HEADER];
695
+ const userId = msg.properties.headers[consts_1.USER_TRACING_HEADER];
696
+ const automationId = msg.properties.headers[consts_1.AUTOMATION_ID_HEADER];
697
+ const parsedMessage = RabbitMq.parseMsg(msg);
698
+ const releaseLock = await this.lockRedisIfNeeded(parsedMessage, optionsWithDefaults);
699
+ const trace = (0, zehut_1.newTrace)(zehut_1.traceTypes.RABBIT);
700
+ // setting also outbreak trace as part of legacy code
701
+ const outbreakTrace = zehut_1.outbreak.newTrace(zehut_1.traceTypes.RABBIT);
702
+ // enableRabbitTrace is a flag to protect from different flows that doesn't work with permission
703
+ // and we don't want to fail the flow because of it
704
+ if (userId && enableRabbitTrace) {
705
+ try {
706
+ await Promise.all([
707
+ (0, zehut_1.createOrSetRabbitTrace)(trace, userId),
708
+ (0, zehut_1.createOrSetRabbitTrace)(outbreakTrace, userId),
709
+ ]);
710
+ }
711
+ catch (e) {
712
+ logger_1.default.error('rabbit: failed to setRabbitTrace', { userId, e });
713
+ return this.nack(confirmChannel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg);
714
+ }
715
+ }
716
+ if (traceId) {
717
+ trace?.context?.set(consts_1.TRACING_HEADER, traceId);
718
+ outbreakTrace?.context.set(consts_1.TRACING_HEADER, traceId);
719
+ }
720
+ if (auditContext) {
721
+ await auditContext(queue, {
722
+ userId,
723
+ automationId,
724
+ });
725
+ }
726
+ const shouldConsume = await this.shouldConsumeMessageByTimestamp(parsedMessage);
727
+ if (!shouldConsume) {
728
+ await this.unlockRedisIfNeeded(releaseLock);
729
+ return this.ack(confirmChannel, msg)(msg);
730
+ }
731
+ let messageAcked = false;
732
+ // setting the localAck function to be used in the callback
733
+ const localAck = async () => {
734
+ if (messageAcked) {
735
+ return;
736
+ }
737
+ messageAcked = true;
738
+ return this.ack(confirmChannel, msg, true, releaseLock)(msg);
739
+ };
740
+ const localNack = async (_, nackOptions = {}) => {
741
+ if (messageAcked) {
742
+ return;
743
+ }
744
+ debug('rabbit localNack', { messageAcked, uniqueId, deliveryTag: msg.fields.deliveryTag });
745
+ messageAcked = true;
746
+ return this.nack(confirmChannel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg, nackOptions);
747
+ };
748
+ try {
749
+ await callback(parsedMessage, localAck, localNack);
750
+ }
751
+ catch (e) {
752
+ await localNack(msg);
753
+ }
754
+ }, types_1.CONSUMER_DEFAULT_OPTIONS);
755
+ if (!consumerTag) {
756
+ logger_1.default.error(`rabbit: failed to consume from queue ${queue}`);
757
+ }
758
+ else {
759
+ logger_1.default.info(`rabbit: adding tag ${consumerTag} to the array.`);
760
+ this.oldConsumersTags.push([confirmChannel, consumerTag]);
761
+ }
762
+ });
763
+ }
764
+ // TODO: [QUORUM-PHASE-3] Delete all the function under this line (getNewChannelOld, getConnectionOld, assertQueueOld, setupQueueOld, saveConsumerOld)
765
+ async getNewChannelOld({ name = (0, utils_1.rand)().toString(), onClose = null, options = {} } = {}) {
766
+ let connection;
767
+ try {
768
+ connection = await this.getConnectionOld();
769
+ }
770
+ catch (e) {
771
+ logger_1.default.error(`rabbit: error on get connection for new channel ${name} `, { e });
772
+ throw e;
773
+ }
774
+ const channel = connection.createChannel({ ...options });
775
+ (0, events_1.once)(channel, 'close').then((args) => {
776
+ logger_1.default.error(`rabbit: channel ${name} closed`);
777
+ onClose?.(args);
778
+ });
779
+ try {
780
+ await (0, events_1.once)(channel, 'connect');
781
+ debug(`rabbit: channel ${name} CONNECTED`);
782
+ return channel;
783
+ }
784
+ catch (err) {
785
+ logger_1.default.error(`rabbit: channel error ${name} error`, { err });
786
+ throw err;
787
+ }
788
+ }
789
+ async getConnectionOld() {
790
+ return new Promise(async (resolve, reject) => {
791
+ if (this.oldBlockReconnect) {
792
+ debug('rabbit: block reconnect');
793
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
794
+ // @ts-ignore
795
+ return resolve();
796
+ }
797
+ if (this.oldConnection !== null) {
798
+ if (this.options?.disableReconnect || this.oldConnection?.isConnected()) {
799
+ debug('rabbit: connection - is connected');
800
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
801
+ // @ts-ignore
802
+ return resolve(this.oldConnection);
803
+ }
804
+ debug('rabbit: connection - reconnecting');
805
+ }
806
+ if (this.oldCreatingConnection) {
807
+ debug('rabbit: creating connection emi');
808
+ this.oldEm.once(consts_1.CONNECTION_CREATED_CONST, resolve);
809
+ this.oldEm.once(consts_1.CONNECTION_FAILED_CONST, reject);
810
+ return;
811
+ }
812
+ this.oldCreatingConnection = true;
813
+ let isResolved = false;
814
+ // It is import to use it as a function and not as a variable
815
+ // because of k8s changes the env variables
816
+ // and we want to use the new values
817
+ const findServers = () => {
818
+ const userName = process.env.RABBITMQ_USERNAME || 'guest';
819
+ const password = process.env.RABBITMQ_PASSWORD || 'guest';
820
+ const host = this.options?.rabbitHost || process.env.RABBITMQ_SERVICE_HOST || 'localhost';
821
+ debug('rabbit: creating connection', { host, userName, HEARTBEAT });
822
+ return [`amqp://${userName}:${password}@${host}?heartbeat=${HEARTBEAT}`];
823
+ };
824
+ const defaultUrls = findServers();
825
+ const connection = await (0, amqp_connection_manager_1.connect)(defaultUrls, {
826
+ findServers,
827
+ });
828
+ this.oldConnection = connection;
829
+ this.oldConnection.on('error', (err) => {
830
+ logger_1.default.error('rabbit: connection error', { err });
831
+ if (!isResolved) {
832
+ isResolved = true;
833
+ reject(err);
834
+ this.oldEm.emit(consts_1.CONNECTION_FAILED_CONST, err);
835
+ }
836
+ });
837
+ this.oldConnection.on('connectFailed', (err) => {
838
+ this.oldConsumersTags = [];
839
+ logger_1.default.error('rabbit: connection connectFailed', { err });
840
+ if (!isResolved) {
841
+ isResolved = true;
842
+ reject(err);
843
+ this.oldEm.emit(consts_1.CONNECTION_FAILED_CONST, err);
844
+ }
845
+ });
846
+ this.oldConnection.on('disconnect', ({ err }) => {
847
+ this.oldConsumersTags = [];
848
+ debug('rabbit: connection closed');
849
+ if (this.options?.disableReconnect) {
850
+ logger_1.default.error(`${this.DISCONNECT_MSG}${err && ` - ${err}`}`);
851
+ this.oldBlockReconnect = true;
852
+ }
853
+ else {
854
+ logger_1.default.error(`${this.RECONNECT_MSG}${err && ` - ${err}`}`);
855
+ }
856
+ });
857
+ this.oldConnection.once('connect', async () => {
858
+ debug('rabbit: connection established');
859
+ this.oldCreatingConnection = false;
860
+ this.oldEm.emit(consts_1.CONNECTION_CREATED_CONST, connection);
861
+ isResolved = true;
862
+ resolve(connection);
863
+ });
864
+ });
865
+ }
866
+ saveConsumerOld(queue, callback, options) {
867
+ const isConsumerExist = this.oldConsumers.some((consumer) => consumer.queue === queue);
868
+ if (!isConsumerExist) {
869
+ logger_1.default.info(`rabbit: consumer: ${queue} saved in consumer array`);
870
+ this.oldConsumers.push({
871
+ queue,
872
+ callback,
873
+ options,
874
+ });
875
+ }
876
+ }
877
+ async assertQueueOld(queueName, options) {
878
+ RabbitMq.validateName('queue', queueName);
879
+ if (this.oldQueues[queueName]) {
880
+ delete this.oldQueueSetupPromises[queueName];
881
+ return this.oldQueues[queueName];
882
+ }
883
+ if (this.oldQueueSetupPromises[queueName]) {
884
+ return this.oldQueueSetupPromises[queueName];
885
+ }
886
+ this.oldQueueSetupPromises[queueName] = this.setupQueueOld(queueName, options);
887
+ return this.oldQueueSetupPromises[queueName];
888
+ }
889
+ async setupQueueOld(queueName, options) {
890
+ let queue;
891
+ const shouldUseQuorum = RabbitMq.shouldUseQuorum(queueName);
892
+ const localeOptions = {
893
+ ...options,
894
+ durable: true,
895
+ arguments: {
896
+ ...options?.arguments,
897
+ 'x-consumer-timeout': 1000 * 60 * 60 * 24,
898
+ 'x-queue-type': shouldUseQuorum ? 'quorum' : 'classic',
899
+ },
900
+ };
901
+ try {
902
+ const channel = await this.assertChannelOld();
903
+ debug('assertQueue->channel.addSetup', { queueName });
904
+ await channel.addSetup((setupChannel) => setupChannel.assertQueue(queueName, localeOptions));
905
+ debug('assertQueue->channel.assertQueue', { queueName });
906
+ queue = await channel.assertQueue(queueName, localeOptions);
907
+ }
908
+ catch (e) {
909
+ logger_1.default.error('rabbit: assertQueue error', { queueName, options, error: e });
910
+ if (!this.options?.dontRetryAssert) {
911
+ debug('retrying assertQueue', { queueName });
912
+ const channel = await this.assertChannelOld({ force: true });
913
+ await this.deleteQueueOld(queueName);
914
+ debug('retrying assertQueue->channel.addSetup', { queueName });
915
+ await channel.addSetup((setupChannel) => setupChannel.assertQueue(queueName, localeOptions));
916
+ debug('retrying assertQueue->channel.assertQueue', { queueName });
917
+ queue = await channel.assertQueue(queueName, localeOptions);
918
+ }
919
+ else {
920
+ throw e;
921
+ }
922
+ }
923
+ this.oldQueues[queueName] = queue;
924
+ return queue;
925
+ }
926
+ async assertChannelOld({ force = false } = {}) {
927
+ if (!this.oldPublishChannelSetupPromise) {
928
+ this.oldPublishChannelSetupPromise = new Promise(async (resolve, reject) => {
929
+ if (this.oldChannel && !force) {
930
+ return resolve(this.oldChannel);
931
+ }
932
+ try {
933
+ const channel = await this.getNewChannelOld({});
934
+ channel.on('error', (err) => {
935
+ logger_1.default.error('rabbit: channel error', { err });
936
+ });
937
+ this.oldChannel = channel;
938
+ resolve(channel);
939
+ }
940
+ catch (e) {
941
+ reject(e);
942
+ }
943
+ });
944
+ }
945
+ return this.oldPublishChannelSetupPromise;
946
+ }
947
+ async deleteQueueOld(queue) {
948
+ RabbitMq.validateName('queue', queue);
949
+ const channel = await this.assertChannelOld();
950
+ logger_1.default.info('rabbit: deleting queue', { queue });
951
+ const deleteQueueRes = await channel.deleteQueue(queue);
952
+ debug('queue deleted', deleteQueueRes);
953
+ return deleteQueueRes;
954
+ }
955
+ async assertExchangeOld(exchangeName, options) {
956
+ const channel = await this.assertChannelOld();
957
+ if (this.oldExchanges[exchangeName]) {
958
+ delete this.oldAssertExchangePromises[exchangeName];
959
+ return this.oldExchanges[exchangeName];
960
+ }
961
+ if (this.oldAssertExchangePromises[exchangeName]) {
962
+ return this.oldAssertExchangePromises[exchangeName];
963
+ }
964
+ this.oldAssertExchangePromises[exchangeName] = (0, utils_1.assertExchangeFanout)(channel, exchangeName);
965
+ this.oldExchanges[exchangeName] = await this.oldAssertExchangePromises[exchangeName];
966
+ return this.oldExchanges[exchangeName];
967
+ }
604
968
  }
605
969
  exports.default = RabbitMq;
606
970
  var celery_1 = require("./lib/celery");