@autofleet/rabbit 3.3.0-beta.4 → 3.3.0

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