Skip to content

Commit a517c70

Browse files
encounterdaniel-cottone
authored andcommitted
Add option to run handlers in separate node processes (dherault#368)
* Add option to run handlers in separate node processes * Use Object.assign instead of spread * Handle child_process exit/error * Use Set for inflight; lodash.omitBy for process.env workaround
1 parent 849fe21 commit a517c70

File tree

4 files changed

+93
-0
lines changed

4 files changed

+93
-0
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@
106106
"jsonpath-plus": "^0.16.0",
107107
"jsonwebtoken": "^7.4.3",
108108
"lodash": "^4.17.4",
109+
"uuid": "^3.2.1",
109110
"velocityjs": "^0.9.3"
110111
},
111112
"devDependencies": {

src/functionHelper.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
'use strict';
22

33
const debugLog = require('./debugLog');
4+
const fork = require('child_process').fork;
5+
const _ = require('lodash');
6+
const path = require('path');
7+
const uuid = require('uuid/v4');
8+
9+
const handlerCache = {};
10+
const messageCallbacks = {};
411

512
module.exports = {
613
getFunctionOptions(fun, funName, servicePath) {
@@ -18,9 +25,69 @@ module.exports = {
1825
};
1926
},
2027

28+
createExternalHandler(funOptions, options) {
29+
let handlerContext = handlerCache[funOptions.handlerPath];
30+
31+
function handleFatal(error) {
32+
debugLog(`External handler receieved fatal error ${JSON.stringify(error)}`);
33+
handlerContext.inflight.forEach(id => messageCallbacks[id](error));
34+
handlerContext.inflight.clear();
35+
delete handlerCache[funOptions.handlerPath];
36+
}
37+
38+
if (!handlerContext) {
39+
debugLog(`Loading external handler... (${funOptions.handlerPath})`);
40+
41+
const helperPath = path.resolve(__dirname, 'ipcHelper.js');
42+
const ipcProcess = fork(helperPath, [funOptions.handlerPath], {
43+
env: _.omitBy(process.env, _.isUndefined),
44+
stdio: [0, 1, 2, 'ipc'],
45+
});
46+
handlerContext = { process: ipcProcess, inflight: new Set() };
47+
if (options.skipCacheInvalidation) {
48+
handlerCache[funOptions.handlerPath] = handlerContext;
49+
}
50+
51+
ipcProcess.on('message', message => {
52+
debugLog(`External handler received message ${JSON.stringify(message)}`);
53+
if (message.id) {
54+
messageCallbacks[message.id](message.error, message.ret);
55+
handlerContext.inflight.delete(message.id);
56+
delete messageCallbacks[message.id];
57+
}
58+
else if (message.error) {
59+
// Handler died!
60+
handleFatal(message.error);
61+
}
62+
63+
if (!options.skipCacheInvalidation) {
64+
handlerContext.process.kill();
65+
delete handlerCache[funOptions.handlerPath];
66+
}
67+
});
68+
69+
ipcProcess.on('error', error => handleFatal(error));
70+
ipcProcess.on('exit', code => handleFatal(`Handler process exited with code ${code}`));
71+
}
72+
else {
73+
debugLog(`Using existing external handler for ${funOptions.handlerPath}`);
74+
}
75+
76+
return (event, context, done) => {
77+
const id = uuid();
78+
messageCallbacks[id] = done;
79+
handlerContext.inflight.add(id);
80+
handlerContext.process.send({ id, name: funOptions.handlerName, event, context });
81+
};
82+
},
83+
2184
// Create a function handler
2285
// The function handler is used to simulate Lambda functions
2386
createHandler(funOptions, options) {
87+
if (options.useSeparateProcesses) {
88+
return this.createExternalHandler(funOptions, options);
89+
}
90+
2491
if (!options.skipCacheInvalidation) {
2592
debugLog('Invalidating cache...');
2693

src/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ class Offline {
117117
noAuth: {
118118
usage: 'Turns off all authorizers',
119119
},
120+
useSeparateProcesses: {
121+
usage: 'Uses separate node processes for handlers',
122+
}
120123
},
121124
},
122125
};
@@ -229,6 +232,7 @@ class Offline {
229232
corsAllowHeaders: 'accept,content-type,x-api-key',
230233
corsAllowCredentials: true,
231234
apiKey: crypto.createHash('md5').digest('hex'),
235+
useSeparateProcesses: false,
232236
};
233237

234238
this.options = _.merge({}, defaultOpts, (this.service.custom || {})['serverless-offline'], this.options);

src/ipcHelper.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
'use strict';
2+
3+
process.on('uncaughtException', e => {
4+
process.send({ error: e });
5+
});
6+
7+
const handler = require(process.argv[2]);
8+
9+
process.on('message', opts => {
10+
function done(error, ret) {
11+
process.send({ id: opts.id, error, ret });
12+
}
13+
14+
const context = Object.assign(opts.context, {
15+
done,
16+
succeed: res => done(null, res),
17+
fail: err => done(err, null),
18+
// TODO implement getRemainingTimeInMillis
19+
});
20+
handler[opts.name](opts.event, context, done);
21+
});

0 commit comments

Comments
 (0)