@autofleet/rabbit 2.5.0 → 2.5.2-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /// <reference types="node" />
2
- import { Options, ConsumeMessage } from 'amqplib';
2
+ import { ConfirmChannel, Options, ConsumeMessage } from 'amqplib';
3
3
  import { AmqpConnectionManager, ChannelWrapper } from 'amqp-connection-manager';
4
4
  import { EventEmitter } from 'events';
5
5
  import { RedisConfig } from './redis';
@@ -62,6 +62,8 @@ declare class RabbitMq implements IAfRabbitMq {
62
62
  options: AfRabbitOptions | undefined;
63
63
  redisClient: any;
64
64
  redisLock?: RedisLockType;
65
+ /** Array of consumers tags used for canceling consumption */
66
+ consumersTags: Array<[ConfirmChannel, string]>;
65
67
  constructor(options?: AfRabbitOptions, redisConfig?: RedisConfig);
66
68
  private shouldConsumeMessageByTimestamp;
67
69
  ack: (channel: ChannelWrapper, msg: ConsumeMessageOrNull, shouldUpdateRedisTimestamp?: boolean, releaseLock?: null) => (userMsg: ConsumeMessage) => Promise<any>;
@@ -81,5 +83,6 @@ declare class RabbitMq implements IAfRabbitMq {
81
83
  publish(exchange: string, content: any, customHeaders?: any): Promise<boolean>;
82
84
  sendToQueue(queue: string, content: any, options?: any, customHeaders?: any): Promise<boolean>;
83
85
  isConnected(): Promise<boolean>;
86
+ gracefulShutdown(signal: string): Promise<void>;
84
87
  }
85
88
  export default RabbitMq;
package/dist/index.js CHANGED
@@ -105,6 +105,14 @@ class RabbitMq {
105
105
  if (this.redisClient) {
106
106
  this.redisLock = util_1.promisify(redis_lock_1.default(this.redisClient));
107
107
  }
108
+ this.consumersTags = [];
109
+ logger_1.default.info(`rabbit: [gracefully-shutdown] adding gracefully shutdown for process.pid ${process.pid}`);
110
+ process.on('SIGTERM', async () => {
111
+ await this.gracefulShutdown('SIGTERM');
112
+ });
113
+ process.on('SIGINT', async () => {
114
+ await this.gracefulShutdown('SIGINT');
115
+ });
108
116
  }
109
117
  static parseMsg(msg) {
110
118
  let { content } = msg;
@@ -244,35 +252,42 @@ class RabbitMq {
244
252
  if (!this.redisLock) {
245
253
  throw new Error('Usage of consumeWithLock requires RedisInstance');
246
254
  }
247
- logger_1.default.info(`Consuming with lock from queue ${queue} with lockTimeout: ${lockTimeout}ms`);
255
+ logger_1.default.info(`rabbit: Consuming with lock from queue ${queue} with lockTimeout: ${lockTimeout}ms`);
248
256
  }
249
257
  const channel = await this.getNewChannel();
250
258
  return channel.addSetup(async (c) => {
251
259
  await c.assertQueue(queue);
252
260
  await c.prefetch(limit, true);
253
- return Promise.all([
254
- c.consume(queue, async (msg) => {
255
- if (!msg) {
256
- return null;
257
- }
258
- const traceId = msg.properties.headers[TRACING_HEADER];
261
+ const { consumerTag } = await c.consume(queue, async (msg) => {
262
+ if (!msg) {
263
+ return null;
264
+ }
265
+ const traceId = msg.properties.headers[TRACING_HEADER];
266
+ if (traceId) {
259
267
  const trace = outbreak_1.newTrace(outbreak_1.traceTypes.RABBIT);
260
268
  trace?.context?.set(TRACING_HEADER, traceId);
261
- const parsedMessage = RabbitMq.parseMsg(msg);
262
- const releaseLock = await this.lockRedisIfNeeded(parsedMessage, optionsWithDefaults);
263
- const shouldConsume = await this.shouldConsumeMessageByTimestamp(parsedMessage);
264
- if (!shouldConsume) {
265
- await this.unlockRedisIfNeeded(releaseLock);
266
- return this.ack(channel, msg)(msg);
267
- }
268
- try {
269
- await callback(parsedMessage, this.ack(channel, msg, true, releaseLock), this.nack(channel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock));
270
- }
271
- catch (e) {
272
- await this.nack(channel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg);
273
- }
274
- }),
275
- ]);
269
+ }
270
+ const parsedMessage = RabbitMq.parseMsg(msg);
271
+ const releaseLock = await this.lockRedisIfNeeded(parsedMessage, optionsWithDefaults);
272
+ const shouldConsume = await this.shouldConsumeMessageByTimestamp(parsedMessage);
273
+ if (!shouldConsume) {
274
+ await this.unlockRedisIfNeeded(releaseLock);
275
+ return this.ack(channel, msg)(msg);
276
+ }
277
+ try {
278
+ await callback(parsedMessage, this.ack(channel, msg, true, releaseLock), this.nack(channel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock));
279
+ }
280
+ catch (e) {
281
+ await this.nack(channel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg);
282
+ }
283
+ });
284
+ if (!consumerTag) {
285
+ logger_1.default.error(`rabbit: failed to consume from queue ${queue}`);
286
+ }
287
+ else {
288
+ logger_1.default.info(`rabbit: adding tag ${consumerTag} to the array.`);
289
+ this.consumersTags.push([c, consumerTag]);
290
+ }
276
291
  });
277
292
  }
278
293
  async consumeFromExchange(queue, exchange, callback, options) {
@@ -312,5 +327,20 @@ class RabbitMq {
312
327
  const connection = await this.getConnection();
313
328
  return connection.isConnected();
314
329
  }
330
+ async gracefulShutdown(signal) {
331
+ const tagsNumber = this.consumersTags.length;
332
+ logger_1.default.info(`rabbit: [gracefully-shutdown] received ${signal}! canceling #${tagsNumber} tags...`);
333
+ const cancelTagPromises = this.consumersTags.map(([channel, tag]) => channel.cancel(tag));
334
+ // Clean the array to avoid race
335
+ this.consumersTags = [];
336
+ const results = await Promise.allSettled(cancelTagPromises);
337
+ const rejected = results.filter((p) => p.status === 'rejected');
338
+ if (rejected.length > 0) {
339
+ logger_1.default.warn(`rabbit: [gracefully-shutdown] #${rejected.length}/${tagsNumber} tags failed to cancel: ${rejected}`);
340
+ }
341
+ else {
342
+ logger_1.default.info('rabbit: [gracefully-shutdown] all tags successfully canceled.');
343
+ }
344
+ }
315
345
  }
316
346
  exports.default = RabbitMq;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autofleet/rabbit",
3
- "version": "2.5.0",
3
+ "version": "2.5.2-beta.0",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
package/src/index.ts CHANGED
@@ -66,7 +66,7 @@ const defaultOptions = {
66
66
  useConsumeWithLock: DEFAULT_USE_CONSUME_WITH_LOCK,
67
67
  };
68
68
 
69
- const assertExchangeFanout = async (c: any, exchangeName: string) => c.assertExchange(exchangeName, 'fanout');
69
+ const assertExchangeFanout = async (c: ChannelWrapper | ConfirmChannel, exchangeName: string) => c.assertExchange(exchangeName, 'fanout');
70
70
 
71
71
  const connectionCreatedConst = 'connectionCreated';
72
72
 
@@ -140,6 +140,9 @@ class RabbitMq implements IAfRabbitMq {
140
140
 
141
141
  redisLock?: RedisLockType;
142
142
 
143
+ /** Array of consumers tags used for canceling consumption */
144
+ consumersTags: Array<[ConfirmChannel, string]>;
145
+
143
146
  constructor(options?: AfRabbitOptions, redisConfig?: RedisConfig) {
144
147
  this.em = new EventEmitter();
145
148
  this.channel = null;
@@ -152,6 +155,14 @@ class RabbitMq implements IAfRabbitMq {
152
155
  if (this.redisClient) {
153
156
  this.redisLock = promisify(RedisLock(this.redisClient)) as RedisLockType;
154
157
  }
158
+ this.consumersTags = [];
159
+ logger.info(`rabbit: [gracefully-shutdown] adding gracefully shutdown for process.pid ${process.pid}`);
160
+ process.on('SIGTERM', async () => {
161
+ await this.gracefulShutdown('SIGTERM');
162
+ });
163
+ process.on('SIGINT', async () => {
164
+ await this.gracefulShutdown('SIGINT');
165
+ });
155
166
  }
156
167
 
157
168
  private shouldConsumeMessageByTimestamp = async (msg: ConsumeMessageOrNull) => {
@@ -349,43 +360,49 @@ class RabbitMq implements IAfRabbitMq {
349
360
  if (!this.redisLock) {
350
361
  throw new Error('Usage of consumeWithLock requires RedisInstance');
351
362
  }
352
- logger.info(`Consuming with lock from queue ${queue} with lockTimeout: ${lockTimeout}ms`);
363
+ logger.info(`rabbit: Consuming with lock from queue ${queue} with lockTimeout: ${lockTimeout}ms`);
353
364
  }
354
365
  const channel: ChannelWrapper = await this.getNewChannel();
355
366
  return channel.addSetup(async (c: ConfirmChannel) => {
356
367
  await c.assertQueue(queue);
357
368
  await c.prefetch(limit, true);
358
- return Promise.all([
359
- c.consume(
360
- queue,
361
- async (msg: ConsumeMessageOrNull) => {
362
- if (!msg) {
363
- return null;
364
- }
369
+ const { consumerTag } = await c.consume(
370
+ queue,
371
+ async (msg: ConsumeMessageOrNull) => {
372
+ if (!msg) {
373
+ return null;
374
+ }
365
375
 
366
- const traceId = msg.properties.headers[TRACING_HEADER];
376
+ const traceId = msg.properties.headers[TRACING_HEADER];
377
+ if (traceId) {
367
378
  const trace = newTrace(traceTypes.RABBIT);
368
379
  (trace as any)?.context?.set(TRACING_HEADER, traceId);
380
+ }
369
381
 
370
- const parsedMessage = RabbitMq.parseMsg(msg);
371
- const releaseLock = await this.lockRedisIfNeeded(parsedMessage, optionsWithDefaults);
372
- const shouldConsume = await this.shouldConsumeMessageByTimestamp(parsedMessage);
373
- if (!shouldConsume) {
374
- await this.unlockRedisIfNeeded(releaseLock);
375
- return this.ack(channel, msg)(msg);
376
- }
377
- try {
378
- await callback(
379
- parsedMessage,
380
- this.ack(channel, msg, true, releaseLock),
381
- this.nack(channel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock),
382
- );
383
- } catch (e) {
384
- await this.nack(channel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg);
385
- }
386
- },
387
- ),
388
- ]);
382
+ const parsedMessage = RabbitMq.parseMsg(msg);
383
+ const releaseLock = await this.lockRedisIfNeeded(parsedMessage, optionsWithDefaults);
384
+ const shouldConsume = await this.shouldConsumeMessageByTimestamp(parsedMessage);
385
+ if (!shouldConsume) {
386
+ await this.unlockRedisIfNeeded(releaseLock);
387
+ return this.ack(channel, msg)(msg);
388
+ }
389
+ try {
390
+ await callback(
391
+ parsedMessage,
392
+ this.ack(channel, msg, true, releaseLock),
393
+ this.nack(channel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock),
394
+ );
395
+ } catch (e) {
396
+ await this.nack(channel, queue, optionsWithDefaults, { messageTtl: deadMessageTtl }, msg, releaseLock)(msg);
397
+ }
398
+ },
399
+ );
400
+ if (!consumerTag) {
401
+ logger.error(`rabbit: failed to consume from queue ${queue}`);
402
+ } else {
403
+ logger.info(`rabbit: adding tag ${consumerTag} to the array.`);
404
+ this.consumersTags.push([c, consumerTag]);
405
+ }
389
406
  });
390
407
  }
391
408
 
@@ -438,6 +455,21 @@ class RabbitMq implements IAfRabbitMq {
438
455
  const connection = await this.getConnection();
439
456
  return connection.isConnected();
440
457
  }
458
+
459
+ async gracefulShutdown(signal: string) : Promise<void> {
460
+ const tagsNumber = this.consumersTags.length;
461
+ logger.info(`rabbit: [gracefully-shutdown] received ${signal}! canceling #${tagsNumber} tags...`);
462
+ const cancelTagPromises = this.consumersTags.map(([channel, tag]) => channel.cancel(tag));
463
+ // Clean the array to avoid race
464
+ this.consumersTags = [];
465
+ const results = await Promise.allSettled(cancelTagPromises);
466
+ const rejected = results.filter((p) => p.status === 'rejected');
467
+ if (rejected.length > 0) {
468
+ logger.warn(`rabbit: [gracefully-shutdown] #${rejected.length}/${tagsNumber} tags failed to cancel: ${rejected}`);
469
+ } else {
470
+ logger.info('rabbit: [gracefully-shutdown] all tags successfully canceled.');
471
+ }
472
+ }
441
473
  }
442
474
 
443
475
  export default RabbitMq;