net: make TCP Server and Socket transferable across worker threads · nodejs/node@fb8a7e1 · GitHub
Skip to content

Commit fb8a7e1

Browse files
mcollinaaduh95
authored andcommitted
net: make TCP Server and Socket transferable across worker threads
Allow a listening net.Server or an accepted net.Socket to be moved to another thread by listing it in the transferList of a worker_threads postMessage() call. Unix only; Windows throws. Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #64225 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Paolo Insogna <paolo@cowtech.it>
1 parent 55e2c2d commit fb8a7e1

12 files changed

Lines changed: 575 additions & 8 deletions

doc/api/errors.md

Lines changed: 16 additions & 0 deletions

doc/api/net.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,11 @@ added: v0.1.90
301301

302302
This class is used to create a TCP or [IPC][] server.
303303

304+
A listening TCP `net.Server` can be transferred to a worker thread by listing it
305+
in the `transferList` of a [`worker_threads`][] `postMessage()` call. This moves
306+
the underlying listening socket to the receiving thread, where it resumes
307+
accepting connections. See [Transferring TCP handles to other threads][].
308+
304309
### `new net.Server([options][, connectionListener])`
305310

306311
* `options` {Object} See
@@ -747,6 +752,41 @@ is received. For example, it is passed to the listeners of a
747752
[`'connection'`][] event emitted on a [`net.Server`][], so the user can use
748753
it to interact with the client.
749754

755+
### Transferring TCP handles to other threads
756+
757+
A connected TCP `net.Socket` can be moved to another thread by listing it in the
758+
`transferList` of a [`worker_threads`][] `postMessage()` call. After the
759+
transfer, the source socket is destroyed on the sending thread (further use
760+
fails with `ERR_STREAM_DESTROYED` rather than silently dropping data), and the
761+
socket continues to work on the receiving thread. This makes it possible to
762+
accept connections on one thread and distribute them across a pool of worker
763+
threads, for example to build a `node:cluster`-like model on top of worker
764+
threads.
765+
766+
The socket must be a freshly accepted or created TCP connection: it must still
767+
be attached to a live handle, must not be connecting or destroyed, and must not
768+
have started reading or have buffered data. Otherwise `postMessage()` throws
769+
`ERR_WORKER_HANDLE_NOT_TRANSFERABLE`. Only TCP sockets are supported, and only
770+
on Unix-like platforms; on Windows `postMessage()` throws
771+
`ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED`.
772+
773+
```cjs
774+
const net = require('node:net');
775+
const { Worker } = require('node:worker_threads');
776+
777+
// worker.js receives `{ socket }` messages and handles each connection.
778+
const worker = new Worker('./worker.js');
779+
780+
const server = net.createServer((socket) => {
781+
// Hand the freshly accepted connection off to the worker thread.
782+
worker.postMessage({ socket }, [socket]);
783+
});
784+
server.listen(8000);
785+
```
786+
787+
A listening [`net.Server`][] can be transferred the same way, which moves the
788+
listening socket itself (and its pending accept queue) to the receiving thread.
789+
750790
### `new net.Socket([options])`
751791

752792
<!-- YAML
@@ -2184,6 +2224,7 @@ net.isIPv6('fhqwhgads'); // returns false
21842224
[Identifying paths for IPC connections]: #identifying-paths-for-ipc-connections
21852225
[RFC 8305]: https://www.rfc-editor.org/rfc/rfc8305.txt
21862226
[Readable Stream]: stream.md#class-streamreadable
2227+
[Transferring TCP handles to other threads]: #transferring-tcp-handles-to-other-threads
21872228
[`'close'`]: #event-close
21882229
[`'connect'`]: #event-connect
21892230
[`'connection'`]: #event-connection
@@ -2240,6 +2281,7 @@ net.isIPv6('fhqwhgads'); // returns false
22402281
[`socket.setTimeout()`]: #socketsettimeouttimeout-callback
22412282
[`socket.setTimeout(timeout)`]: #socketsettimeouttimeout-callback
22422283
[`stream.getDefaultHighWaterMark()`]: stream.md#streamgetdefaulthighwatermarkobjectmode
2284+
[`worker_threads`]: worker_threads.md
22432285
[`writable.destroy()`]: stream.md#writabledestroyerror
22442286
[`writable.destroyed`]: stream.md#writabledestroyed
22452287
[`writable.end()`]: stream.md#writableendchunk-encoding-callback

doc/api/worker_threads.md

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1206,6 +1206,8 @@ In particular, the significant differences to `JSON` are:
12061206
* {KeyObject}s,
12071207
* {MessagePort}s,
12081208
* {net.BlockList}s,
1209+
* {net.Server}s (TCP only, when listed in `transferList`),
1210+
* {net.Socket}s (TCP only, when listed in `transferList`),
12091211
* {net.SocketAddress}es,
12101212
* {X509Certificate}s.
12111213
@@ -1233,12 +1235,20 @@ circularData.foo = circularData;
12331235
port2.postMessage(circularData);
12341236
```
12351237
1236-
`transferList` may be a list of {ArrayBuffer}, [`MessagePort`][], and
1237-
[`FileHandle`][] objects.
1238+
`transferList` may be a list of {ArrayBuffer}, [`MessagePort`][],
1239+
[`FileHandle`][], {net.Server}, and {net.Socket} objects.
12381240
After transferring, they are not usable on the sending side of the channel
1239-
anymore (even if they are not contained in `value`). Unlike with
1240-
[child processes][], transferring handles such as network sockets is currently
1241-
not supported.
1241+
anymore (even if they are not contained in `value`).
1242+
1243+
Transferring a {net.Server} moves its listening socket — together with any
1244+
pending connections in the accept queue — to the receiving thread's event loop.
1245+
Transferring a {net.Socket} moves a single connection; the socket must be a
1246+
freshly accepted or created TCP connection that has not yet started reading and
1247+
has no buffered data, otherwise `postMessage()` throws
1248+
`ERR_WORKER_HANDLE_NOT_TRANSFERABLE`. This makes it possible to accept
1249+
connections on one thread and distribute them across a pool of worker threads.
1250+
Only TCP handles are supported, and only on Unix-like platforms; on Windows
1251+
`postMessage()` throws `ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED`.
12421252
12431253
If `value` contains {SharedArrayBuffer} instances, those are accessible
12441254
from either thread. They cannot be listed in `transferList`.
@@ -2218,7 +2228,6 @@ thread spawned will spawn another until the application crashes.
22182228
[async-resource-worker-pool]: async_context.md#using-asyncresource-for-a-worker-thread-pool
22192229
[browser `LockManager`]: https://developer.mozilla.org/en-US/docs/Web/API/LockManager
22202230
[browser `MessagePort`]: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort
2221-
[child processes]: child_process.md
22222231
[contextified]: vm.md#what-does-it-mean-to-contextify-an-object
22232232
[locks.request()]: #locksrequestname-options-callback
22242233
[v8.serdes]: v8.md#serialization-api

lib/internal/errors.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1928,6 +1928,12 @@ E('ERR_WEBASSEMBLY_NOT_SUPPORTED',
19281928
'WebAssembly is not supported in this environment, but is required for %s',
19291929
Error);
19301930
E('ERR_WEBASSEMBLY_RESPONSE', 'WebAssembly response %s', TypeError);
1931+
E('ERR_WORKER_HANDLE_NOT_TRANSFERABLE',
1932+
'%s cannot be transferred in its current state; it must be a freshly ' +
1933+
'created or accepted handle that has not started reading and has no ' +
1934+
'pending writes', Error);
1935+
E('ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED',
1936+
'Transferring a %s to another thread is not supported on this platform', Error);
19311937
E('ERR_WORKER_INIT_FAILED', 'Worker initialization failure: %s', Error);
19321938
E('ERR_WORKER_INVALID_EXEC_ARGV', (errors, msg = 'invalid execArgv flags') =>
19331939
`Initiated Worker with ${msg}: ${ArrayPrototypeJoin(errors, ', ')}`,

lib/net.js

Lines changed: 122 additions & 0 deletions

0 commit comments

Comments
 (0)