@autofleet/rabbit 4.1.0-beta.0 → 4.1.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.
Files changed (60) hide show
  1. package/dist/index.d.ts +81 -76
  2. package/dist/index.js +581 -472
  3. package/dist/index.js.map +1 -0
  4. package/dist/lib/celery.js +1 -0
  5. package/dist/lib/celery.js.map +1 -0
  6. package/dist/lib/consts.d.ts +12 -4
  7. package/dist/lib/consts.js +12 -3
  8. package/dist/lib/consts.js.map +1 -0
  9. package/dist/lib/rabbitError.js +1 -0
  10. package/dist/lib/rabbitError.js.map +1 -0
  11. package/dist/lib/redis.d.ts +2 -1
  12. package/dist/lib/redis.js +3 -8
  13. package/dist/lib/redis.js.map +1 -0
  14. package/dist/lib/types.d.ts +18 -5
  15. package/dist/lib/types.js +1 -0
  16. package/dist/lib/types.js.map +1 -0
  17. package/dist/lib/utils.d.ts +12 -3
  18. package/dist/lib/utils.js +23 -13
  19. package/dist/lib/utils.js.map +1 -0
  20. package/dist/logger.js +1 -0
  21. package/dist/logger.js.map +1 -0
  22. package/dist/{mock.d.ts → mock/index.d.ts} +1 -1
  23. package/dist/{mock.js → mock/index.js} +2 -1
  24. package/dist/mock/index.js.map +1 -0
  25. package/dist/mock/vitest.d.ts +13 -0
  26. package/dist/mock/vitest.js +18 -0
  27. package/dist/mock/vitest.js.map +1 -0
  28. package/package.json +24 -19
  29. package/src/index.ts +702 -591
  30. package/src/lib/consts.ts +17 -4
  31. package/src/lib/redis.ts +2 -7
  32. package/src/lib/types.ts +22 -7
  33. package/src/lib/utils.ts +35 -12
  34. package/src/{mock.ts → mock/index.ts} +2 -2
  35. package/src/mock/vitest.ts +24 -0
  36. package/src/redis-lock.d.ts +5 -3
  37. package/tsconfig.build.json +5 -0
  38. package/tsconfig.json +2 -2
  39. package/vitest.config.ts +17 -0
  40. package/coverage/clover.xml +0 -669
  41. package/coverage/coverage-final.json +0 -9
  42. package/coverage/lcov-report/base.css +0 -224
  43. package/coverage/lcov-report/block-navigation.js +0 -87
  44. package/coverage/lcov-report/favicon.png +0 -0
  45. package/coverage/lcov-report/index.html +0 -131
  46. package/coverage/lcov-report/prettify.css +0 -1
  47. package/coverage/lcov-report/prettify.js +0 -2
  48. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  49. package/coverage/lcov-report/sorter.js +0 -196
  50. package/coverage/lcov-report/src/index.html +0 -131
  51. package/coverage/lcov-report/src/index.ts.html +0 -3826
  52. package/coverage/lcov-report/src/lib/celery.ts.html +0 -352
  53. package/coverage/lcov-report/src/lib/consts.ts.html +0 -142
  54. package/coverage/lcov-report/src/lib/index.html +0 -191
  55. package/coverage/lcov-report/src/lib/rabbitError.ts.html +0 -103
  56. package/coverage/lcov-report/src/lib/redis.ts.html +0 -133
  57. package/coverage/lcov-report/src/lib/types.ts.html +0 -268
  58. package/coverage/lcov-report/src/lib/utils.ts.html +0 -142
  59. package/coverage/lcov-report/src/logger.ts.html +0 -100
  60. package/coverage/lcov.info +0 -1076
package/src/index.ts CHANGED
@@ -1,99 +1,107 @@
1
- /* eslint-disable no-empty,consistent-return,no-async-promise-executor,@typescript-eslint/no-unused-vars */
2
-
3
- import { EventEmitter, once } from 'events';
4
- import { promisify } from 'util';
1
+ import { env } from 'node:process';
2
+ import { setImmediate } from 'node:timers/promises';
3
+ import { EventEmitter, once } from 'node:events';
5
4
  import moment from 'moment';
6
5
  import RedisLock from 'redis-lock';
7
6
  import {
8
- AmqpConnectionManager, ChannelWrapper, connect, CreateChannelOpts,
7
+ type AmqpConnectionManager, type ChannelWrapper, connect, type CreateChannelOpts,
9
8
  } from 'amqp-connection-manager';
9
+ // eslint-disable-next-line import/no-unresolved
10
+ import type { PublishOptions } from 'amqp-connection-manager/dist/types/ChannelWrapper';
10
11
  import {
11
12
  ConfirmChannel, ConsumeMessage, Options, Replies,
12
13
  } from 'amqplib';
14
+ import { backOff } from 'exponential-backoff';
15
+ import type { LoggerInstanceManager } from '@autofleet/logger';
13
16
  import {
14
- getCurrentPayload, newTrace, traceTypes, createOrSetRabbitTrace, outbreak,
17
+ getUser, getCurrentPayload, newTrace, traceTypes, createOrSetRabbitTrace, outbreak,
15
18
  } from '@autofleet/zehut';
16
19
  import { randomUUID } from 'node:crypto';
17
- import axios from 'axios';
18
20
  import logger from './logger';
19
21
  import RabbitError from './lib/rabbitError';
20
22
  import getRedisInstance, { RedisConfig } from './lib/redis';
21
- import { assertExchangeFanout, rand, wrapSetImmediate } from './lib/utils';
23
+ import {
24
+ assertExchangeFanout, createDeferredPromise, getAssertVhostExponentialBackoffConfig, rand,
25
+ } from './lib/utils';
22
26
  import {
23
27
  AUTOMATION_ID_HEADER,
24
- CONNECTION_CREATED_CONST,
25
- CONNECTION_FAILED_CONST,
26
28
  DEFAULT_LOCK_TIMEOUT,
27
29
  DEFAULT_OPTIONS,
28
30
  RETRY_HEADER,
29
31
  TRACING_HEADER,
30
- USER_OBJECT,
31
32
  USER_TRACING_HEADER,
32
33
  } from './lib/consts';
33
34
  import {
34
- CallbackFunction,
35
- ConsumeMessageOrNull,
36
- ConsumeOptions,
37
- CustomMessageHeaders,
38
- QueuesCache,
39
- RedisLockType,
40
- ExchangesCache,
35
+ type AssertExchangePromisesDictionary,
36
+ type CallbackFunction,
37
+ type ConnectionData,
38
+ type ConsumeMessageOrNull,
39
+ type ConsumeOptions,
41
40
  CONSUMER_DEFAULT_OPTIONS,
42
- QueueSetupPromisesDictionary,
43
- AssertExchangePromisesDictionary,
41
+ type CustomMessageHeaders,
42
+ type ExchangesCache,
43
+ type Message,
44
+ type NackOptions,
45
+ type QueuesCache,
46
+ type QueueSetupPromisesDictionary,
44
47
  } from './lib/types';
45
48
 
46
- // const debug = nodeDebug('af-rabbitmq')
47
- const debug = logger.debug.bind(logger);
48
-
49
49
  const PUBLISH_TIMEOUT = 1000 * 10;
50
50
 
51
- export interface IAfRabbitMq {
52
- ack: any;
53
- nack: any;
54
- assertChannel: any;
55
- assertExchange: any;
56
- assertQueue: any;
57
- consume: any;
58
- consumeFromExchange: any;
59
- publish: any;
60
- sendToQueue: any;
61
- redisClient?: any;
62
- }
51
+ // TODO: [QUORUM-PHASE-3] Delete these env vars
52
+ const {
53
+ DISABLE_QUORUM_QUEUES_CONSUME,
54
+ DISABLE_QUORUM_QUEUES_PUBLISH,
55
+ } = env;
63
56
 
64
- interface NackOptions {
65
- skipRetry?: boolean;
57
+ export interface IAfRabbitMq {
58
+ ack(channel: ConfirmChannel, msg: ConsumeMessageOrNull, shouldUpdateRedisTimestamp?: boolean, releaseLock?: (() => Promise<void>) | null): (userMsg: ConsumeMessage) => Promise<void>;
59
+ nack(
60
+ channel: ConfirmChannel,
61
+ queue: string,
62
+ options: ConsumeOptions,
63
+ deadQueueOptions: Options.AssertQueue,
64
+ msg: ConsumeMessageOrNull,
65
+ releaseLock?: () => Promise<void>,
66
+ ): (userMsg: ConsumeMessageOrNull, nackOptions?: NackOptions) => Promise<void> ;
67
+ assertChannel(options?: assertChannelOpts): Promise<ChannelWrapper>;
68
+ assertExchange(exchangeName: string, connection: ConnectionData): Promise<Replies.AssertExchange>
69
+ assertQueue(queueName: string, options?: Options.AssertQueue | null): Promise<Replies.AssertQueue>;
70
+ consume(queue: string, callback: CallbackFunction, options?: ConsumeOptions): Promise<void>;
71
+ consumeFromExchange(queue: string, exchange: string, callback: CallbackFunction, options?: ConsumeOptions): Promise<void>
72
+ publish(exchange: string, content: any, customHeaders?: PublishOptions['headers']): Promise<void>
73
+ sendToQueue(queue: string, content: any, options?: Options.AssertQueue | null, customHeaders?: PublishOptions['headers']): Promise<boolean | undefined>;
74
+ redisClient?: ReturnType<typeof getRedisInstance>;
66
75
  }
67
76
 
68
77
  export interface AfRabbitOptions {
69
78
  disableReconnect?: boolean;
70
-
71
79
  /**
72
80
  * When you want your own grace-full shutdown, set this to true.
73
81
  * @default false
74
82
  */
75
83
  dontGracefulShutdown?: boolean;
76
-
77
84
  /**
78
- * dont retry on creation error
85
+ * don't retry on creation error
79
86
  * @default false
80
87
  */
81
88
  dontRetryAssert?: boolean;
82
-
83
89
  rabbitHost?: string;
84
-
85
90
  vhost?: string;
91
+ logger?: LoggerInstanceManager;
86
92
  }
87
93
 
88
94
  type newChannelOpts = {
89
95
  name?: string;
90
96
  onClose?: null | ((args: any | null) => void);
91
97
  options?: CreateChannelOpts | undefined;
98
+ connection: ConnectionData;
92
99
  };
93
100
 
94
101
  type assertChannelOpts = {
95
102
  channelName?: string;
96
103
  force?: boolean;
104
+ connection: ConnectionData;
97
105
  }
98
106
 
99
107
  type AfConsumer = {
@@ -105,13 +113,12 @@ type AfConsumer = {
105
113
  const HEARTBEAT = '60';
106
114
 
107
115
  class RabbitMq implements IAfRabbitMq {
108
- static parseMsg(msg: any) : any {
109
- let { content } = msg;
110
- content = content.toString();
116
+ static parseMsg(msg: ConsumeMessage): Message {
117
+ let content = msg.content.toString();
111
118
 
112
119
  try {
113
120
  content = JSON.parse(content);
114
- } catch (e) { }
121
+ } catch { /* ignore error */ }
115
122
 
116
123
  return {
117
124
  ...msg,
@@ -125,11 +132,10 @@ class RabbitMq implements IAfRabbitMq {
125
132
  }
126
133
  }
127
134
 
128
- static getPublishOptions(customHeaders: CustomMessageHeaders = {}) {
129
- const trace = getCurrentPayload();
130
- const user = trace?.context?.get(USER_OBJECT);
131
- const traceId = trace?.context?.get(TRACING_HEADER);
132
- const outbreakTrace = outbreak.getCurrentContext();
135
+ static getPublishOptions(customHeaders: CustomMessageHeaders = {}): PublishOptions {
136
+ const user = getUser();
137
+ const traceId = getCurrentPayload()?.context?.get(TRACING_HEADER) || outbreak.getCurrentContext()?.context?.get(TRACING_HEADER);
138
+
133
139
  return {
134
140
  timestamp: moment().unix(),
135
141
  timeout: PUBLISH_TIMEOUT,
@@ -137,97 +143,110 @@ class RabbitMq implements IAfRabbitMq {
137
143
  creationTimestamp: moment().valueOf(),
138
144
  ...customHeaders,
139
145
  [USER_TRACING_HEADER]: user?.id,
140
- [TRACING_HEADER]: traceId || outbreakTrace?.context?.get(TRACING_HEADER),
146
+ [TRACING_HEADER]: traceId,
141
147
  },
142
148
  };
143
149
  }
144
150
 
145
- DISCONNECT_MSG = 'rabbit: connection disconnect';
146
-
147
- RECONNECT_MSG = 'rabbit: connection disconnect - reconnecting';
148
-
149
- channel: ChannelWrapper | null;
151
+ readonly DISCONNECT_MSG = 'rabbit: connection disconnect';
150
152
 
151
- publishChannelSetupPromise: Promise<ChannelWrapper> | null;
153
+ readonly RECONNECT_MSG = 'rabbit: connection disconnect - reconnecting';
152
154
 
153
- blockReconnect: boolean | null | undefined
155
+ publishChannel: ChannelWrapper | null = null;
154
156
 
155
- connection: AmqpConnectionManager | null | undefined
157
+ publishChannelSetupPromise: Promise<ChannelWrapper> | null = null;
156
158
 
157
- em: EventEmitter;
159
+ readonly publishConnection: ConnectionData = {
160
+ amqpConnection: null,
161
+ creatingConnection: false,
162
+ connectionCreatedEventName: 'publishConnectionCreated',
163
+ connectionFailedEventName: 'publishConnectionFailed',
164
+ blockReconnect: false,
165
+ };
158
166
 
159
- creatingConnection: boolean;
167
+ readonly consumeConnection: ConnectionData = {
168
+ amqpConnection: null,
169
+ creatingConnection: false,
170
+ connectionCreatedEventName: 'consumeConnectionCreated',
171
+ connectionFailedEventName: 'consumeConnectionFailed',
172
+ blockReconnect: false,
173
+ };
160
174
 
161
- exchanges: ExchangesCache;
175
+ readonly em: EventEmitter = new EventEmitter();
162
176
 
163
- queues: QueuesCache;
177
+ readonly exchanges: ExchangesCache = {};
164
178
 
165
- queueSetupPromises: QueueSetupPromisesDictionary;
179
+ readonly queues: QueuesCache = {};
166
180
 
167
- assertExchangePromises: AssertExchangePromisesDictionary;
181
+ readonly queueSetupPromises: QueueSetupPromisesDictionary = {};
168
182
 
169
- options: AfRabbitOptions | undefined;
183
+ readonly assertExchangePromises: AssertExchangePromisesDictionary = {};
170
184
 
171
- redisClient: any;
185
+ readonly redisClient?: ReturnType<typeof getRedisInstance>;
172
186
 
173
- redisLock?: RedisLockType;
187
+ readonly redisLock?: ReturnType<typeof RedisLock>;
174
188
 
175
189
  /** Array of consumers tags used for canceling consumption */
176
- consumersTags: Array<[ConfirmChannel, string]>;
190
+ readonly consumersTags: Map<string, { channel: ConfirmChannel; consumerTag: string }> = new Map<string, { channel: ConfirmChannel; consumerTag: string }>();
177
191
 
178
- private consumers: Array<AfConsumer> = [];
192
+ private readonly consumersToRegister: Array<AfConsumer> = [];
179
193
 
180
- private isVHostExist = false;
194
+ private doesVHostExist = false;
181
195
 
182
- // TODO: DELETE OLD PROPERTIES
196
+ private readonly vhost = 'quorum-vhost';
183
197
 
184
- oldChannel: ChannelWrapper | null;
198
+ private gracefulShutdownStarted = false;
185
199
 
186
- oldPublishChannelSetupPromise: Promise<ChannelWrapper> | null;
200
+ // TODO:[QUORUM-PHASE-3] Delete the old properties that we use for the old consumers and publishers
201
+ oldPublishChannel: ChannelWrapper | null = null;
187
202
 
188
- oldBlockReconnect: boolean | null | undefined
203
+ oldPublishChannelSetupPromise: Promise<ChannelWrapper> | null = null;
189
204
 
190
- oldConnection: AmqpConnectionManager | null | undefined
205
+ readonly oldPublishConnection: ConnectionData = {
206
+ amqpConnection: null,
207
+ creatingConnection: false,
208
+ connectionCreatedEventName: 'oldPublishConnectionCreated',
209
+ connectionFailedEventName: 'oldPublishConnectionFailed',
210
+ blockReconnect: false,
211
+ };
191
212
 
192
- oldEm: EventEmitter;
213
+ readonly oldConsumeConnection: ConnectionData = {
214
+ amqpConnection: null,
215
+ creatingConnection: false,
216
+ connectionCreatedEventName: 'oldConsumeConnectionCreated',
217
+ connectionFailedEventName: 'oldConsumeConnectionFailed',
218
+ blockReconnect: false,
219
+ };
193
220
 
194
- oldCreatingConnection: boolean;
221
+ readonly oldEm: EventEmitter = new EventEmitter();
195
222
 
196
- oldExchanges: ExchangesCache;
223
+ readonly oldExchanges: ExchangesCache = {};
197
224
 
198
- oldQueues: QueuesCache;
225
+ readonly oldQueues: QueuesCache = {};
199
226
 
200
- oldQueueSetupPromises: QueueSetupPromisesDictionary;
227
+ readonly oldQueueSetupPromises: QueueSetupPromisesDictionary = {};
201
228
 
202
- oldAssertExchangePromises: AssertExchangePromisesDictionary;
229
+ readonly oldAssertExchangePromises: AssertExchangePromisesDictionary = {};
203
230
 
204
- oldConsumersTags: Array<[ConfirmChannel, string]>;
231
+ readonly oldConsumersTags: Map<string, { channel: ConfirmChannel; consumerTag: string }> = new Map<string, { channel: ConfirmChannel; consumerTag: string }>();
205
232
 
206
- private oldConsumers: Array<AfConsumer> = [];
233
+ private readonly oldConsumersToRegister: Array<AfConsumer> = [];
207
234
 
208
- constructor(options: AfRabbitOptions = {}, redisConfig?: RedisConfig) {
209
- this.em = new EventEmitter();
210
- this.channel = null;
211
- this.publishChannelSetupPromise = null;
212
- this.connection = null;
213
- this.creatingConnection = false;
214
- this.exchanges = {};
215
- this.queues = {};
216
- this.queueSetupPromises = {};
217
- this.assertExchangePromises = {};
218
- this.consumers = [];
219
- this.options = options;
235
+ #logger: LoggerInstanceManager;
220
236
 
221
- if (!options?.vhost) {
222
- this.options.vhost = process.env.VHOST_NAME || 'quorum-vhost';
223
- }
237
+ constructor(public readonly options: AfRabbitOptions = {}, private readonly redisConfig?: RedisConfig) {
238
+ this.#logger = options?.logger || logger;
224
239
 
225
- this.redisClient = redisConfig && getRedisInstance(redisConfig);
226
- if (this.redisClient) {
227
- this.redisLock = promisify(RedisLock(this.redisClient)) as RedisLockType;
240
+ if (redisConfig) {
241
+ this.redisClient = getRedisInstance(redisConfig).on('error', (err) => {
242
+ this.#logger.error('rabbit: Redis error', { err, redisConfig });
243
+ });
244
+ this.redisClient.connect().catch((err) => {
245
+ this.#logger.error('rabbit: Failed to connect to Redis', { err, redisConfig });
246
+ });
247
+ this.redisLock = RedisLock(this.redisClient);
228
248
  }
229
- this.consumersTags = [];
230
- logger.info(`rabbit: [gracefully-shutdown] adding gracefully shutdown for process.pid ${process.pid}`);
249
+ this.#logger.info(`rabbit: [gracefully-shutdown] adding gracefully shutdown for process.pid ${process.pid}`);
231
250
  if (!this.options?.dontGracefulShutdown) {
232
251
  process.on('SIGTERM', async () => {
233
252
  await this.gracefulShutdown('SIGTERM');
@@ -236,81 +255,73 @@ class RabbitMq implements IAfRabbitMq {
236
255
  await this.gracefulShutdown('SIGINT');
237
256
  });
238
257
  }
258
+ }
239
259
 
240
- // TODO: DELETE OLD PROPERTIES
241
- this.oldEm = new EventEmitter();
242
- this.oldChannel = null;
243
- this.oldPublishChannelSetupPromise = null;
244
- this.oldConnection = null;
245
- this.oldCreatingConnection = false;
246
- this.oldExchanges = {};
247
- this.oldQueues = {};
248
- this.oldQueueSetupPromises = {};
249
- this.oldAssertExchangePromises = {};
250
- this.oldConsumers = [];
251
- this.oldConsumersTags = [];
260
+ private getRedisKey(key: string): string {
261
+ return `${this.redisConfig?.prefix || ''}${key}`;
252
262
  }
253
263
 
254
264
  private assertVHost = async () => {
255
- if (this.isVHostExist) {
265
+ if (this.doesVHostExist) {
256
266
  return;
257
267
  }
258
268
 
259
- if (!this.options?.vhost) {
260
- throw new Error('vhost is not defined');
261
- }
262
-
263
- const { vhost } = this.options;
264
- const auth = {
265
- auth: {
266
- username: process.env.RABBITMQ_USERNAME || 'guest',
267
- password: process.env.RABBITMQ_PASSWORD || 'guest',
268
- },
269
+ const username = process.env.RABBITMQ_USERNAME || 'guest';
270
+ const password = process.env.RABBITMQ_PASSWORD || 'guest';
271
+ const credentials = Buffer.from(`${username}:${password}`).toString('base64');
272
+ const headers = {
273
+ Authorization: `Basic ${credentials}`,
274
+ 'Content-Type': 'application/json',
269
275
  };
270
- const rabbitHost = `http://${(this.options.rabbitHost || process.env.RABBITMQ_SERVICE_HOST || 'localhost').split(':')[0]}:15672`;
276
+
277
+ const rabbitHost = `http://${(this.options?.rabbitHost || process.env.RABBITMQ_SERVICE_HOST || 'localhost').split(':')[0]}:15672`;
278
+
279
+ const url = `${rabbitHost}/api/vhosts/${encodeURIComponent(this.vhost)}`;
271
280
 
272
281
  try {
273
- const response = await axios.get(`${rabbitHost}/api/vhosts/${encodeURIComponent(vhost)}`, auth);
282
+ const response = await fetch(url, {
283
+ method: 'GET',
284
+ headers,
285
+ });
274
286
 
275
- if (response.status !== 200) {
276
- throw new Error('Failed to check vhost');
287
+ if (response.status === 200) {
288
+ this.doesVHostExist = true;
289
+ this.#logger.info('Vhost exists', { vhost: this.vhost });
290
+ return;
277
291
  }
278
292
 
279
- this.isVHostExist = true;
280
- logger.info('Vhost exists', { vhost });
281
- } catch (error) {
282
- // If the vhost does not exist, create it
283
- if (error instanceof axios.AxiosError && error.response?.status === 404) {
284
- try {
285
- await axios.put(
286
- `${rabbitHost}/api/vhosts/${encodeURIComponent(vhost)}`,
287
- {
288
- default_queue_type: 'quorum',
289
- },
290
- auth,
291
- );
293
+ if (response.status !== 404) {
294
+ this.#logger.error('Failed to check vhost', { response });
295
+ throw new RabbitError('Failed to check vhost');
296
+ }
292
297
 
293
- this.isVHostExist = true;
294
- logger.info('Vhost created', { vhost: this.options?.vhost });
295
- return;
296
- } catch (createError) {
297
- logger.error('Failed to create vhost', { error: createError });
298
- throw createError;
299
- }
298
+ const createResponse = await fetch(url, {
299
+ method: 'PUT',
300
+ headers,
301
+ body: JSON.stringify({ default_queue_type: 'quorum' }),
302
+ });
303
+
304
+ if (!createResponse.ok) {
305
+ this.#logger.error('Failed to create vhost', { response: createResponse });
306
+ throw new RabbitError('Failed to create vhost');
300
307
  }
301
308
 
302
- logger.error('Failed to check vhost', { error });
309
+ this.doesVHostExist = true;
310
+ this.#logger.info('Vhost created', { vhost: this.vhost });
311
+ } catch (error) {
312
+ this.#logger.error('Failed to check or create vhost', { error });
303
313
  throw error;
304
314
  }
305
- }
315
+ };
306
316
 
307
317
  private shouldConsumeMessageByTimestamp = async (msg: ConsumeMessageOrNull) => {
308
318
  if (msg) {
309
- const { properties: { headers } } = msg;
319
+ const { headers } = msg.properties;
310
320
  const timestamp = headers?.creationTimestamp;
311
321
 
312
322
  if (timestamp && headers?.redisTimestampValidationKey && this.redisClient) {
313
- const lastMessageTimestamp = await this.redisClient.getAsync(headers.redisTimestampValidationKey);
323
+ const key = this.getRedisKey(headers.redisTimestampValidationKey);
324
+ const lastMessageTimestamp = await this.redisClient.get(key);
314
325
  return !lastMessageTimestamp || (parseInt(lastMessageTimestamp, 10) <= parseInt(timestamp, 10));
315
326
  }
316
327
  return true;
@@ -318,16 +329,22 @@ class RabbitMq implements IAfRabbitMq {
318
329
  return false;
319
330
  }
320
331
 
321
- public ack = (channel: ConfirmChannel, msg: ConsumeMessageOrNull, shouldUpdateRedisTimestamp = false, releaseLock = null) => async (userMsg: ConsumeMessage) : Promise<any> => {
332
+ public ack = (
333
+ channel: ConfirmChannel,
334
+ msg: ConsumeMessageOrNull,
335
+ shouldUpdateRedisTimestamp = false,
336
+ releaseLock: (() => Promise<void>) | null = null,
337
+ ) => async (userMsg: ConsumeMessage) : Promise<void> => { // eslint-disable-line @typescript-eslint/no-unused-vars
322
338
  if (msg) {
323
- debug('rabbit acking message', { deliveryTag: msg.fields.deliveryTag });
339
+ this.#logger.debug('rabbit acking message', { deliveryTag: msg.fields.deliveryTag });
324
340
  await channel.ack(msg);
325
- const { properties: { headers } } = msg;
341
+ const { headers } = msg.properties;
326
342
  const timestamp = headers?.creationTimestamp;
327
343
 
328
344
  if (shouldUpdateRedisTimestamp && timestamp && headers?.redisTimestampValidationKey && this.redisClient) {
329
345
  const parsedTimestamp = parseInt(timestamp, 10);
330
- await this.redisClient.setAsync(headers.redisTimestampValidationKey, parsedTimestamp, 'EX', 3600);
346
+ const key = this.getRedisKey(headers.redisTimestampValidationKey);
347
+ await this.redisClient.set(key, parsedTimestamp, { EX: 3600 });
331
348
  await this.unlockRedisIfNeeded(releaseLock);
332
349
  }
333
350
  }
@@ -336,223 +353,224 @@ class RabbitMq implements IAfRabbitMq {
336
353
  public nack = (
337
354
  channel: ConfirmChannel,
338
355
  queue: string,
339
- options: any,
356
+ options: ConsumeOptions,
340
357
  deadQueueOptions: Options.AssertQueue,
341
358
  msg: ConsumeMessageOrNull,
342
- releaseLock: any,
359
+ releaseLock?: (() => Promise<void>) | null,
343
360
  ) => async (
344
361
  userMsg: ConsumeMessageOrNull,
345
362
  {
346
363
  skipRetry = false,
347
364
  }: NackOptions = { },
348
- ) : Promise<any> => {
365
+ ): Promise<void> => {
349
366
  await this.unlockRedisIfNeeded(releaseLock);
350
- if (channel && msg) {
351
- if (
352
- !skipRetry
353
- && (
354
- !msg.properties.headers[RETRY_HEADER]
355
- || parseInt(msg.properties.headers[RETRY_HEADER], 10) < options.retries
356
- )
357
- ) {
358
- await this.sendToQueue(queue, RabbitMq.parseMsg(msg).content, options, {
359
- ...msg.properties.headers,
360
- [RETRY_HEADER]: msg.properties.headers[RETRY_HEADER]
361
- ? msg.properties.headers[RETRY_HEADER] + 1
362
- : 1,
363
- });
364
- } else {
365
- const deadQueue = `${queue}-dead`;
366
- await this.sendToQueue(deadQueue, RabbitMq.parseMsg(msg).content, deadQueueOptions, {
367
- ...msg.properties.headers,
368
- [RETRY_HEADER]: msg.properties.headers[RETRY_HEADER]
369
- ? msg.properties.headers[RETRY_HEADER] + 1
370
- : 1,
371
- });
372
- }
373
- debug('rabbit nacking message', { deliveryTag: msg.fields.deliveryTag });
374
- await channel.ack(msg);
375
- } else {
376
- logger.error('no channel or msg', {
377
- msg,
378
- });
367
+ if (!channel || !msg) {
368
+ this.#logger.error('no channel or msg', { msg });
369
+ return;
379
370
  }
371
+ const currentRetryHeader = Number.parseInt(msg.properties.headers?.[RETRY_HEADER] || '0', 10) || 0;
372
+ const sendToDeadQueue = skipRetry || currentRetryHeader >= options.retries!;
373
+ await this.sendToQueue(`${queue}${sendToDeadQueue ? '-dead' : ''}`, RabbitMq.parseMsg(msg).content, sendToDeadQueue ? deadQueueOptions : options, {
374
+ ...msg.properties.headers,
375
+ [RETRY_HEADER]: currentRetryHeader + 1,
376
+ });
377
+ this.#logger.debug('rabbit nacking message', { deliveryTag: msg.fields.deliveryTag });
378
+ await channel.ack(msg);
380
379
  }
381
380
 
382
- async getConnection() {
383
- return new Promise<AmqpConnectionManager>(async (resolve, reject) => {
384
- if (this.blockReconnect) {
385
- debug('rabbit: block reconnect');
386
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
387
- // @ts-ignore
388
- return resolve();
389
- }
390
- if (this.connection !== null) {
391
- if (this.options?.disableReconnect || this.connection?.isConnected()) {
392
- debug('rabbit: connection - is connected');
393
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
394
- // @ts-ignore
395
- return resolve(this.connection);
396
- }
397
- debug('rabbit: connection - reconnecting');
381
+ async getConnection(connection: ConnectionData): Promise<AmqpConnectionManager> {
382
+ const {
383
+ amqpConnection: connectionLocal,
384
+ creatingConnection,
385
+ connectionCreatedEventName,
386
+ connectionFailedEventName,
387
+ blockReconnect,
388
+ } = connection;
389
+
390
+ if (blockReconnect) {
391
+ this.#logger.debug('rabbit: block reconnect');
392
+ // @ts-expect-error we are returning undefined while the function clearly expects a connection.
393
+ return undefined;
394
+ }
395
+
396
+ if (connectionLocal !== null) {
397
+ if (this.options?.disableReconnect || connectionLocal?.isConnected()) {
398
+ this.#logger.debug('rabbit: connection - is connected');
399
+ return connectionLocal;
398
400
  }
399
- if (this.creatingConnection) {
400
- debug('rabbit: creating connection emi');
401
- this.em.once(CONNECTION_CREATED_CONST, resolve);
402
- this.em.once(CONNECTION_FAILED_CONST, reject);
403
- return;
401
+ this.#logger.debug('rabbit: connection - reconnecting');
402
+ }
403
+ if (creatingConnection) {
404
+ this.#logger.debug('rabbit: creating connection emi');
405
+ const [event, value] = await Promise.race([connectionCreatedEventName, connectionFailedEventName].map((e) => once(this.em, e).then(([result]) => [e, result] as const)));
406
+ if (event === connectionCreatedEventName) {
407
+ return value;
404
408
  }
405
- this.creatingConnection = true;
406
- let isResolved = false;
407
-
408
- // It is import to use it as a function and not as a variable
409
- // because of k8s changes the env variables
410
- // and we want to use the new values
411
- const findServers = () => {
412
- const userName = process.env.RABBITMQ_USERNAME || 'guest';
413
- const password = process.env.RABBITMQ_PASSWORD || 'guest';
414
- const host = this.options?.rabbitHost || process.env.RABBITMQ_SERVICE_HOST || 'localhost';
415
-
416
- debug('rabbit: creating connection', { host, userName, HEARTBEAT });
417
- return [`amqp://${userName}:${password}@${host}/${this.options?.vhost}?heartbeat=${HEARTBEAT}`];
418
- };
419
-
420
- const defaultUrls = findServers();
421
- const connection: AmqpConnectionManager = await connect(defaultUrls, {
422
- findServers,
423
- });
424
-
425
- this.connection = connection;
426
- this.connection.on('error', (err) => {
427
- logger.error('rabbit: connection error', { err });
428
- if (!isResolved) {
429
- isResolved = true;
430
- reject(err);
431
- this.em.emit(CONNECTION_FAILED_CONST, err);
432
- }
433
- });
409
+ throw value;
410
+ }
411
+ connection.creatingConnection = true;
412
+ let isResolved = false;
413
+
414
+ // It is import to use it as a function and not as a variable
415
+ // because of k8s changes the env variables
416
+ // and we want to use the new values
417
+ const findServers = () => {
418
+ const userName = process.env.RABBITMQ_USERNAME || 'guest';
419
+ const password = process.env.RABBITMQ_PASSWORD || 'guest';
420
+ const host = this.options?.rabbitHost || process.env.RABBITMQ_SERVICE_HOST || 'localhost';
421
+
422
+ this.#logger.debug('rabbit: creating connection', { host, userName, HEARTBEAT });
423
+ return [`amqp://${userName}:${password}@${host}/${this.vhost}?heartbeat=${HEARTBEAT}`];
424
+ };
434
425
 
435
- this.connection.on('connectFailed', (err) => {
436
- this.consumersTags = [];
437
- logger.error('rabbit: connection connectFailed', { err, advice: 'Check if the vhost exist', vhost: this.options?.vhost });
438
- if (!isResolved) {
439
- isResolved = true;
440
- reject(err);
441
- this.em.emit(CONNECTION_FAILED_CONST, err);
442
- }
443
- });
426
+ const defaultUrls = findServers();
427
+ const newConnection: AmqpConnectionManager = connect(defaultUrls, { findServers });
428
+ connection.amqpConnection = newConnection;
444
429
 
445
- this.connection.on('disconnect', ({ err }) => {
446
- // this.channel = null;
447
- this.consumersTags = [];
448
- debug('rabbit: connection closed');
449
- if (this.options?.disableReconnect) {
450
- logger.error(`${this.DISCONNECT_MSG}${err && ` - ${err}`}`);
451
- this.blockReconnect = true;
452
- } else {
453
- logger.error(`${this.RECONNECT_MSG}${err && ` - ${err}`}`);
454
- }
455
- });
430
+ const { promise, reject, resolve } = createDeferredPromise<AmqpConnectionManager>();
431
+ newConnection.on('error', (err) => {
432
+ this.#logger.error('rabbit: connection error', { err });
433
+ if (!isResolved) {
434
+ isResolved = true;
435
+ reject(err);
436
+ this.em.emit(connectionFailedEventName, err);
437
+ }
438
+ });
456
439
 
457
- this.connection.once('connect', async () => {
458
- debug('rabbit: connection established');
459
- this.creatingConnection = false;
460
- this.em.emit(CONNECTION_CREATED_CONST, connection);
440
+ newConnection.on('connectFailed', (err) => {
441
+ this.consumersTags.clear();
442
+ if (typeof err.url === 'string') {
443
+ err.url = this.maskURL(err.url);
444
+ }
445
+ this.#logger.error('rabbit: connection connectFailed', { err, advice: 'Check if the vhost exist', vhost: this.vhost });
446
+ if (!isResolved) {
461
447
  isResolved = true;
462
- resolve(connection);
463
- });
448
+ reject(err);
449
+ this.em.emit(connectionFailedEventName, err);
450
+ }
464
451
  });
452
+
453
+ newConnection.on('disconnect', ({ err }) => {
454
+ this.consumersTags.clear();
455
+ this.#logger.debug('rabbit: connection closed');
456
+ if (this.options?.disableReconnect) {
457
+ this.#logger.error(`${this.DISCONNECT_MSG}${err && ` - ${err}`}`);
458
+ connection.blockReconnect = true;
459
+ } else {
460
+ this.#logger.error(`${this.RECONNECT_MSG}${err && ` - ${err}`}`);
461
+ }
462
+ });
463
+
464
+ newConnection.once('connect', async () => {
465
+ this.#logger.debug('rabbit: connection established');
466
+ connection.creatingConnection = false;
467
+ this.em.emit(connectionCreatedEventName, newConnection);
468
+ isResolved = true;
469
+ resolve(newConnection);
470
+ });
471
+
472
+ return promise;
465
473
  }
466
474
 
467
- async getNewChannel({ name = rand().toString(), onClose = null, options = {} }: newChannelOpts = {}) {
468
- let connection!: AmqpConnectionManager;
475
+ async getNewChannel({
476
+ name = rand().toString(),
477
+ onClose = null,
478
+ options = {},
479
+ connection,
480
+ }: newChannelOpts): Promise<ChannelWrapper> {
481
+ let localConnection!: AmqpConnectionManager;
469
482
  try {
470
- connection = await this.getConnection();
483
+ localConnection = await this.getConnection(connection);
471
484
  } catch (e) {
472
- logger.error(`rabbit: error on get connection for new channel ${name} `, { e });
485
+ this.#logger.error(`rabbit: error on get connection for new channel ${name} `, { e });
473
486
  throw e;
474
487
  }
475
- const channel = connection.createChannel({ ...options });
488
+ const channel = localConnection?.createChannel({ ...options });
476
489
  once(channel, 'close').then((args) => {
477
- logger.error(`rabbit: channel ${name} closed`);
490
+ this.#logger.error(`rabbit: channel ${name} closed`);
478
491
  onClose?.(args);
479
492
  });
480
493
  try {
481
494
  await once(channel, 'connect');
482
- debug(`rabbit: channel ${name} CONNECTED`);
495
+ this.#logger.debug(`rabbit: channel ${name} CONNECTED`);
483
496
  return channel;
484
497
  } catch (err) {
485
- logger.error(`rabbit: channel error ${name} error`, { err });
498
+ this.#logger.error(`rabbit: channel error ${name} error`, { err });
486
499
  throw err;
487
500
  }
488
501
  }
489
502
 
490
- async assertChannel({ force = false } : assertChannelOpts = {}): Promise<ChannelWrapper> {
491
- if (!this.publishChannelSetupPromise) {
492
- this.publishChannelSetupPromise = new Promise<ChannelWrapper>(async (resolve, reject) => {
493
- if (this.channel && !force) {
494
- return resolve(this.channel);
495
- }
503
+ async assertChannel({ force = false, connection }: assertChannelOpts): Promise<ChannelWrapper> {
504
+ if (this.publishChannelSetupPromise) {
505
+ return this.publishChannelSetupPromise;
506
+ }
507
+ const { promise, resolve, reject } = createDeferredPromise<ChannelWrapper>();
508
+ this.publishChannelSetupPromise = promise;
509
+ if (this.publishChannel && !force) {
510
+ resolve(this.publishChannel);
511
+ return promise;
512
+ }
496
513
 
497
- try {
498
- const channel = await this.getNewChannel({});
499
- channel.on('error', (err) => {
500
- logger.error('rabbit: channel error', { err });
501
- });
502
- this.channel = channel;
503
- resolve(channel);
504
- } catch (e) {
505
- reject(e);
506
- }
514
+ try {
515
+ const channel = await this.getNewChannel({ connection });
516
+ channel.on('error', (err) => {
517
+ this.#logger.error('rabbit: channel error', { err });
507
518
  });
519
+ if (this.publishConnection === connection) {
520
+ this.publishChannel = channel;
521
+ }
522
+ resolve(channel);
523
+ } catch (e) {
524
+ reject(e);
508
525
  }
509
- return this.publishChannelSetupPromise;
526
+ return promise;
510
527
  }
511
528
 
512
- async assertExchange(exchangeName: string, options?: any) {
513
- const channel: ChannelWrapper = await this.assertChannel();
529
+ async assertExchange(exchangeName: string, connection: ConnectionData): Promise<Replies.AssertExchange> {
530
+ const channel: ChannelWrapper = await this.assertChannel({ connection });
514
531
 
515
532
  if (this.exchanges[exchangeName]) {
516
533
  delete this.assertExchangePromises[exchangeName];
517
- return this.exchanges[exchangeName];
534
+ return this.exchanges[exchangeName]!;
518
535
  }
519
536
 
520
537
  if (this.assertExchangePromises[exchangeName]) {
521
- return this.assertExchangePromises[exchangeName];
538
+ return this.assertExchangePromises[exchangeName]!;
522
539
  }
523
540
 
524
541
  this.assertExchangePromises[exchangeName] = assertExchangeFanout(channel, exchangeName);
525
542
  this.exchanges[exchangeName] = await this.assertExchangePromises[exchangeName];
526
- return this.exchanges[exchangeName];
543
+ return this.exchanges[exchangeName]!;
527
544
  }
528
545
 
529
- async getQueueLength(queue: string) {
546
+ async getQueueLength(queue: string): Promise<Replies.AssertQueue> {
530
547
  RabbitMq.validateName('queue', queue);
531
- const { channel } = this;
548
+ const { publishChannel: channel } = this;
532
549
  if (!channel) {
533
- throw new Error('channel is not defined');
550
+ throw new RabbitError('channel is not defined');
534
551
  }
535
- debug('rabbit: getting queue length', { queue, connected: this.connection?.isConnected() });
552
+ this.#logger.debug('rabbit: getting queue length', { queue, connected: this.publishConnection.amqpConnection?.isConnected() });
536
553
  return channel?.checkQueue(queue);
537
554
  }
538
555
 
539
- private async deleteQueue(queue: string) {
556
+ private async deleteQueue(queue: string, connection: ConnectionData): Promise<Replies.DeleteQueue> {
540
557
  RabbitMq.validateName('queue', queue);
541
- const channel: ChannelWrapper = await this.assertChannel();
542
- logger.info('rabbit: deleting queue', { queue });
558
+ const channel: ChannelWrapper = await this.assertChannel({ connection });
559
+ this.#logger.info('rabbit: deleting queue', { queue });
543
560
  const deleteQueueRes = await channel.deleteQueue(queue);
544
- debug('queue deleted', deleteQueueRes);
561
+ this.#logger.debug('queue deleted', deleteQueueRes);
545
562
  return deleteQueueRes;
546
563
  }
547
564
 
548
- async bindQueue(queue: string, exchange: string) {
549
- const channel: ChannelWrapper = await this.assertChannel();
565
+ async bindQueue(queue: string, exchange: string): Promise<void> {
566
+ const channel: ChannelWrapper = await this.assertChannel({ connection: this.publishConnection });
550
567
  await channel.addSetup((setupChannel: ConfirmChannel) => setupChannel.bindQueue(queue, exchange, ''));
551
568
  return channel.bindQueue(queue, exchange, '');
552
569
  }
553
570
 
554
571
  async setupQueue(queueName: string, options?: Options.AssertQueue): Promise<Replies.AssertQueue> {
555
572
  let queue: Replies.AssertQueue;
573
+ const connection = this.publishConnection;
556
574
  const localeOptions = {
557
575
  ...options,
558
576
  durable: true,
@@ -563,23 +581,23 @@ class RabbitMq implements IAfRabbitMq {
563
581
  },
564
582
  };
565
583
  try {
566
- const channel: ChannelWrapper = await this.assertChannel();
567
- debug('assertQueue->channel.addSetup', { queueName });
584
+ const channel: ChannelWrapper = await this.assertChannel({ connection });
585
+ this.#logger.debug('assertQueue->channel.addSetup', { queueName });
568
586
  await channel.addSetup(async (setupChannel: ConfirmChannel) => {
569
587
  await setupChannel.assertQueue(queueName, localeOptions);
570
588
  });
571
- debug('assertQueue->channel.assertQueue', { queueName });
589
+ this.#logger.debug('assertQueue->channel.assertQueue', { queueName });
572
590
  queue = await channel.assertQueue(queueName, localeOptions);
573
591
  } catch (e) {
574
- logger.error('rabbit: assertQueue error', { queueName, options, error: e });
592
+ this.#logger.error('rabbit: assertQueue error', { queueName, options, error: e });
575
593
  if (!this.options?.dontRetryAssert) {
576
- debug('retrying assertQueue', { queueName });
577
- const channel = await this.assertChannel({ force: true });
578
- await this.deleteQueue(queueName);
594
+ this.#logger.debug('retrying assertQueue', { queueName });
595
+ const channel = await this.assertChannel({ force: true, connection });
596
+ await this.deleteQueue(queueName, connection);
579
597
 
580
- debug('retrying assertQueue->channel.addSetup', { queueName });
598
+ this.#logger.debug('retrying assertQueue->channel.addSetup', { queueName });
581
599
  await channel.addSetup((setupChannel: ConfirmChannel) => setupChannel.assertQueue(queueName, localeOptions));
582
- debug('retrying assertQueue->channel.assertQueue', { queueName });
600
+ this.#logger.debug('retrying assertQueue->channel.assertQueue', { queueName });
583
601
  queue = await channel.assertQueue(queueName, localeOptions);
584
602
  } else {
585
603
  throw e;
@@ -590,7 +608,7 @@ class RabbitMq implements IAfRabbitMq {
590
608
  return queue;
591
609
  }
592
610
 
593
- // TODO: Can be deleted after deleting the old consumers
611
+ // TODO: [QUORUM-PHASE-3] Can be deleted after deleting the old consumers and publishers because all the queues are created us quorum queues
594
612
  static shouldUseQuorum(queueName: string): boolean {
595
613
  const envQuorumQueuesWhitelist = process.env.QUORUM_QUEUES_WHITELIST;
596
614
 
@@ -606,26 +624,22 @@ class RabbitMq implements IAfRabbitMq {
606
624
  return false;
607
625
  }
608
626
 
609
- async assertQueue(queueName: string, options?: Options.AssertQueue): Promise<any> {
627
+ async assertQueue(queueName: string, options?: Options.AssertQueue): Promise<Replies.AssertQueue> {
610
628
  RabbitMq.validateName('queue', queueName);
611
629
  if (this.queues[queueName]) {
612
630
  delete this.queueSetupPromises[queueName];
613
- return this.queues[queueName];
614
- }
615
-
616
- if (this.queueSetupPromises[queueName]) {
617
- return this.queueSetupPromises[queueName];
631
+ return this.queues[queueName]!;
618
632
  }
619
633
 
620
- this.queueSetupPromises[queueName] = this.setupQueue(queueName, options);
621
- return this.queueSetupPromises[queueName];
634
+ this.queueSetupPromises[queueName] ||= this.setupQueue(queueName, options);
635
+ return this.queueSetupPromises[queueName]!;
622
636
  }
623
637
 
624
- private saveConsumer(queue: string, callback: CallbackFunction, options: ConsumeOptions | undefined) {
625
- const isConsumerExist :boolean = this.consumers.some((consumer) => consumer.queue === queue);
638
+ private saveConsumer(queue: string, callback: CallbackFunction, options: ConsumeOptions | undefined): void {
639
+ const isConsumerExist: boolean = this.consumersToRegister.some((consumer) => consumer.queue === queue);
626
640
  if (!isConsumerExist) {
627
- logger.info(`rabbit: consumer: ${queue} saved in consumer array`);
628
- this.consumers.push({
641
+ this.#logger.info(`rabbit: consumer: ${queue} saved in consumer array`);
642
+ this.consumersToRegister.push({
629
643
  queue,
630
644
  callback,
631
645
  options,
@@ -634,27 +648,30 @@ class RabbitMq implements IAfRabbitMq {
634
648
  }
635
649
 
636
650
  // Used by the microservices to consume messages from the queue
637
- async consume(queue: string, callback: CallbackFunction, options?: ConsumeOptions): Promise<any> {
638
- // TODO: USE THE IMPLEMENTATION OF THE NEW CONSUME AND DELETE THE NEW AND OLD
639
- if (options?.isQuorumQueue !== false) {
640
- await this.assertVHost();
651
+ async consume(queue: string, callback: CallbackFunction, options?: ConsumeOptions): Promise<void> {
652
+ this.saveConsumerOld(queue, callback, options);
653
+
654
+ // TODO: [QUORUM-PHASE-3] Use only the implementation of consumeNew and delete consumeNew and consumeOld
655
+ if (options?.isQuorumQueue !== false && DISABLE_QUORUM_QUEUES_CONSUME !== 'true') {
656
+ this.saveConsumer(queue, callback, options);
657
+ const backoffConfig = getAssertVhostExponentialBackoffConfig();
658
+ await backOff(() => this.assertVHost(), backoffConfig);
641
659
  await this.consumeNew(queue, callback, options);
642
660
  }
643
-
644
661
  await this.consumeOld(queue, callback, options);
645
662
  }
646
663
 
647
- // TODO: DELETE
648
- async consumeNew(queue: string, callback: CallbackFunction, options?: ConsumeOptions): Promise<any> {
664
+ // TODO: [QUORUM-PHASE-3] Delete consumeNew we do not use it anymore
665
+ async consumeNew(queue: string, callback: CallbackFunction, options?: ConsumeOptions): Promise<void> {
649
666
  await this.consumeFromRabbit(queue, callback, options);
650
667
  }
651
668
 
652
- // TODO: DELETE
653
- async consumeOld(queue: string, callback: CallbackFunction, options?: ConsumeOptions): Promise<any> {
669
+ // TODO: [QUORUM-PHASE-3] Delete consumeOld we do not use it anymore
670
+ async consumeOld(queue: string, callback: CallbackFunction, options?: ConsumeOptions): Promise<void> {
654
671
  await this.consumeFromRabbitOld(queue, callback, options);
655
672
  }
656
673
 
657
- private async lockRedisIfNeeded(msg: any, options: any) {
674
+ private async lockRedisIfNeeded(msg: ReturnType<typeof RabbitMq.parseMsg>, options: ConsumeOptions): Promise<(() => Promise<void>) | null> {
658
675
  const { properties: { headers } } = msg;
659
676
  const timestamp = headers?.creationTimestamp;
660
677
  let releaseLock = null;
@@ -668,29 +685,28 @@ class RabbitMq implements IAfRabbitMq {
668
685
  return releaseLock;
669
686
  }
670
687
 
671
- private async unlockRedisIfNeeded(releaseLock: any) {
688
+ private async unlockRedisIfNeeded(releaseLock?: (() => Promise<void>) | null): Promise<void> {
672
689
  if (this.redisLock && releaseLock) {
673
690
  await releaseLock();
674
691
  }
675
692
  }
676
693
 
677
- private async consumeFromRabbit(queue: string, callback: CallbackFunction, options?: ConsumeOptions): Promise<any> {
694
+ private async consumeFromRabbit(queue: string, callback: CallbackFunction, options?: ConsumeOptions): Promise<void> {
678
695
  const optionsWithDefaults = { ...DEFAULT_OPTIONS, ...options };
679
696
  RabbitMq.validateName('queue', queue);
680
- this.saveConsumer(queue, callback, options);
681
697
  const uniqueId = randomUUID();
682
698
  const {
683
699
  limit, deadMessageTtl, useConsumeWithLock, lockTimeout, auditContext, enableRabbitTrace,
684
700
  } = optionsWithDefaults;
685
701
  if (useConsumeWithLock) {
686
702
  if (!this.redisLock) {
687
- throw new Error('Usage of consumeWithLock requires RedisInstance');
703
+ throw new RabbitError('Usage of consumeWithLock requires RedisInstance');
688
704
  }
689
- logger.info(`rabbit: Consuming with lock from queue ${queue} with lockTimeout: ${lockTimeout}ms`);
705
+ this.#logger.info(`rabbit: Consuming with lock from queue ${queue} with lockTimeout: ${lockTimeout}ms`);
690
706
  }
691
- const channel = await this.getNewChannel({});
707
+ const channel = await this.getNewChannel({ connection: this.consumeConnection });
692
708
  return channel.addSetup(async (confirmChannel: ConfirmChannel) => {
693
- const q = await this.assertQueue(queue, optionsWithDefaults);
709
+ await this.assertQueue(queue, optionsWithDefaults);
694
710
  await confirmChannel.prefetch(limit, false);
695
711
  const { consumerTag } = await confirmChannel.consume(
696
712
  queue,
@@ -699,9 +715,7 @@ class RabbitMq implements IAfRabbitMq {
699
715
  return null;
700
716
  }
701
717
 
702
- const traceId = msg.properties.headers[TRACING_HEADER];
703
- const userId = msg.properties.headers[USER_TRACING_HEADER];
704
- const automationId = msg.properties.headers[AUTOMATION_ID_HEADER];
718
+ const { [TRACING_HEADER]: traceId, [USER_TRACING_HEADER]: userId, [AUTOMATION_ID_HEADER]: automationId } = msg.properties.headers ?? {};
705
719
  const parsedMessage = RabbitMq.parseMsg(msg);
706
720
  const releaseLock = await this.lockRedisIfNeeded(parsedMessage, optionsWithDefaults);
707
721
  const trace = newTrace(traceTypes.RABBIT);
@@ -716,13 +730,13 @@ class RabbitMq implements IAfRabbitMq {
716
730
  createOrSetRabbitTrace(outbreakTrace, userId),
717
731
  ]);
718
732
  } catch (e) {
719
- logger.error('rabbit: failed to setRabbitTrace', { userId, e });
733
+ this.#logger.error('rabbit: failed to setRabbitTrace', { userId, e });
720
734
  return this.nack(confirmChannel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg);
721
735
  }
722
736
  }
723
737
 
724
738
  if (traceId) {
725
- (trace as any)?.context?.set(TRACING_HEADER, traceId);
739
+ trace?.context?.set(TRACING_HEADER, traceId);
726
740
  (outbreakTrace as any)?.context.set(TRACING_HEADER, traceId);
727
741
  }
728
742
 
@@ -746,48 +760,53 @@ class RabbitMq implements IAfRabbitMq {
746
760
  return;
747
761
  }
748
762
  messageAcked = true;
749
- return this.ack(confirmChannel, msg, true, releaseLock)(msg);
763
+ await this.ack(confirmChannel, msg, true, releaseLock)(msg);
750
764
  };
751
765
 
752
766
  const localNack = async (_: ConsumeMessageOrNull, nackOptions: NackOptions = {}) => {
753
767
  if (messageAcked) {
754
768
  return;
755
769
  }
756
- debug('rabbit localNack', { messageAcked, uniqueId, deliveryTag: msg.fields.deliveryTag });
770
+ this.#logger.debug('rabbit localNack', { messageAcked, uniqueId, deliveryTag: msg.fields.deliveryTag });
757
771
  messageAcked = true;
758
- return this.nack(confirmChannel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg, nackOptions);
772
+ await this.nack(confirmChannel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg, nackOptions);
759
773
  };
760
774
 
761
775
  try {
762
- await callback(
776
+ return await callback(
763
777
  parsedMessage,
764
778
  localAck,
765
779
  localNack,
766
780
  );
767
781
  } catch (e) {
768
- await localNack(msg);
782
+ return localNack(msg);
769
783
  }
770
784
  }, CONSUMER_DEFAULT_OPTIONS,
771
785
  );
772
786
  if (!consumerTag) {
773
- logger.error(`rabbit: failed to consume from queue ${queue}`);
787
+ this.#logger.error(`rabbit: failed to consume from queue ${queue}`);
774
788
  } else {
775
- logger.info(`rabbit: adding tag ${consumerTag} to the array.`);
776
- this.consumersTags.push([confirmChannel, consumerTag]);
789
+ this.#logger.info(`rabbit: adding tag ${consumerTag} to the array.`);
790
+ this.consumersTags.set(queue, { channel: confirmChannel, consumerTag });
777
791
  }
778
792
  });
779
793
  }
780
794
 
781
795
  // Used by the microservices to consume messages from the exchange
782
- async consumeFromExchange(queue: string, exchange: string, callback: CallbackFunction, options?: ConsumeOptions) : Promise<any> {
796
+ async consumeFromExchange(queue: string, exchange: string, callback: CallbackFunction, options?: ConsumeOptions) : Promise<void> {
783
797
  const optionsWithDefaults = { ...DEFAULT_OPTIONS, ...options };
784
798
  RabbitMq.validateName('exchange', exchange);
785
799
  RabbitMq.validateName('queue', queue);
786
- const { limit, deadMessageTtl } = optionsWithDefaults;
787
- if (options?.isQuorumQueue !== false) {
788
- await this.assertVHost();
800
+ const { limit } = optionsWithDefaults;
801
+ // TODO: [QUORUM-PHASE-3] Delete the if statement after all the queues are created as quorum queues
802
+ if (options?.isQuorumQueue !== false && DISABLE_QUORUM_QUEUES_CONSUME !== 'true') {
789
803
  await this.saveConsumer(queue, callback, options);
790
- const channel: ChannelWrapper = await this.getNewChannel({ name: `consume-exchange-${exchange}-queue-${queue}` });
804
+ const backoffConfig = getAssertVhostExponentialBackoffConfig();
805
+ await backOff(() => this.assertVHost(), backoffConfig);
806
+ const channel: ChannelWrapper = await this.getNewChannel({
807
+ name: `consume-exchange-${exchange}-queue-${queue}`,
808
+ connection: this.consumeConnection,
809
+ });
791
810
 
792
811
  await channel.addSetup(async (c: ConfirmChannel) => {
793
812
  const assertExchange = await assertExchangeFanout(c, exchange);
@@ -804,10 +823,12 @@ class RabbitMq implements IAfRabbitMq {
804
823
  ]);
805
824
  });
806
825
  }
807
-
808
- // TODO: DELETE OLD
826
+ // TODO: [QUORUM-PHASE-3] Delete the old implementation
809
827
  await this.saveConsumerOld(queue, callback, options);
810
- const channelOld: ChannelWrapper = await this.getNewChannelOld({ name: `consume-exchange-${exchange}-queue-${queue}-old` });
828
+ const channelOld: ChannelWrapper = await this.getNewChannelOld({
829
+ name: `consume-exchange-${exchange}-queue-${queue}-old`,
830
+ connection: this.oldConsumeConnection,
831
+ });
811
832
  await channelOld.addSetup(async (c: ConfirmChannel) => {
812
833
  const assertExchange = await assertExchangeFanout(c, exchange);
813
834
  await c.assertQueue(queue);
@@ -825,46 +846,40 @@ class RabbitMq implements IAfRabbitMq {
825
846
  }
826
847
 
827
848
  // Used by the microservices to publish messages to the exchange
828
- async publish(exchange: string, content: any, customHeaders?: any, isQuorumQueue = true) : Promise<boolean> {
829
- // TODO: DELETE THE IF
830
- if (isQuorumQueue) {
831
- return wrapSetImmediate(async () => {
832
- await this.assertVHost();
833
- RabbitMq.validateName('exchange', exchange);
834
- const channel: ChannelWrapper = await this.assertChannel();
835
- await this.assertExchange(exchange);
836
- await channel.publish(exchange, '',
837
- Buffer.from(JSON.stringify(content)),
838
- RabbitMq.getPublishOptions(customHeaders));
839
- });
849
+ // TODO: [QUORUM-PHASE-3] isQuorumQueue flag is not needed anymore because all the queues are created as quorum queues
850
+ async publish(exchange: string, content: any, customHeaders?: PublishOptions['headers'], isQuorumQueue = true): Promise<void> {
851
+ await setImmediate();
852
+ if (isQuorumQueue && DISABLE_QUORUM_QUEUES_PUBLISH !== 'true') {
853
+ await this.assertVHost();
854
+ RabbitMq.validateName('exchange', exchange);
855
+ const channel: ChannelWrapper = await this.assertChannel({ connection: this.publishConnection });
856
+ await this.assertExchange(exchange, this.publishConnection);
857
+ await channel.publish(exchange, '', Buffer.from(JSON.stringify(content)), RabbitMq.getPublishOptions(customHeaders));
858
+ return;
840
859
  }
841
860
 
842
- // TODO: DELETE THE OLD
843
- return wrapSetImmediate(async () => {
844
- RabbitMq.validateName('exchange', exchange);
845
- const channel: ChannelWrapper = await this.assertChannelOld();
846
- await this.assertExchangeOld(exchange);
847
- await channel.publish(exchange, '',
848
- Buffer.from(JSON.stringify(content)),
849
- RabbitMq.getPublishOptions(customHeaders));
850
- });
861
+ // TODO: [QUORUM-PHASE-3] Delete the old implementation
862
+ RabbitMq.validateName('exchange', exchange);
863
+ const channel: ChannelWrapper = await this.assertChannelOld({ connection: this.oldPublishConnection });
864
+ await this.assertExchangeOld(exchange, this.oldPublishConnection);
865
+ await channel.publish(exchange, '', Buffer.from(JSON.stringify(content)), RabbitMq.getPublishOptions(customHeaders));
851
866
  }
852
867
 
853
868
  // Used by the microservices to send messages to the queue
869
+ // TODO: [QUORUM-PHASE-3] isQuorumQueue flag is not needed anymore because all the queues are created as quorum queues
854
870
  async sendToQueue(
855
871
  queue: string,
856
872
  content: any,
857
- options?: any,
858
- customHeaders?: any,
873
+ options?: Options.AssertQueue,
874
+ customHeaders?: PublishOptions['headers'],
859
875
  isQuorumQueue = true,
860
876
  ): Promise<boolean | undefined> {
861
- // TODO: DELETE THE IF AND THE OLD
862
- if (isQuorumQueue) {
877
+ if (isQuorumQueue && DISABLE_QUORUM_QUEUES_PUBLISH !== 'true') {
863
878
  try {
864
879
  await this.assertVHost();
865
- await this.assertChannel();
880
+ await this.assertChannel({ connection: this.publishConnection });
866
881
  } catch (e) {
867
- logger.error(`rabbit sendToQueue: failed to send assert channel when sending to queue ${queue}`, { e });
882
+ this.#logger.error(`rabbit sendToQueue: failed to send assert channel when sending to queue ${queue}`, { e });
868
883
  throw e;
869
884
  }
870
885
 
@@ -872,107 +887,132 @@ class RabbitMq implements IAfRabbitMq {
872
887
  RabbitMq.validateName('queue', queue);
873
888
  await this.assertQueue(queue, options);
874
889
  } catch (e) {
875
- logger.error(`rabbit sendToQueue: failed to assert queue ${queue}`, { e });
890
+ this.#logger.error(`rabbit sendToQueue: failed to assert queue ${queue}`, { e });
876
891
  throw e;
877
892
  }
878
893
 
879
894
  try {
880
- const res = await this.channel?.sendToQueue(queue,
881
- Buffer.from(JSON.stringify(content)),
882
- RabbitMq.getPublishOptions(customHeaders));
883
- debug(`rabbit: sending to queue ${queue}`, { res });
895
+ const res = await this.publishChannel?.sendToQueue(queue, Buffer.from(JSON.stringify(content)), RabbitMq.getPublishOptions(customHeaders));
896
+ this.#logger.debug(`rabbit: sending to queue ${queue}`, { res });
884
897
  return res;
885
898
  } catch (e) {
886
899
  const isConnected = await this.isConnected();
887
- logger.error(`rabbit sendToQueue: failed to send to queue ${queue}, isConnected: ${isConnected}`, { e });
900
+ this.#logger.error(`rabbit sendToQueue: failed to send to queue ${queue}, isConnected: ${isConnected}`, { e });
888
901
  throw e;
889
902
  }
890
903
  } else {
891
904
  try {
892
- await this.assertChannelOld();
905
+ await this.assertChannelOld({ connection: this.oldPublishConnection });
893
906
  } catch (e) {
894
- logger.error(`rabbit sendToQueue: failed to send assert channel when sending to queue ${queue}`, { e });
907
+ this.#logger.error(`rabbit sendToQueue: failed to send assert channel when sending to queue ${queue}`, { e });
895
908
  throw e;
896
909
  }
897
- }
898
910
 
899
- try {
900
- RabbitMq.validateName('queue', queue);
901
- await this.assertQueueOld(queue, options);
902
- } catch (e) {
903
- logger.error(`rabbit sendToQueue: failed to assert queue ${queue}`, { e });
904
- throw e;
905
- }
911
+ try {
912
+ RabbitMq.validateName('queue', queue);
913
+ await this.assertQueueOld(queue, options);
914
+ } catch (e) {
915
+ this.#logger.error(`rabbit sendToQueue: failed to assert queue ${queue}`, { e });
916
+ throw e;
917
+ }
906
918
 
907
- try {
908
- const res = await this.oldChannel?.sendToQueue(queue,
909
- Buffer.from(JSON.stringify(content)),
910
- RabbitMq.getPublishOptions(customHeaders));
911
- debug(`rabbit: sending to queue ${queue}`, { res });
912
- return res;
913
- } catch (e) {
914
- const isConnected = await this.isConnected();
915
- logger.error(`rabbit sendToQueue: failed to send to queue ${queue}, isConnected: ${isConnected}`, { e });
916
- throw e;
919
+ try {
920
+ const res = await this.oldPublishChannel?.sendToQueue(queue, Buffer.from(JSON.stringify(content)), RabbitMq.getPublishOptions(customHeaders));
921
+ this.#logger.debug(`rabbit: sending to queue ${queue}`, { res });
922
+ return res;
923
+ } catch (e) {
924
+ const isConnected = await this.isConnectedOld();
925
+ this.#logger.error(`rabbit sendToQueue: failed to send to queue ${queue}, isConnected: ${isConnected}`, { e });
926
+ throw e;
927
+ }
917
928
  }
918
929
  }
919
930
 
920
- async isConnected() : Promise<boolean> {
921
- const connection = await this.getConnection();
922
- const isConnected = connection.isConnected();
923
- if (!isConnected) {
924
- logger.error('rabbit: isConnected - false');
925
- return false;
926
- }
927
- const channel: any = await this.assertChannel();
928
- try {
929
- await Promise.all([
930
- channel.waitForConnect(),
931
- ...this.consumers.map((c: AfConsumer) => channel.checkQueue(c.queue)),
932
- ]);
933
- } catch (e) {
934
- logger.error('rabbit: isConnected - false');
935
- return false;
931
+ async isConnected(): Promise<boolean> {
932
+ let isConnected = true;
933
+ // TODO:[QUORUM-PHASE-3] Remove the condition
934
+ if (!this.gracefulShutdownStarted) {
935
+ if (DISABLE_QUORUM_QUEUES_CONSUME !== 'true' || DISABLE_QUORUM_QUEUES_PUBLISH !== 'true') {
936
+ this.#logger.debug('rabbit: start is connected');
937
+ const [consumeConnection, publishConnection] = await Promise.all([
938
+ this.getConnection(this.consumeConnection),
939
+ this.getConnection(this.publishConnection),
940
+ ]);
941
+ isConnected = consumeConnection.isConnected() && publishConnection.isConnected();
942
+ if (!isConnected) {
943
+ this.#logger.error('rabbit: isConnected - false');
944
+ return false;
945
+ }
946
+ try {
947
+ const unRegisteredConsumers = this.consumersToRegister.filter((c) => !this.consumersTags.get(c.queue));
948
+
949
+ if (unRegisteredConsumers.length > 0) {
950
+ const queueNames = unRegisteredConsumers.map((c) => c.queue);
951
+
952
+ this.#logger.error('rabbit: found unregistered consumers for queues', {
953
+ count: queueNames.length,
954
+ queues: queueNames,
955
+ });
956
+ throw new RabbitError('Found unregistered consumers');
957
+ }
958
+ const channel: ChannelWrapper = await this.assertChannel({ connection: this.publishConnection });
959
+ await channel.waitForConnect();
960
+
961
+ await Promise.all(
962
+ this.consumersToRegister.map((c) => channel.checkQueue(c.queue)),
963
+ );
964
+ } catch (e: RabbitError | any) {
965
+ this.#logger.error('rabbit: isConnected - false', { msg: e.message });
966
+ return false;
967
+ }
968
+ }
969
+ // TODO:[QUORUM-PHASE-3] Delete the old is connected and adjust the return value to return true
970
+ isConnected = await this.isConnectedOld();
936
971
  }
937
- logger.info('rabbit: isConnected - true');
938
- return true;
972
+
973
+ this.#logger.debug(`rabbit: isConnected - ${isConnected}`);
974
+ return isConnected;
939
975
  }
940
976
 
941
- async gracefulShutdown(signal: string) : Promise<void> {
942
- // TODO: DELETE OLD
943
- const tagsNumber = this.consumersTags.length + this.oldConsumersTags.length;
944
- logger.info(`rabbit: [gracefully-shutdown] received ${signal}! canceling #${tagsNumber} tags...`);
945
- const cancelTagPromises = this.consumersTags.map(([channel, tag]) => channel.cancel(tag));
946
- const cancelTagPromisesOld = this.oldConsumersTags.map(([channel, tag]) => channel.cancel(tag));
977
+ async gracefulShutdown(signal: string): Promise<void> {
978
+ // TODO: [QUORUM-PHASE-3] Delete the old implementation they are not needed anymore
979
+ this.gracefulShutdownStarted = true;
980
+ const tagsNumber = this.consumersTags.size + this.oldConsumersTags.size;
981
+ this.#logger.info(`rabbit: [gracefully-shutdown] received ${signal}! canceling #${tagsNumber} tags...`);
982
+ const cancelTagPromises = [...this.consumersTags].map(
983
+ ([, { channel, consumerTag }]) => channel.cancel(consumerTag),
984
+ );
985
+ const cancelTagPromisesOld = [...this.oldConsumersTags].map(
986
+ ([, { channel, consumerTag }]) => channel.cancel(consumerTag),
987
+ );
947
988
  // Clean the array to avoid race
948
- this.consumersTags = [];
949
- this.oldConsumersTags = [];
989
+ this.consumersTags.clear();
990
+ this.oldConsumersTags.clear();
950
991
  const results = await Promise.allSettled([...cancelTagPromises, ...cancelTagPromisesOld]);
951
992
  const rejected = results.filter((p) => p.status === 'rejected');
952
993
  if (rejected.length > 0) {
953
- logger.warn(`rabbit: [gracefully-shutdown] #${rejected.length}/${tagsNumber} tags failed to cancel: ${rejected}`);
994
+ this.#logger.warn(`rabbit: [gracefully-shutdown] #${rejected.length}/${tagsNumber} tags failed to cancel: ${rejected}`);
954
995
  } else {
955
- logger.info('rabbit: [gracefully-shutdown] all tags successfully canceled.');
996
+ this.#logger.info('rabbit: [gracefully-shutdown] all tags successfully canceled.');
956
997
  }
957
998
  }
958
999
 
959
- private async consumeFromRabbitOld(queue: string, callback: CallbackFunction, options?: ConsumeOptions): Promise<any> {
1000
+ private async consumeFromRabbitOld(queue: string, callback: CallbackFunction, options?: ConsumeOptions): Promise<void> {
960
1001
  const optionsWithDefaults = { ...DEFAULT_OPTIONS, ...options };
961
1002
  RabbitMq.validateName('queue', queue);
962
- this.saveConsumerOld(queue, callback, options);
963
1003
  const uniqueId = randomUUID();
964
1004
  const {
965
1005
  limit, deadMessageTtl, useConsumeWithLock, lockTimeout, auditContext, enableRabbitTrace,
966
1006
  } = optionsWithDefaults;
967
1007
  if (useConsumeWithLock) {
968
1008
  if (!this.redisLock) {
969
- throw new Error('Usage of consumeWithLock requires RedisInstance');
1009
+ throw new RabbitError('Usage of consumeWithLock requires RedisInstance');
970
1010
  }
971
- logger.info(`rabbit: Consuming with lock from queue ${queue} with lockTimeout: ${lockTimeout}ms`);
1011
+ this.#logger.info(`rabbit: Consuming with lock from queue ${queue} with lockTimeout: ${lockTimeout}ms`);
972
1012
  }
973
- const channel = await this.getNewChannelOld({});
1013
+ const channel = await this.getNewChannelOld({ connection: this.oldConsumeConnection });
974
1014
  return channel.addSetup(async (confirmChannel: ConfirmChannel) => {
975
- const q = await this.assertQueueOld(queue, optionsWithDefaults);
1015
+ await this.assertQueueOld(queue, optionsWithDefaults);
976
1016
  await confirmChannel.prefetch(limit, false);
977
1017
  const { consumerTag } = await confirmChannel.consume(
978
1018
  queue,
@@ -981,9 +1021,7 @@ class RabbitMq implements IAfRabbitMq {
981
1021
  return null;
982
1022
  }
983
1023
 
984
- const traceId = msg.properties.headers[TRACING_HEADER];
985
- const userId = msg.properties.headers[USER_TRACING_HEADER];
986
- const automationId = msg.properties.headers[AUTOMATION_ID_HEADER];
1024
+ const { [TRACING_HEADER]: traceId, [USER_TRACING_HEADER]: userId, [AUTOMATION_ID_HEADER]: automationId } = msg.properties.headers ?? {};
987
1025
  const parsedMessage = RabbitMq.parseMsg(msg);
988
1026
  const releaseLock = await this.lockRedisIfNeeded(parsedMessage, optionsWithDefaults);
989
1027
  const trace = newTrace(traceTypes.RABBIT);
@@ -998,13 +1036,13 @@ class RabbitMq implements IAfRabbitMq {
998
1036
  createOrSetRabbitTrace(outbreakTrace, userId),
999
1037
  ]);
1000
1038
  } catch (e) {
1001
- logger.error('rabbit: failed to setRabbitTrace', { userId, e });
1039
+ this.#logger.error('rabbit: failed to setRabbitTrace', { userId, e });
1002
1040
  return this.nack(confirmChannel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg);
1003
1041
  }
1004
1042
  }
1005
1043
 
1006
1044
  if (traceId) {
1007
- (trace as any)?.context?.set(TRACING_HEADER, traceId);
1045
+ trace?.context?.set(TRACING_HEADER, traceId);
1008
1046
  (outbreakTrace as any)?.context.set(TRACING_HEADER, traceId);
1009
1047
  }
1010
1048
 
@@ -1028,152 +1066,173 @@ class RabbitMq implements IAfRabbitMq {
1028
1066
  return;
1029
1067
  }
1030
1068
  messageAcked = true;
1031
- return this.ack(confirmChannel, msg, true, releaseLock)(msg);
1069
+ await this.ack(confirmChannel, msg, true, releaseLock)(msg);
1032
1070
  };
1033
1071
 
1034
1072
  const localNack = async (_: ConsumeMessageOrNull, nackOptions: NackOptions = {}) => {
1035
1073
  if (messageAcked) {
1036
1074
  return;
1037
1075
  }
1038
- debug('rabbit localNack', { messageAcked, uniqueId, deliveryTag: msg.fields.deliveryTag });
1076
+ this.#logger.debug('rabbit localNack', { messageAcked, uniqueId, deliveryTag: msg.fields.deliveryTag });
1039
1077
  messageAcked = true;
1040
- return this.nack(confirmChannel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg, nackOptions);
1078
+ await this.nack(confirmChannel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg, nackOptions);
1041
1079
  };
1042
1080
 
1043
1081
  try {
1044
- await callback(
1082
+ return await callback(
1045
1083
  parsedMessage,
1046
1084
  localAck,
1047
1085
  localNack,
1048
1086
  );
1049
1087
  } catch (e) {
1050
- await localNack(msg);
1088
+ return localNack(msg);
1051
1089
  }
1052
1090
  }, CONSUMER_DEFAULT_OPTIONS,
1053
1091
  );
1054
1092
  if (!consumerTag) {
1055
- logger.error(`rabbit: failed to consume from queue ${queue}`);
1093
+ this.#logger.error(`rabbit: failed to consume from queue ${queue} old`);
1056
1094
  } else {
1057
- logger.info(`rabbit: adding tag ${consumerTag} to the array.`);
1058
- this.oldConsumersTags.push([confirmChannel, consumerTag]);
1095
+ this.#logger.info(`rabbit: adding tag ${consumerTag} to the array old`);
1096
+ this.oldConsumersTags.set(queue, { channel: confirmChannel, consumerTag });
1059
1097
  }
1060
1098
  });
1061
1099
  }
1062
1100
 
1063
- // TODO: DELETE OLD PROPERTIES UNDER THIS LINE
1064
- async getNewChannelOld({ name = rand().toString(), onClose = null, options = {} }: newChannelOpts = {}) {
1065
- let connection!: AmqpConnectionManager;
1101
+ // TODO: [QUORUM-PHASE-3] Delete all the function under this line.
1102
+ async getNewChannelOld({
1103
+ name = rand().toString(),
1104
+ onClose = null,
1105
+ options = {},
1106
+ connection,
1107
+ }: newChannelOpts): Promise<ChannelWrapper> {
1108
+ let localConnection!: AmqpConnectionManager;
1066
1109
  try {
1067
- connection = await this.getConnectionOld();
1110
+ localConnection = await this.getConnectionOld(connection);
1068
1111
  } catch (e) {
1069
- logger.error(`rabbit: error on get connection for new channel ${name} `, { e });
1112
+ this.#logger.error(`rabbit: error on get connection for new channel ${name} `, { e });
1070
1113
  throw e;
1071
1114
  }
1072
- const channel = connection.createChannel({ ...options });
1115
+
1116
+ if (!localConnection) {
1117
+ throw new Error(`rabbit: couldnt get connection for new channel ${name}`);
1118
+ }
1119
+
1120
+ const channel = localConnection?.createChannel({ ...options });
1073
1121
  once(channel, 'close').then((args) => {
1074
- logger.error(`rabbit: channel ${name} closed`);
1122
+ this.#logger.error(`rabbit: channel ${name} closed`);
1075
1123
  onClose?.(args);
1076
1124
  });
1077
1125
  try {
1078
1126
  await once(channel, 'connect');
1079
- debug(`rabbit: channel ${name} CONNECTED`);
1127
+ this.#logger.debug(`rabbit: channel ${name} CONNECTED`);
1080
1128
  return channel;
1081
1129
  } catch (err) {
1082
- logger.error(`rabbit: channel error ${name} error`, { err });
1130
+ this.#logger.error(`rabbit: channel error ${name} error`, { err });
1083
1131
  throw err;
1084
1132
  }
1085
1133
  }
1086
1134
 
1087
- async getConnectionOld() {
1088
- return new Promise<AmqpConnectionManager>(async (resolve, reject) => {
1089
- if (this.oldBlockReconnect) {
1090
- debug('rabbit: block reconnect');
1135
+ async getConnectionOld(connection: ConnectionData): Promise<AmqpConnectionManager> {
1136
+ const {
1137
+ amqpConnection: connectionLocal,
1138
+ creatingConnection,
1139
+ connectionCreatedEventName,
1140
+ connectionFailedEventName,
1141
+ blockReconnect,
1142
+ } = connection;
1143
+
1144
+ if (blockReconnect) {
1145
+ this.#logger.debug('rabbit: block reconnect');
1146
+ // @ts-expect-error we are returning undefined while the function clearly expects a connection.
1147
+ return undefined;
1148
+ }
1149
+ if (connectionLocal !== null) {
1150
+ if (this.options?.disableReconnect || connectionLocal?.isConnected()) {
1151
+ this.#logger.debug('rabbit: connection - is connected');
1091
1152
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1092
1153
  // @ts-ignore
1093
- return resolve();
1154
+ return connectionLocal;
1094
1155
  }
1095
- if (this.oldConnection !== null) {
1096
- if (this.options?.disableReconnect || this.oldConnection?.isConnected()) {
1097
- debug('rabbit: connection - is connected');
1098
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
1099
- // @ts-ignore
1100
- return resolve(this.oldConnection);
1101
- }
1102
- debug('rabbit: connection - reconnecting');
1103
- }
1104
- if (this.oldCreatingConnection) {
1105
- debug('rabbit: creating connection emi');
1106
- this.oldEm.once(CONNECTION_CREATED_CONST, resolve);
1107
- this.oldEm.once(CONNECTION_FAILED_CONST, reject);
1108
- return;
1156
+ this.#logger.debug('rabbit: connection - reconnecting');
1157
+ }
1158
+ if (creatingConnection) {
1159
+ const [event, value] = await Promise.race([connectionCreatedEventName, connectionFailedEventName].map((e) => once(this.oldEm, e).then(([result]) => [e, result] as const)));
1160
+ if (event === connectionCreatedEventName) {
1161
+ return value;
1109
1162
  }
1110
- this.oldCreatingConnection = true;
1111
- let isResolved = false;
1112
-
1113
- // It is import to use it as a function and not as a variable
1114
- // because of k8s changes the env variables
1115
- // and we want to use the new values
1116
- const findServers = () => {
1117
- const userName = process.env.RABBITMQ_USERNAME || 'guest';
1118
- const password = process.env.RABBITMQ_PASSWORD || 'guest';
1119
- const host = this.options?.rabbitHost || process.env.RABBITMQ_SERVICE_HOST || 'localhost';
1163
+ throw value;
1164
+ }
1165
+ connection.creatingConnection = true;
1166
+ let isResolved = false;
1120
1167
 
1121
- debug('rabbit: creating connection', { host, userName, HEARTBEAT });
1168
+ // It is import to use it as a function and not as a variable
1169
+ // because of k8s changes the env variables
1170
+ // and we want to use the new values
1171
+ const findServers = () => {
1172
+ const userName = process.env.RABBITMQ_USERNAME || 'guest';
1173
+ const password = process.env.RABBITMQ_PASSWORD || 'guest';
1174
+ const host = this.options?.rabbitHost || process.env.RABBITMQ_SERVICE_HOST || 'localhost';
1122
1175
 
1123
- return [`amqp://${userName}:${password}@${host}?heartbeat=${HEARTBEAT}`];
1124
- };
1176
+ this.#logger.debug('rabbit: creating connection', { host, userName, HEARTBEAT });
1125
1177
 
1126
- const defaultUrls = findServers();
1127
- const connection: AmqpConnectionManager = await connect(defaultUrls, {
1128
- findServers,
1129
- });
1178
+ return [`amqp://${userName}:${password}@${host}?heartbeat=${HEARTBEAT}`];
1179
+ };
1130
1180
 
1131
- this.oldConnection = connection;
1132
- this.oldConnection.on('error', (err) => {
1133
- logger.error('rabbit: connection error', { err });
1134
- if (!isResolved) {
1135
- isResolved = true;
1136
- reject(err);
1137
- this.oldEm.emit(CONNECTION_FAILED_CONST, err);
1138
- }
1139
- });
1181
+ const defaultUrls = findServers();
1182
+ const newConnection: AmqpConnectionManager = await connect(defaultUrls, { findServers });
1140
1183
 
1141
- this.oldConnection.on('connectFailed', (err) => {
1142
- this.oldConsumersTags = [];
1143
- logger.error('rabbit: connection connectFailed', { err });
1144
- if (!isResolved) {
1145
- isResolved = true;
1146
- reject(err);
1147
- this.oldEm.emit(CONNECTION_FAILED_CONST, err);
1148
- }
1149
- });
1184
+ connection.amqpConnection = newConnection;
1185
+ const { promise, reject, resolve } = createDeferredPromise<AmqpConnectionManager>();
1150
1186
 
1151
- this.oldConnection.on('disconnect', ({ err }) => {
1152
- this.oldConsumersTags = [];
1153
- debug('rabbit: connection closed');
1154
- if (this.options?.disableReconnect) {
1155
- logger.error(`${this.DISCONNECT_MSG}${err && ` - ${err}`}`);
1156
- this.oldBlockReconnect = true;
1157
- } else {
1158
- logger.error(`${this.RECONNECT_MSG}${err && ` - ${err}`}`);
1159
- }
1160
- });
1187
+ newConnection.on('error', (err) => {
1188
+ this.#logger.error('rabbit: connection error', { err });
1189
+ if (!isResolved) {
1190
+ isResolved = true;
1191
+ reject(err);
1192
+ this.oldEm.emit(connectionFailedEventName, err);
1193
+ }
1194
+ });
1161
1195
 
1162
- this.oldConnection.once('connect', async () => {
1163
- debug('rabbit: connection established');
1164
- this.oldCreatingConnection = false;
1165
- this.oldEm.emit(CONNECTION_CREATED_CONST, connection);
1196
+ newConnection.on('connectFailed', (err) => {
1197
+ this.oldConsumersTags.clear();
1198
+ if (typeof err.url === 'string') {
1199
+ err.url = this.maskURL(err.url);
1200
+ }
1201
+ logger.error('rabbit: connection connectFailed', { err });
1202
+ if (!isResolved) {
1166
1203
  isResolved = true;
1167
- resolve(connection);
1168
- });
1204
+ reject(err);
1205
+ this.oldEm.emit(connectionFailedEventName, err);
1206
+ }
1207
+ });
1208
+
1209
+ newConnection.on('disconnect', ({ err }) => {
1210
+ this.oldConsumersTags.clear();
1211
+ this.#logger.debug('rabbit: connection closed');
1212
+ if (this.options?.disableReconnect) {
1213
+ logger.error(`${this.DISCONNECT_MSG}${err && ` - ${err}`}`);
1214
+ connection.blockReconnect = true;
1215
+ } else {
1216
+ logger.error(`${this.RECONNECT_MSG}${err && ` - ${err}`}`);
1217
+ }
1218
+ });
1219
+
1220
+ newConnection.once('connect', async () => {
1221
+ this.#logger.debug('rabbit: connection established');
1222
+ connection.creatingConnection = false;
1223
+ this.oldEm.emit(connectionCreatedEventName, newConnection);
1224
+ isResolved = true;
1225
+ resolve(newConnection);
1169
1226
  });
1227
+
1228
+ return promise;
1170
1229
  }
1171
1230
 
1172
- private saveConsumerOld(queue: string, callback: CallbackFunction, options: ConsumeOptions | undefined) {
1173
- const isConsumerExist :boolean = this.oldConsumers.some((consumer) => consumer.queue === queue);
1231
+ private saveConsumerOld(queue: string, callback: CallbackFunction, options: ConsumeOptions | undefined): void {
1232
+ const isConsumerExist: boolean = this.oldConsumersToRegister.some((consumer) => consumer.queue === queue);
1174
1233
  if (!isConsumerExist) {
1175
1234
  logger.info(`rabbit: consumer: ${queue} saved in consumer array`);
1176
- this.oldConsumers.push({
1235
+ this.oldConsumersToRegister.push({
1177
1236
  queue,
1178
1237
  callback,
1179
1238
  options,
@@ -1181,23 +1240,20 @@ class RabbitMq implements IAfRabbitMq {
1181
1240
  }
1182
1241
  }
1183
1242
 
1184
- async assertQueueOld(queueName: string, options?: Options.AssertQueue): Promise<any> {
1243
+ async assertQueueOld(queueName: string, options?: Options.AssertQueue): Promise<Replies.AssertQueue> {
1185
1244
  RabbitMq.validateName('queue', queueName);
1186
1245
  if (this.oldQueues[queueName]) {
1187
1246
  delete this.oldQueueSetupPromises[queueName];
1188
- return this.oldQueues[queueName];
1189
- }
1190
-
1191
- if (this.oldQueueSetupPromises[queueName]) {
1192
- return this.oldQueueSetupPromises[queueName];
1247
+ return this.oldQueues[queueName]!;
1193
1248
  }
1194
1249
 
1195
- this.oldQueueSetupPromises[queueName] = this.setupQueueOld(queueName, options);
1196
- return this.oldQueueSetupPromises[queueName];
1250
+ this.oldQueueSetupPromises[queueName] ||= this.setupQueueOld(queueName, options);
1251
+ return this.oldQueueSetupPromises[queueName]!;
1197
1252
  }
1198
1253
 
1199
1254
  async setupQueueOld(queueName: string, options?: Options.AssertQueue): Promise<Replies.AssertQueue> {
1200
1255
  let queue: Replies.AssertQueue;
1256
+ const connection = this.oldPublishConnection;
1201
1257
  const shouldUseQuorum = RabbitMq.shouldUseQuorum(queueName);
1202
1258
  const localeOptions = {
1203
1259
  ...options,
@@ -1209,21 +1265,21 @@ class RabbitMq implements IAfRabbitMq {
1209
1265
  },
1210
1266
  };
1211
1267
  try {
1212
- const channel: ChannelWrapper = await this.assertChannelOld();
1213
- debug('assertQueue->channel.addSetup', { queueName });
1268
+ const channel: ChannelWrapper = await this.assertChannelOld({ connection });
1269
+ this.#logger.debug('assertQueue->channel.addSetup', { queueName });
1214
1270
  await channel.addSetup((setupChannel: ConfirmChannel) => setupChannel.assertQueue(queueName, localeOptions));
1215
- debug('assertQueue->channel.assertQueue', { queueName });
1271
+ this.#logger.debug('assertQueue->channel.assertQueue', { queueName });
1216
1272
  queue = await channel.assertQueue(queueName, localeOptions);
1217
1273
  } catch (e) {
1218
1274
  logger.error('rabbit: assertQueue error', { queueName, options, error: e });
1219
1275
  if (!this.options?.dontRetryAssert) {
1220
- debug('retrying assertQueue', { queueName });
1221
- const channel = await this.assertChannelOld({ force: true });
1222
- await this.deleteQueueOld(queueName);
1276
+ this.#logger.debug('retrying assertQueue', { queueName });
1277
+ const channel = await this.assertChannelOld({ force: true, connection });
1278
+ await this.deleteQueueOld(queueName, connection);
1223
1279
 
1224
- debug('retrying assertQueue->channel.addSetup', { queueName });
1280
+ this.#logger.debug('retrying assertQueue->channel.addSetup', { queueName });
1225
1281
  await channel.addSetup((setupChannel: ConfirmChannel) => setupChannel.assertQueue(queueName, localeOptions));
1226
- debug('retrying assertQueue->channel.assertQueue', { queueName });
1282
+ this.#logger.debug('retrying assertQueue->channel.assertQueue', { queueName });
1227
1283
  queue = await channel.assertQueue(queueName, localeOptions);
1228
1284
  } else {
1229
1285
  throw e;
@@ -1234,52 +1290,107 @@ class RabbitMq implements IAfRabbitMq {
1234
1290
  return queue;
1235
1291
  }
1236
1292
 
1237
- async assertChannelOld({ force = false } : assertChannelOpts = {}): Promise<ChannelWrapper> {
1238
- if (!this.oldPublishChannelSetupPromise) {
1239
- this.oldPublishChannelSetupPromise = new Promise<ChannelWrapper>(async (resolve, reject) => {
1240
- if (this.oldChannel && !force) {
1241
- return resolve(this.oldChannel);
1242
- }
1293
+ async assertChannelOld({ force = false, connection }: assertChannelOpts): Promise<ChannelWrapper> {
1294
+ if (this.oldPublishChannelSetupPromise) {
1295
+ return this.oldPublishChannelSetupPromise;
1296
+ }
1297
+ const { promise, resolve, reject } = createDeferredPromise<ChannelWrapper>();
1298
+ this.oldPublishChannelSetupPromise = promise;
1299
+ if (this.oldPublishChannel && !force) {
1300
+ resolve(this.oldPublishChannel);
1301
+ return promise;
1302
+ }
1243
1303
 
1244
- try {
1245
- const channel = await this.getNewChannelOld({});
1246
- channel.on('error', (err) => {
1247
- logger.error('rabbit: channel error', { err });
1248
- });
1249
- this.oldChannel = channel;
1250
- resolve(channel);
1251
- } catch (e) {
1252
- reject(e);
1253
- }
1304
+ try {
1305
+ const channel = await this.getNewChannelOld({ connection });
1306
+ channel.on('error', (err) => {
1307
+ logger.error('rabbit: channel error', { err });
1254
1308
  });
1309
+ if (this.oldPublishConnection === connection) {
1310
+ this.oldPublishChannel = channel;
1311
+ }
1312
+ resolve(channel);
1313
+ } catch (e) {
1314
+ reject(e);
1255
1315
  }
1256
- return this.oldPublishChannelSetupPromise;
1316
+ return promise;
1257
1317
  }
1258
1318
 
1259
- private async deleteQueueOld(queue: string) {
1319
+ private async deleteQueueOld(queue: string, connection: ConnectionData): Promise<Replies.DeleteQueue> {
1260
1320
  RabbitMq.validateName('queue', queue);
1261
- const channel: ChannelWrapper = await this.assertChannelOld();
1321
+ const channel: ChannelWrapper = await this.assertChannelOld({ connection });
1262
1322
  logger.info('rabbit: deleting queue', { queue });
1263
1323
  const deleteQueueRes = await channel.deleteQueue(queue);
1264
- debug('queue deleted', deleteQueueRes);
1324
+ this.#logger.debug('queue deleted', deleteQueueRes);
1265
1325
  return deleteQueueRes;
1266
1326
  }
1267
1327
 
1268
- async assertExchangeOld(exchangeName: string, options?: any) {
1269
- const channel: ChannelWrapper = await this.assertChannelOld();
1328
+ async assertExchangeOld(exchangeName: string, connection: ConnectionData): Promise<Replies.AssertExchange> {
1329
+ const channel: ChannelWrapper = await this.assertChannelOld({ connection });
1270
1330
 
1271
1331
  if (this.oldExchanges[exchangeName]) {
1272
1332
  delete this.oldAssertExchangePromises[exchangeName];
1273
- return this.oldExchanges[exchangeName];
1333
+ return this.oldExchanges[exchangeName]!;
1274
1334
  }
1275
1335
 
1276
1336
  if (this.oldAssertExchangePromises[exchangeName]) {
1277
- return this.oldAssertExchangePromises[exchangeName];
1337
+ return this.oldAssertExchangePromises[exchangeName]!;
1278
1338
  }
1279
1339
 
1280
1340
  this.oldAssertExchangePromises[exchangeName] = assertExchangeFanout(channel, exchangeName);
1281
1341
  this.oldExchanges[exchangeName] = await this.oldAssertExchangePromises[exchangeName];
1282
- return this.oldExchanges[exchangeName];
1342
+ return this.oldExchanges[exchangeName]!;
1343
+ }
1344
+
1345
+ async isConnectedOld(): Promise<boolean> {
1346
+ if (!this.gracefulShutdownStarted) {
1347
+ this.#logger.debug('rabbit: start is connected old');
1348
+ const [oldConsumeConnection, oldPublishConnection] = await Promise.all([
1349
+ this.getConnectionOld(this.oldConsumeConnection),
1350
+ this.getConnectionOld(this.oldPublishConnection),
1351
+ ]);
1352
+ const isConnected = oldConsumeConnection.isConnected() && oldPublishConnection.isConnected();
1353
+ if (!isConnected) {
1354
+ logger.error('rabbit: isConnected old - false');
1355
+ return false;
1356
+ }
1357
+
1358
+ try {
1359
+ const unRegisteredConsumersOld = this.consumersToRegister.filter((c) => !this.consumersTags.get(c.queue));
1360
+
1361
+ if (unRegisteredConsumersOld.length > 0) {
1362
+ const queueNames = unRegisteredConsumersOld.map((c) => c.queue);
1363
+
1364
+ logger.error('rabbit: found unregistered consumers for queues old', {
1365
+ count: queueNames.length,
1366
+ queues: queueNames,
1367
+ });
1368
+ throw new RabbitError('Found unregistered consumers old');
1369
+ }
1370
+
1371
+ const channelOld = await this.assertChannelOld({ connection: this.oldPublishConnection });
1372
+ await channelOld.waitForConnect();
1373
+
1374
+ await Promise.all(
1375
+ this.oldConsumersToRegister.map((c: AfConsumer) => channelOld.checkQueue(c.queue)),
1376
+ );
1377
+ } catch (e: RabbitError | any) {
1378
+ logger.error('rabbit: isConnected - false old', { msg: e.message });
1379
+ return false;
1380
+ }
1381
+ }
1382
+ return true;
1383
+ }
1384
+
1385
+ private maskURL = (url: string): string => {
1386
+ try {
1387
+ const urlObj = new URL(url);
1388
+ urlObj.username = '***';
1389
+ urlObj.password = '***';
1390
+ return urlObj.toString();
1391
+ } catch {
1392
+ return url;
1393
+ }
1283
1394
  }
1284
1395
  }
1285
1396