Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

worker: Add experimental SynchronousWorker implementation #45018

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
"""
17 changes: 17 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,22 @@ added: REPLACEME

Use this flag to enable [ShadowRealm][] support.

### `--experimental-synchronousworker`
GeoffreyBooth marked this conversation as resolved.
Show resolved Hide resolved

<!-- YAML
added: REPLACEME
-->

Enable experimental support for `worker_threads.SynchronousWorker`.

### `--no-experimental-synchronousworker`

<!-- YAML
added: REPLACEME
-->

Disable experimental support for `worker_threads.SynchronousWorker`.

### `--experimental-vm-modules`

<!-- YAML
Expand Down Expand Up @@ -1839,6 +1855,7 @@ Node.js options that are allowed are:
* `--experimental-policy`
* `--experimental-shadow-realm`
* `--experimental-specifier-resolution`
* `--experimental-synchronousworker`
* `--experimental-top-level-await`
* `--experimental-vm-modules`
* `--experimental-wasi-unstable-preview1`
Expand Down
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
Copy link
Member

@legendecas legendecas Oct 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind sharing one or two outstanding use cases on these options to be true? I find it would be counter to the design goals of SynchronousWorker if these options are true:

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use case is hot module reloading. I can create a new "node" environment from Node with my full application and reboot it every time there is a change.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for sharing! Does HMR require sharing the event loop and the microtask queue, as these two options suggest here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, exactly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should include a docs example of using this for hot module reload.

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);
```
Comment on lines +1510 to +1520
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the ESM version. Also could we rewrite this example to use the fetch that’s now part of core, rather than node-fetch?

Suggested change
```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);
```
```mjs
import { SynchronousWorker } from 'node:worker_threads';
const w = new SynchronousWorker();
let promise;
w.runInWorkerScope(() => {
promise = (async () => {
return w.createRequire(import.meta.url)('node-fetch')('...');
})();
});
w.runLoopUntilPromiseResolved(promise);
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);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I note in the test, there is something stopping this from working with the built-in fetch that I do not quite yet understand. Before we update the documentation to use the internal fetch we'll need to investigate that issue (which should not need to be done before this lands)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I specifically left the mjs examples out for now because I want to do further testing to validate those cases before adding the examples.


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);
```
Comment on lines +1541 to +1552
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding ESM version; does w.createRequire accept import.meta.url? Could this be an import instead, because I assume the generated require only accepts CJS code as input?

Suggested change
```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);
```
```mjs
import { SynchronousWorker } = from 'node:worker_threads';
const w = new SynchronousWorker();
const req = w.createRequire(import.meta.url);
let promise;
w.runInWorkerScope(() => {
promise = req('node:vm').runInThisContext(`(async(req) => {
return await req('node-fetch')('...');
})`)(req);
});
w.runLoopUntilPromiseResolved(promise);
const { SynchronousWorker } = require('node:worker_threads');
const w = new SynchronousWorker();
const req = w.createRequire(__filename);
let promise;
w.runInWorkerScope(() => {
  promise = req('node: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