GeezerVison is showing github.com — worker: allow specifying resource limits · nodejs/node@886ef09
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/errors.md +5Lines changed: 5 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -1990,6 +1990,11 @@ meaning of the error depends on the specific function. | 1990 | 1990 | The `execArgv` option passed to the `Worker` constructor contains | 1991 | 1991 | invalid flags. | 1992 | 1992 |
| | 1993 | +<a id="ERR_WORKER_OUT_OF_MEMORY"></a> | | 1994 | +### `ERR_WORKER_OUT_OF_MEMORY` | | 1995 | + | | 1996 | +The `Worker` instance terminated because it reached its memory limit. | | 1997 | + | 1993 | 1998 | <a id="ERR_WORKER_PATH"></a> | 1994 | 1999 | ### `ERR_WORKER_PATH` | 1995 | 2000 |
| |
Collapse file Copy file name to clipboardExpand all lines: doc/api/worker_threads.md +48Lines changed: 48 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -157,6 +157,22 @@ console.log(receiveMessageOnPort(port2)); | 157 | 157 | When this function is used, no `'message'` event will be emitted and the | 158 | 158 | `onmessage` listener will not be invoked. | 159 | 159 |
| | 160 | +### `worker.resourceLimits` | | 161 | +<!-- YAML | | 162 | +added: REPLACEME | | 163 | +--> | | 164 | + | | 165 | +* {Object|undefined} | | 166 | + * `maxYoungGenerationSizeMb` {number} | | 167 | + * `maxOldGenerationSizeMb` {number} | | 168 | + * `codeRangeSizeMb` {number} | | 169 | + | | 170 | +Provides the set of JS engine resource constraints inside this Worker thread. | | 171 | +If the `resourceLimits` option was passed to the [`Worker`][] constructor, | | 172 | +this matches its values. | | 173 | + | | 174 | +If this is used in the main thread, its value is an empty object. | | 175 | + | 160 | 176 | ## `worker.SHARE_ENV` | 161 | 177 | <!-- YAML | 162 | 178 | added: v11.14.0 | @@ -488,6 +504,13 @@ if (isMainThread) { | 488 | 504 | ``` | 489 | 505 |
| 490 | 506 | ### `new Worker(filename[, options])` | | 507 | +<!-- YAML | | 508 | +added: v10.5.0 | | 509 | +changes: | | 510 | + - version: REPLACEME | | 511 | + pr-url: https://github.com/nodejs/node/pull/26628 | | 512 | + description: The `resourceLimits` option was introduced. | | 513 | +--> | 491 | 514 |
| 492 | 515 | * `filename` {string} The path to the Worker’s main script. Must be | 493 | 516 | either an absolute path or a relative path (i.e. relative to the | @@ -519,6 +542,16 @@ if (isMainThread) { | 519 | 542 | occur as described in the [HTML structured clone algorithm][], and an error | 520 | 543 | will be thrown if the object cannot be cloned (e.g. because it contains | 521 | 544 | `function`s). | | 545 | + * `resourceLimits` {Object} An optional set of resource limits for the new | | 546 | + JS engine instance. Reaching these limits will lead to termination of the | | 547 | + `Worker` instance. These limits only affect the JS engine, and no external | | 548 | + data, including no `ArrayBuffer`s. Even if these limits are set, the process | | 549 | + may still abort if it encounters a global out-of-memory situation. | | 550 | + * `maxOldGenerationSizeMb` {number} The maximum size of the main heap in MB. | | 551 | + * `maxYoungGenerationSizeMb` {number} The maximum size of a heap space for | | 552 | + recently created objects. | | 553 | + * `codeRangeSizeMb` {number} The size of a pre-allocated memory range | | 554 | + used for generated code. | 522 | 555 |
| 523 | 556 | ### Event: `'error'` | 524 | 557 | <!-- YAML | @@ -583,6 +616,21 @@ Opposite of `unref()`, calling `ref()` on a previously `unref()`ed worker will | 583 | 616 | behavior). If the worker is `ref()`ed, calling `ref()` again will have | 584 | 617 | no effect. | 585 | 618 |
| | 619 | +### `worker.resourceLimits` | | 620 | +<!-- YAML | | 621 | +added: REPLACEME | | 622 | +--> | | 623 | + | | 624 | +* {Object} | | 625 | + * `maxYoungGenerationSizeMb` {number} | | 626 | + * `maxOldGenerationSizeMb` {number} | | 627 | + * `codeRangeSizeMb` {number} | | 628 | + | | 629 | +Provides the set of JS engine resource constraints for this Worker thread. | | 630 | +If the `resourceLimits` option was passed to the [`Worker`][] constructor, | | 631 | +this matches its values. | | 632 | + | | 633 | +If the worker has stopped, the return value is an empty object. | 586 | 634 | ### `worker.stderr` | 587 | 635 | <!-- YAML | 588 | 636 | added: v10.5.0 | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/errors.js +2Lines changed: 2 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -1218,6 +1218,8 @@ E('ERR_VM_MODULE_STATUS', 'Module status %s', Error); | 1218 | 1218 | E('ERR_WORKER_INVALID_EXEC_ARGV', (errors) => | 1219 | 1219 | `Initiated Worker with invalid execArgv flags: ${errors.join(', ')}`, | 1220 | 1220 | Error); | | 1221 | +E('ERR_WORKER_OUT_OF_MEMORY', 'Worker terminated due to reaching memory limit', | | 1222 | + Error); | 1221 | 1223 | E('ERR_WORKER_PATH', | 1222 | 1224 | 'The worker script filename must be an absolute path or a relative ' + | 1223 | 1225 | 'path starting with \'./\' or \'../\'. Received "%s"', | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/worker.js +47-5Lines changed: 47 additions & 5 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 2 | 2 |
| 3 | 3 | /* global SharedArrayBuffer */ | 4 | 4 |
| 5 | | -const { Object } = primordials; | | 5 | +const { Math, Object } = primordials; | 6 | 6 |
| 7 | 7 | const EventEmitter = require('events'); | 8 | 8 | const assert = require('internal/assert'); | 9 | 9 | const path = require('path'); | 10 | 10 |
| | 11 | +const errorCodes = require('internal/errors').codes; | 11 | 12 | const { | 12 | 13 | ERR_WORKER_PATH, | 13 | 14 | ERR_WORKER_UNSERIALIZABLE_ERROR, | 14 | 15 | ERR_WORKER_UNSUPPORTED_EXTENSION, | 15 | 16 | ERR_WORKER_INVALID_EXEC_ARGV, | 16 | 17 | ERR_INVALID_ARG_TYPE, | 17 | | -} = require('internal/errors').codes; | | 18 | +} = errorCodes; | 18 | 19 | const { validateString } = require('internal/validators'); | 19 | 20 | const { getOptionValue } = require('internal/options'); | 20 | 21 |
| @@ -37,8 +38,13 @@ const { pathToFileURL } = require('url'); | 37 | 38 | const { | 38 | 39 | ownsProcessState, | 39 | 40 | isMainThread, | | 41 | + resourceLimits: resourceLimitsRaw, | 40 | 42 | threadId, | 41 | 43 | Worker: WorkerImpl, | | 44 | + kMaxYoungGenerationSizeMb, | | 45 | + kMaxOldGenerationSizeMb, | | 46 | + kCodeRangeSizeMb, | | 47 | + kTotalResourceLimitCount | 42 | 48 | } = internalBinding('worker'); | 43 | 49 |
| 44 | 50 | const kHandle = Symbol('kHandle'); | @@ -102,7 +108,8 @@ class Worker extends EventEmitter { | 102 | 108 |
| 103 | 109 | const url = options.eval ? null : pathToFileURL(filename); | 104 | 110 | // Set up the C++ handle for the worker, as well as some internal wiring. | 105 | | - this[kHandle] = new WorkerImpl(url, options.execArgv); | | 111 | + this[kHandle] = new WorkerImpl(url, options.execArgv, | | 112 | + parseResourceLimits(options.resourceLimits)); | 106 | 113 | if (this[kHandle].invalidExecArgv) { | 107 | 114 | throw new ERR_WORKER_INVALID_EXEC_ARGV(this[kHandle].invalidExecArgv); | 108 | 115 | } | @@ -113,7 +120,7 @@ class Worker extends EventEmitter { | 113 | 120 | } else if (env !== undefined) { | 114 | 121 | this[kHandle].setEnvVars(env); | 115 | 122 | } | 116 | | - this[kHandle].onexit = (code) => this[kOnExit](code); | | 123 | + this[kHandle].onexit = (code, customErr) => this[kOnExit](code, customErr); | 117 | 124 | this[kPort] = this[kHandle].messagePort; | 118 | 125 | this[kPort].on('message', (data) => this[kOnMessage](data)); | 119 | 126 | this[kPort].start(); | @@ -157,11 +164,15 @@ class Worker extends EventEmitter { | 157 | 164 | this[kHandle].startThread(); | 158 | 165 | } | 159 | 166 |
| 160 | | - [kOnExit](code) { | | 167 | + [kOnExit](code, customErr) { | 161 | 168 | debug(`[${threadId}] hears end event for Worker ${this.threadId}`); | 162 | 169 | drainMessagePort(this[kPublicPort]); | 163 | 170 | drainMessagePort(this[kPort]); | 164 | 171 | this[kDispose](); | | 172 | + if (customErr) { | | 173 | + debug(`[${threadId}] failing with custom error ${customErr}`); | | 174 | + this.emit('error', new errorCodes[customErr]()); | | 175 | + } | 165 | 176 | this.emit('exit', code); | 166 | 177 | this.removeAllListeners(); | 167 | 178 | } | @@ -280,6 +291,12 @@ class Worker extends EventEmitter { | 280 | 291 | get stderr() { | 281 | 292 | return this[kParentSideStdio].stderr; | 282 | 293 | } | | 294 | + | | 295 | + get resourceLimits() { | | 296 | + if (this[kHandle] === null) return {}; | | 297 | + | | 298 | + return makeResourceLimits(this[kHandle].getResourceLimits()); | | 299 | + } | 283 | 300 | } | 284 | 301 |
| 285 | 302 | function pipeWithoutWarning(source, dest) { | @@ -294,10 +311,35 @@ function pipeWithoutWarning(source, dest) { | 294 | 311 | dest._maxListeners = destMaxListeners; | 295 | 312 | } | 296 | 313 |
| | 314 | +const resourceLimitsArray = new Float64Array(kTotalResourceLimitCount); | | 315 | +function parseResourceLimits(obj) { | | 316 | + const ret = resourceLimitsArray; | | 317 | + ret.fill(-1); | | 318 | + if (typeof obj !== 'object' || obj === null) return ret; | | 319 | + | | 320 | + if (typeof obj.maxOldGenerationSizeMb === 'number') | | 321 | + ret[kMaxOldGenerationSizeMb] = Math.max(obj.maxOldGenerationSizeMb, 2); | | 322 | + if (typeof obj.maxYoungGenerationSizeMb === 'number') | | 323 | + ret[kMaxYoungGenerationSizeMb] = obj.maxYoungGenerationSizeMb; | | 324 | + if (typeof obj.codeRangeSizeMb === 'number') | | 325 | + ret[kCodeRangeSizeMb] = obj.codeRangeSizeMb; | | 326 | + return ret; | | 327 | +} | | 328 | + | | 329 | +function makeResourceLimits(float64arr) { | | 330 | + return { | | 331 | + maxYoungGenerationSizeMb: float64arr[kMaxYoungGenerationSizeMb], | | 332 | + maxOldGenerationSizeMb: float64arr[kMaxOldGenerationSizeMb], | | 333 | + codeRangeSizeMb: float64arr[kCodeRangeSizeMb] | | 334 | + }; | | 335 | +} | | 336 | + | 297 | 337 | module.exports = { | 298 | 338 | ownsProcessState, | 299 | 339 | isMainThread, | 300 | 340 | SHARE_ENV, | | 341 | + resourceLimits: | | 342 | + !isMainThread ? makeResourceLimits(resourceLimitsRaw) : {}, | 301 | 343 | threadId, | 302 | 344 | Worker, | 303 | 345 | }; |
Collapse file Copy file name to clipboardExpand all lines: lib/worker_threads.js +2Lines changed: 2 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 3 | 3 | const { | 4 | 4 | isMainThread, | 5 | 5 | SHARE_ENV, | | 6 | + resourceLimits, | 6 | 7 | threadId, | 7 | 8 | Worker | 8 | 9 | } = require('internal/worker'); | @@ -20,6 +21,7 @@ module.exports = { | 20 | 21 | MessageChannel, | 21 | 22 | moveMessagePortToContext, | 22 | 23 | receiveMessageOnPort, | | 24 | + resourceLimits, | 23 | 25 | threadId, | 24 | 26 | SHARE_ENV, | 25 | 27 | Worker, | |
|