elasticio-sailor-nodejs 2.7.0-dev3 → 2.7.1-dev1

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.
@@ -0,0 +1,8 @@
1
+ # https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners
2
+
3
+ # These owners will be the default owners for everything in
4
+ # the repo. Unless a later match takes precedence,
5
+ # elasticio/platform-qa and @elasticio/platform will be requested for
6
+ # review when someone opens a pull request.
7
+ # Introduced to make QA-team's code reivew approve required https://github.com/elasticio/elasticio/issues/5139
8
+ * @elasticio/platform-qa
package/.nsprc ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "GHSA-wc69-rhjr-hc9g": {
3
+ "active": true,
4
+ "notes": "Bunyan library set only new Date to momentjs as parameter"
5
+ }
6
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,144 @@
1
+ ## 2.7.0 (September 15, 2022)
2
+
3
+ * Add AMQP_PERSISTENT_MESSAGES configuration env var to enable persistent delivery mode.
4
+
5
+ ## 2.6.29 (July 14, 2022)
6
+
7
+ * Enabled keep-alive for global HTTPS agent ([#6359](https://github.com/elasticio/elasticio/issues/6359))
8
+
9
+ ## 2.6.28 (June 21, 2022)
10
+
11
+ * Fix: "sailor-nodejs ignores errors from maester during lightweight message upload" [#6233](https://github.com/elasticio/elasticio/issues/6233))
12
+
13
+ ## 2.6.27 (March 10, 2022)
14
+
15
+ * Added npm audit to CI and fixed all dependencies
16
+
17
+ ## 2.6.26 (June 24, 2021)
18
+
19
+ * Replaced exit on "unhandledRejection" with error logging
20
+
21
+ ## 2.6.25 (June 17, 2021)
22
+
23
+ * Improve consume performance by switching back to pushing instead of polling, keeping reconnections mechanism
24
+ ([#5432](https://github.com/elasticio/elasticio/issues/5432))
25
+
26
+ ## 2.6.24 (January 27, 2021)
27
+
28
+ * Fix "Big messages processing is not stable" errors ([#5051](https://github.com/elasticio/elasticio/issues/5051))
29
+
30
+ ## 2.6.23 (January 4, 2021)
31
+
32
+ * Fix "Cannot read property 'publish' of undefined" error ([#5036](https://github.com/elasticio/elasticio/issues/5036))
33
+
34
+ ## 2.6.22 (December 23, 2020)
35
+
36
+ * Enabled keep-alive for global HTTP agent ([#5012](#https://github.com/elasticio/elasticio/issues/5012))
37
+
38
+ ## 2.6.21 (December 3, 2020)
39
+
40
+ * Fixed rebound message expiry time ([#4950](https://github.com/elasticio/elasticio/issues/4950))
41
+
42
+ ## 2.6.20 (December 1, 2020)
43
+
44
+ * Fixed rebound message headers ([#4950](https://github.com/elasticio/elasticio/issues/4950))
45
+
46
+ ## 2.6.19 (November 23, 2020)
47
+
48
+ * Separate connections for consuming and publishing
49
+ * Consuming is done with polling instead of pushing
50
+ * Reconnects on connection errors
51
+ * Handling consumer cancel notification
52
+ * Lowered log levels of some developers' log messages
53
+ * Addded env vars:
54
+ * AMQP_RECONNECT_ATTEMPTS - number of retries on connection close
55
+ * AMQP_RECONNECT_TIMEOUT - delay between connection retries
56
+ * WAIT_MESSAGES_TIMEOUT - delay between next poll when queue is empty
57
+
58
+ ## 2.6.18 (October 26, 2020)
59
+
60
+ * Remove the logging of triggers and actions processing errors stack
61
+
62
+ ## 2.6.17 (October 15, 2020)
63
+
64
+ * Annual audit of the component code to check if it exposes a sensitive data in the logs
65
+
66
+ ## 2.6.16 (October 12, 2020)
67
+
68
+ * Fix incoming headers appearance in the logs (part 2)
69
+
70
+ ## 2.6.15 (October 12, 2020)
71
+
72
+ * Fix incoming headers appearance in the logs (part 1)
73
+
74
+ ## 2.6.14 (July 06, 2020)
75
+
76
+ * Add Lightweight messages support
77
+ * Sync this.emit() calls are not supported anymore. Use async process() interface and await this.emit() calls instead
78
+
79
+ ## 2.6.13 (July 01, 2020)
80
+
81
+ * Error as incoming message in custom error handler
82
+
83
+ ## 2.6.10 (June 03, 2020)
84
+
85
+ * Fix bug with incorrect publish retry policy.Dynamic flow control
86
+ * Dynamic flow control
87
+
88
+ ## 2.6.9 (May 26, 2020)
89
+
90
+ * Fix bugs with 'Retention policy' notification and 'Nodejs sailor return promise interface does not support dynamic flow control'
91
+
92
+ ## 2.6.8 (May 18, 2020)
93
+
94
+ * Fix bug when Lookout throws exception if incoming message from error queue doesn't have errorInput property
95
+ * From now on errors (description + stack) happening during component initialization won't be ignored and you will see them on frontend and in logs
96
+
97
+ ## 2.6.7 (May 07, 2020)
98
+
99
+ * Add ability to publish to arbitrary exchange
100
+
101
+ ## 2.6.6 (May 06, 2020)
102
+
103
+ * From now on Wiper will not suspend overloaded flows if all the components in the flow have the latest sailor (which supports dynamic flow control)
104
+
105
+ ## 2.6.5 (February 20, 2020)
106
+
107
+ * Add support of non-base64 message in Admiral. This function is activated if 2 neighbour components' Sailors support a feature of non-base64 message
108
+
109
+ ## 2.6.4 (February 14, 2020)
110
+
111
+ * Enable graceful restart for tasks pods
112
+
113
+ ## 2.6.2 (January 29, 2020)
114
+
115
+ * Fix bug when Sailor does not reliably publish large messages
116
+
117
+ ## 2.6.1 (January 15, 2020)
118
+
119
+ * Fix bug when publishing fail leads to fail in all subsequent pending messages
120
+
121
+ ## 2.6.0 (January 06, 2020)
122
+
123
+ * A step must not put its own output message into the passthrough object
124
+
125
+ ## 2.5.4 (December 23, 2019)
126
+
127
+ * Introduce new environment variable for Sailor: ELASTICIO_OUTGOING_MESSAGE_SIZE_LIMIT, which controls the outgoing message size limit
128
+
129
+ ## 2.5.3 (December 11, 2019)
130
+
131
+ * Introduce new component environment variable ELASTICIO_ADDITIONAL_VARS_FOR_HEADERS. It contains comma-separated environment variables that will be passed to message headers
132
+
133
+ ## 2.4.1 (July 18, 2019)
134
+
135
+ * Add additional information to RabbitMQ messages
136
+
137
+ ## 2.4.0 (June 11, 2019)
138
+
139
+ * Add elastic's Threads functionality support
140
+ * Add component custom logger. E.g. `this.logger.info('hello')`
141
+
142
+ ## 2.3.0 (October 19, 2018)
143
+
144
+ * Sailor now handles RabbitMQ disconnects correctly
package/lib/amqp.js CHANGED
@@ -1,10 +1,11 @@
1
1
  const log = require('./logging.js');
2
2
  const amqplib = require('amqplib');
3
3
  const { IllegalOperationError } = require('amqplib/lib/error');
4
- const encryptor = require('./encryptor.js');
4
+ const Encryptor = require('./encryptor.js');
5
5
  const _ = require('lodash');
6
6
  const eventToPromise = require('event-to-promise');
7
7
  const uuid = require('uuid');
8
+ const os = require('os');
8
9
 
9
10
  const HEADER_ROUTING_KEY = 'x-eio-routing-key';
10
11
  const HEADER_ERROR_RESPONSE = 'x-eio-error-response';
@@ -12,32 +13,34 @@ const HEADER_ERROR_RESPONSE = 'x-eio-error-response';
12
13
  class Amqp {
13
14
  constructor(settings) {
14
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;
15
19
  }
16
20
 
17
- async connect(uri) {
18
- this.consumerTag = null;
19
- this.amqp = await amqplib.connect(uri);
20
- if (process.env.NODE_ENV !== 'test') {
21
- this.amqp.on('error', log.criticalErrorAndExit.bind(log, 'amqp.error'));
22
- this.amqp.on('close', log.criticalErrorAndExit.bind(log, 'amqp.close'));
23
- }
24
- log.debug('Connected to AMQP');
25
-
26
- this.subscribeChannel = await this.amqp.createChannel();
27
- this.subscribeChannel.on('error', log.criticalErrorAndExit.bind(log, 'subscribeChannel.error'));
28
- log.debug('Opened subscribe channel');
29
-
30
- this.publishChannel = await this.amqp.createConfirmChannel();
31
- this.publishChannel.on('error', log.criticalErrorAndExit.bind(log, 'publishChannel.error'));
32
- log.debug('Opened publish channel');
21
+ async connect() {
22
+ await Promise.all([this._ensurePublishChannel(), this._ensureConsumerChannel()]);
23
+ this.closed = false;
33
24
  }
34
25
 
35
26
  async disconnect() {
27
+ await this.stopConsume();
28
+ this.closed = true;
36
29
  log.trace('Close AMQP connections');
37
- this.amqp.removeAllListeners('close');
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
+ }
38
42
  try {
39
- await this.listenQueueCancel();
40
- await this.subscribeChannel.close();
43
+ await this.consumerChannel.close();
41
44
  } catch (alreadyClosed) {
42
45
  log.debug('Subscribe channel is closed already');
43
46
  }
@@ -47,41 +50,227 @@ class Amqp {
47
50
  log.debug('Publish channel is closed already');
48
51
  }
49
52
  try {
50
- await this.amqp.close();
53
+ await this._readConnection.close();
51
54
  } catch (alreadyClosed) {
52
- log.debug('AMQP connection is closed already');
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');
53
61
  }
54
62
  log.debug('Successfully closed AMQP connections');
55
63
  }
56
64
 
57
- async listenQueue(queueName, callback) {
58
- if (this.consumerTag) {
59
- throw new Error('listenQueue MUST NOT be called more than once');
65
+ async _ensureReadConnection() {
66
+ if (this._readConnection) {
67
+ return this._readConnection;
60
68
  }
61
- this.subscribeChannel.prefetch(this.settings.RABBITMQ_PREFETCH_SAILOR);
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
+ }
62
87
 
63
- const result = await this.subscribeChannel.consume(queueName, this.decryptMessage.bind(this, callback));
64
- this.consumerTag = result.consumerTag;
65
- return result;
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;
66
109
  }
67
110
 
68
- async listenQueueCancel() {
69
- log.trace('listenQueueCancel starting');
70
- if (!this.consumerTag) {
71
- log.debug('listenQueueCancel is not executed since this.consumerTag is falsy');
72
- return;
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;
73
146
  }
74
- if (!this.subscribeChannel) {
75
- log.warn('listenQueueCancel called when this.subscribeChannel is falsy');
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) {
76
196
  return;
77
197
  }
78
- await this.subscribeChannel.cancel(this.consumerTag);
79
- this.consumerTag = null;
80
- log.debug('listenQueueCancel success');
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
+ };
81
270
  }
82
271
 
83
272
  decryptMessage(callback, message) {
84
- log.trace('Message received: %j', message);
273
+ log.trace('Message received');
85
274
 
86
275
  if (message === null) {
87
276
  log.warn('NULL message received');
@@ -91,7 +280,7 @@ class Amqp {
91
280
  const protocolVersion = Number(message.properties.headers.protocolVersion || 1);
92
281
  let decryptedContent;
93
282
  try {
94
- decryptedContent = encryptor.decryptMessageContent(
283
+ decryptedContent = this._encryptor.decryptMessageContent(
95
284
  message.content,
96
285
  protocolVersion < 2 ? 'base64' : undefined
97
286
  );
@@ -118,16 +307,17 @@ class Amqp {
118
307
 
119
308
  ack(message) {
120
309
  log.debug('Message #%j ack', message.fields.deliveryTag);
121
- this.subscribeChannel.ack(message);
310
+ this.consumerChannel.ack(message);
122
311
  }
123
312
 
124
313
  reject(message) {
125
314
  log.debug('Message #%j reject', message.fields.deliveryTag);
126
- return this.subscribeChannel.reject(message, false);
315
+ return this.consumerChannel.reject(message, false);
127
316
  }
128
317
 
129
318
  async sendToExchange(exchangeName, routingKey, payload, options, throttle) {
130
319
  if (throttle) {
320
+ log.debug('Throttling outgoing message');
131
321
  await throttle();
132
322
  }
133
323
  const buffer = Buffer.from(payload);
@@ -140,6 +330,8 @@ class Amqp {
140
330
  if (iteration) {
141
331
  options.headers.retry = iteration;
142
332
  }
333
+ // AMQP_PERSISTENT_MESSAGES is false by default if not specified by env var
334
+ options.persistent = this.settings.AMQP_PERSISTENT_MESSAGES;
143
335
 
144
336
  log.debug('Current memory usage: %s Mb', process.memoryUsage().heapUsed / 1048576);
145
337
  log.trace('Pushing to exchange=%s, routingKey=%s, messageSize=%d, options=%j, iteration=%d',
@@ -185,6 +377,7 @@ class Amqp {
185
377
  }
186
378
 
187
379
  async _promisifiedPublish(exchangeName, routingKey, payloadBuffer, options) {
380
+ await this._ensurePublishChannel();
188
381
  try {
189
382
  let result;
190
383
  const publishChannel = this.publishChannel;
@@ -211,7 +404,7 @@ class Amqp {
211
404
  }
212
405
 
213
406
  encryptMessageContent(body, protocolVersion = 1) {
214
- return encryptor.encryptMessageContent(
407
+ return this._encryptor.encryptMessageContent(
215
408
  body,
216
409
  protocolVersion < 2
217
410
  ? 'base64'
@@ -241,7 +434,6 @@ class Amqp {
241
434
  const settings = this.settings;
242
435
  const routingKey = getRoutingKeyFromHeaders(data.headers) || settings.DATA_ROUTING_KEY;
243
436
  properties.headers.protocolVersion = settings.PROTOCOL_VERSION;
244
-
245
437
  return this.prepareMessageAndSendToExchange(data, properties, routingKey, throttle);
246
438
  }
247
439
 
@@ -257,10 +449,13 @@ class Amqp {
257
449
  }
258
450
 
259
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
260
455
  const properties = this._createPropsFromHeaders(headers);
261
456
  const settings = this.settings;
262
457
 
263
- const encryptedError = encryptor.encryptMessageContent({
458
+ const encryptedError = this._encryptor.encryptMessageContent({
264
459
  name: err.name,
265
460
  message: err.message,
266
461
  stack: err.stack
@@ -270,12 +465,10 @@ class Amqp {
270
465
  error: encryptedError
271
466
  };
272
467
  if (originalMessage && originalMessage.content) {
273
- // TODO inconsistency with sendData. sendData build protocol version from settings
274
- // but send Error takes it from headers.
275
468
  const protocolVersion = Number(originalMessage.properties.headers.protocolVersion || 1);
276
469
  if (protocolVersion >= 2) {
277
- payload.errorInput = encryptor.encryptMessageContent(
278
- encryptor.decryptMessageContent(originalMessage.content),
470
+ payload.errorInput = this._encryptor.encryptMessageContent(
471
+ this._encryptor.decryptMessageContent(originalMessage.content),
279
472
  'base64'
280
473
  ).toString();
281
474
  } else {
@@ -284,10 +477,14 @@ class Amqp {
284
477
  }
285
478
  const errorPayload = JSON.stringify(payload);
286
479
 
287
- let result = this.sendToExchange(settings.PUBLISH_MESSAGES_TO, settings.ERROR_ROUTING_KEY,
288
- errorPayload, properties, throttle);
480
+ let result = this.sendToExchange(
481
+ settings.PUBLISH_MESSAGES_TO,
482
+ settings.ERROR_ROUTING_KEY,
483
+ errorPayload, properties,
484
+ throttle
485
+ );
289
486
 
290
- if (headers.reply_to) {
487
+ if (!settings.NO_ERROR_REPLIES && headers.reply_to) {
291
488
  log.debug('Sending error to %s', headers.reply_to);
292
489
  const replyToOptions = _.cloneDeep(properties);
293
490
  replyToOptions.headers[HEADER_ERROR_RESPONSE] = true;
@@ -298,20 +495,20 @@ class Amqp {
298
495
  return result;
299
496
  }
300
497
 
301
- async sendRebound(reboundError, originalMessage, headers) {
302
- // TODO: inconsistency
303
- // rebound message should be
304
- // a) repacked to currently used protocol version
305
- // b) passed as is
306
- // not as it's done now: send rebound but take protocolVersion header from current context
307
- // TODO double think about: why we need headers as argument here?
308
- // seems that rebound should be published with same headers as received
309
- // seems that answer is error, when rebound limit is exceeded.
310
- const properties = this._createPropsFromHeaders(headers);
311
- const settings = this.settings;
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;
312
508
 
313
- log.trace('Rebound message: %j', originalMessage);
314
- const reboundIteration = getReboundIteration(originalMessage.properties.headers.reboundIteration);
509
+ if (headers.reboundIteration && typeof headers.reboundIteration === 'number') {
510
+ reboundIteration = headers.reboundIteration + 1;
511
+ }
315
512
 
316
513
  if (reboundIteration > settings.REBOUND_LIMIT) {
317
514
  return this.sendError(
@@ -320,8 +517,15 @@ class Amqp {
320
517
  originalMessage
321
518
  );
322
519
  } else {
323
- properties.expiration = getExpiration(reboundIteration);
324
- properties.headers.reboundIteration = reboundIteration;
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
+ };
325
529
 
326
530
  return this.sendToExchange(
327
531
  settings.PUBLISH_MESSAGES_TO,
@@ -330,18 +534,6 @@ class Amqp {
330
534
  properties
331
535
  );
332
536
  }
333
-
334
- function getReboundIteration(previousIteration) {
335
- if (previousIteration && typeof previousIteration === 'number') {
336
- return previousIteration + 1;
337
- }
338
- return 1;
339
- }
340
-
341
- // retry in 15 sec, 30 sec, 1 min, 2 min, 4 min, 8 min, etc.
342
- function getExpiration(iteration) {
343
- return Math.pow(2, iteration - 1) * settings.REBOUND_INITIAL_EXPIRATION;
344
- }
345
537
  }
346
538
 
347
539
  async sendSnapshot(data, headers, throttle) {