@autofleet/rabbit 3.3.0-beta.4 → 3.3.2

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