Skip to content

Commit 886ef09

Browse files
addaleaxMylesBorins
authored andcommitted
worker: allow specifying resource limits
Allow specifying resource limits for the JS engine instance created as part of a Worker. PR-URL: #26628 Reviewed-By: Joyee Cheung <[email protected]> Reviewed-By: Gireesh Punathil <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Franziska Hinkelmann <[email protected]>
1 parent fcc4bf9 commit 886ef09

8 files changed

+315
-26
lines changed

doc/api/errors.md

+5
Original file line numberDiff line numberDiff line change
@@ -1990,6 +1990,11 @@ meaning of the error depends on the specific function.
19901990
The `execArgv` option passed to the `Worker` constructor contains
19911991
invalid flags.
19921992

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+
19931998
<a id="ERR_WORKER_PATH"></a>
19941999
### `ERR_WORKER_PATH`
19952000

doc/api/worker_threads.md

+48
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,22 @@ console.log(receiveMessageOnPort(port2));
157157
When this function is used, no `'message'` event will be emitted and the
158158
`onmessage` listener will not be invoked.
159159

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+
160176
## `worker.SHARE_ENV`
161177
<!-- YAML
162178
added: v11.14.0
@@ -488,6 +504,13 @@ if (isMainThread) {
488504
```
489505

490506
### `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+
-->
491514

492515
* `filename` {string} The path to the Worker’s main script. Must be
493516
either an absolute path or a relative path (i.e. relative to the
@@ -519,6 +542,16 @@ if (isMainThread) {
519542
occur as described in the [HTML structured clone algorithm][], and an error
520543
will be thrown if the object cannot be cloned (e.g. because it contains
521544
`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.
522555

523556
### Event: `'error'`
524557
<!-- YAML
@@ -583,6 +616,21 @@ Opposite of `unref()`, calling `ref()` on a previously `unref()`ed worker will
583616
behavior). If the worker is `ref()`ed, calling `ref()` again will have
584617
no effect.
585618

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.
586634
### `worker.stderr`
587635
<!-- YAML
588636
added: v10.5.0

lib/internal/errors.js

+2
Original file line numberDiff line numberDiff line change
@@ -1218,6 +1218,8 @@ E('ERR_VM_MODULE_STATUS', 'Module status %s', Error);
12181218
E('ERR_WORKER_INVALID_EXEC_ARGV', (errors) =>
12191219
`Initiated Worker with invalid execArgv flags: ${errors.join(', ')}`,
12201220
Error);
1221+
E('ERR_WORKER_OUT_OF_MEMORY', 'Worker terminated due to reaching memory limit',
1222+
Error);
12211223
E('ERR_WORKER_PATH',
12221224
'The worker script filename must be an absolute path or a relative ' +
12231225
'path starting with \'./\' or \'../\'. Received "%s"',

lib/internal/worker.js

+47-5
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,20 @@
22

33
/* global SharedArrayBuffer */
44

5-
const { Object } = primordials;
5+
const { Math, Object } = primordials;
66

77
const EventEmitter = require('events');
88
const assert = require('internal/assert');
99
const path = require('path');
1010

11+
const errorCodes = require('internal/errors').codes;
1112
const {
1213
ERR_WORKER_PATH,
1314
ERR_WORKER_UNSERIALIZABLE_ERROR,
1415
ERR_WORKER_UNSUPPORTED_EXTENSION,
1516
ERR_WORKER_INVALID_EXEC_ARGV,
1617
ERR_INVALID_ARG_TYPE,
17-
} = require('internal/errors').codes;
18+
} = errorCodes;
1819
const { validateString } = require('internal/validators');
1920
const { getOptionValue } = require('internal/options');
2021

@@ -37,8 +38,13 @@ const { pathToFileURL } = require('url');
3738
const {
3839
ownsProcessState,
3940
isMainThread,
41+
resourceLimits: resourceLimitsRaw,
4042
threadId,
4143
Worker: WorkerImpl,
44+
kMaxYoungGenerationSizeMb,
45+
kMaxOldGenerationSizeMb,
46+
kCodeRangeSizeMb,
47+
kTotalResourceLimitCount
4248
} = internalBinding('worker');
4349

4450
const kHandle = Symbol('kHandle');
@@ -102,7 +108,8 @@ class Worker extends EventEmitter {
102108

103109
const url = options.eval ? null : pathToFileURL(filename);
104110
// 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));
106113
if (this[kHandle].invalidExecArgv) {
107114
throw new ERR_WORKER_INVALID_EXEC_ARGV(this[kHandle].invalidExecArgv);
108115
}
@@ -113,7 +120,7 @@ class Worker extends EventEmitter {
113120
} else if (env !== undefined) {
114121
this[kHandle].setEnvVars(env);
115122
}
116-
this[kHandle].onexit = (code) => this[kOnExit](code);
123+
this[kHandle].onexit = (code, customErr) => this[kOnExit](code, customErr);
117124
this[kPort] = this[kHandle].messagePort;
118125
this[kPort].on('message', (data) => this[kOnMessage](data));
119126
this[kPort].start();
@@ -157,11 +164,15 @@ class Worker extends EventEmitter {
157164
this[kHandle].startThread();
158165
}
159166

160-
[kOnExit](code) {
167+
[kOnExit](code, customErr) {
161168
debug(`[${threadId}] hears end event for Worker ${this.threadId}`);
162169
drainMessagePort(this[kPublicPort]);
163170
drainMessagePort(this[kPort]);
164171
this[kDispose]();
172+
if (customErr) {
173+
debug(`[${threadId}] failing with custom error ${customErr}`);
174+
this.emit('error', new errorCodes[customErr]());
175+
}
165176
this.emit('exit', code);
166177
this.removeAllListeners();
167178
}
@@ -280,6 +291,12 @@ class Worker extends EventEmitter {
280291
get stderr() {
281292
return this[kParentSideStdio].stderr;
282293
}
294+
295+
get resourceLimits() {
296+
if (this[kHandle] === null) return {};
297+
298+
return makeResourceLimits(this[kHandle].getResourceLimits());
299+
}
283300
}
284301

285302
function pipeWithoutWarning(source, dest) {
@@ -294,10 +311,35 @@ function pipeWithoutWarning(source, dest) {
294311
dest._maxListeners = destMaxListeners;
295312
}
296313

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+
297337
module.exports = {
298338
ownsProcessState,
299339
isMainThread,
300340
SHARE_ENV,
341+
resourceLimits:
342+
!isMainThread ? makeResourceLimits(resourceLimitsRaw) : {},
301343
threadId,
302344
Worker,
303345
};

lib/worker_threads.js

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const {
44
isMainThread,
55
SHARE_ENV,
6+
resourceLimits,
67
threadId,
78
Worker
89
} = require('internal/worker');
@@ -20,6 +21,7 @@ module.exports = {
2021
MessageChannel,
2122
moveMessagePortToContext,
2223
receiveMessageOnPort,
24+
resourceLimits,
2325
threadId,
2426
SHARE_ENV,
2527
Worker,

0 commit comments

Comments
 (0)