elasticio-sailor-nodejs 2.7.1-dev3 → 2.7.1-dev4
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/.eslintrc.js +150 -150
- package/.github/CODEOWNERS +8 -8
- package/.nsprc +18 -18
- package/CHANGELOG.md +144 -144
- package/README.md +247 -247
- package/lib/amqp.js +584 -584
- package/lib/component_reader.js +109 -109
- package/lib/emitter.js +198 -198
- package/lib/encryptor.js +114 -114
- package/lib/executor.js +74 -74
- package/lib/hooksData.js +68 -68
- package/lib/ipc.js +13 -13
- package/lib/logging.js +97 -97
- package/lib/sailor.js +670 -665
- package/lib/service.js +294 -294
- package/lib/settings.js +126 -126
- package/package.json +53 -53
- package/postpublish.js +24 -24
- package/run.js +139 -139
- package/runService.js +19 -19
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;
|