Skip to content

Commit

Permalink
worker: add experimental SynchronousWorker implementation
Browse files Browse the repository at this point in the history
Adds SynchronousWorker from the standalone synchronous-worker module
directly into Node.js (as Experimental).

The synchronous-worker module was originally written by Anna
Henningsen and is incorporated here at Matteo Collina's request and
Anna's permission.

Signed-off-by: James M Snell <[email protected]>
  • Loading branch information
jasnell committed Oct 16, 2022
1 parent 84064bf commit ba13228
Show file tree
Hide file tree
Showing 9 changed files with 1,131 additions and 4 deletions.
28 changes: 26 additions & 2 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -984,8 +984,7 @@ The externally maintained libraries used by Node.js are:
- Strongtalk assembler, the basis of the files assembler-arm-inl.h,
assembler-arm.cc, assembler-arm.h, assembler-ia32-inl.h,
assembler-ia32.cc, assembler-ia32.h, assembler-x64-inl.h,
assembler-x64.cc, assembler-x64.h, assembler-mips-inl.h,
assembler-mips.cc, assembler-mips.h, assembler.cc and assembler.h.
assembler-x64.cc, assembler-x64.h, assembler.cc and assembler.h.
This code is copyrighted by Sun Microsystems Inc. and released
under a 3-clause BSD license.

Expand Down Expand Up @@ -1871,3 +1870,28 @@ The externally maintained libraries used by Node.js are:
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""

- synchronous-worker, located at lib/worker_threads.js, is licensed as follows:
"""
The MIT License (MIT)

Copyright (c) 2020 Anna Henningsen

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
226 changes: 226 additions & 0 deletions doc/api/worker_threads.md
Original file line number Diff line number Diff line change
Expand Up @@ -1329,6 +1329,228 @@ from the running process and will preload the same preload scripts as the main
thread. If the preload script unconditionally launches a worker thread, every
thread spawned will spawn another until the application crashes.
## Synchronous Workers
> Stability: 1 - Experimental
### Class: `SynchronousWorker`
<!-- YAML
added: REPLACEME
-->
* Extends: {EventEmitter}
A `SynchronousWorker` is effectively a Node.js environment that runs within the
same thread.
```cjs
const { SynchronousWorker } = require('node:worker_threads');
const w = new SynchronousWorker();
const myAsyncFunction = w.createRequire(__filename)('my-module');
const response = w.runLoopUntilPromiseResolved(myAsyncFunction('http://example.org'));
const text = w.runLoopUntilPromiseResolved(response.text());
console.log(text);
```
#### `new SynchronousWorker([options])`
<!-- YAML
added: REPLACEME
-->
* `options` {Object}
* `sharedEventLoop` {boolean} When `true`, use the same event loop as the
outer Node.js instance. If this is passed, the `runLoop()` and
`runLoopUntilPromiseResolved()` methods become unavailable.
**Default:** `false`.
* `sharedMicrotaskQueue` {boolean} When true, use the same microtask queue as
the outer Node.js instance. This is used for resolving promises created in
the inner context, including those implicitly generated by `async/await`.
If this is passed, the `runLoopUntilPromiseResolved()` method becomes
unavailable. **Default:** `false`.
While setting `sharedEventLoop` to `false` and `sharedMicrotaskQueue` to `true`
is accepted, they typically do not make sense together.
#### `synchronousWorker.runLoop([mode])`
<!-- YAML
added: REPLACEME
-->
* `mode` {string} One of either `'default'`, `'once'`, or `'nowait'`.
Spin the event loop of the inner Node.js instance. `mode` can be either
`default`, `once` or `nowait`. See the [libuv documentation for `uv_run()`][]
for details on these modes.
#### `synchronousWorker.runLoopUntilPromiseResolved(promise)`
<!-- YAML
added: REPLACEME
-->
* `promise` {Promise}
Spin the event loop of the inner Node.js instance until a specific `Promise`
is resolved.
#### `synchronousWorker.runInWorkerScope(fn)`
<!-- YAML
added: REPLACEME
-->
* `fn` {Function}
Wrap `fn` and run it as if it were run on the event loop of the inner Node.js
instance. In particular, this ensures that Promises created by the function
itself are resolved correctly. You should generally use this to run any code
inside the innert Node.js instance that performs asynchronous activity and that
is not already running in an asynchronous context (you can compare this to
the code that runs synchronously from the main file of a Node.js application).
#### `synchronousWorker.loopAlive`
<!-- YAML
added: REPLACEME
-->
* Type: {boolean}
This is a read-only boolean property indicating whether there are currently any
items on the event loop of the inner Node.js instance.
#### `synchronousWorker.stop()`
<!-- YAML
added: REPLACEME
-->
Interrupt any execution of code inside the inner Node.js instance, i.e.
return directly from a `.runLoop()`, `.runLoopUntilPromiseResolved()` or
`.runInWorkerScope()` call. This will render the Node.js instance unusable
and is generally comparable to running `process.exit()`.
This method returns a `Promise` that will be resolved when all resources
associated with this Node.js instance are released. This `Promise` resolves on
the event loop of the _outer_ Node.js instance.
#### `synchronousWorker.createRequire(filename)`
<!-- YAML
added: REPLACEME
-->
* `filename` {string}
Create a `require()` function that can be used for loading code inside the
inner Node.js instance.
#### `synchronousWorker.globalThis`
<!-- YAML
added: REPLACEME
-->
* Type: {Object}
Returns a reference to the global object of the inner Node.js instance.
#### `synchronousWorker.process`
<!-- YAML
added: REPLACEME
-->
* Type: {Object}
Returns a reference to the `process` object of the inner Node.js instance.
### FAQ
#### What does a SynchronousWorker do?
Creates a new Node.js instance, using the same thread and the same JS heap.
You can create Node.js API objects, like network sockets, inside the new
Node.js instance, and spin the underlying event loop manually.
#### Where did SynchronousWorker come from?
`SynchronousWorker` was originally developer by Node.js core contributor
Anna Henningsen and published as a separate module [`synchronous-worker`][] on
npm under the MIT license. It was integrated into Node.js core with Anna's
permission. The majority of the code, documentation, and tests were adopted
almost verbatim from the original module.
#### Why would I use a SynchronousWorker?
The most common use case is probably running asynchronous code synchronously,
in situations where doing so cannot be avoided (even though one should try
really hard to avoid it). Another popular npm package that does this is
[`deasync`][], but `deasync`
* solves this problem by starting the event loop while it is already running
(which is explicitly _not_ supported by libuv and may lead to crashes)
* doesn’t allow specifying _which_ resources or callbacks should be waited for,
and instead allows everything inside the current thread to progress.
#### How can I avoid using SynchronousWorker?
If you do not need to directly interact with the objects inside the inner
Node.js instance, a lot of the time Worker threads together with
[`Atomics.wait()`][] will give you what you need.
#### My async functions/Promises/… don’t work
If you run a `SynchronousWorker` with its own microtask queue (i.e. in default
mode), code like this will not work as expected:
```cjs
const { SynchronousWorker } = require('node:worker_threads');
const w = new SynchronousWorker();
let promise;
w.runInWorkerScope(() => {
promise = (async () => {
return w.createRequire(__filename)('node-fetch')('...');
})();
});
w.runLoopUntilPromiseResolved(promise);
```
The reason for this is that `async` functions (and Promise `.then()` handlers)
add their microtasks to the microtask queue for the Context in which the
async function (or `.then()` callback) was defined, and not the Context in which
the original `Promise` was created. Put in other words, it is possible for a
`Promise` chain to be run on different microtask queues.
While this behavior may be counterintuitive, it is what the V8 engine does,
and is not under the control of Node.js.
What this means is that you will need to make sure that the functions are
compiled in the Context in which they are supposed to be run; the two main
ways to achieve that are to:
* Put them in a separate file that is loaded through `w.createRequire()`
* Use `w.createRequire(__filename)('vm').runInThisContext()` to manually compile
the code for the function in the Context of the target Node.js instance.
For example:
```cjs
const { SynchronousWorker } = require('node:worker_threads');
const w = new SynchronousWorker();
const req = w.createRequire(__filename);
let promise;
w.runInWorkerScope(() => {
promise = req('vm').runInThisContext(`(async(req) => {
return await req('node-fetch')('...');
})`)(req);
});
w.runLoopUntilPromiseResolved(promise);
```
[Addons worker support]: addons.md#worker-support
[ECMAScript module loader]: esm.md#data-imports
[HTML structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
Expand All @@ -1341,6 +1563,7 @@ thread spawned will spawn another until the application crashes.
[`--max-semi-space-size`]: cli.md#--max-semi-space-sizesize-in-megabytes
[`ArrayBuffer`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
[`AsyncResource`]: async_hooks.md#class-asyncresource
[`Atomics.wait()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/wait
[`Buffer.allocUnsafe()`]: buffer.md#static-method-bufferallocunsafesize
[`Buffer`]: buffer.md
[`ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST`]: errors.md#err_missing_message_port_in_transfer_list
Expand All @@ -1354,6 +1577,7 @@ thread spawned will spawn another until the application crashes.
[`Worker constructor options`]: #new-workerfilename-options
[`Worker`]: #class-worker
[`data:` URL]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
[`deasync`]: https://www.npmjs.com/package/deasync
[`fs.close()`]: fs.md#fsclosefd-callback
[`fs.open()`]: fs.md#fsopenpath-flags-mode-callback
[`markAsUntransferable()`]: #workermarkasuntransferableobject
Expand All @@ -1378,6 +1602,7 @@ thread spawned will spawn another until the application crashes.
[`require('node:worker_threads').parentPort`]: #workerparentport
[`require('node:worker_threads').threadId`]: #workerthreadid
[`require('node:worker_threads').workerData`]: #workerworkerdata
[`synchronous-worker`]: https://github.com/addaleax/synchronous-worker
[`trace_events`]: tracing.md
[`v8.getHeapSnapshot()`]: v8.md#v8getheapsnapshot
[`vm`]: vm.md
Expand All @@ -1390,4 +1615,5 @@ thread spawned will spawn another until the application crashes.
[browser `MessagePort`]: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort
[child processes]: child_process.md
[contextified]: vm.md#what-does-it-mean-to-contextify-an-object
[libuv documentation for `uv_run()`]: http://docs.libuv.org/en/v1.x/loop.html#c.uv_run
[v8.serdes]: v8.md#serialization-api
Loading

0 comments on commit ba13228

Please sign in to comment.