From eaea46724bee1844b47a79f04cc7041afed63f99 Mon Sep 17 00:00:00 2001 From: James M Snell Date: Fri, 1 May 2020 13:13:45 -0700 Subject: [PATCH 1/2] Add resourceLimits support --- README.md | 11 ++++++++++ examples/resourceLimits/index.js | 23 +++++++++++++++++++++ examples/resourceLimits/worker.js | 10 +++++++++ src/index.ts | 18 +++++++++++++--- test/fixtures/resource-limits.js | 10 +++++++++ test/option-validation.ts | 6 ++++++ test/test-resourcelimits.ts | 34 +++++++++++++++++++++++++++++++ 7 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 examples/resourceLimits/index.js create mode 100644 examples/resourceLimits/worker.js create mode 100644 test/fixtures/resource-limits.js create mode 100644 test/test-resourcelimits.ts diff --git a/README.md b/README.md index a085b178..259f6e1c 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ * ✔ Proper async tracking integration * ✔ Tracking statistics for run and wait times * ✔ Cancelation Support +* ✔ Supports enforcing memory resource limits For Node.js 12.x and higher. @@ -147,6 +148,16 @@ This class extends [`EventEmitter`][] from Node.js. handling I/O in parallel. * `useAtomics`: (`boolean`) Use the [`Atomics`][] API for faster communication between threads. This is on by default. + * `resourceLimits`: (`object`) + * `maxOldGenerationSizeMb`: (`number`) The maximum size of each worker threads + main heap in MB. + * `maxYoungGenerationSizeMb`: (`number`) The maximum size of a heap space for + recently created objects. + * `codeRangeSizeMb`: (`number`) The size of a pre-allocated memory range used + for generated code. + +Use caution when setting resource limits. Setting limits that are too low may +result in the `Piscina` worker threads being unusable. ### Method: `runTask(task[, transferList][, filename][, abortSignal])` diff --git a/examples/resourceLimits/index.js b/examples/resourceLimits/index.js new file mode 100644 index 00000000..6a8804ad --- /dev/null +++ b/examples/resourceLimits/index.js @@ -0,0 +1,23 @@ +'use strict'; + +const Piscina = require('../..'); +const { resolve } = require('path'); +const { strictEqual } = require('assert'); + +const piscina = new Piscina({ + filename: resolve(__dirname, 'worker.js'), + resourceLimits: { + maxOldGenerationSizeMb: 16, + maxYoungGenerationSizeMb: 4, + codeRangeSizeMb: 16 + } +}); + +(async function() { + try { + await piscina.runTask(); + } catch (err) { + console.log('Worker terminated due to resource limits'); + strictEqual(err.code, 'ERR_WORKER_OUT_OF_MEMORY'); + } +})(); diff --git a/examples/resourceLimits/worker.js b/examples/resourceLimits/worker.js new file mode 100644 index 00000000..76bfacd3 --- /dev/null +++ b/examples/resourceLimits/worker.js @@ -0,0 +1,10 @@ +'use strict'; + +module.exports = () => { + const array = []; + while (true) { + for (let i = 0; i < 100; i++) { + array.push([array]); + } + } +}; diff --git a/src/index.ts b/src/index.ts index b2374b33..998d3d98 100644 --- a/src/index.ts +++ b/src/index.ts @@ -43,6 +43,10 @@ class AbortError extends Error { } } +type ResourceLimits = Worker extends { + resourceLimits? : infer T; +} ? T : {}; + interface Options { // Probably also support URL here filename? : string | null, @@ -50,8 +54,9 @@ interface Options { maxThreads? : number, idleTimeout? : number, maxQueue? : number, - concurrentTasksPerWorker? : number - useAtomics? : boolean + concurrentTasksPerWorker? : number, + useAtomics? : boolean, + resourceLimits? : ResourceLimits } interface FilledOptions extends Options { @@ -279,7 +284,9 @@ class ThreadPool { _addNewWorker () : WorkerInfo { const pool = this; - const worker = new Worker(resolve(__dirname, 'worker.js')); + const worker = new Worker(resolve(__dirname, 'worker.js'), { + resourceLimits: this.options.resourceLimits + }); const { port1, port2 } = new MessageChannel(); const workerInfo = new WorkerInfo(worker, port1, onMessage); @@ -542,6 +549,11 @@ class Piscina extends EventEmitter { typeof options.useAtomics !== 'boolean') { throw new TypeError('options.useAtomics must be a boolean value'); } + if (options.resourceLimits !== undefined && + (typeof options.resourceLimits !== 'object' || + options.resourceLimits === null)) { + throw new TypeError('options.resourceLimits must be an object'); + } this.#pool = new ThreadPool(this, options); } diff --git a/test/fixtures/resource-limits.js b/test/fixtures/resource-limits.js new file mode 100644 index 00000000..76bfacd3 --- /dev/null +++ b/test/fixtures/resource-limits.js @@ -0,0 +1,10 @@ +'use strict'; + +module.exports = () => { + const array = []; + while (true) { + for (let i = 0; i < 100; i++) { + array.push([array]); + } + } +}; diff --git a/test/option-validation.ts b/test/option-validation.ts index 444f4334..e0a61dd5 100644 --- a/test/option-validation.ts +++ b/test/option-validation.ts @@ -75,3 +75,9 @@ test('useAtomics must be a boolean', async ({ throws }) => { useAtomics: 'string' }) as any), /options.useAtomics must be a boolean/); }); + +test('resourceLimits must be an object', async ({ throws }) => { + throws(() => new Piscina(({ + resourceLimits: 0 + }) as any), /options.resourceLimits must be an object/); +}); diff --git a/test/test-resourcelimits.ts b/test/test-resourcelimits.ts new file mode 100644 index 00000000..290e4c98 --- /dev/null +++ b/test/test-resourcelimits.ts @@ -0,0 +1,34 @@ +import Piscina from '..'; +import { test } from 'tap'; +import { resolve } from 'path'; + +test('resourceLimits causes task to reject', async ({ is, rejects }) => { + const worker = new Piscina({ + filename: resolve(__dirname, 'fixtures/resource-limits.js'), + resourceLimits: { + maxOldGenerationSizeMb: 16, + maxYoungGenerationSizeMb: 4, + codeRangeSizeMb: 16 + } + }); + worker.on('error', () => { + // Ignore any additional errors that may occur. + // This may happen because when the Worker is + // killed a new worker is created that may hit + // the memory limits immediately. When that + // happens, there is no associated Promise to + // reject so we emit an error event instead. + // We don't care so much about that here. We + // could potentially avoid the issue by setting + // higher limits above but rather than try to + // guess at limits that may work consistently, + // let's just ignore the additional error for + // now. + }); + const limits : any = worker.options.resourceLimits; + is(limits.maxOldGenerationSizeMb, 16); + is(limits.maxYoungGenerationSizeMb, 4); + is(limits.codeRangeSizeMb, 16); + rejects(worker.runTask(null), + /Worker terminated due to reaching memory limit: JS heap out of memory/); +}); From c3597e715139797b3078466b25283f448f52987c Mon Sep 17 00:00:00 2001 From: James M Snell Date: Fri, 1 May 2020 15:24:47 -0700 Subject: [PATCH 2/2] [Squash] additional nits --- README.md | 3 ++- examples/resourceLimits/worker.js | 4 +--- test/fixtures/resource-limits.js | 4 +--- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 259f6e1c..128b4de0 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ This class extends [`EventEmitter`][] from Node.js. handling I/O in parallel. * `useAtomics`: (`boolean`) Use the [`Atomics`][] API for faster communication between threads. This is on by default. - * `resourceLimits`: (`object`) + * `resourceLimits`: (`object`) See [Node.js new Worker options][] * `maxOldGenerationSizeMb`: (`number`) The maximum size of each worker threads main heap in MB. * `maxYoungGenerationSizeMb`: (`number`) The maximum size of a heap space for @@ -346,5 +346,6 @@ Piscina development is sponsored by [NearForm Research][]. [`Atomics`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics [`EventEmitter`]: https://nodejs.org/api/events.html [`postMessage`]: https://nodejs.org/api/worker_threads.html#worker_threads_port_postmessage_value_transferlist +[Node.js new Worker options]: https://nodejs.org/api/worker_threads.html#worker_threads_new_worker_filename_options [MIT Licensed]: LICENSE.md [NearForm Research]: https://www.nearform.com/research/ diff --git a/examples/resourceLimits/worker.js b/examples/resourceLimits/worker.js index 76bfacd3..cc1f5bf0 100644 --- a/examples/resourceLimits/worker.js +++ b/examples/resourceLimits/worker.js @@ -3,8 +3,6 @@ module.exports = () => { const array = []; while (true) { - for (let i = 0; i < 100; i++) { - array.push([array]); - } + array.push([array]); } }; diff --git a/test/fixtures/resource-limits.js b/test/fixtures/resource-limits.js index 76bfacd3..cc1f5bf0 100644 --- a/test/fixtures/resource-limits.js +++ b/test/fixtures/resource-limits.js @@ -3,8 +3,6 @@ module.exports = () => { const array = []; while (true) { - for (let i = 0; i < 100; i++) { - array.push([array]); - } + array.push([array]); } };