diff --git a/lib/internal/utils.js b/lib/internal/utils.js index f068ea2..433b6c4 100644 --- a/lib/internal/utils.js +++ b/lib/internal/utils.js @@ -110,6 +110,11 @@ const errors = { 'MessagePort was found in message but not listed in transferList' ], + OUT_OF_MEMORY: [ + 'ERR_WORKER_OUT_OF_MEMORY', + 'Worker terminated due to reaching memory limit' + ], + // Custom Worker Errors BUNDLED_EVAL: [ 'ERR_WORKER_BUNDLED_EVAL', diff --git a/lib/process/flags.js b/lib/process/flags.js index bd1080c..049cd3d 100644 --- a/lib/process/flags.js +++ b/lib/process/flags.js @@ -155,7 +155,11 @@ const invalidOptions = new Set([ '--completion-bash', '-h', '--help', '-v', '--version', - '--v8-options' + '--v8-options', + + // To filter out resource limits: + '--max-old-space-size', + '--max-semi-space-size' ]); // At some point in the future, --frozen-intrinsics diff --git a/lib/process/parser.js b/lib/process/parser.js index 0f88278..aad330e 100644 --- a/lib/process/parser.js +++ b/lib/process/parser.js @@ -27,9 +27,21 @@ class Parser extends EventEmitter { this.header = null; this.pending = []; this.total = 0; + this.closed = false; + } + + destroy() { + this.closed = true; + this.waiting = -1 >>> 0; + this.header = null; + this.pending.length = 0; + this.total = 0; } feed(data) { + if (this.closed) + return; + this.total += data.length; this.pending.push(data); diff --git a/lib/process/worker.js b/lib/process/worker.js index 33221a6..5cde655 100644 --- a/lib/process/worker.js +++ b/lib/process/worker.js @@ -79,12 +79,16 @@ class Worker extends EventEmitter { if (options.execArgv && !Array.isArray(options.execArgv)) throw new ArgError('execArgv', options.execArgv, 'Array'); + if (options.resourceLimits && typeof options.resourceLimits !== 'object') + throw new ArgError('resourceLimits', options.resourceLimits, 'object'); + this._child = null; this._parser = new Parser(this); this._ports = new Map(); this._writable = true; this._exited = false; this._killed = false; + this._limits = false; this._exitCode = -1; this._stdioRef = null; this._stdioRefs = 0; @@ -104,6 +108,7 @@ class Worker extends EventEmitter { _init(file, options) { const bin = process.execPath || process.argv[0]; + const limits = options.resourceLimits; const args = []; // Validate filename. @@ -181,6 +186,24 @@ class Worker extends EventEmitter { } } + // Enforce resource limits. + if (limits) { + const maxOld = limits.maxOldSpaceSizeMb; + const maxSemi = limits.maxSemiSpaceSizeMb; + + if (typeof maxOld === 'number') { + args.push(`--max-old-space-size=${Math.max(maxOld, 2)}`); + this._limits = true; + } + + if (typeof maxSemi === 'number') { + args.push(`--max-semi-space-size=${maxSemi}`); + this._limits = true; + } + + // Todo: figure out how to do codeRangeSizeMb. + } + // Require bthreads on boot, but make // sure we're not bundled or something. if (!hasRequireArg(args, __dirname)) { @@ -298,7 +321,18 @@ class Worker extends EventEmitter { }); this._parser.on('error', (err) => { - this.emit('error', err); + this._parser.destroy(); + + if (this._limits) { + // Probably an OOM: + // v8 writes the last few GC attempts to stdout, + // and then writes some debugging info to stderr. + this.emit('error', new WorkerError(errors.OUT_OF_MEMORY)); + } else { + this.emit('error', err); + } + + this._kill(1); }); this._parser.on('packet', (pkt) => { @@ -403,6 +437,9 @@ class Worker extends EventEmitter { } _handleExit(code, signal) { + if (signal === 'SIGABRT') + code = 1; + // Child was terminated with signal handler. if (code === 143 && signal == null) { // Convert to SIGTERM signal. diff --git a/test/threads-test.js b/test/threads-test.js index 7061bef..4033081 100644 --- a/test/threads-test.js +++ b/test/threads-test.js @@ -1074,4 +1074,44 @@ describe(`Threads (${threads.backend})`, (ctx) => { await pool.close(); }); + + it('should die on resource limit excess', async (ctx) => { + if (threads.backend !== 'child_process') + ctx.skip(); + + const func = () => setInterval(() => {}, 1000); + + const worker = new threads.Worker(`(${func})();`, { + eval: true, + resourceLimits: { + maxOldSpaceSizeMb: 2 + } + }); + + const err = await waitFor(worker, 'error'); + + assert(err); + assert.strictEqual(err.code, 'ERR_WORKER_OUT_OF_MEMORY'); + assert.strictEqual(await wait(worker), 1); + }); + + it('should clean reports', (ctx) => { + // v8 writes files like "report.20190313.143017.5753.001.json" + // when it dies from an oom. + if (threads.backend !== 'child_process') + ctx.skip(); + + const fs = require('fs'); + const dir = join(__dirname, '..'); + + for (const name of fs.readdirSync(dir)) { + if (!name.endsWith('.json')) + continue; + + if (!name.startsWith('report.')) + continue; + + fs.unlinkSync(join(dir, name)); + } + }); });