Skip to content

Commit

Permalink
worker: add postMessageToWorker
Browse files Browse the repository at this point in the history
  • Loading branch information
ShogunPanda committed Jul 2, 2024
1 parent 8f71a1b commit f40fa69
Show file tree
Hide file tree
Showing 11 changed files with 550 additions and 5 deletions.
21 changes: 21 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -3085,6 +3085,16 @@ The `Worker` initialization failed.
The `execArgv` option passed to the `Worker` constructor contains
invalid flags.

<a id="ERR_WORKER_INVALID_ID"></a>

### `ERR_WORKER_INVALID_ID`

<!-- YAML
added: REPLACEME
-->

The thread ID requested in [`postMessageToWorker()`][] is invalid or has no `workerMessage` listener.

<a id="ERR_WORKER_NOT_RUNNING"></a>

### `ERR_WORKER_NOT_RUNNING`
Expand All @@ -3104,6 +3114,16 @@ The `Worker` instance terminated because it reached its memory limit.
The path for the main script of a worker is neither an absolute path
nor a relative path starting with `./` or `../`.

<a id="ERR_WORKER_SAME_THREAD"></a>

### `ERR_WORKER_SAME_THREAD`

<!-- YAML
added: REPLACEME
-->

The thread id requested in [`postMessageToWorker()`][] is the current thread id.

<a id="ERR_WORKER_UNSERIALIZABLE_ERROR"></a>

### `ERR_WORKER_UNSERIALIZABLE_ERROR`
Expand Down Expand Up @@ -4027,6 +4047,7 @@ An error occurred trying to allocate memory. This should never happen.
[`new URLSearchParams(iterable)`]: url.md#new-urlsearchparamsiterable
[`package.json`]: packages.md#nodejs-packagejson-field-definitions
[`postMessage()`]: worker_threads.md#portpostmessagevalue-transferlist
[`postMessageToWorker()`]: worker_threads.md#workerpostmessagetoworkerdestination-value-transferlist-timeout
[`process.on('exit')`]: process.md#event-exit
[`process.send()`]: process.md#processsendmessage-sendhandle-options-callback
[`process.setUncaughtExceptionCaptureCallback()`]: process.md#processsetuncaughtexceptioncapturecallbackfn
Expand Down
13 changes: 13 additions & 0 deletions doc/api/process.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,18 @@ possible to record such errors in an error log, either periodically (which is
likely best for long-running application) or upon process exit (which is likely
most convenient for scripts).

### Event: `'workerMessage'`

<!-- YAML
added: REPLACEME
-->

* `value` {any} A value transmitted using [`postMessageToWorker()`][].
* `source` {number} The transmitting worker thread ID or `0` for the main thread.

The `'workerMessage'` event is emitted for any incoming message send by the other
party by using [`postMessageToWorker()`][].

### Event: `'uncaughtException'`

<!-- YAML
Expand Down Expand Up @@ -4073,6 +4085,7 @@ cases:
[`net.Server`]: net.md#class-netserver
[`net.Socket`]: net.md#class-netsocket
[`os.constants.dlopen`]: os.md#dlopen-constants
[`postMessageToWorker()`]: worker_threads.md#workerpostmessagetoworkerdestination-value-transferlist-timeout
[`process.argv`]: #processargv
[`process.config`]: #processconfig
[`process.execPath`]: #processexecpath
Expand Down
105 changes: 105 additions & 0 deletions doc/api/worker_threads.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,111 @@ if (isMainThread) {
}
```

## `worker.postMessageToWorker(destination, value[, transferList][, timeout])`

<!-- YAML
added: REPLACEME
-->

> Stability: 1.1 - Active development
* `destination` {number} The target thread ID.
* `value` {any} The value to send.
* `transferList` {Object\[]} If one or more `MessagePort`-like objects are passed in `value`,
a `transferList` is required for those items or [`ERR_MISSING_MESSAGE_PORT_IN_TRANSFER_LIST`][] is thrown.
See [`port.postMessage()`][] for more information.
* `timeout` {number} Time to wait for the message to be delivered in milliseconds.
By default it's `undefined`, which means wait forever.
* Returns: {Promise} A promise which is fulfilled if the message was successfully sent.

Sends a value to another worker, identified by its thread ID.

If the target thread has no listener for the `workerMessage` event, then the operation will throw an error.

This method should be used when the target thread is not the direct
parent or child of the current thread.
If the two threads are parent-children, use the [`require('node:worker_threads').parentPort.postMessage()`][]
and the [`worker.postMessage()`][] to let the threads communicate.

The example below shows the use of of `postMessageToWorker`: it creates 10 nested threads,
the last one will try to communicate with the main thread.

```mjs
import { fileURLToPath } from 'node:url';
import { once } from 'node:events';
import process from 'node:process';
import {
isMainThread,
postMessageToWorker,
threadId,
workerData,
Worker,
} from 'node:worker_threads';

const channel = new BroadcastChannel('sync');
const level = workerData?.level ?? 0;

if (level < 10) {
const worker = new Worker(fileURLToPath(import.meta.url), {
workerData: { level: level + 1 },
});
}

if (level === 0) {
process.on('workerMessage', (value, source) => {
console.log(`${source} -> ${threadId}:`, value);
postMessageToWorker(source, { message: 'pong' });
});
} else if (level === 10) {
process.on('workerMessage', (value, source) => {
console.log(`${source} -> ${threadId}:`, value);
channel.postMessage('done');
channel.close();
});

await postMessageToWorker(0, { message: 'ping' });
}

channel.onmessage = channel.close;
```
```cjs
const { once } = require('node:events');
const {
isMainThread,
postMessageToWorker,
threadId,
workerData,
Worker,
} = require('node:worker_threads');

const channel = new BroadcastChannel('sync');
const level = workerData?.level ?? 0;

if (level < 10) {
const worker = new Worker(__filename, {
workerData: { level: level + 1 },
});
}

if (level === 0) {
process.on('workerMessage', (value, source) => {
console.log(`${source} -> ${threadId}:`, value);
postMessageToWorker(source, { message: 'pong' });
});
} else if (level === 10) {
process.on('workerMessage', (value, source) => {
console.log(`${source} -> ${threadId}:`, value);
channel.postMessage('done');
channel.close();
});

postMessageToWorker(0, { message: 'ping' });
}

channel.onmessage = channel.close;
```
## `worker.receiveMessageOnPort(port)`
<!-- YAML
Expand Down
2 changes: 2 additions & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1862,6 +1862,7 @@ E('ERR_WORKER_INIT_FAILED', 'Worker initialization failure: %s', Error);
E('ERR_WORKER_INVALID_EXEC_ARGV', (errors, msg = 'invalid execArgv flags') =>
`Initiated Worker with ${msg}: ${ArrayPrototypeJoin(errors, ', ')}`,
Error);
E('ERR_WORKER_INVALID_ID', 'Invalid worker id %d', Error);
E('ERR_WORKER_NOT_RUNNING', 'Worker instance not running', Error);
E('ERR_WORKER_OUT_OF_MEMORY',
'Worker terminated due to reaching memory limit: %s', Error);
Expand All @@ -1876,6 +1877,7 @@ E('ERR_WORKER_PATH', (filename) =>
) +
` Received "${filename}"`,
TypeError);
E('ERR_WORKER_SAME_THREAD', 'Cannot connect to the same thread', Error);
E('ERR_WORKER_UNSERIALIZABLE_ERROR',
'Serializing an uncaught exception failed', Error);
E('ERR_WORKER_UNSUPPORTED_OPERATION',
Expand Down
4 changes: 4 additions & 0 deletions lib/internal/main/worker_thread.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ const {
kStdioWantsMoreDataCallback,
} = workerIo;

const { setupMainThreadPort } = require('internal/worker/messaging');

const {
onGlobalUncaughtException,
} = require('internal/process/execution');
Expand Down Expand Up @@ -96,6 +98,7 @@ port.on('message', (message) => {
hasStdin,
publicPort,
workerData,
mainThreadPort,
} = message;

if (doEval !== 'internal') {
Expand All @@ -109,6 +112,7 @@ port.on('message', (message) => {
}

require('internal/worker').assignEnvironmentData(environmentData);
setupMainThreadPort(mainThreadPort);

if (SharedArrayBuffer !== undefined) {
// The counter is only passed to the workers created by the main thread,
Expand Down
15 changes: 11 additions & 4 deletions lib/internal/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const {
ReadableWorkerStdio,
WritableWorkerStdio,
} = workerIo;
const { createMainThreadPort, destroyMainThreadPort } = require('internal/worker/messaging');
const { deserializeError } = require('internal/error_serdes');
const { fileURLToPath, isURL, pathToFileURL } = require('internal/url');
const { kEmptyObject } = require('internal/util');
Expand Down Expand Up @@ -250,14 +251,18 @@ class Worker extends EventEmitter {

this[kParentSideStdio] = { stdin, stdout, stderr };

const { port1, port2 } = new MessageChannel();
const transferList = [port2];
const mainThreadPortToWorker = createMainThreadPort(this.threadId);
const {
port1: publicPortToParent,
port2: publicPortToWorker,
} = new MessageChannel();
const transferList = [mainThreadPortToWorker, publicPortToWorker];
// If transferList is provided.
if (options.transferList)
ArrayPrototypePush(transferList,
...new SafeArrayIterator(options.transferList));

this[kPublicPort] = port1;
this[kPublicPort] = publicPortToParent;
ArrayPrototypeForEach(['message', 'messageerror'], (event) => {
this[kPublicPort].on(event, (message) => this.emit(event, message));
});
Expand All @@ -271,8 +276,9 @@ class Worker extends EventEmitter {
cwdCounter: cwdCounter || workerIo.sharedCwdCounter,
workerData: options.workerData,
environmentData,
publicPort: port2,
hasStdin: !!options.stdin,
publicPort: publicPortToWorker,
mainThreadPort: mainThreadPortToWorker,
}, transferList);
// Use this to cache the Worker's loopStart value once available.
this[kLoopStartTime] = -1;
Expand All @@ -295,6 +301,7 @@ class Worker extends EventEmitter {
debug(`[${threadId}] hears end event for Worker ${this.threadId}`);
drainMessagePort(this[kPublicPort]);
drainMessagePort(this[kPort]);
destroyMainThreadPort(this.threadId);
this.removeAllListeners('message');
this.removeAllListeners('messageerrors');
this[kPublicPort].unref();
Expand Down
Loading

0 comments on commit f40fa69

Please sign in to comment.