elasticio-sailor-nodejs 2.7.1-dev6 → 2.7.2-dev.1

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/sailor.js CHANGED
@@ -1,669 +1,664 @@
1
- const uuid = require('uuid');
2
- const ComponentReader = require('./component_reader.js').ComponentReader;
3
- const amqp = require('./amqp.js');
4
- const TaskExec = require('./executor.js').TaskExec;
5
- const log = require('./logging.js');
6
- const _ = require('lodash');
7
- const hooksData = require('./hooksData');
8
- const Encryptor = require('../lib/encryptor');
9
- const RestApiClient = require('elasticio-rest-node');
10
- const assert = require('assert');
11
- const co = require('co');
12
- const pThrottle = require('p-throttle');
13
- const { ObjectStorage } = require('@elastic.io/maester-client');
14
- const { Readable } = require('stream');
15
-
16
- const AMQP_HEADER_META_PREFIX = 'x-eio-meta-';
17
- const OBJECT_ID_HEADER = 'x-ipaas-object-storage-id';
18
-
19
- function convertSettingsToCamelCase(settings) {
20
- return _.mapKeys(settings, (value, key) => _.camelCase(key));
21
- }
22
-
23
- function getAdditionalHeadersFromSettings(settings) {
24
- return convertSettingsToCamelCase(settings.additionalVars);
25
- }
26
-
27
- class Sailor {
28
- static get OBJECT_ID_HEADER() {
29
- return OBJECT_ID_HEADER;
30
- }
31
- constructor(settings) {
32
- log.debug(JSON.stringify(settings, null, 2));
33
- this.settings = settings;
34
- this.messagesCount = 0;
35
- this.amqpConnection = new amqp.Amqp(settings);
36
- this.componentReader = new ComponentReader();
37
- this.snapshot = {};
38
- this.stepData = {};
39
- this.shutdownCallback = null;
40
- //eslint-disable-next-line new-cap
41
- this.apiClient = RestApiClient(
42
- settings.API_USERNAME,
43
- settings.API_KEY,
44
- {
45
- retryCount: settings.API_REQUEST_RETRY_ATTEMPTS,
46
- retryDelay: settings.API_REQUEST_RETRY_DELAY
47
- });
48
-
49
- const objectStorage = new ObjectStorage({
50
- uri: settings.OBJECT_STORAGE_URI,
51
- jwtSecret: settings.OBJECT_STORAGE_TOKEN
52
- });
53
-
54
- const encryptor = new Encryptor(settings.MESSAGE_CRYPTO_PASSWORD, settings.MESSAGE_CRYPTO_IV);
55
- this.objectStorage = objectStorage.use(
56
- () => encryptor.createCipher(),
57
- () => encryptor.createDecipher()
58
- );
59
-
60
- this.throttles = {
61
- // 100 Messages per Second
62
- data: pThrottle(() => Promise.resolve(true),
63
- settings.DATA_RATE_LIMIT,
64
- settings.RATE_INTERVAL),
65
- error: pThrottle(() => Promise.resolve(true),
66
- settings.ERROR_RATE_LIMIT,
67
- settings.RATE_INTERVAL),
68
- snapshot: pThrottle(() => Promise.resolve(true),
69
- settings.SNAPSHOT_RATE_LIMIT,
70
- settings.RATE_INTERVAL)
71
- };
72
- }
73
-
74
- async connect() {
75
- return this.amqpConnection.connect(this.settings.AMQP_URI);
76
- }
77
-
78
- async prepare() {
79
- await delay(60_000);
80
- const {
81
- settings: {
82
- COMPONENT_PATH: compPath,
83
- FLOW_ID: flowId,
84
- STEP_ID: stepId
85
- },
86
- apiClient,
87
- componentReader
88
- } = this;
89
- const stepData = await apiClient.tasks.retrieveStep(flowId, stepId);
90
- log.debug('Received step data');
91
- assert(stepData);
92
-
93
- Object.assign(this, {
94
- snapshot: stepData.snapshot || {},
95
- stepData
96
- });
97
-
98
- this.stepData = stepData;
99
-
100
- await componentReader.init(compPath);
101
- }
102
-
103
- async disconnect() {
104
- log.debug('Disconnecting, %s messages in processing', this.messagesCount);
105
- return this.amqpConnection.disconnect();
106
- }
107
-
108
- reportError(err) {
109
- const headers = Object.assign({}, getAdditionalHeadersFromSettings(this.settings), {
110
- execId: this.settings.EXEC_ID,
111
- taskId: this.settings.FLOW_ID,
112
- workspaceId: this.settings.WORKSPACE_ID,
113
- containerId: this.settings.CONTAINER_ID,
114
- userId: this.settings.USER_ID,
115
- stepId: this.settings.STEP_ID,
116
- compId: this.settings.COMP_ID,
117
- function: this.settings.FUNCTION
118
- });
119
- return this.amqpConnection.sendError(err, headers);
120
- }
121
-
122
- startup() {
123
- return co(function* doStartup() {
124
- log.debug('Starting up component');
125
- const result = yield this.invokeModuleFunction('startup');
126
- log.trace('Startup data received');
127
- const handle = hooksData.startup(this.settings);
128
- try {
129
- const state = _.isEmpty(result) ? {} : result;
130
- yield handle.create(state);
131
- } catch (e) {
132
- if (e.statusCode === 409) {
133
- log.warn('Startup data already exists. Rewriting.');
134
- yield handle.delete();
135
- yield handle.create(result);
136
- } else {
137
- log.warn('Component starting error');
138
- throw e;
139
- }
140
- }
141
- log.debug('Component started up');
142
- return result;
143
- }.bind(this));
144
- }
145
-
146
- runHookShutdown() {
147
- return co(function* doShutdown() {
148
- log.debug('About to shut down');
149
- const handle = hooksData.startup(this.settings);
150
- const state = yield handle.retrieve();
151
- yield this.invokeModuleFunction('shutdown', state);
152
- yield handle.delete();
153
- log.debug('Shut down successfully');
154
- }.bind(this));
155
- }
156
-
157
- runHookInit() {
158
- return co(function* doInit() {
159
- log.debug('About to initialize component for execution');
160
- const res = yield this.invokeModuleFunction('init');
161
- log.debug('Component execution initialized successfully');
162
- return res;
163
- }.bind(this));
164
- }
165
-
166
- invokeModuleFunction(moduleFunction, data) {
167
- const settings = this.settings;
168
- const stepData = this.stepData;
169
- return co(function* gen() {
170
- const module = yield this.componentReader.loadTriggerOrAction(settings.FUNCTION);
171
- if (!module[moduleFunction]) {
172
- log.warn(`invokeModuleFunction – ${moduleFunction} is not found`);
173
- return Promise.resolve();
174
- }
175
- const cfg = _.cloneDeep(stepData.config) || {};
176
- return new Promise((resolve, reject) => {
177
- try {
178
- resolve(module[moduleFunction](cfg, data));
179
- } catch (e) {
180
- reject(e);
181
- }
182
- });
183
- }.bind(this));
184
- }
185
-
186
- run() {
187
- const incomingQueue = this.settings.LISTEN_MESSAGES_ON;
188
- const handler = this.processMessageAndMaybeShutdownCallback.bind(this);
189
- log.debug('Start listening for messages on %s', incomingQueue);
190
- return this.amqpConnection.listenQueue(incomingQueue, handler);
191
- }
192
-
193
- async processMessageAndMaybeShutdownCallback(payload, message) {
194
- try {
195
- return await this.processMessage(payload, message);
196
- } catch (e) {
197
- log.error('Something very bad happened during message processing');
198
- } finally {
199
- if (this.shutdownCallback) {
200
- if (this.messagesCount === 0) {
201
- // there is no another processMessage invocation, so it's time to call shutdownCallback
202
- log.debug('About to invoke shutdownCallback');
203
- this.shutdownCallback();
204
- this.shutdownCallback = null;
205
- } else {
206
- // there is another not finished processMessage invocation
207
- log.debug('No shutdownCallback since messagesCount is not zero');
208
- }
209
- }
210
- }
211
- }
212
-
213
- async scheduleShutdown() {
214
- if (this.shutdownCallback) {
215
- log.debug('scheduleShutdown shutdown is already scheduled, do nothing');
216
- return new Promise(resolve => this.shutdownCallback = resolve);
217
- }
218
-
219
- await this.amqpConnection.stopConsume();
220
- if (this.messagesCount === 0) {
221
- // there is no unfinished processMessage invocation, let's just resolve scheduleShutdown now
222
- log.debug('scheduleShutdown – about to shutdown immediately');
223
- return Promise.resolve();
224
- }
225
- // at least one processMessage invocation is not finished yet
226
- // let's return a Promise, which will be resolved by processMessageAndMaybeShutdownCallback
227
- log.debug('scheduleShutdown shutdown is scheduled');
228
- return new Promise(resolve => this.shutdownCallback = resolve);
229
- }
230
-
231
-
232
- readIncomingMessageHeaders(message) {
233
- const { headers } = message.properties;
234
-
235
- // Get meta headers
236
- const metaHeaderNames = Object.keys(headers)
237
- .filter(key => key.toLowerCase().startsWith(AMQP_HEADER_META_PREFIX));
238
-
239
- const metaHeaders = _.pick(headers, metaHeaderNames);
240
- const metaHeadersLowerCased = _.mapKeys(metaHeaders, (value, key) => key.toLowerCase());
241
-
242
- const result = {
243
- stepId: headers.stepId, // the only use is passthrough mechanism
244
- ...metaHeadersLowerCased,
245
- threadId: headers.threadId || metaHeadersLowerCased['x-eio-meta-trace-id'],
246
- messageId: headers.messageId,
247
- parentMessageId: headers.parentMessageId
248
- };
249
- if (!result.threadId) {
250
- const threadId = uuid.v4();
251
- log.debug({ threadId }, 'Initiate new thread as it is not started ATM');
252
- result.threadId = threadId;
253
- }
254
- if (headers.reply_to) {
255
- result.reply_to = headers.reply_to;
256
- }
257
- return result;
258
- }
259
-
260
- async fetchMessageBody(message, logger) {
261
- const { body, headers } = message;
262
-
263
- logger.info('Checking if incoming messages is lightweight...');
264
-
265
- if (!headers) {
266
- logger.info('Empty headers so not lightweight.');
267
- return body;
268
- }
269
-
270
- const { [OBJECT_ID_HEADER]: objectId } = headers;
271
-
272
- if (!objectId) {
273
- logger.trace('No object id header so not lightweight.');
274
- return body;
275
- }
276
-
277
- logger.info('Object id header found, message is lightweight.', { objectId });
278
-
279
- let object;
280
-
281
- logger.info('Going to fetch message body.', { objectId });
282
-
283
- try {
284
- object = await this.objectStorage.getOne(
285
- objectId,
286
- { jwtPayloadOrToken: this.settings.OBJECT_STORAGE_TOKEN }
287
- );
288
- } catch (e) {
289
- log.error(e);
290
- throw new Error(`Failed to get message body with id=${objectId}`);
291
- }
292
-
293
- logger.info('Successfully obtained message body.', { objectId });
294
- logger.trace('Message body object received');
295
-
296
- return object;
297
- }
298
-
299
- uploadMessageBody(bodyBuf) {
300
- const stream = () => Readable.from(bodyBuf);
301
- return this.objectStorage.add(
302
- stream,
303
- { jwtPayloadOrToken: this.settings.OBJECT_STORAGE_TOKEN }
304
- );
305
- }
306
-
307
- async runExec(module, payload, message, outgoingMessageHeaders, stepData, timeStart, logger) {
308
- const origPassthrough = _.cloneDeep(payload.passthrough) || {};
309
- const incomingMessageHeaders = this.readIncomingMessageHeaders(message);
310
- const settings = this.settings;
311
- const cfg = _.cloneDeep(stepData.config) || {};
312
- const snapshot = _.cloneDeep(this.snapshot);
313
- const { deliveryTag } = message.fields;
314
-
315
- const that = this;
316
-
317
- await new Promise(resolve => {
318
- let endWasEmitted;
319
-
320
-
321
- const taskExec = new TaskExec({
322
- loggerOptions: _.pick(incomingMessageHeaders, ['threadId', 'messageId', 'parentMessageId']),
323
- variables: stepData.variables,
324
- services: {
325
- apiClient: this.apiClient,
326
- amqp: this.amqpConnection,
327
- config: this.settings
328
- }
329
- });
330
-
331
- taskExec
332
- .on('data', onData)
333
- .on('error', onError)
334
- .on('rebound', onRebound)
335
- .on('snapshot', onSnapshot)
336
- .on('updateSnapshot', onUpdateSnapshot)
337
- .on('updateKeys', onUpdateKeys)
338
- .on('httpReply', onHttpReply)
339
- .on('end', onEnd);
340
-
341
- taskExec.process(module, payload, cfg, snapshot);
342
-
343
- async function onData(data) {
344
- const headers = _.clone(outgoingMessageHeaders);
345
- headers.messageId = data.id || headers.messageId;
346
- logger.trace({
347
- messagesCount: that.messagesCount,
348
- messageProcessingTime: Date.now() - timeStart
349
- }, 'processMessage emit data');
350
-
351
- headers.end = new Date().getTime();
352
-
353
- if (stepData.is_passthrough === true) {
354
- data.passthrough = { ...origPassthrough };
355
- if (settings.NO_SELF_PASSTRHOUGH) {
356
- const { stepId } = incomingMessageHeaders;
357
- if (stepId) {
358
- data.passthrough = Object.assign({}, origPassthrough, {
359
- [stepId]: Object.assign({}, _.omit(payload, 'passthrough'))
360
- });
361
- }
362
- }
363
- }
364
-
365
- data.headers = data.headers || {};
366
- const { body, passthrough = {} } = data;
367
-
368
- if (settings.EMIT_LIGHTWEIGHT_MESSAGE) {
369
- logger.trace('Outgoing lightweight is enabled, going to check size.');
370
- const bodyBuf = Buffer.from(JSON.stringify(body), 'utf-8');
371
- const passthroughBufs = Object.keys(passthrough).map(stepId => ({
372
- stepId,
373
- body: Buffer.from(JSON.stringify(passthrough[stepId].body), 'utf-8'),
374
- id: passthrough[stepId].headers && passthrough[stepId].headers[OBJECT_ID_HEADER]
375
- }));
376
-
377
- const totalLength = passthroughBufs.reduce((len, { body }) =>
378
- len + body.length, bodyBuf.length);
379
-
380
- if (totalLength > settings.OBJECT_STORAGE_SIZE_THRESHOLD) {
381
- logger.info(
382
- 'Message size is above threshold, going to upload',
383
- {
384
- totalLength,
385
- OBJECT_STORAGE_SIZE_THRESHOLD: settings.OBJECT_STORAGE_SIZE_THRESHOLD
386
- }
387
- );
388
-
389
- let bodyId;
390
- let passthroughIds;
391
- try {
392
- [bodyId, ...passthroughIds] = await Promise.all([
393
- that.uploadMessageBody(bodyBuf),
394
- ...passthroughBufs.map(async ({ stepId, body, id }) => {
395
- const bodyId = id || await that.uploadMessageBody(body);
396
- return { stepId, bodyId };
397
- })
398
- ]);
399
- } catch (e) {
400
- logger.error(e, 'Error during message/passthrough body upload');
401
- return onError(new Error('Lightweight message/passthrough body upload error'));
402
- }
403
-
404
- logger.info('Message body uploaded', { id: bodyId });
405
- const { headers } = data;
406
- data.body = {};
407
- data.headers = {
408
- ...(headers || {}),
409
- [OBJECT_ID_HEADER]: bodyId
410
- };
411
-
412
- for (const { stepId, bodyId } of passthroughIds) {
413
- logger.info('Passthrough Message body uploaded', { stepId, id: bodyId });
414
- const { [stepId]: { headers } } = passthrough;
415
- data.passthrough[stepId].body = {};
416
- data.passthrough[stepId].headers = {
417
- ...(headers || {}),
418
- [OBJECT_ID_HEADER]: bodyId
419
- };
420
- }
421
-
422
- } else {
423
- logger.trace(
424
- 'Message size is below threshold.',
425
- {
426
- totalLength,
427
- OBJECT_STORAGE_SIZE_THRESHOLD: settings.OBJECT_STORAGE_SIZE_THRESHOLD
428
- }
429
- );
430
- }
431
- } else if (passthrough) {
432
- logger.trace('Outgoing lightweight is disabled, going to download all bodies.');
433
- try {
434
- await Promise.all(Object.keys(passthrough).map(async stepId => {
435
- logger.trace('Going to check if passthrough for step is lightweight.', { stepId });
436
- // if body is not empty then we've downloaded before processing, no need to redownload
437
- if (!_.isEmpty(data.passthrough[stepId].body)) {
438
- logger.trace('Body is not empty.', { stepId });
439
- return;
440
- }
441
- data.passthrough[stepId].body = await that.fetchMessageBody(
442
- passthrough[stepId],
443
- logger
444
- );
445
- }));
446
- } catch (e) {
447
- return onError(e);
448
- }
449
- }
450
-
451
- if (stepData.is_passthrough === true && !settings.NO_SELF_PASSTRHOUGH) {
452
- data.passthrough = Object.assign({}, origPassthrough, {
453
- [settings.STEP_ID]: Object.assign({}, _.omit(data, 'passthrough'))
454
- });
455
- }
456
-
457
- log.trace('Going to send outgoing message');
458
-
459
- try {
460
- await that.amqpConnection.sendData(data, headers, that.throttles.data);
461
- log.trace('Outgoing message sent');
462
- } catch (err) {
463
- return onError(err);
464
- }
465
- }
466
-
467
- async function onHttpReply(reply) {
468
- const headers = _.clone(outgoingMessageHeaders);
469
- logger.trace({
470
- messageProcessingTime: Date.now() - timeStart
471
- }, 'processMessage emit HttpReply');
472
-
473
- return that.amqpConnection.sendHttpReply(reply, headers);
474
- }
475
-
476
- async function onError(err) {
477
- const headers = _.clone(outgoingMessageHeaders);
478
- err = formatError(err);
479
- taskExec.errorCount++;
480
- logger.trace({
481
- err,
482
- messagesCount: that.messagesCount,
483
- messageProcessingTime: Date.now() - timeStart
484
- }, 'processMessage emit error');
485
- headers.end = new Date().getTime();
486
- return that.amqpConnection.sendError(err, headers, message, that.throttles.error);
487
- }
488
-
489
- async function onRebound(err) {
490
- err = formatError(err);
491
- logger.trace({
492
- err,
493
- messagesCount: that.messagesCount,
494
- messageProcessingTime: Date.now() - timeStart
495
- }, 'processMessage emit rebound');
496
- return that.amqpConnection.sendRebound(err, message);
497
- }
498
-
499
- async function onSnapshot(data) {
500
- const headers = _.clone(outgoingMessageHeaders);
501
- headers.snapshotEvent = 'snapshot';
502
- that.snapshot = data; //replacing `local` snapshot
503
- return that.amqpConnection.sendSnapshot(data, headers, that.throttles.snapshot);
504
- }
505
-
506
- async function onUpdateSnapshot(data) {
507
- const headers = _.clone(outgoingMessageHeaders);
508
- headers.snapshotEvent = 'updateSnapshot';
509
-
510
- if (_.isPlainObject(data)) {
511
- if (data.$set) {
512
- return log.warn('ERROR: $set is not supported any more in `updateSnapshot` event');
513
- }
514
- _.extend(that.snapshot, data); //updating `local` snapshot
515
- return that.amqpConnection.sendSnapshot(data, headers);
516
- } else {
517
- log.error('You should pass an object to the `updateSnapshot` event');
518
- }
519
- }
520
-
521
- async function onUpdateKeys(keys) {
522
- logger.trace({
523
- messageProcessingTime: Date.now() - timeStart
524
- }, 'processMessage emit updateKeys');
525
-
526
- try {
527
- await that.apiClient.accounts.update(cfg._account, { keys: keys });
528
- logger.debug('Successfully updated keys #%s', deliveryTag);
529
- } catch (error) {
530
- logger.error('Failed to updated keys #%s', deliveryTag);
531
- await onError(error);
532
- }
533
- }
534
-
535
- function onEnd() {
536
- if (endWasEmitted) {
537
- logger.warn({
538
- messagesCount: that.messagesCount,
539
- errorCount: taskExec.errorCount,
540
- messageProcessingTime: Date.now() - timeStart
541
- }, 'processMessage emit end was called more than once');
542
- return;
543
- }
544
-
545
- endWasEmitted = true;
546
-
547
- if (taskExec.errorCount > 0) {
548
- that.amqpConnection.reject(message);
549
- } else {
550
- that.amqpConnection.ack(message);
551
- }
552
- that.messagesCount -= 1;
553
- logger.trace({
554
- messagesCount: that.messagesCount,
555
- errorCount: taskExec.errorCount,
556
- messageProcessingTime: Date.now() - timeStart
557
- }, 'processMessage emit end');
558
- resolve();
559
- }
560
- });
561
-
562
-
563
- function formatError(err) {
564
- if (err instanceof Error || (_.isObject(err) && _.has(err, 'message'))) {
565
- return {
566
- message: err.message,
567
- stack: err.stack || 'Not Available',
568
- name: err.name || 'Error'
569
- };
570
- } else {
571
- return {
572
- message: err || 'Not Available',
573
- stack: 'Not Available',
574
- name: 'Error'
575
- };
576
- }
577
- }
578
- }
579
-
580
- async processMessage(payload, message) {
581
- //eslint-disable-next-line consistent-this
582
- const self = this;
583
- const settings = this.settings;
584
- const incomingMessageHeaders = this.readIncomingMessageHeaders(message);
585
-
586
- self.messagesCount += 1;
587
-
588
- const timeStart = Date.now();
589
- const { deliveryTag } = message.fields;
590
-
591
- const logger = log.child({
592
- threadId: incomingMessageHeaders.threadId || 'unknown',
593
- messageId: incomingMessageHeaders.messageId || 'unknown',
594
- parentMessageId: incomingMessageHeaders.parentMessageId || 'unknown',
595
- deliveryTag
596
- });
597
-
598
- logger.trace({ messagesCount: this.messagesCount }, 'processMessage received');
599
-
600
- const stepData = this.stepData;
601
-
602
- log.debug('Trigger or action: %s', settings.FUNCTION);
603
- const outgoingMessageId = uuid.v4();
604
- const outgoingMessageHeaders = {
605
- ...incomingMessageHeaders,
606
- ...getAdditionalHeadersFromSettings(settings),
607
- parentMessageId: incomingMessageHeaders.messageId,
608
- threadId: incomingMessageHeaders.threadId,
609
- messageId: outgoingMessageId,
610
- execId: settings.EXEC_ID,
611
- taskId: settings.FLOW_ID,
612
- workspaceId: settings.WORKSPACE_ID,
613
- containerId: settings.CONTAINER_ID,
614
- userId: settings.USER_ID,
615
- stepId: settings.STEP_ID,
616
- compId: settings.COMP_ID,
617
- function: settings.FUNCTION,
618
- start: new Date().getTime()
619
- };
620
- let module;
621
- try {
622
- module = await this.componentReader.loadTriggerOrAction(settings.FUNCTION);
623
- } catch (e) {
624
- log.error(e);
625
- outgoingMessageHeaders.end = new Date().getTime();
626
- self.amqpConnection.sendError(e, outgoingMessageHeaders, message);
627
- self.amqpConnection.reject(message);
628
- return;
629
- }
630
-
631
- const method = this.componentReader.findTriggerOrActionDefinition(settings.FUNCTION);
632
-
633
- if (method.autoResolveObjectReferences) {
634
- const { passthrough } = payload;
635
-
636
- try {
637
- await Promise.all([
638
- (async () => {
639
- logger.trace('Going to check if incoming message body is lightweight.');
640
- payload.body = await this.fetchMessageBody(payload, logger);
641
- })(),
642
- ...(passthrough
643
- ? Object.keys(passthrough).map(async stepId => {
644
- logger.trace('Going to check if passthrough for step is lightweight.', { stepId });
645
- payload.passthrough[stepId].body = await this.fetchMessageBody(
646
- payload.passthrough[stepId],
647
- logger
648
- );
649
- })
650
- : [])
651
- ]);
652
- } catch (e) {
653
- logger.error(e);
654
- outgoingMessageHeaders.end = new Date().getTime();
655
- self.amqpConnection.sendError(e, outgoingMessageHeaders, message);
656
- self.amqpConnection.reject(message);
657
- return;
658
- }
659
- }
660
-
661
- await this.runExec(module, payload, message, outgoingMessageHeaders, stepData, timeStart, logger);
662
- }
663
- }
664
-
665
- function delay(ms) {
666
- return new Promise(resolve => setTimeout(resolve, ms));
667
- }
668
-
669
- exports.Sailor = Sailor;
1
+ const uuid = require('uuid');
2
+ const ComponentReader = require('./component_reader.js').ComponentReader;
3
+ const amqp = require('./amqp.js');
4
+ const TaskExec = require('./executor.js').TaskExec;
5
+ const log = require('./logging.js');
6
+ const _ = require('lodash');
7
+ const hooksData = require('./hooksData');
8
+ const Encryptor = require('../lib/encryptor');
9
+ const RestApiClient = require('elasticio-rest-node');
10
+ const assert = require('assert');
11
+ const co = require('co');
12
+ const pThrottle = require('p-throttle');
13
+ const { ObjectStorage } = require('@elastic.io/maester-client');
14
+ const { Readable } = require('stream');
15
+
16
+ const AMQP_HEADER_META_PREFIX = 'x-eio-meta-';
17
+ const OBJECT_ID_HEADER = 'x-ipaas-object-storage-id';
18
+
19
+ function convertSettingsToCamelCase(settings) {
20
+ return _.mapKeys(settings, (value, key) => _.camelCase(key));
21
+ }
22
+
23
+ function getAdditionalHeadersFromSettings(settings) {
24
+ return convertSettingsToCamelCase(settings.additionalVars);
25
+ }
26
+
27
+ class Sailor {
28
+ static get OBJECT_ID_HEADER() {
29
+ return OBJECT_ID_HEADER;
30
+ }
31
+ constructor(settings) {
32
+ this.settings = settings;
33
+ this.messagesCount = 0;
34
+ this.amqpConnection = new amqp.Amqp(settings);
35
+ this.componentReader = new ComponentReader();
36
+ this.snapshot = {};
37
+ this.stepData = {};
38
+ this.shutdownCallback = null;
39
+ //eslint-disable-next-line new-cap
40
+ this.apiClient = RestApiClient(
41
+ settings.API_USERNAME,
42
+ settings.API_KEY,
43
+ {
44
+ retryCount: settings.API_REQUEST_RETRY_ATTEMPTS,
45
+ retryDelay: settings.API_REQUEST_RETRY_DELAY
46
+ });
47
+
48
+ const objectStorage = new ObjectStorage({
49
+ uri: settings.OBJECT_STORAGE_URI,
50
+ jwtSecret: settings.OBJECT_STORAGE_TOKEN
51
+ });
52
+
53
+ const encryptor = new Encryptor(settings.MESSAGE_CRYPTO_PASSWORD, settings.MESSAGE_CRYPTO_IV);
54
+ this.objectStorage = objectStorage.use(
55
+ () => encryptor.createCipher(),
56
+ () => encryptor.createDecipher()
57
+ );
58
+
59
+ this.throttles = {
60
+ // 100 Messages per Second
61
+ data: pThrottle(() => Promise.resolve(true),
62
+ settings.DATA_RATE_LIMIT,
63
+ settings.RATE_INTERVAL),
64
+ error: pThrottle(() => Promise.resolve(true),
65
+ settings.ERROR_RATE_LIMIT,
66
+ settings.RATE_INTERVAL),
67
+ snapshot: pThrottle(() => Promise.resolve(true),
68
+ settings.SNAPSHOT_RATE_LIMIT,
69
+ settings.RATE_INTERVAL)
70
+ };
71
+ }
72
+
73
+ async connect() {
74
+ return this.amqpConnection.connect(this.settings.AMQP_URI);
75
+ }
76
+
77
+ async prepare() {
78
+ const {
79
+ settings: {
80
+ COMPONENT_PATH: compPath,
81
+ FLOW_ID: flowId,
82
+ STEP_ID: stepId
83
+ },
84
+ apiClient,
85
+ componentReader
86
+ } = this;
87
+
88
+ const stepData = await apiClient.tasks.retrieveStep(flowId, stepId);
89
+ log.debug('Received step data');
90
+ assert(stepData);
91
+
92
+ Object.assign(this, {
93
+ snapshot: stepData.snapshot || {},
94
+ stepData
95
+ });
96
+
97
+ this.stepData = stepData;
98
+
99
+ await componentReader.init(compPath);
100
+ }
101
+
102
+ async disconnect() {
103
+ log.debug('Disconnecting, %s messages in processing', this.messagesCount);
104
+ return this.amqpConnection.disconnect();
105
+ }
106
+
107
+ reportError(err) {
108
+ const headers = Object.assign({}, getAdditionalHeadersFromSettings(this.settings), {
109
+ execId: this.settings.EXEC_ID,
110
+ taskId: this.settings.FLOW_ID,
111
+ workspaceId: this.settings.WORKSPACE_ID,
112
+ containerId: this.settings.CONTAINER_ID,
113
+ userId: this.settings.USER_ID,
114
+ stepId: this.settings.STEP_ID,
115
+ compId: this.settings.COMP_ID,
116
+ function: this.settings.FUNCTION
117
+ });
118
+ return this.amqpConnection.sendError(err, headers);
119
+ }
120
+
121
+ startup() {
122
+ return co(function* doStartup() {
123
+ log.debug('Starting up component');
124
+ const result = yield this.invokeModuleFunction('startup');
125
+ log.trace('Startup data received');
126
+ const handle = hooksData.startup(this.settings);
127
+ try {
128
+ const state = _.isEmpty(result) ? {} : result;
129
+ yield handle.create(state);
130
+ } catch (e) {
131
+ if (e.statusCode === 409) {
132
+ log.warn('Startup data already exists. Rewriting.');
133
+ yield handle.delete();
134
+ yield handle.create(result);
135
+ } else {
136
+ log.warn('Component starting error');
137
+ throw e;
138
+ }
139
+ }
140
+ log.debug('Component started up');
141
+ return result;
142
+ }.bind(this));
143
+ }
144
+
145
+ runHookShutdown() {
146
+ return co(function* doShutdown() {
147
+ log.debug('About to shut down');
148
+ const handle = hooksData.startup(this.settings);
149
+ const state = yield handle.retrieve();
150
+ yield this.invokeModuleFunction('shutdown', state);
151
+ yield handle.delete();
152
+ log.debug('Shut down successfully');
153
+ }.bind(this));
154
+ }
155
+
156
+ runHookInit() {
157
+ return co(function* doInit() {
158
+ log.debug('About to initialize component for execution');
159
+ const res = yield this.invokeModuleFunction('init');
160
+ log.debug('Component execution initialized successfully');
161
+ return res;
162
+ }.bind(this));
163
+ }
164
+
165
+ invokeModuleFunction(moduleFunction, data) {
166
+ const settings = this.settings;
167
+ const stepData = this.stepData;
168
+ return co(function* gen() {
169
+ const module = yield this.componentReader.loadTriggerOrAction(settings.FUNCTION);
170
+ if (!module[moduleFunction]) {
171
+ log.warn(`invokeModuleFunction – ${moduleFunction} is not found`);
172
+ return Promise.resolve();
173
+ }
174
+ const cfg = _.cloneDeep(stepData.config) || {};
175
+ return new Promise((resolve, reject) => {
176
+ try {
177
+ resolve(module[moduleFunction](cfg, data));
178
+ } catch (e) {
179
+ reject(e);
180
+ }
181
+ });
182
+ }.bind(this));
183
+ }
184
+
185
+ run() {
186
+ const incomingQueue = this.settings.LISTEN_MESSAGES_ON;
187
+ const handler = this.processMessageAndMaybeShutdownCallback.bind(this);
188
+ log.debug('Start listening for messages on %s', incomingQueue);
189
+ return this.amqpConnection.listenQueue(incomingQueue, handler);
190
+ }
191
+
192
+ async processMessageAndMaybeShutdownCallback(payload, message) {
193
+ try {
194
+ return await this.processMessage(payload, message);
195
+ } catch (e) {
196
+ log.error('Something very bad happened during message processing');
197
+ } finally {
198
+ if (this.shutdownCallback) {
199
+ if (this.messagesCount === 0) {
200
+ // there is no another processMessage invocation, so it's time to call shutdownCallback
201
+ log.debug('About to invoke shutdownCallback');
202
+ this.shutdownCallback();
203
+ this.shutdownCallback = null;
204
+ } else {
205
+ // there is another not finished processMessage invocation
206
+ log.debug('No shutdownCallback since messagesCount is not zero');
207
+ }
208
+ }
209
+ }
210
+ }
211
+
212
+ async scheduleShutdown() {
213
+ if (this.shutdownCallback) {
214
+ log.debug('scheduleShutdown – shutdown is already scheduled, do nothing');
215
+ return new Promise(resolve => this.shutdownCallback = resolve);
216
+ }
217
+
218
+ await this.amqpConnection.stopConsume();
219
+ if (this.messagesCount === 0) {
220
+ // there is no unfinished processMessage invocation, let's just resolve scheduleShutdown now
221
+ log.debug('scheduleShutdown about to shutdown immediately');
222
+ return Promise.resolve();
223
+ }
224
+ // at least one processMessage invocation is not finished yet
225
+ // let's return a Promise, which will be resolved by processMessageAndMaybeShutdownCallback
226
+ log.debug('scheduleShutdown shutdown is scheduled');
227
+ return new Promise(resolve => this.shutdownCallback = resolve);
228
+ }
229
+
230
+
231
+ readIncomingMessageHeaders(message) {
232
+ const { headers } = message.properties;
233
+
234
+ // Get meta headers
235
+ const metaHeaderNames = Object.keys(headers)
236
+ .filter(key => key.toLowerCase().startsWith(AMQP_HEADER_META_PREFIX));
237
+
238
+ const metaHeaders = _.pick(headers, metaHeaderNames);
239
+ const metaHeadersLowerCased = _.mapKeys(metaHeaders, (value, key) => key.toLowerCase());
240
+
241
+ const result = {
242
+ stepId: headers.stepId, // the only use is passthrough mechanism
243
+ ...metaHeadersLowerCased,
244
+ threadId: headers.threadId || metaHeadersLowerCased['x-eio-meta-trace-id'],
245
+ messageId: headers.messageId,
246
+ parentMessageId: headers.parentMessageId
247
+ };
248
+ if (!result.threadId) {
249
+ const threadId = uuid.v4();
250
+ log.debug({ threadId }, 'Initiate new thread as it is not started ATM');
251
+ result.threadId = threadId;
252
+ }
253
+ if (headers.reply_to) {
254
+ result.reply_to = headers.reply_to;
255
+ }
256
+ return result;
257
+ }
258
+
259
+ async fetchMessageBody(message, logger) {
260
+ const { body, headers } = message;
261
+
262
+ logger.info('Checking if incoming messages is lightweight...');
263
+
264
+ if (!headers) {
265
+ logger.info('Empty headers so not lightweight.');
266
+ return body;
267
+ }
268
+
269
+ const { [OBJECT_ID_HEADER]: objectId } = headers;
270
+
271
+ if (!objectId) {
272
+ logger.trace('No object id header so not lightweight.');
273
+ return body;
274
+ }
275
+
276
+ logger.info('Object id header found, message is lightweight.', { objectId });
277
+
278
+ let object;
279
+
280
+ logger.info('Going to fetch message body.', { objectId });
281
+
282
+ try {
283
+ object = await this.objectStorage.getOne(
284
+ objectId,
285
+ { jwtPayloadOrToken: this.settings.OBJECT_STORAGE_TOKEN }
286
+ );
287
+ } catch (e) {
288
+ log.error(e);
289
+ throw new Error(`Failed to get message body with id=${objectId}`);
290
+ }
291
+
292
+ logger.info('Successfully obtained message body.', { objectId });
293
+ logger.trace('Message body object received');
294
+
295
+ return object.data;
296
+ }
297
+
298
+ uploadMessageBody(bodyBuf) {
299
+ const stream = () => Readable.from(bodyBuf);
300
+ return this.objectStorage.add(
301
+ stream,
302
+ { jwtPayloadOrToken: this.settings.OBJECT_STORAGE_TOKEN }
303
+ );
304
+ }
305
+
306
+ async runExec(module, payload, message, outgoingMessageHeaders, stepData, timeStart, logger) {
307
+ const origPassthrough = _.cloneDeep(payload.passthrough) || {};
308
+ const incomingMessageHeaders = this.readIncomingMessageHeaders(message);
309
+ const settings = this.settings;
310
+ const cfg = _.cloneDeep(stepData.config) || {};
311
+ const snapshot = _.cloneDeep(this.snapshot);
312
+ const { deliveryTag } = message.fields;
313
+
314
+ const that = this;
315
+
316
+ await new Promise(resolve => {
317
+ let endWasEmitted;
318
+
319
+
320
+ const taskExec = new TaskExec({
321
+ loggerOptions: _.pick(incomingMessageHeaders, ['threadId', 'messageId', 'parentMessageId']),
322
+ variables: stepData.variables,
323
+ services: {
324
+ apiClient: this.apiClient,
325
+ amqp: this.amqpConnection,
326
+ config: this.settings
327
+ }
328
+ });
329
+
330
+ taskExec
331
+ .on('data', onData)
332
+ .on('error', onError)
333
+ .on('rebound', onRebound)
334
+ .on('snapshot', onSnapshot)
335
+ .on('updateSnapshot', onUpdateSnapshot)
336
+ .on('updateKeys', onUpdateKeys)
337
+ .on('httpReply', onHttpReply)
338
+ .on('end', onEnd);
339
+
340
+ taskExec.process(module, payload, cfg, snapshot);
341
+
342
+ async function onData(data) {
343
+ const headers = _.clone(outgoingMessageHeaders);
344
+ headers.messageId = data.id || headers.messageId;
345
+ logger.trace({
346
+ messagesCount: that.messagesCount,
347
+ messageProcessingTime: Date.now() - timeStart
348
+ }, 'processMessage emit data');
349
+
350
+ headers.end = new Date().getTime();
351
+
352
+ if (stepData.is_passthrough === true) {
353
+ data.passthrough = { ...origPassthrough };
354
+ if (settings.NO_SELF_PASSTRHOUGH) {
355
+ const { stepId } = incomingMessageHeaders;
356
+ if (stepId) {
357
+ data.passthrough = Object.assign({}, origPassthrough, {
358
+ [stepId]: Object.assign({}, _.omit(payload, 'passthrough'))
359
+ });
360
+ }
361
+ }
362
+ }
363
+
364
+ data.headers = data.headers || {};
365
+ const { body, passthrough = {} } = data;
366
+
367
+ if (settings.EMIT_LIGHTWEIGHT_MESSAGE) {
368
+ logger.trace('Outgoing lightweight is enabled, going to check size.');
369
+ const bodyBuf = Buffer.from(JSON.stringify(body), 'utf-8');
370
+ const passthroughBufs = Object.keys(passthrough).map(stepId => ({
371
+ stepId,
372
+ body: Buffer.from(JSON.stringify(passthrough[stepId].body), 'utf-8'),
373
+ id: passthrough[stepId].headers && passthrough[stepId].headers[OBJECT_ID_HEADER]
374
+ }));
375
+
376
+ const totalLength = passthroughBufs.reduce((len, { body }) =>
377
+ len + body.length, bodyBuf.length);
378
+
379
+ if (totalLength > settings.OBJECT_STORAGE_SIZE_THRESHOLD) {
380
+ logger.info(
381
+ 'Message size is above threshold, going to upload',
382
+ {
383
+ totalLength,
384
+ OBJECT_STORAGE_SIZE_THRESHOLD: settings.OBJECT_STORAGE_SIZE_THRESHOLD
385
+ }
386
+ );
387
+
388
+ let bodyId;
389
+ let passthroughIds;
390
+ try {
391
+ [bodyId, ...passthroughIds] = await Promise.all([
392
+ that.uploadMessageBody(bodyBuf),
393
+ ...passthroughBufs.map(async ({ stepId, body, id }) => {
394
+ const bodyId = id || await that.uploadMessageBody(body);
395
+ return { stepId, bodyId };
396
+ })
397
+ ]);
398
+ } catch (e) {
399
+ logger.error(e, 'Error during message/passthrough body upload');
400
+ return onError(new Error('Lightweight message/passthrough body upload error'));
401
+ }
402
+
403
+ logger.info('Message body uploaded', { id: bodyId });
404
+ const { headers } = data;
405
+ data.body = {};
406
+ data.headers = {
407
+ ...(headers || {}),
408
+ [OBJECT_ID_HEADER]: bodyId
409
+ };
410
+
411
+ for (const { stepId, bodyId } of passthroughIds) {
412
+ logger.info('Passthrough Message body uploaded', { stepId, id: bodyId });
413
+ const { [stepId]: { headers } } = passthrough;
414
+ data.passthrough[stepId].body = {};
415
+ data.passthrough[stepId].headers = {
416
+ ...(headers || {}),
417
+ [OBJECT_ID_HEADER]: bodyId
418
+ };
419
+ }
420
+
421
+ } else {
422
+ logger.trace(
423
+ 'Message size is below threshold.',
424
+ {
425
+ totalLength,
426
+ OBJECT_STORAGE_SIZE_THRESHOLD: settings.OBJECT_STORAGE_SIZE_THRESHOLD
427
+ }
428
+ );
429
+ }
430
+ } else if (passthrough) {
431
+ logger.trace('Outgoing lightweight is disabled, going to download all bodies.');
432
+ try {
433
+ await Promise.all(Object.keys(passthrough).map(async stepId => {
434
+ logger.trace('Going to check if passthrough for step is lightweight.', { stepId });
435
+ // if body is not empty then we've downloaded before processing, no need to redownload
436
+ if (!_.isEmpty(data.passthrough[stepId].body)) {
437
+ logger.trace('Body is not empty.', { stepId });
438
+ return;
439
+ }
440
+ data.passthrough[stepId].body = await that.fetchMessageBody(
441
+ passthrough[stepId],
442
+ logger
443
+ );
444
+ }));
445
+ } catch (e) {
446
+ return onError(e);
447
+ }
448
+ }
449
+
450
+ if (stepData.is_passthrough === true && !settings.NO_SELF_PASSTRHOUGH) {
451
+ data.passthrough = Object.assign({}, origPassthrough, {
452
+ [settings.STEP_ID]: Object.assign({}, _.omit(data, 'passthrough'))
453
+ });
454
+ }
455
+
456
+ log.trace('Going to send outgoing message');
457
+
458
+ try {
459
+ await that.amqpConnection.sendData(data, headers, that.throttles.data);
460
+ log.trace('Outgoing message sent');
461
+ } catch (err) {
462
+ return onError(err);
463
+ }
464
+ }
465
+
466
+ async function onHttpReply(reply) {
467
+ const headers = _.clone(outgoingMessageHeaders);
468
+ logger.trace({
469
+ messageProcessingTime: Date.now() - timeStart
470
+ }, 'processMessage emit HttpReply');
471
+
472
+ return that.amqpConnection.sendHttpReply(reply, headers);
473
+ }
474
+
475
+ async function onError(err) {
476
+ const headers = _.clone(outgoingMessageHeaders);
477
+ err = formatError(err);
478
+ taskExec.errorCount++;
479
+ logger.trace({
480
+ err,
481
+ messagesCount: that.messagesCount,
482
+ messageProcessingTime: Date.now() - timeStart
483
+ }, 'processMessage emit error');
484
+ headers.end = new Date().getTime();
485
+ return that.amqpConnection.sendError(err, headers, message, that.throttles.error);
486
+ }
487
+
488
+ async function onRebound(err) {
489
+ err = formatError(err);
490
+ logger.trace({
491
+ err,
492
+ messagesCount: that.messagesCount,
493
+ messageProcessingTime: Date.now() - timeStart
494
+ }, 'processMessage emit rebound');
495
+ return that.amqpConnection.sendRebound(err, message);
496
+ }
497
+
498
+ async function onSnapshot(data) {
499
+ const headers = _.clone(outgoingMessageHeaders);
500
+ headers.snapshotEvent = 'snapshot';
501
+ that.snapshot = data; //replacing `local` snapshot
502
+ return that.amqpConnection.sendSnapshot(data, headers, that.throttles.snapshot);
503
+ }
504
+
505
+ async function onUpdateSnapshot(data) {
506
+ const headers = _.clone(outgoingMessageHeaders);
507
+ headers.snapshotEvent = 'updateSnapshot';
508
+
509
+ if (_.isPlainObject(data)) {
510
+ if (data.$set) {
511
+ return log.warn('ERROR: $set is not supported any more in `updateSnapshot` event');
512
+ }
513
+ _.extend(that.snapshot, data); //updating `local` snapshot
514
+ return that.amqpConnection.sendSnapshot(data, headers);
515
+ } else {
516
+ log.error('You should pass an object to the `updateSnapshot` event');
517
+ }
518
+ }
519
+
520
+ async function onUpdateKeys(keys) {
521
+ logger.trace({
522
+ messageProcessingTime: Date.now() - timeStart
523
+ }, 'processMessage emit updateKeys');
524
+
525
+ try {
526
+ await that.apiClient.accounts.update(cfg._account, { keys: keys });
527
+ logger.debug('Successfully updated keys #%s', deliveryTag);
528
+ } catch (error) {
529
+ logger.error('Failed to updated keys #%s', deliveryTag);
530
+ await onError(error);
531
+ }
532
+ }
533
+
534
+ function onEnd() {
535
+ if (endWasEmitted) {
536
+ logger.warn({
537
+ messagesCount: that.messagesCount,
538
+ errorCount: taskExec.errorCount,
539
+ messageProcessingTime: Date.now() - timeStart
540
+ }, 'processMessage emit end was called more than once');
541
+ return;
542
+ }
543
+
544
+ endWasEmitted = true;
545
+
546
+ if (taskExec.errorCount > 0) {
547
+ that.amqpConnection.reject(message);
548
+ } else {
549
+ that.amqpConnection.ack(message);
550
+ }
551
+ that.messagesCount -= 1;
552
+ logger.trace({
553
+ messagesCount: that.messagesCount,
554
+ errorCount: taskExec.errorCount,
555
+ messageProcessingTime: Date.now() - timeStart
556
+ }, 'processMessage emit end');
557
+ resolve();
558
+ }
559
+ });
560
+
561
+
562
+ function formatError(err) {
563
+ if (err instanceof Error || (_.isObject(err) && _.has(err, 'message'))) {
564
+ return {
565
+ message: err.message,
566
+ stack: err.stack || 'Not Available',
567
+ name: err.name || 'Error'
568
+ };
569
+ } else {
570
+ return {
571
+ message: err || 'Not Available',
572
+ stack: 'Not Available',
573
+ name: 'Error'
574
+ };
575
+ }
576
+ }
577
+ }
578
+
579
+ async processMessage(payload, message) {
580
+ //eslint-disable-next-line consistent-this
581
+ const self = this;
582
+ const settings = this.settings;
583
+ const incomingMessageHeaders = this.readIncomingMessageHeaders(message);
584
+
585
+ self.messagesCount += 1;
586
+
587
+ const timeStart = Date.now();
588
+ const { deliveryTag } = message.fields;
589
+
590
+ const logger = log.child({
591
+ threadId: incomingMessageHeaders.threadId || 'unknown',
592
+ messageId: incomingMessageHeaders.messageId || 'unknown',
593
+ parentMessageId: incomingMessageHeaders.parentMessageId || 'unknown',
594
+ deliveryTag
595
+ });
596
+
597
+ logger.trace({ messagesCount: this.messagesCount }, 'processMessage received');
598
+
599
+ const stepData = this.stepData;
600
+
601
+ log.debug('Trigger or action: %s', settings.FUNCTION);
602
+ const outgoingMessageId = uuid.v4();
603
+ const outgoingMessageHeaders = {
604
+ ...incomingMessageHeaders,
605
+ ...getAdditionalHeadersFromSettings(settings),
606
+ parentMessageId: incomingMessageHeaders.messageId,
607
+ threadId: incomingMessageHeaders.threadId,
608
+ messageId: outgoingMessageId,
609
+ execId: settings.EXEC_ID,
610
+ taskId: settings.FLOW_ID,
611
+ workspaceId: settings.WORKSPACE_ID,
612
+ containerId: settings.CONTAINER_ID,
613
+ userId: settings.USER_ID,
614
+ stepId: settings.STEP_ID,
615
+ compId: settings.COMP_ID,
616
+ function: settings.FUNCTION,
617
+ start: new Date().getTime()
618
+ };
619
+ let module;
620
+ try {
621
+ module = await this.componentReader.loadTriggerOrAction(settings.FUNCTION);
622
+ } catch (e) {
623
+ log.error(e);
624
+ outgoingMessageHeaders.end = new Date().getTime();
625
+ self.amqpConnection.sendError(e, outgoingMessageHeaders, message);
626
+ self.amqpConnection.reject(message);
627
+ return;
628
+ }
629
+
630
+ const method = this.componentReader.findTriggerOrActionDefinition(settings.FUNCTION);
631
+
632
+ if (method.autoResolveObjectReferences) {
633
+ const { passthrough } = payload;
634
+
635
+ try {
636
+ await Promise.all([
637
+ (async () => {
638
+ logger.trace('Going to check if incoming message body is lightweight.');
639
+ payload.body = await this.fetchMessageBody(payload, logger);
640
+ })(),
641
+ ...(passthrough
642
+ ? Object.keys(passthrough).map(async stepId => {
643
+ logger.trace('Going to check if passthrough for step is lightweight.', { stepId });
644
+ payload.passthrough[stepId].body = await this.fetchMessageBody(
645
+ payload.passthrough[stepId],
646
+ logger
647
+ );
648
+ })
649
+ : [])
650
+ ]);
651
+ } catch (e) {
652
+ logger.error(e);
653
+ outgoingMessageHeaders.end = new Date().getTime();
654
+ self.amqpConnection.sendError(e, outgoingMessageHeaders, message);
655
+ self.amqpConnection.reject(message);
656
+ return;
657
+ }
658
+ }
659
+
660
+ await this.runExec(module, payload, message, outgoingMessageHeaders, stepData, timeStart, logger);
661
+ }
662
+ }
663
+
664
+ exports.Sailor = Sailor;