Skip to content

Commit 6bd7eec

Browse files
committed
worker: Add experimental SynchronousWorker implementation
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]>
1 parent 84064bf commit 6bd7eec

11 files changed

+1142
-2
lines changed

LICENSE

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -984,8 +984,7 @@ The externally maintained libraries used by Node.js are:
984984
- Strongtalk assembler, the basis of the files assembler-arm-inl.h,
985985
assembler-arm.cc, assembler-arm.h, assembler-ia32-inl.h,
986986
assembler-ia32.cc, assembler-ia32.h, assembler-x64-inl.h,
987-
assembler-x64.cc, assembler-x64.h, assembler-mips-inl.h,
988-
assembler-mips.cc, assembler-mips.h, assembler.cc and assembler.h.
987+
assembler-x64.cc, assembler-x64.h, assembler.cc and assembler.h.
989988
This code is copyrighted by Sun Microsystems Inc. and released
990989
under a 3-clause BSD license.
991990

@@ -1871,3 +1870,28 @@ The externally maintained libraries used by Node.js are:
18711870
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
18721871
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
18731872
"""
1873+
1874+
- synchronous-worker, located at lib/worker_threads.js, is licensed as follows:
1875+
"""
1876+
The MIT License (MIT)
1877+
1878+
Copyright (c) 2020 Anna Henningsen
1879+
1880+
Permission is hereby granted, free of charge, to any person obtaining a copy
1881+
of this software and associated documentation files (the "Software"), to deal
1882+
in the Software without restriction, including without limitation the rights
1883+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1884+
copies of the Software, and to permit persons to whom the Software is
1885+
furnished to do so, subject to the following conditions:
1886+
1887+
The above copyright notice and this permission notice shall be included in all
1888+
copies or substantial portions of the Software.
1889+
1890+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1891+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1892+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1893+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1894+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1895+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1896+
SOFTWARE.
1897+
"""

doc/api/worker_threads.md

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1329,6 +1329,228 @@ from the running process and will preload the same preload scripts as the main
13291329
thread. If the preload script unconditionally launches a worker thread, every
13301330
thread spawned will spawn another until the application crashes.
13311331
1332+
## Synchronous Workers
1333+
1334+
> Stability: 1 - Experimental
1335+
1336+
### Class: `SynchronousWorker`
1337+
1338+
<!-- YAML
1339+
added: REPLACEME
1340+
-->
1341+
1342+
* Extends: {EventEmitter}
1343+
1344+
A `SynchronousWorker` is effectively a Node.js environment that runs within the
1345+
same thread.
1346+
1347+
```cjs
1348+
const { SynchronousWorker } = require('node:worker_threads');
1349+
const w = new SynchronousWorker();
1350+
const myAsyncFunction = w.createRequire(__filename)('my-module');
1351+
const response = w.runLoopUntilPromiseResolved(myAsyncFunction('http://example.org'));
1352+
const text = w.runLoopUntilPromiseResolved(response.text());
1353+
console.log(text);
1354+
```
1355+
1356+
#### `new SynchronousWorker([options])`
1357+
1358+
<!-- YAML
1359+
added: REPLACEME
1360+
-->
1361+
1362+
* `options` {Object}
1363+
* `sharedEventLoop` {boolean} When `true`, use the same event loop as the
1364+
outer Node.js instance. If this is passed, the `runLoop()` and
1365+
`runLoopUntilPromiseResolved()` methods become unavailable.
1366+
**Default:** `false`.
1367+
* `sharedMicrotaskQueue` {boolean} When true, use the same microtask queue as
1368+
the outer Node.js instance. This is used for resolving promises created in
1369+
the inner context, including those implicitly generated by `async/await`.
1370+
If this is passed, the `runLoopUntilPromiseResolved()` method becomes
1371+
unavailable. **Default:** `false`.
1372+
1373+
While setting `sharedEventLoop` to `false` and `sharedMicrotaskQueue` to `true`
1374+
is accepted, they typically do not make sense together.
1375+
1376+
#### `synchronousWorker.runLoop([mode])`
1377+
1378+
<!-- YAML
1379+
added: REPLACEME
1380+
-->
1381+
1382+
* `mode` {string} One of either `'default'`, `'once'`, or `'nowait'`.
1383+
1384+
Spin the event loop of the inner Node.js instance. `mode` can be either
1385+
`default`, `once` or `nowait`. See the [libuv documentation for `uv_run()`][]
1386+
for details on these modes.
1387+
1388+
#### `synchronousWorker.runLoopUntilPromiseResolved(promise)`
1389+
1390+
<!-- YAML
1391+
added: REPLACEME
1392+
-->
1393+
1394+
* `promise` {Promise}
1395+
1396+
Spin the event loop of the innsert Node.js instance until a specific `Promise`
1397+
is resolved.
1398+
1399+
#### `synchronousWorker.runInWorkerScope(fn)`
1400+
1401+
<!-- YAML
1402+
added: REPLACEME
1403+
-->
1404+
1405+
* `fn` {Function}
1406+
1407+
Wrap `fn` and run it as if it were run on the event loop of the inner Node.js
1408+
instance. In particular, this ensures that Promises created by the function
1409+
itself are resolved correctly. You should generally use this to run any code
1410+
inside the innert Node.js instance that performs asynchronous activity and that
1411+
is not already running in an asynchronous context (you can compare this to
1412+
the code that runs synchronously from the main file of a Node.js application).
1413+
1414+
#### `synchronousWorker.loopAlive`
1415+
1416+
<!-- YAML
1417+
added: REPLACEME
1418+
-->
1419+
1420+
* Type: {boolean}
1421+
1422+
This is a read-only boolean property indicating whether there are currently any
1423+
items on the event loop of the inner Node.js instance.
1424+
1425+
#### `synchronousWorker.stop()`
1426+
1427+
<!-- YAML
1428+
added: REPLACEME
1429+
-->
1430+
1431+
Interrupt any execution of code inside the inner Node.js instance, i.e.
1432+
return directly from a `.runLoop()`, `.runLoopUntilPromiseResolved()` or
1433+
`.runInWorkerScope()` call. This will render the Node.js instance unusable
1434+
and is generally comparable to running `process.exit()`.
1435+
1436+
This method returns a `Promise` that will be resolved when all resources
1437+
associated with this Node.js instance are released. This `Promise` resolves on
1438+
the event loop of the _outer_ Node.js instance.
1439+
1440+
#### `synchronousWorker.createRequire(filename)`
1441+
1442+
<!-- YAML
1443+
added: REPLACEME
1444+
-->
1445+
1446+
* `filename` {string}
1447+
1448+
Create a `require()` function that can be used for loading code inside the
1449+
inner Node.js instance.
1450+
1451+
#### `synchronousWorker.globalThis`
1452+
1453+
<!-- YAML
1454+
added: REPLACEME
1455+
-->
1456+
1457+
* Type: {Object}
1458+
1459+
Returns a reference to the global object of the inner Node.js instance.
1460+
1461+
#### `synchronousWorker.process`
1462+
1463+
<!-- YAML
1464+
added: REPLACEME
1465+
-->
1466+
1467+
* Type: {Object}
1468+
1469+
Returns a reference to the `process` object of the inner Node.js instance.
1470+
1471+
### FAQ
1472+
1473+
#### What does a SynchronousWorker do?
1474+
1475+
Creates a new Node.js instance, using the same thread and the same JS heap.
1476+
You can create Node.js API objects, like network sockets, inside the new
1477+
Node.js instance, and spin the underlying event loop manually.
1478+
1479+
#### Where did SynchronousWorker come from?
1480+
1481+
`SynchronousWorker` was originally developer by Node.js core contributor
1482+
Anna Henningsen and published as a separate module [`synchronous-worker`][] on
1483+
npm under the MIT license. It was integrated into Node.js core with Anna's
1484+
permission. The majority of the code, documentation, and tests were adopted
1485+
almost verbatim from the original module.
1486+
1487+
#### Why would I use a SynchronousWorker?
1488+
1489+
The most common use case is probably running asynchronous code synchronously,
1490+
in situations where doing so cannot be avoided (even though one should try
1491+
really hard to avoid it). Another popular npm package that does this is
1492+
[`deasync`][], but `deasync`
1493+
1494+
* solves this problem by starting the event loop while it is already running
1495+
(which is explicitly _not_ supported by libuv and may lead to crashes)
1496+
* doesn’t allow specifying _which_ resources or callbacks should be waited for,
1497+
and instead allows everything inside the current thread to progress.
1498+
1499+
#### How can I avoid using SynchronousWorker?
1500+
1501+
If you do not need to directly interact with the objects inside the inner
1502+
Node.js instance, a lot of the time Worker threads together with
1503+
[`Atomics.wait()`][] will give you what you need.
1504+
1505+
#### My async functions/Promises/… don’t work
1506+
1507+
If you run a `SynchronousWorker` with its own microtask queue (i.e. in default
1508+
mode), code like this will not work as expected:
1509+
1510+
```cjs
1511+
const { SynchronousWorker } = require('node:worker_threads');
1512+
const w = new SynchronousWorker();
1513+
let promise;
1514+
w.runInWorkerScope(() => {
1515+
promise = (async () => {
1516+
return w.createRequire(__filename)('node-fetch')('...');
1517+
})();
1518+
});
1519+
w.runLoopUntilPromiseResolved(promise);
1520+
```
1521+
1522+
The reason for this is that `async` functions (and Promise `.then()` handlers)
1523+
add their microtasks to the microtask queue for the Context in which the
1524+
async function (or `.then()` callback) was defined, and not the Context in which
1525+
the original `Promise` was created. Put in other words, it is possible for a
1526+
`Promise` chain to be run on different microtask queues.
1527+
1528+
While this behavior may be counterintuitive, it is what the V8 engine does,
1529+
and is not under the control of Node.js.
1530+
1531+
What this means is that you will need to make sure that the functions are
1532+
compiled in the Context in which they are supposed to be run; the two main
1533+
ways to achieve that are to:
1534+
1535+
* Put them in a separate file that is loaded through `w.createRequire()`
1536+
* Use `w.createRequire(__filename)('vm').runInThisContext()` to manually compile
1537+
the code for the function in the Context of the target Node.js instance.
1538+
1539+
For example:
1540+
1541+
```cjs
1542+
const { SynchronousWorker } = require('node:worker_threads');
1543+
const w = new SynchronousWorker();
1544+
const req = w.createRequire(__filename);
1545+
let promise;
1546+
w.runInWorkerScope(() => {
1547+
promise = req('vm').runInThisContext(`(async(req) => {
1548+
return await req('node-fetch')('...');
1549+
})`)(req);
1550+
});
1551+
w.runLoopUntilPromiseResolved(promise);
1552+
```
1553+
13321554
[Addons worker support]: addons.md#worker-support
13331555
[ECMAScript module loader]: esm.md#data-imports
13341556
[HTML structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
@@ -1341,6 +1563,7 @@ thread spawned will spawn another until the application crashes.
13411563
[`--max-semi-space-size`]: cli.md#--max-semi-space-sizesize-in-megabytes
13421564
[`ArrayBuffer`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
13431565
[`AsyncResource`]: async_hooks.md#class-asyncresource
1566+
[`Atomics.wait()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/wait
13441567
[`Buffer.allocUnsafe()`]: buffer.md#static-method-bufferallocunsafesize
13451568
[`Buffer`]: buffer.md
13461569
[`ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST`]: errors.md#err_missing_message_port_in_transfer_list
@@ -1354,6 +1577,7 @@ thread spawned will spawn another until the application crashes.
13541577
[`Worker constructor options`]: #new-workerfilename-options
13551578
[`Worker`]: #class-worker
13561579
[`data:` URL]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs
1580+
[`deasync`]: https://www.npmjs.com/package/deasync
13571581
[`fs.close()`]: fs.md#fsclosefd-callback
13581582
[`fs.open()`]: fs.md#fsopenpath-flags-mode-callback
13591583
[`markAsUntransferable()`]: #workermarkasuntransferableobject
@@ -1378,6 +1602,7 @@ thread spawned will spawn another until the application crashes.
13781602
[`require('node:worker_threads').parentPort`]: #workerparentport
13791603
[`require('node:worker_threads').threadId`]: #workerthreadid
13801604
[`require('node:worker_threads').workerData`]: #workerworkerdata
1605+
[`synchronous-worker`]: https://github.com/addaleax/synchronous-worker
13811606
[`trace_events`]: tracing.md
13821607
[`v8.getHeapSnapshot()`]: v8.md#v8getheapsnapshot
13831608
[`vm`]: vm.md
@@ -1390,4 +1615,5 @@ thread spawned will spawn another until the application crashes.
13901615
[browser `MessagePort`]: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort
13911616
[child processes]: child_process.md
13921617
[contextified]: vm.md#what-does-it-mean-to-contextify-an-object
1618+
[libuv documentation for `uv_run()`]: http://docs.libuv.org/en/v1.x/loop.html#c.uv_run
13931619
[v8.serdes]: v8.md#serialization-api

0 commit comments

Comments
 (0)