elasticio-sailor-nodejs 2.7.1-dev3 → 2.7.1-dev5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.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 +669 -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/test.json +51 -0
- package/testOk.json +51 -0
- package/testOk1.json +51 -0
package/lib/sailor.js
CHANGED
|
@@ -1,665 +1,669 @@
|
|
|
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.
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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(1000*90);
|
|
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;
|