elasticio-sailor-nodejs 2.7.1-dev3 → 2.7.1-dev5

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/lib/amqp.js CHANGED
@@ -1,584 +1,584 @@
1
- const log = require('./logging.js');
2
- const amqplib = require('amqplib');
3
- const { IllegalOperationError } = require('amqplib/lib/error');
4
- const Encryptor = require('./encryptor.js');
5
- const _ = require('lodash');
6
- const eventToPromise = require('event-to-promise');
7
- const uuid = require('uuid');
8
- const os = require('os');
9
-
10
- const HEADER_ROUTING_KEY = 'x-eio-routing-key';
11
- const HEADER_ERROR_RESPONSE = 'x-eio-error-response';
12
-
13
- class Amqp {
14
- constructor(settings) {
15
- this.settings = settings;
16
- this._encryptor = new Encryptor(this.settings.MESSAGE_CRYPTO_PASSWORD, this.settings.MESSAGE_CRYPTO_IV);
17
- this.closed = true;
18
- this.consume = undefined;
19
- }
20
-
21
- async connect() {
22
- await Promise.all([this._ensurePublishChannel(), this._ensureConsumerChannel()]);
23
- this.closed = false;
24
- }
25
-
26
- async disconnect() {
27
- await this.stopConsume();
28
- this.closed = true;
29
- log.trace('Close AMQP connections');
30
- if (this._readConnection) {
31
- this._readConnection.removeAllListeners('close');
32
- }
33
- if (this._writeConnection) {
34
- this._writeConnection.removeAllListeners('close');
35
- }
36
- if (this.consumerChannel) {
37
- this.consumerChannel.removeAllListeners('close');
38
- }
39
- if (this.publishChannel) {
40
- this.publishChannel.removeAllListeners('close');
41
- }
42
- try {
43
- await this.consumerChannel.close();
44
- } catch (alreadyClosed) {
45
- log.debug('Subscribe channel is closed already');
46
- }
47
- try {
48
- await this.publishChannel.close();
49
- } catch (alreadyClosed) {
50
- log.debug('Publish channel is closed already');
51
- }
52
- try {
53
- await this._readConnection.close();
54
- } catch (alreadyClosed) {
55
- log.debug('AMQP read connection is closed already');
56
- }
57
- try {
58
- await this._writeConnection.close();
59
- } catch (alreadyClosed) {
60
- log.debug('AMQP write connection is closed already');
61
- }
62
- log.debug('Successfully closed AMQP connections');
63
- }
64
-
65
- async _ensureReadConnection() {
66
- if (this._readConnection) {
67
- return this._readConnection;
68
- }
69
- if (this._creatingReadConnection) {
70
- do {
71
- await new Promise(resolve => setImmediate(resolve));
72
- } while (!this._readConnection);
73
- return this._readConnection;
74
- }
75
- log.debug('Creating new read connection');
76
- this._creatingReadConnection = true;
77
- this._readConnection = await this._createConnection('read');
78
- this._creatingReadConnection = false;
79
- log.debug('Read connection created');
80
- this._readConnection.on('error', err => log.error({ err }, 'AMQP read Connection error'));
81
- this._readConnection.once('close', err => {
82
- log.error({ err }, 'Unexpected connection close.');
83
- delete this._readConnection;
84
- });
85
- return this._readConnection;
86
- }
87
-
88
- async _ensureWriteConnection() {
89
- if (this._writeConnection) {
90
- return this._writeConnection;
91
- }
92
- if (this._creatingWriteConnection) {
93
- do {
94
- await new Promise(resolve => setImmediate(resolve));
95
- } while (!this._writeConnection);
96
- return this._writeConnection;
97
- }
98
- log.debug('Creating new write connection');
99
- this._creatingWriteConnection = true;
100
- this._writeConnection = await this._createConnection('write');
101
- this._creatingWriteConnection = false;
102
- log.debug('Write connection created');
103
- this._writeConnection.on('error', err => log.error({ err }, 'AMQP write Connection error'));
104
- this._writeConnection.once('close', err => {
105
- log.error({ err }, 'Unexpected connection close.');
106
- delete this._writeConnection;
107
- });
108
- return this._writeConnection;
109
- }
110
-
111
- async _ensureConsumerChannel() {
112
- if (this.consumerChannel) {
113
- return this.consumerChannel;
114
- }
115
- if (this._creatingConsumerChannel) {
116
- do {
117
- await new Promise(resolve => setImmediate(resolve));
118
- } while (!this.consumerChannel);
119
- return this.consumerChannel;
120
- }
121
- log.debug({ prefetch: this.settings.RABBITMQ_PREFETCH_SAILOR }, 'Creating new consume channel');
122
- this._creatingConsumerChannel = true;
123
- const amqp = await this._ensureReadConnection();
124
- this.consumerChannel = await amqp.createChannel();
125
- log.debug('Consume channel created');
126
- this._creatingConsumerChannel = false;
127
- this.consumerChannel.prefetch(this.settings.RABBITMQ_PREFETCH_SAILOR);
128
- this.consumerChannel.on('error', err => log.error({ err }, 'Consumer channel error'));
129
- this.consumerChannel.once('close', () => {
130
- delete this.consumerChannel;
131
- if (this.consume) {
132
- log.warn('Channel unexpectedly closed, but we were listening. Reconnecting and re-listening queue');
133
- this.consume.consumerTag = undefined;
134
- // when RabbitMQ closes connection, amqplib will first emit 'close' for channel and then for connection
135
- // we use setImmediate to wait for connection 'close' event which will unset connection property
136
- // otherwise listenQueue will try to create channel on closing connection and fail hard
137
- setImmediate(() => this.listenQueue(this.consume.queue, this.consume.messageHandler));
138
- }
139
- });
140
- return this.consumerChannel;
141
- }
142
-
143
- async _ensurePublishChannel() {
144
- if (this.publishChannel) {
145
- return this.publishChannel;
146
- }
147
- if (this._creatingPublishChannel) {
148
- do {
149
- await new Promise(resolve => setImmediate(resolve));
150
- } while (!this.publishChannel);
151
- return this.publishChannel;
152
- }
153
- log.debug('Creating new publish connection and channel');
154
- this._creatingPublishChannel = true;
155
- const amqp = await this._ensureWriteConnection();
156
- this.publishChannel = await amqp.createConfirmChannel();
157
- log.debug('Publish connection and channel created');
158
- this._creatingPublishChannel = false;
159
- this.publishChannel.on('error', err => log.error({ err }, 'Publish channel error'));
160
- this.publishChannel.once('close', () => {
161
- delete this.publishChannel;
162
- });
163
- return this.publishChannel;
164
- }
165
-
166
- async _createConnection(name) {
167
- const uri = this.settings.AMQP_URI;
168
- const allowedAttempts = parseInt(this.settings.AMQP_RECONNECT_ATTEMPTS);
169
- let lastErr;
170
- let attempts = 0;
171
- while (attempts <= allowedAttempts) {
172
- if (attempts > 0) {
173
- await new Promise(resolve => setTimeout(resolve, parseInt(this.settings.AMQP_RECONNECT_TIMEOUT)));
174
- log.debug(
175
- {
176
- reconnectAttempt: attempts,
177
- AMQP_RECONNECT_ATTEMPTS: this.settings.AMQP_RECONNECT_ATTEMPTS
178
- },
179
- 'AMQP Reconnecting'
180
- );
181
- }
182
- try {
183
- return await amqplib.connect(uri,
184
- { clientProperties: { connection_name: `${os.hostname()}-${name}` } });
185
- } catch (err) {
186
- lastErr = err;
187
- log.error(err, 'AMQP Connection error');
188
- attempts++;
189
- }
190
- }
191
- throw lastErr;
192
- }
193
-
194
- async stopConsume() {
195
- if (!this.consume) {
196
- return;
197
- }
198
- const consume = this.consume;
199
- this.consume = undefined;
200
- await this.consumerChannel.cancel(consume.consumerTag);
201
- log.debug({ queue: consume.queue }, 'Stopped listening for messages');
202
- }
203
-
204
- async listenQueue(queue, messageHandler) {
205
- await this._ensureConsumerChannel();
206
-
207
- const { consumerTag } = await this.consumerChannel.consume(queue, async (amqpMessage) => {
208
- if (!amqpMessage) {
209
- log.warn('Consumer cancelled by rabbitmq');
210
- return;
211
- }
212
- let message;
213
- try {
214
- message = this._decodeMessage(amqpMessage);
215
- } catch (err) {
216
- log.error({ err, deliveryTag: amqpMessage.fields.deliveryTag },
217
- 'Error occurred while parsing message payload');
218
- this.reject(amqpMessage);
219
- return;
220
- }
221
- try {
222
- await messageHandler(message, amqpMessage);
223
- } catch (err) {
224
- log.error({ err, deliveryTag: amqpMessage.fields.deliveryTag }, 'Failed to process message, reject');
225
- this.reject(amqpMessage);
226
- }
227
- });
228
- log.debug({ queue }, 'Started listening for messages');
229
- this.consume = { queue, messageHandler, consumerTag };
230
- }
231
-
232
- _decodeMessage(amqpMessage) {
233
- log.trace('Message received');
234
- let message;
235
- if (this.settings.INPUT_FORMAT === 'error') {
236
- message = this._decodeErrorMessage(amqpMessage);
237
- } else {
238
- message = this._decodeDefaultMessage(amqpMessage);
239
- }
240
- message.headers = message.headers || {};
241
- if (amqpMessage.properties.headers.reply_to) {
242
- message.headers.reply_to = amqpMessage.properties.headers.reply_to;
243
- }
244
- return message;
245
- }
246
-
247
- _decodeDefaultMessage(amqpMessage) {
248
- const protocolVersion = Number(amqpMessage.properties.headers.protocolVersion || 1);
249
- return this._encryptor.decryptMessageContent(
250
- amqpMessage.content,
251
- protocolVersion < 2 ? 'base64' : undefined
252
- );
253
- }
254
-
255
- _decodeErrorMessage(amqpMessage) {
256
- const errorBody = JSON.parse(amqpMessage.content.toString());
257
- // NOTICE both error and errorInput are transferred as base64 encoded.
258
- // this does not depend on protocolVersion header of message (see _decodeDefault message)
259
- // this should be fixed in future, but it's OK at this moment
260
- if (errorBody.error) {
261
- errorBody.error = this._encryptor.decryptMessageContent(Buffer.from(errorBody.error), 'base64');
262
- }
263
- if (errorBody.errorInput) {
264
- errorBody.errorInput = this._encryptor.decryptMessageContent(errorBody.errorInput, 'base64');
265
- }
266
- return {
267
- body: errorBody,
268
- headers: amqpMessage.properties.headers
269
- };
270
- }
271
-
272
- decryptMessage(callback, message) {
273
- log.trace('Message received');
274
-
275
- if (message === null) {
276
- log.warn('NULL message received');
277
- return;
278
- }
279
-
280
- const protocolVersion = Number(message.properties.headers.protocolVersion || 1);
281
- let decryptedContent;
282
- try {
283
- decryptedContent = this._encryptor.decryptMessageContent(
284
- message.content,
285
- protocolVersion < 2 ? 'base64' : undefined
286
- );
287
- } catch (err) {
288
- log.error(err,
289
- 'Error occurred while parsing message #%j payload',
290
- message.fields.deliveryTag
291
- );
292
- return this.reject(message);
293
- }
294
- decryptedContent.headers = decryptedContent.headers || {};
295
- if (message.properties.headers.reply_to) {
296
- decryptedContent.headers.reply_to = message.properties.headers.reply_to;
297
- }
298
-
299
- try {
300
- // pass to callback both decrypted content & original message
301
- callback(decryptedContent, message);
302
- } catch (err) {
303
- log.error(err, 'Failed to process message #%j, reject', message.fields.deliveryTag);
304
- return this.reject(message);
305
- }
306
- }
307
-
308
- ack(message) {
309
- log.debug('Message #%j ack', message.fields.deliveryTag);
310
- this.consumerChannel.ack(message);
311
- }
312
-
313
- reject(message) {
314
- log.debug('Message #%j reject', message.fields.deliveryTag);
315
- return this.consumerChannel.reject(message, false);
316
- }
317
-
318
- async sendToExchange(exchangeName, routingKey, payload, options, throttle) {
319
- if (throttle) {
320
- log.debug('Throttling outgoing message');
321
- await throttle();
322
- }
323
- const buffer = Buffer.from(payload);
324
-
325
- return this.publishMessage(exchangeName, routingKey, buffer, options, 0);
326
- }
327
-
328
- async publishMessage(exchangeName, routingKey, payloadBuffer, options, iteration) {
329
- const settings = this.settings;
330
- if (iteration) {
331
- options.headers.retry = iteration;
332
- }
333
- // AMQP_PERSISTENT_MESSAGES is false by default if not specified by env var
334
- options.persistent = this.settings.AMQP_PERSISTENT_MESSAGES;
335
-
336
- log.debug('Current memory usage: %s Mb', process.memoryUsage().heapUsed / 1048576);
337
- log.trace('Pushing to exchange=%s, routingKey=%s, messageSize=%d, options=%j, iteration=%d',
338
- exchangeName, routingKey, payloadBuffer.length, options, iteration);
339
- try {
340
- const result = await this._promisifiedPublish(exchangeName, routingKey, payloadBuffer, options);
341
- if (!result) {
342
- log.warn('Buffer full when publishing a message to '
343
- + 'exchange=%s with routingKey=%s', exchangeName, routingKey);
344
- }
345
- return result;
346
- } catch (error) {
347
- if (error instanceof IllegalOperationError) {
348
- log.error(error, `Failed on publishing ${options.headers.messageId} message to MQ`);
349
- throw new Error(`Failed on publishing ${options.headers.messageId} message to MQ: ` + error);
350
- }
351
- log.error(error, 'Failed on publishing message to queue');
352
- const delay = this._getDelay(
353
- settings.AMQP_PUBLISH_RETRY_DELAY,
354
- settings.AMQP_PUBLISH_MAX_RETRY_DELAY,
355
- iteration
356
- );
357
- await this._sleep(delay);
358
- iteration += 1;
359
- if (iteration < settings.AMQP_PUBLISH_RETRY_ATTEMPTS) {
360
- return this.publishMessage(exchangeName, routingKey, payloadBuffer, options, iteration);
361
- } else {
362
- throw new Error(`Failed on publishing ${options.headers.messageId} message to MQ: ` + error);
363
- }
364
- }
365
- }
366
-
367
- _getDelay(defaultDelay, maxDelay, iteration) {
368
- log.debug({ defaultDelay }, 'Current delay');
369
- log.debug({ maxDelay }, 'Current delay');
370
- const delay = Math.min(defaultDelay * Math.pow(2, iteration), maxDelay);
371
- log.debug({ delay }, 'Calculated delay');
372
- return delay;
373
- }
374
-
375
- async _sleep(time) {
376
- await new Promise(resolve => setTimeout(resolve, time));
377
- }
378
-
379
- async _promisifiedPublish(exchangeName, routingKey, payloadBuffer, options) {
380
- await this._ensurePublishChannel();
381
- try {
382
- let result;
383
- const publishChannel = this.publishChannel;
384
- const publishPromise = new Promise((resolve, reject) => {
385
- result = publishChannel.publish(exchangeName, routingKey, payloadBuffer, options, (err, ok) => {
386
- err ? reject(err) : resolve(ok);
387
- });
388
- });
389
- await Promise.all([
390
- (async () => {
391
- if (this.settings.PROCESS_AMQP_DRAIN && !result) {
392
- log.debug('Amqp buffer is full: waiting for drain event..');
393
- await eventToPromise(this.publishChannel, 'drain');
394
- log.debug('Amqp buffer drained!');
395
- result = true;
396
- }
397
- })(),
398
- publishPromise
399
- ]);
400
- return result;
401
- } catch (error) {
402
- throw error;
403
- }
404
- }
405
-
406
- encryptMessageContent(body, protocolVersion = 1) {
407
- return this._encryptor.encryptMessageContent(
408
- body,
409
- protocolVersion < 2
410
- ? 'base64'
411
- : undefined
412
- );
413
- }
414
-
415
- async prepareMessageAndSendToExchange(data, properties, routingKey, throttle) {
416
- const settings = this.settings;
417
-
418
- data.headers = filterMessageHeaders(data.headers);
419
- const protocolVersion = Number(properties.headers.protocolVersion || 1);
420
- const encryptedData = this.encryptMessageContent(data, protocolVersion);
421
-
422
- if (encryptedData.length > settings.OUTGOING_MESSAGE_SIZE_LIMIT) {
423
- const error = new Error(`Outgoing message size ${encryptedData.length}`
424
- + ` exceeds limit of ${settings.OUTGOING_MESSAGE_SIZE_LIMIT}.`);
425
- log.error(error);
426
- throw error;
427
- }
428
-
429
- return this.sendToExchange(settings.PUBLISH_MESSAGES_TO, routingKey, encryptedData, properties, throttle);
430
- }
431
-
432
- async sendData(data, headers, throttle) {
433
- const properties = this._createPropsFromHeaders(headers);
434
- const settings = this.settings;
435
- const routingKey = getRoutingKeyFromHeaders(data.headers) || settings.DATA_ROUTING_KEY;
436
- properties.headers.protocolVersion = settings.PROTOCOL_VERSION;
437
- return this.prepareMessageAndSendToExchange(data, properties, routingKey, throttle);
438
- }
439
-
440
- async sendHttpReply(data, headers) {
441
- const properties = this._createPropsFromHeaders(headers);
442
- const routingKey = headers.reply_to;
443
- properties.headers.protocolVersion = 1;
444
-
445
- if (!routingKey) {
446
- throw new Error(`Component emitted 'httpReply' event but 'reply_to' was not found in AMQP headers`);
447
- }
448
- return this.prepareMessageAndSendToExchange(data, properties, routingKey);
449
- }
450
-
451
- async sendError(err, headers, originalMessage, throttle) {
452
- // NOTICE both error and errorInput are transferred as base64 encoded.
453
- // this does not depend on protocolVersion header of message (see _decodeDefaultMessage or sendData methods)
454
- // this should be fixed in future, but it's OK at this moment
455
- const properties = this._createPropsFromHeaders(headers);
456
- const settings = this.settings;
457
-
458
- const encryptedError = this._encryptor.encryptMessageContent({
459
- name: err.name,
460
- message: err.message,
461
- stack: err.stack
462
- }, 'base64').toString();
463
-
464
- const payload = {
465
- error: encryptedError
466
- };
467
- if (originalMessage && originalMessage.content) {
468
- const protocolVersion = Number(originalMessage.properties.headers.protocolVersion || 1);
469
- if (protocolVersion >= 2) {
470
- payload.errorInput = this._encryptor.encryptMessageContent(
471
- this._encryptor.decryptMessageContent(originalMessage.content),
472
- 'base64'
473
- ).toString();
474
- } else {
475
- payload.errorInput = originalMessage.content.toString();
476
- }
477
- }
478
- const errorPayload = JSON.stringify(payload);
479
-
480
- let result = this.sendToExchange(
481
- settings.PUBLISH_MESSAGES_TO,
482
- settings.ERROR_ROUTING_KEY,
483
- errorPayload, properties,
484
- throttle
485
- );
486
-
487
- if (!settings.NO_ERROR_REPLIES && headers.reply_to) {
488
- log.debug('Sending error to %s', headers.reply_to);
489
- const replyToOptions = _.cloneDeep(properties);
490
- replyToOptions.headers[HEADER_ERROR_RESPONSE] = true;
491
- result = this.sendToExchange(settings.PUBLISH_MESSAGES_TO,
492
- headers.reply_to, encryptedError, replyToOptions);
493
- }
494
-
495
- return result;
496
- }
497
-
498
- async sendRebound(reboundError, originalMessage) {
499
- const { settings } = this;
500
- let { properties: { headers } } = originalMessage;
501
- headers = {
502
- ...headers,
503
- end: new Date().getTime(),
504
- reboundReason: reboundError.message
505
- };
506
- log.trace('Rebound message');
507
- let reboundIteration = 1;
508
-
509
- if (headers.reboundIteration && typeof headers.reboundIteration === 'number') {
510
- reboundIteration = headers.reboundIteration + 1;
511
- }
512
-
513
- if (reboundIteration > settings.REBOUND_LIMIT) {
514
- return this.sendError(
515
- new Error('Rebound limit exceeded'),
516
- headers,
517
- originalMessage
518
- );
519
- } else {
520
- const properties = {
521
- ...originalMessage.properties,
522
- // retry in 15 sec, 30 sec, 1 min, 2 min, 4 min, 8 min, etc.
523
- expiration: Math.pow(2, reboundIteration - 1) * settings.REBOUND_INITIAL_EXPIRATION,
524
- headers: {
525
- ...headers,
526
- reboundIteration
527
- }
528
- };
529
-
530
- return this.sendToExchange(
531
- settings.PUBLISH_MESSAGES_TO,
532
- settings.REBOUND_ROUTING_KEY,
533
- originalMessage.content,
534
- properties
535
- );
536
- }
537
- }
538
-
539
- async sendSnapshot(data, headers, throttle) {
540
- const settings = this.settings;
541
- const exchange = settings.PUBLISH_MESSAGES_TO;
542
- const routingKey = settings.SNAPSHOT_ROUTING_KEY;
543
- const payload = JSON.stringify(data);
544
- const properties = this._createPropsFromHeaders(headers);
545
- return this.sendToExchange(exchange, routingKey, payload, properties, throttle);
546
- }
547
-
548
- _createPropsFromHeaders(headers) {
549
- return {
550
- contentType: 'application/json',
551
- contentEncoding: 'utf8',
552
- mandatory: true,
553
- headers: {
554
- ...headers,
555
- messageId: headers.messageId || uuid.v4()
556
- }
557
- };
558
- }
559
- }
560
-
561
- function getRoutingKeyFromHeaders(headers) {
562
- if (!headers) {
563
- return null;
564
- }
565
-
566
- function headerNamesToLowerCase(result, value, key) {
567
- result[key.toLowerCase()] = value;
568
- }
569
-
570
- const lowerCaseHeaders = _.transform(headers, headerNamesToLowerCase, {});
571
-
572
- return lowerCaseHeaders[HEADER_ROUTING_KEY];
573
- }
574
-
575
- function filterMessageHeaders(headers = {}) {
576
- return _.transform(headers, (result, value, key) => {
577
- if ([HEADER_ROUTING_KEY].includes(key.toLowerCase())) {
578
- return;
579
- }
580
- result[key] = value;
581
- }, {});
582
- }
583
-
584
- exports.Amqp = Amqp;
1
+ const log = require('./logging.js');
2
+ const amqplib = require('amqplib');
3
+ const { IllegalOperationError } = require('amqplib/lib/error');
4
+ const Encryptor = require('./encryptor.js');
5
+ const _ = require('lodash');
6
+ const eventToPromise = require('event-to-promise');
7
+ const uuid = require('uuid');
8
+ const os = require('os');
9
+
10
+ const HEADER_ROUTING_KEY = 'x-eio-routing-key';
11
+ const HEADER_ERROR_RESPONSE = 'x-eio-error-response';
12
+
13
+ class Amqp {
14
+ constructor(settings) {
15
+ this.settings = settings;
16
+ this._encryptor = new Encryptor(this.settings.MESSAGE_CRYPTO_PASSWORD, this.settings.MESSAGE_CRYPTO_IV);
17
+ this.closed = true;
18
+ this.consume = undefined;
19
+ }
20
+
21
+ async connect() {
22
+ await Promise.all([this._ensurePublishChannel(), this._ensureConsumerChannel()]);
23
+ this.closed = false;
24
+ }
25
+
26
+ async disconnect() {
27
+ await this.stopConsume();
28
+ this.closed = true;
29
+ log.trace('Close AMQP connections');
30
+ if (this._readConnection) {
31
+ this._readConnection.removeAllListeners('close');
32
+ }
33
+ if (this._writeConnection) {
34
+ this._writeConnection.removeAllListeners('close');
35
+ }
36
+ if (this.consumerChannel) {
37
+ this.consumerChannel.removeAllListeners('close');
38
+ }
39
+ if (this.publishChannel) {
40
+ this.publishChannel.removeAllListeners('close');
41
+ }
42
+ try {
43
+ await this.consumerChannel.close();
44
+ } catch (alreadyClosed) {
45
+ log.debug('Subscribe channel is closed already');
46
+ }
47
+ try {
48
+ await this.publishChannel.close();
49
+ } catch (alreadyClosed) {
50
+ log.debug('Publish channel is closed already');
51
+ }
52
+ try {
53
+ await this._readConnection.close();
54
+ } catch (alreadyClosed) {
55
+ log.debug('AMQP read connection is closed already');
56
+ }
57
+ try {
58
+ await this._writeConnection.close();
59
+ } catch (alreadyClosed) {
60
+ log.debug('AMQP write connection is closed already');
61
+ }
62
+ log.debug('Successfully closed AMQP connections');
63
+ }
64
+
65
+ async _ensureReadConnection() {
66
+ if (this._readConnection) {
67
+ return this._readConnection;
68
+ }
69
+ if (this._creatingReadConnection) {
70
+ do {
71
+ await new Promise(resolve => setImmediate(resolve));
72
+ } while (!this._readConnection);
73
+ return this._readConnection;
74
+ }
75
+ log.debug('Creating new read connection');
76
+ this._creatingReadConnection = true;
77
+ this._readConnection = await this._createConnection('read');
78
+ this._creatingReadConnection = false;
79
+ log.debug('Read connection created');
80
+ this._readConnection.on('error', err => log.error({ err }, 'AMQP read Connection error'));
81
+ this._readConnection.once('close', err => {
82
+ log.error({ err }, 'Unexpected connection close.');
83
+ delete this._readConnection;
84
+ });
85
+ return this._readConnection;
86
+ }
87
+
88
+ async _ensureWriteConnection() {
89
+ if (this._writeConnection) {
90
+ return this._writeConnection;
91
+ }
92
+ if (this._creatingWriteConnection) {
93
+ do {
94
+ await new Promise(resolve => setImmediate(resolve));
95
+ } while (!this._writeConnection);
96
+ return this._writeConnection;
97
+ }
98
+ log.debug('Creating new write connection');
99
+ this._creatingWriteConnection = true;
100
+ this._writeConnection = await this._createConnection('write');
101
+ this._creatingWriteConnection = false;
102
+ log.debug('Write connection created');
103
+ this._writeConnection.on('error', err => log.error({ err }, 'AMQP write Connection error'));
104
+ this._writeConnection.once('close', err => {
105
+ log.error({ err }, 'Unexpected connection close.');
106
+ delete this._writeConnection;
107
+ });
108
+ return this._writeConnection;
109
+ }
110
+
111
+ async _ensureConsumerChannel() {
112
+ if (this.consumerChannel) {
113
+ return this.consumerChannel;
114
+ }
115
+ if (this._creatingConsumerChannel) {
116
+ do {
117
+ await new Promise(resolve => setImmediate(resolve));
118
+ } while (!this.consumerChannel);
119
+ return this.consumerChannel;
120
+ }
121
+ log.debug({ prefetch: this.settings.RABBITMQ_PREFETCH_SAILOR }, 'Creating new consume channel');
122
+ this._creatingConsumerChannel = true;
123
+ const amqp = await this._ensureReadConnection();
124
+ this.consumerChannel = await amqp.createChannel();
125
+ log.debug('Consume channel created');
126
+ this._creatingConsumerChannel = false;
127
+ this.consumerChannel.prefetch(this.settings.RABBITMQ_PREFETCH_SAILOR);
128
+ this.consumerChannel.on('error', err => log.error({ err }, 'Consumer channel error'));
129
+ this.consumerChannel.once('close', () => {
130
+ delete this.consumerChannel;
131
+ if (this.consume) {
132
+ log.warn('Channel unexpectedly closed, but we were listening. Reconnecting and re-listening queue');
133
+ this.consume.consumerTag = undefined;
134
+ // when RabbitMQ closes connection, amqplib will first emit 'close' for channel and then for connection
135
+ // we use setImmediate to wait for connection 'close' event which will unset connection property
136
+ // otherwise listenQueue will try to create channel on closing connection and fail hard
137
+ setImmediate(() => this.listenQueue(this.consume.queue, this.consume.messageHandler));
138
+ }
139
+ });
140
+ return this.consumerChannel;
141
+ }
142
+
143
+ async _ensurePublishChannel() {
144
+ if (this.publishChannel) {
145
+ return this.publishChannel;
146
+ }
147
+ if (this._creatingPublishChannel) {
148
+ do {
149
+ await new Promise(resolve => setImmediate(resolve));
150
+ } while (!this.publishChannel);
151
+ return this.publishChannel;
152
+ }
153
+ log.debug('Creating new publish connection and channel');
154
+ this._creatingPublishChannel = true;
155
+ const amqp = await this._ensureWriteConnection();
156
+ this.publishChannel = await amqp.createConfirmChannel();
157
+ log.debug('Publish connection and channel created');
158
+ this._creatingPublishChannel = false;
159
+ this.publishChannel.on('error', err => log.error({ err }, 'Publish channel error'));
160
+ this.publishChannel.once('close', () => {
161
+ delete this.publishChannel;
162
+ });
163
+ return this.publishChannel;
164
+ }
165
+
166
+ async _createConnection(name) {
167
+ const uri = this.settings.AMQP_URI;
168
+ const allowedAttempts = parseInt(this.settings.AMQP_RECONNECT_ATTEMPTS);
169
+ let lastErr;
170
+ let attempts = 0;
171
+ while (attempts <= allowedAttempts) {
172
+ if (attempts > 0) {
173
+ await new Promise(resolve => setTimeout(resolve, parseInt(this.settings.AMQP_RECONNECT_TIMEOUT)));
174
+ log.debug(
175
+ {
176
+ reconnectAttempt: attempts,
177
+ AMQP_RECONNECT_ATTEMPTS: this.settings.AMQP_RECONNECT_ATTEMPTS
178
+ },
179
+ 'AMQP Reconnecting'
180
+ );
181
+ }
182
+ try {
183
+ return await amqplib.connect(uri,
184
+ { clientProperties: { connection_name: `${os.hostname()}-${name}` } });
185
+ } catch (err) {
186
+ lastErr = err;
187
+ log.error(err, 'AMQP Connection error');
188
+ attempts++;
189
+ }
190
+ }
191
+ throw lastErr;
192
+ }
193
+
194
+ async stopConsume() {
195
+ if (!this.consume) {
196
+ return;
197
+ }
198
+ const consume = this.consume;
199
+ this.consume = undefined;
200
+ await this.consumerChannel.cancel(consume.consumerTag);
201
+ log.debug({ queue: consume.queue }, 'Stopped listening for messages');
202
+ }
203
+
204
+ async listenQueue(queue, messageHandler) {
205
+ await this._ensureConsumerChannel();
206
+
207
+ const { consumerTag } = await this.consumerChannel.consume(queue, async (amqpMessage) => {
208
+ if (!amqpMessage) {
209
+ log.warn('Consumer cancelled by rabbitmq');
210
+ return;
211
+ }
212
+ let message;
213
+ try {
214
+ message = this._decodeMessage(amqpMessage);
215
+ } catch (err) {
216
+ log.error({ err, deliveryTag: amqpMessage.fields.deliveryTag },
217
+ 'Error occurred while parsing message payload');
218
+ this.reject(amqpMessage);
219
+ return;
220
+ }
221
+ try {
222
+ await messageHandler(message, amqpMessage);
223
+ } catch (err) {
224
+ log.error({ err, deliveryTag: amqpMessage.fields.deliveryTag }, 'Failed to process message, reject');
225
+ this.reject(amqpMessage);
226
+ }
227
+ });
228
+ log.debug({ queue }, 'Started listening for messages');
229
+ this.consume = { queue, messageHandler, consumerTag };
230
+ }
231
+
232
+ _decodeMessage(amqpMessage) {
233
+ log.trace('Message received');
234
+ let message;
235
+ if (this.settings.INPUT_FORMAT === 'error') {
236
+ message = this._decodeErrorMessage(amqpMessage);
237
+ } else {
238
+ message = this._decodeDefaultMessage(amqpMessage);
239
+ }
240
+ message.headers = message.headers || {};
241
+ if (amqpMessage.properties.headers.reply_to) {
242
+ message.headers.reply_to = amqpMessage.properties.headers.reply_to;
243
+ }
244
+ return message;
245
+ }
246
+
247
+ _decodeDefaultMessage(amqpMessage) {
248
+ const protocolVersion = Number(amqpMessage.properties.headers.protocolVersion || 1);
249
+ return this._encryptor.decryptMessageContent(
250
+ amqpMessage.content,
251
+ protocolVersion < 2 ? 'base64' : undefined
252
+ );
253
+ }
254
+
255
+ _decodeErrorMessage(amqpMessage) {
256
+ const errorBody = JSON.parse(amqpMessage.content.toString());
257
+ // NOTICE both error and errorInput are transferred as base64 encoded.
258
+ // this does not depend on protocolVersion header of message (see _decodeDefault message)
259
+ // this should be fixed in future, but it's OK at this moment
260
+ if (errorBody.error) {
261
+ errorBody.error = this._encryptor.decryptMessageContent(Buffer.from(errorBody.error), 'base64');
262
+ }
263
+ if (errorBody.errorInput) {
264
+ errorBody.errorInput = this._encryptor.decryptMessageContent(errorBody.errorInput, 'base64');
265
+ }
266
+ return {
267
+ body: errorBody,
268
+ headers: amqpMessage.properties.headers
269
+ };
270
+ }
271
+
272
+ decryptMessage(callback, message) {
273
+ log.trace('Message received');
274
+
275
+ if (message === null) {
276
+ log.warn('NULL message received');
277
+ return;
278
+ }
279
+
280
+ const protocolVersion = Number(message.properties.headers.protocolVersion || 1);
281
+ let decryptedContent;
282
+ try {
283
+ decryptedContent = this._encryptor.decryptMessageContent(
284
+ message.content,
285
+ protocolVersion < 2 ? 'base64' : undefined
286
+ );
287
+ } catch (err) {
288
+ log.error(err,
289
+ 'Error occurred while parsing message #%j payload',
290
+ message.fields.deliveryTag
291
+ );
292
+ return this.reject(message);
293
+ }
294
+ decryptedContent.headers = decryptedContent.headers || {};
295
+ if (message.properties.headers.reply_to) {
296
+ decryptedContent.headers.reply_to = message.properties.headers.reply_to;
297
+ }
298
+
299
+ try {
300
+ // pass to callback both decrypted content & original message
301
+ callback(decryptedContent, message);
302
+ } catch (err) {
303
+ log.error(err, 'Failed to process message #%j, reject', message.fields.deliveryTag);
304
+ return this.reject(message);
305
+ }
306
+ }
307
+
308
+ ack(message) {
309
+ log.debug('Message #%j ack', message.fields.deliveryTag);
310
+ this.consumerChannel.ack(message);
311
+ }
312
+
313
+ reject(message) {
314
+ log.debug('Message #%j reject', message.fields.deliveryTag);
315
+ return this.consumerChannel.reject(message, false);
316
+ }
317
+
318
+ async sendToExchange(exchangeName, routingKey, payload, options, throttle) {
319
+ if (throttle) {
320
+ log.debug('Throttling outgoing message');
321
+ await throttle();
322
+ }
323
+ const buffer = Buffer.from(payload);
324
+
325
+ return this.publishMessage(exchangeName, routingKey, buffer, options, 0);
326
+ }
327
+
328
+ async publishMessage(exchangeName, routingKey, payloadBuffer, options, iteration) {
329
+ const settings = this.settings;
330
+ if (iteration) {
331
+ options.headers.retry = iteration;
332
+ }
333
+ // AMQP_PERSISTENT_MESSAGES is false by default if not specified by env var
334
+ options.persistent = this.settings.AMQP_PERSISTENT_MESSAGES;
335
+
336
+ log.debug('Current memory usage: %s Mb', process.memoryUsage().heapUsed / 1048576);
337
+ log.trace('Pushing to exchange=%s, routingKey=%s, messageSize=%d, options=%j, iteration=%d',
338
+ exchangeName, routingKey, payloadBuffer.length, options, iteration);
339
+ try {
340
+ const result = await this._promisifiedPublish(exchangeName, routingKey, payloadBuffer, options);
341
+ if (!result) {
342
+ log.warn('Buffer full when publishing a message to '
343
+ + 'exchange=%s with routingKey=%s', exchangeName, routingKey);
344
+ }
345
+ return result;
346
+ } catch (error) {
347
+ if (error instanceof IllegalOperationError) {
348
+ log.error(error, `Failed on publishing ${options.headers.messageId} message to MQ`);
349
+ throw new Error(`Failed on publishing ${options.headers.messageId} message to MQ: ` + error);
350
+ }
351
+ log.error(error, 'Failed on publishing message to queue');
352
+ const delay = this._getDelay(
353
+ settings.AMQP_PUBLISH_RETRY_DELAY,
354
+ settings.AMQP_PUBLISH_MAX_RETRY_DELAY,
355
+ iteration
356
+ );
357
+ await this._sleep(delay);
358
+ iteration += 1;
359
+ if (iteration < settings.AMQP_PUBLISH_RETRY_ATTEMPTS) {
360
+ return this.publishMessage(exchangeName, routingKey, payloadBuffer, options, iteration);
361
+ } else {
362
+ throw new Error(`Failed on publishing ${options.headers.messageId} message to MQ: ` + error);
363
+ }
364
+ }
365
+ }
366
+
367
+ _getDelay(defaultDelay, maxDelay, iteration) {
368
+ log.debug({ defaultDelay }, 'Current delay');
369
+ log.debug({ maxDelay }, 'Current delay');
370
+ const delay = Math.min(defaultDelay * Math.pow(2, iteration), maxDelay);
371
+ log.debug({ delay }, 'Calculated delay');
372
+ return delay;
373
+ }
374
+
375
+ async _sleep(time) {
376
+ await new Promise(resolve => setTimeout(resolve, time));
377
+ }
378
+
379
+ async _promisifiedPublish(exchangeName, routingKey, payloadBuffer, options) {
380
+ await this._ensurePublishChannel();
381
+ try {
382
+ let result;
383
+ const publishChannel = this.publishChannel;
384
+ const publishPromise = new Promise((resolve, reject) => {
385
+ result = publishChannel.publish(exchangeName, routingKey, payloadBuffer, options, (err, ok) => {
386
+ err ? reject(err) : resolve(ok);
387
+ });
388
+ });
389
+ await Promise.all([
390
+ (async () => {
391
+ if (this.settings.PROCESS_AMQP_DRAIN && !result) {
392
+ log.debug('Amqp buffer is full: waiting for drain event..');
393
+ await eventToPromise(this.publishChannel, 'drain');
394
+ log.debug('Amqp buffer drained!');
395
+ result = true;
396
+ }
397
+ })(),
398
+ publishPromise
399
+ ]);
400
+ return result;
401
+ } catch (error) {
402
+ throw error;
403
+ }
404
+ }
405
+
406
+ encryptMessageContent(body, protocolVersion = 1) {
407
+ return this._encryptor.encryptMessageContent(
408
+ body,
409
+ protocolVersion < 2
410
+ ? 'base64'
411
+ : undefined
412
+ );
413
+ }
414
+
415
+ async prepareMessageAndSendToExchange(data, properties, routingKey, throttle) {
416
+ const settings = this.settings;
417
+
418
+ data.headers = filterMessageHeaders(data.headers);
419
+ const protocolVersion = Number(properties.headers.protocolVersion || 1);
420
+ const encryptedData = this.encryptMessageContent(data, protocolVersion);
421
+
422
+ if (encryptedData.length > settings.OUTGOING_MESSAGE_SIZE_LIMIT) {
423
+ const error = new Error(`Outgoing message size ${encryptedData.length}`
424
+ + ` exceeds limit of ${settings.OUTGOING_MESSAGE_SIZE_LIMIT}.`);
425
+ log.error(error);
426
+ throw error;
427
+ }
428
+
429
+ return this.sendToExchange(settings.PUBLISH_MESSAGES_TO, routingKey, encryptedData, properties, throttle);
430
+ }
431
+
432
+ async sendData(data, headers, throttle) {
433
+ const properties = this._createPropsFromHeaders(headers);
434
+ const settings = this.settings;
435
+ const routingKey = getRoutingKeyFromHeaders(data.headers) || settings.DATA_ROUTING_KEY;
436
+ properties.headers.protocolVersion = settings.PROTOCOL_VERSION;
437
+ return this.prepareMessageAndSendToExchange(data, properties, routingKey, throttle);
438
+ }
439
+
440
+ async sendHttpReply(data, headers) {
441
+ const properties = this._createPropsFromHeaders(headers);
442
+ const routingKey = headers.reply_to;
443
+ properties.headers.protocolVersion = 1;
444
+
445
+ if (!routingKey) {
446
+ throw new Error(`Component emitted 'httpReply' event but 'reply_to' was not found in AMQP headers`);
447
+ }
448
+ return this.prepareMessageAndSendToExchange(data, properties, routingKey);
449
+ }
450
+
451
+ async sendError(err, headers, originalMessage, throttle) {
452
+ // NOTICE both error and errorInput are transferred as base64 encoded.
453
+ // this does not depend on protocolVersion header of message (see _decodeDefaultMessage or sendData methods)
454
+ // this should be fixed in future, but it's OK at this moment
455
+ const properties = this._createPropsFromHeaders(headers);
456
+ const settings = this.settings;
457
+
458
+ const encryptedError = this._encryptor.encryptMessageContent({
459
+ name: err.name,
460
+ message: err.message,
461
+ stack: err.stack
462
+ }, 'base64').toString();
463
+
464
+ const payload = {
465
+ error: encryptedError
466
+ };
467
+ if (originalMessage && originalMessage.content) {
468
+ const protocolVersion = Number(originalMessage.properties.headers.protocolVersion || 1);
469
+ if (protocolVersion >= 2) {
470
+ payload.errorInput = this._encryptor.encryptMessageContent(
471
+ this._encryptor.decryptMessageContent(originalMessage.content),
472
+ 'base64'
473
+ ).toString();
474
+ } else {
475
+ payload.errorInput = originalMessage.content.toString();
476
+ }
477
+ }
478
+ const errorPayload = JSON.stringify(payload);
479
+
480
+ let result = this.sendToExchange(
481
+ settings.PUBLISH_MESSAGES_TO,
482
+ settings.ERROR_ROUTING_KEY,
483
+ errorPayload, properties,
484
+ throttle
485
+ );
486
+
487
+ if (!settings.NO_ERROR_REPLIES && headers.reply_to) {
488
+ log.debug('Sending error to %s', headers.reply_to);
489
+ const replyToOptions = _.cloneDeep(properties);
490
+ replyToOptions.headers[HEADER_ERROR_RESPONSE] = true;
491
+ result = this.sendToExchange(settings.PUBLISH_MESSAGES_TO,
492
+ headers.reply_to, encryptedError, replyToOptions);
493
+ }
494
+
495
+ return result;
496
+ }
497
+
498
+ async sendRebound(reboundError, originalMessage) {
499
+ const { settings } = this;
500
+ let { properties: { headers } } = originalMessage;
501
+ headers = {
502
+ ...headers,
503
+ end: new Date().getTime(),
504
+ reboundReason: reboundError.message
505
+ };
506
+ log.trace('Rebound message');
507
+ let reboundIteration = 1;
508
+
509
+ if (headers.reboundIteration && typeof headers.reboundIteration === 'number') {
510
+ reboundIteration = headers.reboundIteration + 1;
511
+ }
512
+
513
+ if (reboundIteration > settings.REBOUND_LIMIT) {
514
+ return this.sendError(
515
+ new Error('Rebound limit exceeded'),
516
+ headers,
517
+ originalMessage
518
+ );
519
+ } else {
520
+ const properties = {
521
+ ...originalMessage.properties,
522
+ // retry in 15 sec, 30 sec, 1 min, 2 min, 4 min, 8 min, etc.
523
+ expiration: Math.pow(2, reboundIteration - 1) * settings.REBOUND_INITIAL_EXPIRATION,
524
+ headers: {
525
+ ...headers,
526
+ reboundIteration
527
+ }
528
+ };
529
+
530
+ return this.sendToExchange(
531
+ settings.PUBLISH_MESSAGES_TO,
532
+ settings.REBOUND_ROUTING_KEY,
533
+ originalMessage.content,
534
+ properties
535
+ );
536
+ }
537
+ }
538
+
539
+ async sendSnapshot(data, headers, throttle) {
540
+ const settings = this.settings;
541
+ const exchange = settings.PUBLISH_MESSAGES_TO;
542
+ const routingKey = settings.SNAPSHOT_ROUTING_KEY;
543
+ const payload = JSON.stringify(data);
544
+ const properties = this._createPropsFromHeaders(headers);
545
+ return this.sendToExchange(exchange, routingKey, payload, properties, throttle);
546
+ }
547
+
548
+ _createPropsFromHeaders(headers) {
549
+ return {
550
+ contentType: 'application/json',
551
+ contentEncoding: 'utf8',
552
+ mandatory: true,
553
+ headers: {
554
+ ...headers,
555
+ messageId: headers.messageId || uuid.v4()
556
+ }
557
+ };
558
+ }
559
+ }
560
+
561
+ function getRoutingKeyFromHeaders(headers) {
562
+ if (!headers) {
563
+ return null;
564
+ }
565
+
566
+ function headerNamesToLowerCase(result, value, key) {
567
+ result[key.toLowerCase()] = value;
568
+ }
569
+
570
+ const lowerCaseHeaders = _.transform(headers, headerNamesToLowerCase, {});
571
+
572
+ return lowerCaseHeaders[HEADER_ROUTING_KEY];
573
+ }
574
+
575
+ function filterMessageHeaders(headers = {}) {
576
+ return _.transform(headers, (result, value, key) => {
577
+ if ([HEADER_ROUTING_KEY].includes(key.toLowerCase())) {
578
+ return;
579
+ }
580
+ result[key] = value;
581
+ }, {});
582
+ }
583
+
584
+ exports.Amqp = Amqp;