Skip to content

Commit 2d6b7ab

Browse files
committed
process: implement resource limits.
- see nodejs/node#26628
1 parent fb36b9d commit 2d6b7ab

File tree

5 files changed

+100
-2
lines changed

5 files changed

+100
-2
lines changed

lib/internal/utils.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ const errors = {
110110
'MessagePort was found in message but not listed in transferList'
111111
],
112112

113+
OUT_OF_MEMORY: [
114+
'ERR_WORKER_OUT_OF_MEMORY',
115+
'Worker terminated due to reaching memory limit'
116+
],
117+
113118
// Custom Worker Errors
114119
BUNDLED_EVAL: [
115120
'ERR_WORKER_BUNDLED_EVAL',

lib/process/flags.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,11 @@ const invalidOptions = new Set([
155155
'--completion-bash',
156156
'-h', '--help',
157157
'-v', '--version',
158-
'--v8-options'
158+
'--v8-options',
159+
160+
// To filter out resource limits:
161+
'--max-old-space-size',
162+
'--max-semi-space-size'
159163
]);
160164

161165
// At some point in the future, --frozen-intrinsics

lib/process/parser.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,21 @@ class Parser extends EventEmitter {
2727
this.header = null;
2828
this.pending = [];
2929
this.total = 0;
30+
this.closed = false;
31+
}
32+
33+
destroy() {
34+
this.closed = true;
35+
this.waiting = -1 >>> 0;
36+
this.header = null;
37+
this.pending.length = 0;
38+
this.total = 0;
3039
}
3140

3241
feed(data) {
42+
if (this.closed)
43+
return;
44+
3345
this.total += data.length;
3446
this.pending.push(data);
3547

lib/process/worker.js

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,16 @@ class Worker extends EventEmitter {
7979
if (options.execArgv && !Array.isArray(options.execArgv))
8080
throw new ArgError('execArgv', options.execArgv, 'Array');
8181

82+
if (options.resourceLimits && typeof options.resourceLimits !== 'object')
83+
throw new ArgError('resourceLimits', options.resourceLimits, 'object');
84+
8285
this._child = null;
8386
this._parser = new Parser(this);
8487
this._ports = new Map();
8588
this._writable = true;
8689
this._exited = false;
8790
this._killed = false;
91+
this._limits = false;
8892
this._exitCode = -1;
8993
this._stdioRef = null;
9094
this._stdioRefs = 0;
@@ -104,6 +108,7 @@ class Worker extends EventEmitter {
104108

105109
_init(file, options) {
106110
const bin = process.execPath || process.argv[0];
111+
const limits = options.resourceLimits;
107112
const args = [];
108113

109114
// Validate filename.
@@ -181,6 +186,24 @@ class Worker extends EventEmitter {
181186
}
182187
}
183188

189+
// Enforce resource limits.
190+
if (limits) {
191+
const maxOld = limits.maxOldSpaceSizeMb;
192+
const maxSemi = limits.maxSemiSpaceSizeMb;
193+
194+
if (typeof maxOld === 'number') {
195+
args.push(`--max-old-space-size=${Math.max(maxOld, 2)}`);
196+
this._limits = true;
197+
}
198+
199+
if (typeof maxSemi === 'number') {
200+
args.push(`--max-semi-space-size=${maxSemi}`);
201+
this._limits = true;
202+
}
203+
204+
// Todo: figure out how to do codeRangeSizeMb.
205+
}
206+
184207
// Require bthreads on boot, but make
185208
// sure we're not bundled or something.
186209
if (!hasRequireArg(args, __dirname)) {
@@ -298,7 +321,18 @@ class Worker extends EventEmitter {
298321
});
299322

300323
this._parser.on('error', (err) => {
301-
this.emit('error', err);
324+
this._parser.destroy();
325+
326+
if (this._limits) {
327+
// Probably an OOM:
328+
// v8 writes the last few GC attempts to stdout,
329+
// and then writes some debugging info to stderr.
330+
this.emit('error', new WorkerError(errors.OUT_OF_MEMORY));
331+
} else {
332+
this.emit('error', err);
333+
}
334+
335+
this._kill(1);
302336
});
303337

304338
this._parser.on('packet', (pkt) => {
@@ -403,6 +437,9 @@ class Worker extends EventEmitter {
403437
}
404438

405439
_handleExit(code, signal) {
440+
if (signal === 'SIGABRT')
441+
code = 1;
442+
406443
// Child was terminated with signal handler.
407444
if (code === 143 && signal == null) {
408445
// Convert to SIGTERM signal.

test/threads-test.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,4 +1074,44 @@ describe(`Threads (${threads.backend})`, (ctx) => {
10741074

10751075
await pool.close();
10761076
});
1077+
1078+
it('should die on resource limit excess', async (ctx) => {
1079+
if (threads.backend !== 'child_process')
1080+
ctx.skip();
1081+
1082+
const func = () => setInterval(() => {}, 1000);
1083+
1084+
const worker = new threads.Worker(`(${func})();`, {
1085+
eval: true,
1086+
resourceLimits: {
1087+
maxOldSpaceSizeMb: 2
1088+
}
1089+
});
1090+
1091+
const err = await waitFor(worker, 'error');
1092+
1093+
assert(err);
1094+
assert.strictEqual(err.code, 'ERR_WORKER_OUT_OF_MEMORY');
1095+
assert.strictEqual(await wait(worker), 1);
1096+
});
1097+
1098+
it('should clean reports', (ctx) => {
1099+
// v8 writes files like "report.20190313.143017.5753.001.json"
1100+
// when it dies from an oom.
1101+
if (threads.backend !== 'child_process')
1102+
ctx.skip();
1103+
1104+
const fs = require('fs');
1105+
const dir = join(__dirname, '..');
1106+
1107+
for (const name of fs.readdirSync(dir)) {
1108+
if (!name.endsWith('.json'))
1109+
continue;
1110+
1111+
if (!name.startsWith('report.'))
1112+
continue;
1113+
1114+
fs.unlinkSync(join(dir, name));
1115+
}
1116+
});
10771117
});

0 commit comments

Comments
 (0)