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
Original file line numberDiff line numberDiff line change
@@ -3504,6 +3504,22 @@ added: v18.1.0
35043504
The `Response` that has been passed to `WebAssembly.compileStreaming` or to
35053505
`WebAssembly.instantiateStreaming` is not a valid WebAssembly response.
35063506

3507+
<aid="ERR_WORKER_HANDLE_NOT_TRANSFERABLE"></a>
3508+
3509+
### `ERR_WORKER_HANDLE_NOT_TRANSFERABLE`
3510+
3511+
An attempt was made to transfer a `net.Socket` or `net.Server` to another thread
3512+
via a `worker_threads``postMessage()` call while it was not in a transferable
3513+
state, for example because it had already started reading or had buffered data.
3514+
3515+
<aid="ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED"></a>
3516+
3517+
### `ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED`
3518+
3519+
An attempt was made to transfer a `net.Socket` or `net.Server` to another thread
3520+
on a platform where moving the underlying handle between event loops is not
3521+
supported (currently Windows).
3522+
35073523
<aid="ERR_WORKER_INIT_FAILED"></a>
35083524

35093525
### `ERR_WORKER_INIT_FAILED`

β€Ž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+
constnet=require('node:net');
775+
const { Worker } =require('node:worker_threads');
776+
777+
// worker.js receives `{ socket }` messages and handles each connection.
778+
constworker=newWorker('./worker.js');
779+
780+
constserver=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
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,17 @@ const {
119119
ERR_SOCKET_CLOSED_BEFORE_CONNECTION,
120120
ERR_SOCKET_CONNECTION_TIMEOUT,
121121
ERR_SOCKET_HANDLE_ADOPTED,
122+
ERR_WORKER_HANDLE_NOT_TRANSFERABLE,
123+
ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED,
122124
},
123125
genericNodeError,
124126
}=require('internal/errors');
127+
const{
128+
markTransferMode,
129+
kDeserialize,
130+
kTransfer,
131+
kTransferList,
132+
}=require('internal/worker/js_transferable');
125133
const{ isUint8Array }=require('internal/util/types');
126134
const{ queueMicrotask }=require('internal/process/task_queues');
127135
const{
@@ -484,6 +492,9 @@ class BoundSocket {
484492

485493
functionSocket(options){
486494
if(!(thisinstanceofSocket))returnnewSocket(options);
495+
// A connected TCP Socket can be moved to another thread by listing it in the
496+
// transferList of a worker_threads postMessage() call. See [kTransfer]().
497+
markTransferMode(this,false,true);
487498
if(options?.objectMode){
488499
thrownewERR_INVALID_ARG_VALUE(
489500
'options.objectMode',
@@ -1501,6 +1512,58 @@ Socket.prototype[kReinitializeHandle] = function reinitializeHandle(handle) {
15011512
initSocketHandle(this);
15021513
};
15031514

1515+
// A Socket can be transferred to another thread only while it is a freshly
1516+
// accepted/created TCP connection: still attached to a live handle, not
1517+
// connecting or destroyed, and with no data already buffered in either
1518+
// direction (which would otherwise be lost on the sending side).
1519+
functionassertTransferableSocket(socket){
1520+
if(isWindows){
1521+
thrownewERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED('net.Socket');
1522+
}
1523+
consthandle=socket._handle;
1524+
if(handle==null||!(handleinstanceofTCP)||
1525+
socket.destroyed||socket.connecting||
1526+
socket.bytesRead>0||socket.bytesWritten>0||
1527+
socket.readableLength>0||socket.writableLength>0||
1528+
socket.readableEncoding!=null){
1529+
thrownewERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Socket');
1530+
}
1531+
}
1532+
1533+
Socket.prototype[kTransferList]=function(){
1534+
assertTransferableSocket(this);
1535+
return[this._handle];
1536+
};
1537+
1538+
Socket.prototype[kTransfer]=function(){
1539+
assertTransferableSocket(this);
1540+
consthandle=this._handle;
1541+
constdata={
1542+
handle,
1543+
allowHalfOpen: this.allowHalfOpen,
1544+
};
1545+
// Detach the handle from this source socket; the messaging layer takes
1546+
// ownership of it via TCPWrap::TransferForMessaging(). Destroy the source so
1547+
// any further use on the sending side fails cleanly (the socket is now owned
1548+
// by the receiving thread) instead of silently dropping data.
1549+
this._handle=null;
1550+
this.destroy();
1551+
return{
1552+
data,
1553+
deserializeInfo: 'net:Socket',
1554+
};
1555+
};
1556+
1557+
Socket.prototype[kDeserialize]=function(data){
1558+
consthandle=data?.handle;
1559+
if(handle==null||!(handleinstanceofTCP)){
1560+
thrownewERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Socket');
1561+
}
1562+
this.allowHalfOpen=Boolean(data.allowHalfOpen);
1563+
this[kReinitializeHandle](handle);
1564+
this.readable=this.writable=true;
1565+
};
1566+
15041567
functionsocketToDnsFamily(family){
15051568
switch(family){
15061569
case'IPv4':
@@ -1993,6 +2056,10 @@ function Server(options, connectionListener) {
19932056

19942057
EventEmitter.call(this);
19952058

2059+
// A listening TCP Server can be moved to another thread by listing it in the
2060+
// transferList of a worker_threads postMessage() call. See [kTransfer]().
2061+
markTransferMode(this,false,true);
2062+
19962063
if(typeofoptions==='function'){
19972064
connectionListener=options;
19982065
options=kEmptyObject;
@@ -2199,6 +2266,61 @@ function setupListenHandle(address, port, addressType, backlog, fd, flags) {
21992266

22002267
Server.prototype._listen2=setupListenHandle;// legacy alias
22012268

2269+
// A listening TCP Server can be transferred to another thread, which moves the
2270+
// underlying listening socket (and its pending accept queue) to that thread's
2271+
// event loop. Only a server bound to a live TCP handle can be transferred.
2272+
functionassertTransferableServer(server){
2273+
if(isWindows){
2274+
thrownewERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED('net.Server');
2275+
}
2276+
if(server._handle==null||!(server._handleinstanceofTCP)){
2277+
thrownewERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Server');
2278+
}
2279+
}
2280+
2281+
Server.prototype[kTransferList]=function(){
2282+
assertTransferableServer(this);
2283+
return[this._handle];
2284+
};
2285+
2286+
Server.prototype[kTransfer]=function(){
2287+
assertTransferableServer(this);
2288+
consthandle=this._handle;
2289+
constdata={
2290+
handle,
2291+
// Construction-time options that govern accepted sockets, so the receiving
2292+
// server reproduces the same behaviour.
2293+
allowHalfOpen: this.allowHalfOpen,
2294+
pauseOnConnect: this.pauseOnConnect,
2295+
noDelay: this.noDelay,
2296+
keepAlive: this.keepAlive,
2297+
keepAliveInitialDelay: this.keepAliveInitialDelay*1000,
2298+
highWaterMark: this.highWaterMark,
2299+
};
2300+
// Detach so the source server no longer references the handle being moved.
2301+
this._handle=null;
2302+
return{
2303+
data,
2304+
deserializeInfo: 'net:Server',
2305+
};
2306+
};
2307+
2308+
Server.prototype[kDeserialize]=function(data){
2309+
const{ handle, ...options}=data??{};
2310+
if(handle==null||!(handleinstanceofTCP)){
2311+
thrownewERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Server');
2312+
}
2313+
this.allowHalfOpen=Boolean(options.allowHalfOpen);
2314+
this.pauseOnConnect=Boolean(options.pauseOnConnect);
2315+
this.noDelay=Boolean(options.noDelay);
2316+
this.keepAlive=Boolean(options.keepAlive);
2317+
this.keepAliveInitialDelay=~~(options.keepAliveInitialDelay/1000);
2318+
this.highWaterMark=options.highWaterMark??getDefaultHighWaterMark();
2319+
// Adopt the transferred listening handle (mirrors the child_process server
2320+
// hand-off path), which re-arms accept() on this thread's event loop.
2321+
this.listen(handle);
2322+
};
2323+
22022324
functionemitErrorNT(self,err){
22032325
self.emit('error',err);
22042326
}

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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
Original file line numberDiff line numberDiff line change
@@ -3504,6 +3504,22 @@ added: v18.1.0
35043504
The `Response` that has been passed to `WebAssembly.compileStreaming` or to
35053505
`WebAssembly.instantiateStreaming` is not a valid WebAssembly response.
35063506

3507+
<aid="ERR_WORKER_HANDLE_NOT_TRANSFERABLE"></a>
3508+
3509+
### `ERR_WORKER_HANDLE_NOT_TRANSFERABLE`
3510+
3511+
An attempt was made to transfer a `net.Socket` or `net.Server` to another thread
3512+
via a `worker_threads``postMessage()` call while it was not in a transferable
3513+
state, for example because it had already started reading or had buffered data.
3514+
3515+
<aid="ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED"></a>
3516+
3517+
### `ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED`
3518+
3519+
An attempt was made to transfer a `net.Socket` or `net.Server` to another thread
3520+
on a platform where moving the underlying handle between event loops is not
3521+
supported (currently Windows).
3522+
35073523
<aid="ERR_WORKER_INIT_FAILED"></a>
35083524

35093525
### `ERR_WORKER_INIT_FAILED`

β€Ž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+
constnet=require('node:net');
775+
const { Worker } =require('node:worker_threads');
776+
777+
// worker.js receives `{ socket }` messages and handles each connection.
778+
constworker=newWorker('./worker.js');
779+
780+
constserver=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
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,17 @@ const {
119119
ERR_SOCKET_CLOSED_BEFORE_CONNECTION,
120120
ERR_SOCKET_CONNECTION_TIMEOUT,
121121
ERR_SOCKET_HANDLE_ADOPTED,
122+
ERR_WORKER_HANDLE_NOT_TRANSFERABLE,
123+
ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED,
122124
},
123125
genericNodeError,
124126
}=require('internal/errors');
127+
const{
128+
markTransferMode,
129+
kDeserialize,
130+
kTransfer,
131+
kTransferList,
132+
}=require('internal/worker/js_transferable');
125133
const{ isUint8Array }=require('internal/util/types');
126134
const{ queueMicrotask }=require('internal/process/task_queues');
127135
const{
@@ -484,6 +492,9 @@ class BoundSocket {
484492

485493
functionSocket(options){
486494
if(!(thisinstanceofSocket))returnnewSocket(options);
495+
// A connected TCP Socket can be moved to another thread by listing it in the
496+
// transferList of a worker_threads postMessage() call. See [kTransfer]().
497+
markTransferMode(this,false,true);
487498
if(options?.objectMode){
488499
thrownewERR_INVALID_ARG_VALUE(
489500
'options.objectMode',
@@ -1501,6 +1512,58 @@ Socket.prototype[kReinitializeHandle] = function reinitializeHandle(handle) {
15011512
initSocketHandle(this);
15021513
};
15031514

1515+
// A Socket can be transferred to another thread only while it is a freshly
1516+
// accepted/created TCP connection: still attached to a live handle, not
1517+
// connecting or destroyed, and with no data already buffered in either
1518+
// direction (which would otherwise be lost on the sending side).
1519+
functionassertTransferableSocket(socket){
1520+
if(isWindows){
1521+
thrownewERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED('net.Socket');
1522+
}
1523+
consthandle=socket._handle;
1524+
if(handle==null||!(handleinstanceofTCP)||
1525+
socket.destroyed||socket.connecting||
1526+
socket.bytesRead>0||socket.bytesWritten>0||
1527+
socket.readableLength>0||socket.writableLength>0||
1528+
socket.readableEncoding!=null){
1529+
thrownewERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Socket');
1530+
}
1531+
}
1532+
1533+
Socket.prototype[kTransferList]=function(){
1534+
assertTransferableSocket(this);
1535+
return[this._handle];
1536+
};
1537+
1538+
Socket.prototype[kTransfer]=function(){
1539+
assertTransferableSocket(this);
1540+
consthandle=this._handle;
1541+
constdata={
1542+
handle,
1543+
allowHalfOpen: this.allowHalfOpen,
1544+
};
1545+
// Detach the handle from this source socket; the messaging layer takes
1546+
// ownership of it via TCPWrap::TransferForMessaging(). Destroy the source so
1547+
// any further use on the sending side fails cleanly (the socket is now owned
1548+
// by the receiving thread) instead of silently dropping data.
1549+
this._handle=null;
1550+
this.destroy();
1551+
return{
1552+
data,
1553+
deserializeInfo: 'net:Socket',
1554+
};
1555+
};
1556+
1557+
Socket.prototype[kDeserialize]=function(data){
1558+
consthandle=data?.handle;
1559+
if(handle==null||!(handleinstanceofTCP)){
1560+
thrownewERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Socket');
1561+
}
1562+
this.allowHalfOpen=Boolean(data.allowHalfOpen);
1563+
this[kReinitializeHandle](handle);
1564+
this.readable=this.writable=true;
1565+
};
1566+
15041567
functionsocketToDnsFamily(family){
15051568
switch(family){
15061569
case'IPv4':
@@ -1993,6 +2056,10 @@ function Server(options, connectionListener) {
19932056

19942057
EventEmitter.call(this);
19952058

2059+
// A listening TCP Server can be moved to another thread by listing it in the
2060+
// transferList of a worker_threads postMessage() call. See [kTransfer]().
2061+
markTransferMode(this,false,true);
2062+
19962063
if(typeofoptions==='function'){
19972064
connectionListener=options;
19982065
options=kEmptyObject;
@@ -2199,6 +2266,61 @@ function setupListenHandle(address, port, addressType, backlog, fd, flags) {
21992266

22002267
Server.prototype._listen2=setupListenHandle;// legacy alias
22012268

2269+
// A listening TCP Server can be transferred to another thread, which moves the
2270+
// underlying listening socket (and its pending accept queue) to that thread's
2271+
// event loop. Only a server bound to a live TCP handle can be transferred.
2272+
functionassertTransferableServer(server){
2273+
if(isWindows){
2274+
thrownewERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED('net.Server');
2275+
}
2276+
if(server._handle==null||!(server._handleinstanceofTCP)){
2277+
thrownewERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Server');
2278+
}
2279+
}
2280+
2281+
Server.prototype[kTransferList]=function(){
2282+
assertTransferableServer(this);
2283+
return[this._handle];
2284+
};
2285+
2286+
Server.prototype[kTransfer]=function(){
2287+
assertTransferableServer(this);
2288+
consthandle=this._handle;
2289+
constdata={
2290+
handle,
2291+
// Construction-time options that govern accepted sockets, so the receiving
2292+
// server reproduces the same behaviour.
2293+
allowHalfOpen: this.allowHalfOpen,
2294+
pauseOnConnect: this.pauseOnConnect,
2295+
noDelay: this.noDelay,
2296+
keepAlive: this.keepAlive,
2297+
keepAliveInitialDelay: this.keepAliveInitialDelay*1000,
2298+
highWaterMark: this.highWaterMark,
2299+
};
2300+
// Detach so the source server no longer references the handle being moved.
2301+
this._handle=null;
2302+
return{
2303+
data,
2304+
deserializeInfo: 'net:Server',
2305+
};
2306+
};
2307+
2308+
Server.prototype[kDeserialize]=function(data){
2309+
const{ handle, ...options}=data??{};
2310+
if(handle==null||!(handleinstanceofTCP)){
2311+
thrownewERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Server');
2312+
}
2313+
this.allowHalfOpen=Boolean(options.allowHalfOpen);
2314+
this.pauseOnConnect=Boolean(options.pauseOnConnect);
2315+
this.noDelay=Boolean(options.noDelay);
2316+
this.keepAlive=Boolean(options.keepAlive);
2317+
this.keepAliveInitialDelay=~~(options.keepAliveInitialDelay/1000);
2318+
this.highWaterMark=options.highWaterMark??getDefaultHighWaterMark();
2319+
// Adopt the transferred listening handle (mirrors the child_process server
2320+
// hand-off path), which re-arms accept() on this thread's event loop.
2321+
this.listen(handle);
2322+
};
2323+
22022324
functionemitErrorNT(self,err){
22032325
self.emit('error',err);
22042326
}

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
Original file line numberDiff line numberDiff line change
@@ -3504,6 +3504,22 @@ added: v18.1.0
35043504
The `Response` that has been passed to `WebAssembly.compileStreaming` or to
35053505
`WebAssembly.instantiateStreaming` is not a valid WebAssembly response.
35063506

3507+
<aid="ERR_WORKER_HANDLE_NOT_TRANSFERABLE"></a>
3508+
3509+
### `ERR_WORKER_HANDLE_NOT_TRANSFERABLE`
3510+
3511+
An attempt was made to transfer a `net.Socket` or `net.Server` to another thread
3512+
via a `worker_threads``postMessage()` call while it was not in a transferable
3513+
state, for example because it had already started reading or had buffered data.
3514+
3515+
<aid="ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED"></a>
3516+
3517+
### `ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED`
3518+
3519+
An attempt was made to transfer a `net.Socket` or `net.Server` to another thread
3520+
on a platform where moving the underlying handle between event loops is not
3521+
supported (currently Windows).
3522+
35073523
<aid="ERR_WORKER_INIT_FAILED"></a>
35083524

35093525
### `ERR_WORKER_INIT_FAILED`

β€Ž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+
constnet=require('node:net');
775+
const { Worker } =require('node:worker_threads');
776+
777+
// worker.js receives `{ socket }` messages and handles each connection.
778+
constworker=newWorker('./worker.js');
779+
780+
constserver=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
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,17 @@ const {
119119
ERR_SOCKET_CLOSED_BEFORE_CONNECTION,
120120
ERR_SOCKET_CONNECTION_TIMEOUT,
121121
ERR_SOCKET_HANDLE_ADOPTED,
122+
ERR_WORKER_HANDLE_NOT_TRANSFERABLE,
123+
ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED,
122124
},
123125
genericNodeError,
124126
}=require('internal/errors');
127+
const{
128+
markTransferMode,
129+
kDeserialize,
130+
kTransfer,
131+
kTransferList,
132+
}=require('internal/worker/js_transferable');
125133
const{ isUint8Array }=require('internal/util/types');
126134
const{ queueMicrotask }=require('internal/process/task_queues');
127135
const{
@@ -484,6 +492,9 @@ class BoundSocket {
484492

485493
functionSocket(options){
486494
if(!(thisinstanceofSocket))returnnewSocket(options);
495+
// A connected TCP Socket can be moved to another thread by listing it in the
496+
// transferList of a worker_threads postMessage() call. See [kTransfer]().
497+
markTransferMode(this,false,true);
487498
if(options?.objectMode){
488499
thrownewERR_INVALID_ARG_VALUE(
489500
'options.objectMode',
@@ -1501,6 +1512,58 @@ Socket.prototype[kReinitializeHandle] = function reinitializeHandle(handle) {
15011512
initSocketHandle(this);
15021513
};
15031514

1515+
// A Socket can be transferred to another thread only while it is a freshly
1516+
// accepted/created TCP connection: still attached to a live handle, not
1517+
// connecting or destroyed, and with no data already buffered in either
1518+
// direction (which would otherwise be lost on the sending side).
1519+
functionassertTransferableSocket(socket){
1520+
if(isWindows){
1521+
thrownewERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED('net.Socket');
1522+
}
1523+
consthandle=socket._handle;
1524+
if(handle==null||!(handleinstanceofTCP)||
1525+
socket.destroyed||socket.connecting||
1526+
socket.bytesRead>0||socket.bytesWritten>0||
1527+
socket.readableLength>0||socket.writableLength>0||
1528+
socket.readableEncoding!=null){
1529+
thrownewERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Socket');
1530+
}
1531+
}
1532+
1533+
Socket.prototype[kTransferList]=function(){
1534+
assertTransferableSocket(this);
1535+
return[this._handle];
1536+
};
1537+
1538+
Socket.prototype[kTransfer]=function(){
1539+
assertTransferableSocket(this);
1540+
consthandle=this._handle;
1541+
constdata={
1542+
handle,
1543+
allowHalfOpen: this.allowHalfOpen,
1544+
};
1545+
// Detach the handle from this source socket; the messaging layer takes
1546+
// ownership of it via TCPWrap::TransferForMessaging(). Destroy the source so
1547+
// any further use on the sending side fails cleanly (the socket is now owned
1548+
// by the receiving thread) instead of silently dropping data.
1549+
this._handle=null;
1550+
this.destroy();
1551+
return{
1552+
data,
1553+
deserializeInfo: 'net:Socket',
1554+
};
1555+
};
1556+
1557+
Socket.prototype[kDeserialize]=function(data){
1558+
consthandle=data?.handle;
1559+
if(handle==null||!(handleinstanceofTCP)){
1560+
thrownewERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Socket');
1561+
}
1562+
this.allowHalfOpen=Boolean(data.allowHalfOpen);
1563+
this[kReinitializeHandle](handle);
1564+
this.readable=this.writable=true;
1565+
};
1566+
15041567
functionsocketToDnsFamily(family){
15051568
switch(family){
15061569
case'IPv4':
@@ -1993,6 +2056,10 @@ function Server(options, connectionListener) {
19932056

19942057
EventEmitter.call(this);
19952058

2059+
// A listening TCP Server can be moved to another thread by listing it in the
2060+
// transferList of a worker_threads postMessage() call. See [kTransfer]().
2061+
markTransferMode(this,false,true);
2062+
19962063
if(typeofoptions==='function'){
19972064
connectionListener=options;
19982065
options=kEmptyObject;
@@ -2199,6 +2266,61 @@ function setupListenHandle(address, port, addressType, backlog, fd, flags) {
21992266

22002267
Server.prototype._listen2=setupListenHandle;// legacy alias
22012268

2269+
// A listening TCP Server can be transferred to another thread, which moves the
2270+
// underlying listening socket (and its pending accept queue) to that thread's
2271+
// event loop. Only a server bound to a live TCP handle can be transferred.
2272+
functionassertTransferableServer(server){
2273+
if(isWindows){
2274+
thrownewERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED('net.Server');
2275+
}
2276+
if(server._handle==null||!(server._handleinstanceofTCP)){
2277+
thrownewERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Server');
2278+
}
2279+
}
2280+
2281+
Server.prototype[kTransferList]=function(){
2282+
assertTransferableServer(this);
2283+
return[this._handle];
2284+
};
2285+
2286+
Server.prototype[kTransfer]=function(){
2287+
assertTransferableServer(this);
2288+
consthandle=this._handle;
2289+
constdata={
2290+
handle,
2291+
// Construction-time options that govern accepted sockets, so the receiving
2292+
// server reproduces the same behaviour.
2293+
allowHalfOpen: this.allowHalfOpen,
2294+
pauseOnConnect: this.pauseOnConnect,
2295+
noDelay: this.noDelay,
2296+
keepAlive: this.keepAlive,
2297+
keepAliveInitialDelay: this.keepAliveInitialDelay*1000,
2298+
highWaterMark: this.highWaterMark,
2299+
};
2300+
// Detach so the source server no longer references the handle being moved.
2301+
this._handle=null;
2302+
return{
2303+
data,
2304+
deserializeInfo: 'net:Server',
2305+
};
2306+
};
2307+
2308+
Server.prototype[kDeserialize]=function(data){
2309+
const{ handle, ...options}=data??{};
2310+
if(handle==null||!(handleinstanceofTCP)){
2311+
thrownewERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Server');
2312+
}
2313+
this.allowHalfOpen=Boolean(options.allowHalfOpen);
2314+
this.pauseOnConnect=Boolean(options.pauseOnConnect);
2315+
this.noDelay=Boolean(options.noDelay);
2316+
this.keepAlive=Boolean(options.keepAlive);
2317+
this.keepAliveInitialDelay=~~(options.keepAliveInitialDelay/1000);
2318+
this.highWaterMark=options.highWaterMark??getDefaultHighWaterMark();
2319+
// Adopt the transferred listening handle (mirrors the child_process server
2320+
// hand-off path), which re-arms accept() on this thread's event loop.
2321+
this.listen(handle);
2322+
};
2323+
22022324
functionemitErrorNT(self,err){
22032325
self.emit('error',err);
22042326
}

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
Original file line numberDiff line numberDiff line change
@@ -3504,6 +3504,22 @@ added: v18.1.0
35043504
The `Response` that has been passed to `WebAssembly.compileStreaming` or to
35053505
`WebAssembly.instantiateStreaming` is not a valid WebAssembly response.
35063506

3507+
<aid="ERR_WORKER_HANDLE_NOT_TRANSFERABLE"></a>
3508+
3509+
### `ERR_WORKER_HANDLE_NOT_TRANSFERABLE`
3510+
3511+
An attempt was made to transfer a `net.Socket` or `net.Server` to another thread
3512+
via a `worker_threads``postMessage()` call while it was not in a transferable
3513+
state, for example because it had already started reading or had buffered data.
3514+
3515+
<aid="ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED"></a>
3516+
3517+
### `ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED`
3518+
3519+
An attempt was made to transfer a `net.Socket` or `net.Server` to another thread
3520+
on a platform where moving the underlying handle between event loops is not
3521+
supported (currently Windows).
3522+
35073523
<aid="ERR_WORKER_INIT_FAILED"></a>
35083524

35093525
### `ERR_WORKER_INIT_FAILED`

β€Ž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+
constnet=require('node:net');
775+
const { Worker } =require('node:worker_threads');
776+
777+
// worker.js receives `{ socket }` messages and handles each connection.
778+
constworker=newWorker('./worker.js');
779+
780+
constserver=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
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,17 @@ const {
119119
ERR_SOCKET_CLOSED_BEFORE_CONNECTION,
120120
ERR_SOCKET_CONNECTION_TIMEOUT,
121121
ERR_SOCKET_HANDLE_ADOPTED,
122+
ERR_WORKER_HANDLE_NOT_TRANSFERABLE,
123+
ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED,
122124
},
123125
genericNodeError,
124126
}=require('internal/errors');
127+
const{
128+
markTransferMode,
129+
kDeserialize,
130+
kTransfer,
131+
kTransferList,
132+
}=require('internal/worker/js_transferable');
125133
const{ isUint8Array }=require('internal/util/types');
126134
const{ queueMicrotask }=require('internal/process/task_queues');
127135
const{
@@ -484,6 +492,9 @@ class BoundSocket {
484492

485493
functionSocket(options){
486494
if(!(thisinstanceofSocket))returnnewSocket(options);
495+
// A connected TCP Socket can be moved to another thread by listing it in the
496+
// transferList of a worker_threads postMessage() call. See [kTransfer]().
497+
markTransferMode(this,false,true);
487498
if(options?.objectMode){
488499
thrownewERR_INVALID_ARG_VALUE(
489500
'options.objectMode',
@@ -1501,6 +1512,58 @@ Socket.prototype[kReinitializeHandle] = function reinitializeHandle(handle) {
15011512
initSocketHandle(this);
15021513
};
15031514

1515+
// A Socket can be transferred to another thread only while it is a freshly
1516+
// accepted/created TCP connection: still attached to a live handle, not
1517+
// connecting or destroyed, and with no data already buffered in either
1518+
// direction (which would otherwise be lost on the sending side).
1519+
functionassertTransferableSocket(socket){
1520+
if(isWindows){
1521+
thrownewERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED('net.Socket');
1522+
}
1523+
consthandle=socket._handle;
1524+
if(handle==null||!(handleinstanceofTCP)||
1525+
socket.destroyed||socket.connecting||
1526+
socket.bytesRead>0||socket.bytesWritten>0||
1527+
socket.readableLength>0||socket.writableLength>0||
1528+
socket.readableEncoding!=null){
1529+
thrownewERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Socket');
1530+
}
1531+
}
1532+
1533+
Socket.prototype[kTransferList]=function(){
1534+
assertTransferableSocket(this);
1535+
return[this._handle];
1536+
};
1537+
1538+
Socket.prototype[kTransfer]=function(){
1539+
assertTransferableSocket(this);
1540+
consthandle=this._handle;
1541+
constdata={
1542+
handle,
1543+
allowHalfOpen: this.allowHalfOpen,
1544+
};
1545+
// Detach the handle from this source socket; the messaging layer takes
1546+
// ownership of it via TCPWrap::TransferForMessaging(). Destroy the source so
1547+
// any further use on the sending side fails cleanly (the socket is now owned
1548+
// by the receiving thread) instead of silently dropping data.
1549+
this._handle=null;
1550+
this.destroy();
1551+
return{
1552+
data,
1553+
deserializeInfo: 'net:Socket',
1554+
};
1555+
};
1556+
1557+
Socket.prototype[kDeserialize]=function(data){
1558+
consthandle=data?.handle;
1559+
if(handle==null||!(handleinstanceofTCP)){
1560+
thrownewERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Socket');
1561+
}
1562+
this.allowHalfOpen=Boolean(data.allowHalfOpen);
1563+
this[kReinitializeHandle](handle);
1564+
this.readable=this.writable=true;
1565+
};
1566+
15041567
functionsocketToDnsFamily(family){
15051568
switch(family){
15061569
case'IPv4':
@@ -1993,6 +2056,10 @@ function Server(options, connectionListener) {
19932056

19942057
EventEmitter.call(this);
19952058

2059+
// A listening TCP Server can be moved to another thread by listing it in the
2060+
// transferList of a worker_threads postMessage() call. See [kTransfer]().
2061+
markTransferMode(this,false,true);
2062+
19962063
if(typeofoptions==='function'){
19972064
connectionListener=options;
19982065
options=kEmptyObject;
@@ -2199,6 +2266,61 @@ function setupListenHandle(address, port, addressType, backlog, fd, flags) {
21992266

22002267
Server.prototype._listen2=setupListenHandle;// legacy alias
22012268

2269+
// A listening TCP Server can be transferred to another thread, which moves the
2270+
// underlying listening socket (and its pending accept queue) to that thread's
2271+
// event loop. Only a server bound to a live TCP handle can be transferred.
2272+
functionassertTransferableServer(server){
2273+
if(isWindows){
2274+
thrownewERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED('net.Server');
2275+
}
2276+
if(server._handle==null||!(server._handleinstanceofTCP)){
2277+
thrownewERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Server');
2278+
}
2279+
}
2280+
2281+
Server.prototype[kTransferList]=function(){
2282+
assertTransferableServer(this);
2283+
return[this._handle];
2284+
};
2285+
2286+
Server.prototype[kTransfer]=function(){
2287+
assertTransferableServer(this);
2288+
consthandle=this._handle;
2289+
constdata={
2290+
handle,
2291+
// Construction-time options that govern accepted sockets, so the receiving
2292+
// server reproduces the same behaviour.
2293+
allowHalfOpen: this.allowHalfOpen,
2294+
pauseOnConnect: this.pauseOnConnect,
2295+
noDelay: this.noDelay,
2296+
keepAlive: this.keepAlive,
2297+
keepAliveInitialDelay: this.keepAliveInitialDelay*1000,
2298+
highWaterMark: this.highWaterMark,
2299+
};
2300+
// Detach so the source server no longer references the handle being moved.
2301+
this._handle=null;
2302+
return{
2303+
data,
2304+
deserializeInfo: 'net:Server',
2305+
};
2306+
};
2307+
2308+
Server.prototype[kDeserialize]=function(data){
2309+
const{ handle, ...options}=data??{};
2310+
if(handle==null||!(handleinstanceofTCP)){
2311+
thrownewERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Server');
2312+
}
2313+
this.allowHalfOpen=Boolean(options.allowHalfOpen);
2314+
this.pauseOnConnect=Boolean(options.pauseOnConnect);
2315+
this.noDelay=Boolean(options.noDelay);
2316+
this.keepAlive=Boolean(options.keepAlive);
2317+
this.keepAliveInitialDelay=~~(options.keepAliveInitialDelay/1000);
2318+
this.highWaterMark=options.highWaterMark??getDefaultHighWaterMark();
2319+
// Adopt the transferred listening handle (mirrors the child_process server
2320+
// hand-off path), which re-arms accept() on this thread's event loop.
2321+
this.listen(handle);
2322+
};
2323+
22022324
functionemitErrorNT(self,err){
22032325
self.emit('error',err);
22042326
}

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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
Original file line numberDiff line numberDiff line change
@@ -3504,6 +3504,22 @@ added: v18.1.0
35043504
The `Response` that has been passed to `WebAssembly.compileStreaming` or to
35053505
`WebAssembly.instantiateStreaming` is not a valid WebAssembly response.
35063506

3507+
<aid="ERR_WORKER_HANDLE_NOT_TRANSFERABLE"></a>
3508+
3509+
### `ERR_WORKER_HANDLE_NOT_TRANSFERABLE`
3510+
3511+
An attempt was made to transfer a `net.Socket` or `net.Server` to another thread
3512+
via a `worker_threads``postMessage()` call while it was not in a transferable
3513+
state, for example because it had already started reading or had buffered data.
3514+
3515+
<aid="ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED"></a>
3516+
3517+
### `ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED`
3518+
3519+
An attempt was made to transfer a `net.Socket` or `net.Server` to another thread
3520+
on a platform where moving the underlying handle between event loops is not
3521+
supported (currently Windows).
3522+
35073523
<aid="ERR_WORKER_INIT_FAILED"></a>
35083524

35093525
### `ERR_WORKER_INIT_FAILED`

β€Ž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+
constnet=require('node:net');
775+
const { Worker } =require('node:worker_threads');
776+
777+
// worker.js receives `{ socket }` messages and handles each connection.
778+
constworker=newWorker('./worker.js');
779+
780+
constserver=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
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,17 @@ const {
119119
ERR_SOCKET_CLOSED_BEFORE_CONNECTION,
120120
ERR_SOCKET_CONNECTION_TIMEOUT,
121121
ERR_SOCKET_HANDLE_ADOPTED,
122+
ERR_WORKER_HANDLE_NOT_TRANSFERABLE,
123+
ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED,
122124
},
123125
genericNodeError,
124126
}=require('internal/errors');
127+
const{
128+
markTransferMode,
129+
kDeserialize,
130+
kTransfer,
131+
kTransferList,
132+
}=require('internal/worker/js_transferable');
125133
const{ isUint8Array }=require('internal/util/types');
126134
const{ queueMicrotask }=require('internal/process/task_queues');
127135
const{
@@ -484,6 +492,9 @@ class BoundSocket {
484492

485493
functionSocket(options){
486494
if(!(thisinstanceofSocket))returnnewSocket(options);
495+
// A connected TCP Socket can be moved to another thread by listing it in the
496+
// transferList of a worker_threads postMessage() call. See [kTransfer]().
497+
markTransferMode(this,false,true);
487498
if(options?.objectMode){
488499
thrownewERR_INVALID_ARG_VALUE(
489500
'options.objectMode',
@@ -1501,6 +1512,58 @@ Socket.prototype[kReinitializeHandle] = function reinitializeHandle(handle) {
15011512
initSocketHandle(this);
15021513
};
15031514

1515+
// A Socket can be transferred to another thread only while it is a freshly
1516+
// accepted/created TCP connection: still attached to a live handle, not
1517+
// connecting or destroyed, and with no data already buffered in either
1518+
// direction (which would otherwise be lost on the sending side).
1519+
functionassertTransferableSocket(socket){
1520+
if(isWindows){
1521+
thrownewERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED('net.Socket');
1522+
}
1523+
consthandle=socket._handle;
1524+
if(handle==null||!(handleinstanceofTCP)||
1525+
socket.destroyed||socket.connecting||
1526+
socket.bytesRead>0||socket.bytesWritten>0||
1527+
socket.readableLength>0||socket.writableLength>0||
1528+
socket.readableEncoding!=null){
1529+
thrownewERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Socket');
1530+
}
1531+
}
1532+
1533+
Socket.prototype[kTransferList]=function(){
1534+
assertTransferableSocket(this);
1535+
return[this._handle];
1536+
};
1537+
1538+
Socket.prototype[kTransfer]=function(){
1539+
assertTransferableSocket(this);
1540+
consthandle=this._handle;
1541+
constdata={
1542+
handle,
1543+
allowHalfOpen: this.allowHalfOpen,
1544+
};
1545+
// Detach the handle from this source socket; the messaging layer takes
1546+
// ownership of it via TCPWrap::TransferForMessaging(). Destroy the source so
1547+
// any further use on the sending side fails cleanly (the socket is now owned
1548+
// by the receiving thread) instead of silently dropping data.
1549+
this._handle=null;
1550+
this.destroy();
1551+
return{
1552+
data,
1553+
deserializeInfo: 'net:Socket',
1554+
};
1555+
};
1556+
1557+
Socket.prototype[kDeserialize]=function(data){
1558+
consthandle=data?.handle;
1559+
if(handle==null||!(handleinstanceofTCP)){
1560+
thrownewERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Socket');
1561+
}
1562+
this.allowHalfOpen=Boolean(data.allowHalfOpen);
1563+
this[kReinitializeHandle](handle);
1564+
this.readable=this.writable=true;
1565+
};
1566+
15041567
functionsocketToDnsFamily(family){
15051568
switch(family){
15061569
case'IPv4':
@@ -1993,6 +2056,10 @@ function Server(options, connectionListener) {
19932056

19942057
EventEmitter.call(this);
19952058

2059+
// A listening TCP Server can be moved to another thread by listing it in the
2060+
// transferList of a worker_threads postMessage() call. See [kTransfer]().
2061+
markTransferMode(this,false,true);
2062+
19962063
if(typeofoptions==='function'){
19972064
connectionListener=options;
19982065
options=kEmptyObject;
@@ -2199,6 +2266,61 @@ function setupListenHandle(address, port, addressType, backlog, fd, flags) {
21992266

22002267
Server.prototype._listen2=setupListenHandle;// legacy alias
22012268

2269+
// A listening TCP Server can be transferred to another thread, which moves the
2270+
// underlying listening socket (and its pending accept queue) to that thread's
2271+
// event loop. Only a server bound to a live TCP handle can be transferred.
2272+
functionassertTransferableServer(server){
2273+
if(isWindows){
2274+
thrownewERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED('net.Server');
2275+
}
2276+
if(server._handle==null||!(server._handleinstanceofTCP)){
2277+
thrownewERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Server');
2278+
}
2279+
}
2280+
2281+
Server.prototype[kTransferList]=function(){
2282+
assertTransferableServer(this);
2283+
return[this._handle];
2284+
};
2285+
2286+
Server.prototype[kTransfer]=function(){
2287+
assertTransferableServer(this);
2288+
consthandle=this._handle;
2289+
constdata={
2290+
handle,
2291+
// Construction-time options that govern accepted sockets, so the receiving
2292+
// server reproduces the same behaviour.
2293+
allowHalfOpen: this.allowHalfOpen,
2294+
pauseOnConnect: this.pauseOnConnect,
2295+
noDelay: this.noDelay,
2296+
keepAlive: this.keepAlive,
2297+
keepAliveInitialDelay: this.keepAliveInitialDelay*1000,
2298+
highWaterMark: this.highWaterMark,
2299+
};
2300+
// Detach so the source server no longer references the handle being moved.
2301+
this._handle=null;
2302+
return{
2303+
data,
2304+
deserializeInfo: 'net:Server',
2305+
};
2306+
};
2307+
2308+
Server.prototype[kDeserialize]=function(data){
2309+
const{ handle, ...options}=data??{};
2310+
if(handle==null||!(handleinstanceofTCP)){
2311+
thrownewERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Server');
2312+
}
2313+
this.allowHalfOpen=Boolean(options.allowHalfOpen);
2314+
this.pauseOnConnect=Boolean(options.pauseOnConnect);
2315+
this.noDelay=Boolean(options.noDelay);
2316+
this.keepAlive=Boolean(options.keepAlive);
2317+
this.keepAliveInitialDelay=~~(options.keepAliveInitialDelay/1000);
2318+
this.highWaterMark=options.highWaterMark??getDefaultHighWaterMark();
2319+
// Adopt the transferred listening handle (mirrors the child_process server
2320+
// hand-off path), which re-arms accept() on this thread's event loop.
2321+
this.listen(handle);
2322+
};
2323+
22022324
functionemitErrorNT(self,err){
22032325
self.emit('error',err);
22042326
}

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
Original file line numberDiff line numberDiff line change
@@ -3504,6 +3504,22 @@ added: v18.1.0
35043504
The `Response` that has been passed to `WebAssembly.compileStreaming` or to
35053505
`WebAssembly.instantiateStreaming` is not a valid WebAssembly response.
35063506

3507+
<aid="ERR_WORKER_HANDLE_NOT_TRANSFERABLE"></a>
3508+
3509+
### `ERR_WORKER_HANDLE_NOT_TRANSFERABLE`
3510+
3511+
An attempt was made to transfer a `net.Socket` or `net.Server` to another thread
3512+
via a `worker_threads``postMessage()` call while it was not in a transferable
3513+
state, for example because it had already started reading or had buffered data.
3514+
3515+
<aid="ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED"></a>
3516+
3517+
### `ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED`
3518+
3519+
An attempt was made to transfer a `net.Socket` or `net.Server` to another thread
3520+
on a platform where moving the underlying handle between event loops is not
3521+
supported (currently Windows).
3522+
35073523
<aid="ERR_WORKER_INIT_FAILED"></a>
35083524

35093525
### `ERR_WORKER_INIT_FAILED`

β€Ž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+
constnet=require('node:net');
775+
const { Worker } =require('node:worker_threads');
776+
777+
// worker.js receives `{ socket }` messages and handles each connection.
778+
constworker=newWorker('./worker.js');
779+
780+
constserver=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
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,17 @@ const {
119119
ERR_SOCKET_CLOSED_BEFORE_CONNECTION,
120120
ERR_SOCKET_CONNECTION_TIMEOUT,
121121
ERR_SOCKET_HANDLE_ADOPTED,
122+
ERR_WORKER_HANDLE_NOT_TRANSFERABLE,
123+
ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED,
122124
},
123125
genericNodeError,
124126
}=require('internal/errors');
127+
const{
128+
markTransferMode,
129+
kDeserialize,
130+
kTransfer,
131+
kTransferList,
132+
}=require('internal/worker/js_transferable');
125133
const{ isUint8Array }=require('internal/util/types');
126134
const{ queueMicrotask }=require('internal/process/task_queues');
127135
const{
@@ -484,6 +492,9 @@ class BoundSocket {
484492

485493
functionSocket(options){
486494
if(!(thisinstanceofSocket))returnnewSocket(options);
495+
// A connected TCP Socket can be moved to another thread by listing it in the
496+
// transferList of a worker_threads postMessage() call. See [kTransfer]().
497+
markTransferMode(this,false,true);
487498
if(options?.objectMode){
488499
thrownewERR_INVALID_ARG_VALUE(
489500
'options.objectMode',
@@ -1501,6 +1512,58 @@ Socket.prototype[kReinitializeHandle] = function reinitializeHandle(handle) {
15011512
initSocketHandle(this);
15021513
};
15031514

1515+
// A Socket can be transferred to another thread only while it is a freshly
1516+
// accepted/created TCP connection: still attached to a live handle, not
1517+
// connecting or destroyed, and with no data already buffered in either
1518+
// direction (which would otherwise be lost on the sending side).
1519+
functionassertTransferableSocket(socket){
1520+
if(isWindows){
1521+
thrownewERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED('net.Socket');
1522+
}
1523+
consthandle=socket._handle;
1524+
if(handle==null||!(handleinstanceofTCP)||
1525+
socket.destroyed||socket.connecting||
1526+
socket.bytesRead>0||socket.bytesWritten>0||
1527+
socket.readableLength>0||socket.writableLength>0||
1528+
socket.readableEncoding!=null){
1529+
thrownewERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Socket');
1530+
}
1531+
}
1532+
1533+
Socket.prototype[kTransferList]=function(){
1534+
assertTransferableSocket(this);
1535+
return[this._handle];
1536+
};
1537+
1538+
Socket.prototype[kTransfer]=function(){
1539+
assertTransferableSocket(this);
1540+
consthandle=this._handle;
1541+
constdata={
1542+
handle,
1543+
allowHalfOpen: this.allowHalfOpen,
1544+
};
1545+
// Detach the handle from this source socket; the messaging layer takes
1546+
// ownership of it via TCPWrap::TransferForMessaging(). Destroy the source so
1547+
// any further use on the sending side fails cleanly (the socket is now owned
1548+
// by the receiving thread) instead of silently dropping data.
1549+
this._handle=null;
1550+
this.destroy();
1551+
return{
1552+
data,
1553+
deserializeInfo: 'net:Socket',
1554+
};
1555+
};
1556+
1557+
Socket.prototype[kDeserialize]=function(data){
1558+
consthandle=data?.handle;
1559+
if(handle==null||!(handleinstanceofTCP)){
1560+
thrownewERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Socket');
1561+
}
1562+
this.allowHalfOpen=Boolean(data.allowHalfOpen);
1563+
this[kReinitializeHandle](handle);
1564+
this.readable=this.writable=true;
1565+
};
1566+
15041567
functionsocketToDnsFamily(family){
15051568
switch(family){
15061569
case'IPv4':
@@ -1993,6 +2056,10 @@ function Server(options, connectionListener) {
19932056

19942057
EventEmitter.call(this);
19952058

2059+
// A listening TCP Server can be moved to another thread by listing it in the
2060+
// transferList of a worker_threads postMessage() call. See [kTransfer]().
2061+
markTransferMode(this,false,true);
2062+
19962063
if(typeofoptions==='function'){
19972064
connectionListener=options;
19982065
options=kEmptyObject;
@@ -2199,6 +2266,61 @@ function setupListenHandle(address, port, addressType, backlog, fd, flags) {
21992266

22002267
Server.prototype._listen2=setupListenHandle;// legacy alias
22012268

2269+
// A listening TCP Server can be transferred to another thread, which moves the
2270+
// underlying listening socket (and its pending accept queue) to that thread's
2271+
// event loop. Only a server bound to a live TCP handle can be transferred.
2272+
functionassertTransferableServer(server){
2273+
if(isWindows){
2274+
thrownewERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED('net.Server');
2275+
}
2276+
if(server._handle==null||!(server._handleinstanceofTCP)){
2277+
thrownewERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Server');
2278+
}
2279+
}
2280+
2281+
Server.prototype[kTransferList]=function(){
2282+
assertTransferableServer(this);
2283+
return[this._handle];
2284+
};
2285+
2286+
Server.prototype[kTransfer]=function(){
2287+
assertTransferableServer(this);
2288+
consthandle=this._handle;
2289+
constdata={
2290+
handle,
2291+
// Construction-time options that govern accepted sockets, so the receiving
2292+
// server reproduces the same behaviour.
2293+
allowHalfOpen: this.allowHalfOpen,
2294+
pauseOnConnect: this.pauseOnConnect,
2295+
noDelay: this.noDelay,
2296+
keepAlive: this.keepAlive,
2297+
keepAliveInitialDelay: this.keepAliveInitialDelay*1000,
2298+
highWaterMark: this.highWaterMark,
2299+
};
2300+
// Detach so the source server no longer references the handle being moved.
2301+
this._handle=null;
2302+
return{
2303+
data,
2304+
deserializeInfo: 'net:Server',
2305+
};
2306+
};
2307+
2308+
Server.prototype[kDeserialize]=function(data){
2309+
const{ handle, ...options}=data??{};
2310+
if(handle==null||!(handleinstanceofTCP)){
2311+
thrownewERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Server');
2312+
}
2313+
this.allowHalfOpen=Boolean(options.allowHalfOpen);
2314+
this.pauseOnConnect=Boolean(options.pauseOnConnect);
2315+
this.noDelay=Boolean(options.noDelay);
2316+
this.keepAlive=Boolean(options.keepAlive);
2317+
this.keepAliveInitialDelay=~~(options.keepAliveInitialDelay/1000);
2318+
this.highWaterMark=options.highWaterMark??getDefaultHighWaterMark();
2319+
// Adopt the transferred listening handle (mirrors the child_process server
2320+
// hand-off path), which re-arms accept() on this thread's event loop.
2321+
this.listen(handle);
2322+
};
2323+
22022324
functionemitErrorNT(self,err){
22032325
self.emit('error',err);
22042326
}

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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
Original file line numberDiff line numberDiff line change
@@ -3504,6 +3504,22 @@ added: v18.1.0
35043504
The `Response` that has been passed to `WebAssembly.compileStreaming` or to
35053505
`WebAssembly.instantiateStreaming` is not a valid WebAssembly response.
35063506

3507+
<aid="ERR_WORKER_HANDLE_NOT_TRANSFERABLE"></a>
3508+
3509+
### `ERR_WORKER_HANDLE_NOT_TRANSFERABLE`
3510+
3511+
An attempt was made to transfer a `net.Socket` or `net.Server` to another thread
3512+
via a `worker_threads``postMessage()` call while it was not in a transferable
3513+
state, for example because it had already started reading or had buffered data.
3514+
3515+
<aid="ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED"></a>
3516+
3517+
### `ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED`
3518+
3519+
An attempt was made to transfer a `net.Socket` or `net.Server` to another thread
3520+
on a platform where moving the underlying handle between event loops is not
3521+
supported (currently Windows).
3522+
35073523
<aid="ERR_WORKER_INIT_FAILED"></a>
35083524

35093525
### `ERR_WORKER_INIT_FAILED`

β€Ž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+
constnet=require('node:net');
775+
const { Worker } =require('node:worker_threads');
776+
777+
// worker.js receives `{ socket }` messages and handles each connection.
778+
constworker=newWorker('./worker.js');
779+
780+
constserver=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
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,17 @@ const {
119119
ERR_SOCKET_CLOSED_BEFORE_CONNECTION,
120120
ERR_SOCKET_CONNECTION_TIMEOUT,
121121
ERR_SOCKET_HANDLE_ADOPTED,
122+
ERR_WORKER_HANDLE_NOT_TRANSFERABLE,
123+
ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED,
122124
},
123125
genericNodeError,
124126
}=require('internal/errors');
127+
const{
128+
markTransferMode,
129+
kDeserialize,
130+
kTransfer,
131+
kTransferList,
132+
}=require('internal/worker/js_transferable');
125133
const{ isUint8Array }=require('internal/util/types');
126134
const{ queueMicrotask }=require('internal/process/task_queues');
127135
const{
@@ -484,6 +492,9 @@ class BoundSocket {
484492

485493
functionSocket(options){
486494
if(!(thisinstanceofSocket))returnnewSocket(options);
495+
// A connected TCP Socket can be moved to another thread by listing it in the
496+
// transferList of a worker_threads postMessage() call. See [kTransfer]().
497+
markTransferMode(this,false,true);
487498
if(options?.objectMode){
488499
thrownewERR_INVALID_ARG_VALUE(
489500
'options.objectMode',
@@ -1501,6 +1512,58 @@ Socket.prototype[kReinitializeHandle] = function reinitializeHandle(handle) {
15011512
initSocketHandle(this);
15021513
};
15031514

1515+
// A Socket can be transferred to another thread only while it is a freshly
1516+
// accepted/created TCP connection: still attached to a live handle, not
1517+
// connecting or destroyed, and with no data already buffered in either
1518+
// direction (which would otherwise be lost on the sending side).
1519+
functionassertTransferableSocket(socket){
1520+
if(isWindows){
1521+
thrownewERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED('net.Socket');
1522+
}
1523+
consthandle=socket._handle;
1524+
if(handle==null||!(handleinstanceofTCP)||
1525+
socket.destroyed||socket.connecting||
1526+
socket.bytesRead>0||socket.bytesWritten>0||
1527+
socket.readableLength>0||socket.writableLength>0||
1528+
socket.readableEncoding!=null){
1529+
thrownewERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Socket');
1530+
}
1531+
}
1532+
1533+
Socket.prototype[kTransferList]=function(){
1534+
assertTransferableSocket(this);
1535+
return[this._handle];
1536+
};
1537+
1538+
Socket.prototype[kTransfer]=function(){
1539+
assertTransferableSocket(this);
1540+
consthandle=this._handle;
1541+
constdata={
1542+
handle,
1543+
allowHalfOpen: this.allowHalfOpen,
1544+
};
1545+
// Detach the handle from this source socket; the messaging layer takes
1546+
// ownership of it via TCPWrap::TransferForMessaging(). Destroy the source so
1547+
// any further use on the sending side fails cleanly (the socket is now owned
1548+
// by the receiving thread) instead of silently dropping data.
1549+
this._handle=null;
1550+
this.destroy();
1551+
return{
1552+
data,
1553+
deserializeInfo: 'net:Socket',
1554+
};
1555+
};
1556+
1557+
Socket.prototype[kDeserialize]=function(data){
1558+
consthandle=data?.handle;
1559+
if(handle==null||!(handleinstanceofTCP)){
1560+
thrownewERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Socket');
1561+
}
1562+
this.allowHalfOpen=Boolean(data.allowHalfOpen);
1563+
this[kReinitializeHandle](handle);
1564+
this.readable=this.writable=true;
1565+
};
1566+
15041567
functionsocketToDnsFamily(family){
15051568
switch(family){
15061569
case'IPv4':
@@ -1993,6 +2056,10 @@ function Server(options, connectionListener) {
19932056

19942057
EventEmitter.call(this);
19952058

2059+
// A listening TCP Server can be moved to another thread by listing it in the
2060+
// transferList of a worker_threads postMessage() call. See [kTransfer]().
2061+
markTransferMode(this,false,true);
2062+
19962063
if(typeofoptions==='function'){
19972064
connectionListener=options;
19982065
options=kEmptyObject;
@@ -2199,6 +2266,61 @@ function setupListenHandle(address, port, addressType, backlog, fd, flags) {
21992266

22002267
Server.prototype._listen2=setupListenHandle;// legacy alias
22012268

2269+
// A listening TCP Server can be transferred to another thread, which moves the
2270+
// underlying listening socket (and its pending accept queue) to that thread's
2271+
// event loop. Only a server bound to a live TCP handle can be transferred.
2272+
functionassertTransferableServer(server){
2273+
if(isWindows){
2274+
thrownewERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED('net.Server');
2275+
}
2276+
if(server._handle==null||!(server._handleinstanceofTCP)){
2277+
thrownewERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Server');
2278+
}
2279+
}
2280+
2281+
Server.prototype[kTransferList]=function(){
2282+
assertTransferableServer(this);
2283+
return[this._handle];
2284+
};
2285+
2286+
Server.prototype[kTransfer]=function(){
2287+
assertTransferableServer(this);
2288+
consthandle=this._handle;
2289+
constdata={
2290+
handle,
2291+
// Construction-time options that govern accepted sockets, so the receiving
2292+
// server reproduces the same behaviour.
2293+
allowHalfOpen: this.allowHalfOpen,
2294+
pauseOnConnect: this.pauseOnConnect,
2295+
noDelay: this.noDelay,
2296+
keepAlive: this.keepAlive,
2297+
keepAliveInitialDelay: this.keepAliveInitialDelay*1000,
2298+
highWaterMark: this.highWaterMark,
2299+
};
2300+
// Detach so the source server no longer references the handle being moved.
2301+
this._handle=null;
2302+
return{
2303+
data,
2304+
deserializeInfo: 'net:Server',
2305+
};
2306+
};
2307+
2308+
Server.prototype[kDeserialize]=function(data){
2309+
const{ handle, ...options}=data??{};
2310+
if(handle==null||!(handleinstanceofTCP)){
2311+
thrownewERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Server');
2312+
}
2313+
this.allowHalfOpen=Boolean(options.allowHalfOpen);
2314+
this.pauseOnConnect=Boolean(options.pauseOnConnect);
2315+
this.noDelay=Boolean(options.noDelay);
2316+
this.keepAlive=Boolean(options.keepAlive);
2317+
this.keepAliveInitialDelay=~~(options.keepAliveInitialDelay/1000);
2318+
this.highWaterMark=options.highWaterMark??getDefaultHighWaterMark();
2319+
// Adopt the transferred listening handle (mirrors the child_process server
2320+
// hand-off path), which re-arms accept() on this thread's event loop.
2321+
this.listen(handle);
2322+
};
2323+
22022324
functionemitErrorNT(self,err){
22032325
self.emit('error',err);
22042326
}

0 commit comments

Comments
Β (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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
Original file line numberDiff line numberDiff line change
@@ -3504,6 +3504,22 @@ added: v18.1.0
35043504
The `Response` that has been passed to `WebAssembly.compileStreaming` or to
35053505
`WebAssembly.instantiateStreaming` is not a valid WebAssembly response.
35063506

3507+
<aid="ERR_WORKER_HANDLE_NOT_TRANSFERABLE"></a>
3508+
3509+
### `ERR_WORKER_HANDLE_NOT_TRANSFERABLE`
3510+
3511+
An attempt was made to transfer a `net.Socket` or `net.Server` to another thread
3512+
via a `worker_threads``postMessage()` call while it was not in a transferable
3513+
state, for example because it had already started reading or had buffered data.
3514+
3515+
<aid="ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED"></a>
3516+
3517+
### `ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED`
3518+
3519+
An attempt was made to transfer a `net.Socket` or `net.Server` to another thread
3520+
on a platform where moving the underlying handle between event loops is not
3521+
supported (currently Windows).
3522+
35073523
<aid="ERR_WORKER_INIT_FAILED"></a>
35083524

35093525
### `ERR_WORKER_INIT_FAILED`

β€Ž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+
constnet=require('node:net');
775+
const { Worker } =require('node:worker_threads');
776+
777+
// worker.js receives `{ socket }` messages and handles each connection.
778+
constworker=newWorker('./worker.js');
779+
780+
constserver=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
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,17 @@ const {
119119
ERR_SOCKET_CLOSED_BEFORE_CONNECTION,
120120
ERR_SOCKET_CONNECTION_TIMEOUT,
121121
ERR_SOCKET_HANDLE_ADOPTED,
122+
ERR_WORKER_HANDLE_NOT_TRANSFERABLE,
123+
ERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED,
122124
},
123125
genericNodeError,
124126
}=require('internal/errors');
127+
const{
128+
markTransferMode,
129+
kDeserialize,
130+
kTransfer,
131+
kTransferList,
132+
}=require('internal/worker/js_transferable');
125133
const{ isUint8Array }=require('internal/util/types');
126134
const{ queueMicrotask }=require('internal/process/task_queues');
127135
const{
@@ -484,6 +492,9 @@ class BoundSocket {
484492

485493
functionSocket(options){
486494
if(!(thisinstanceofSocket))returnnewSocket(options);
495+
// A connected TCP Socket can be moved to another thread by listing it in the
496+
// transferList of a worker_threads postMessage() call. See [kTransfer]().
497+
markTransferMode(this,false,true);
487498
if(options?.objectMode){
488499
thrownewERR_INVALID_ARG_VALUE(
489500
'options.objectMode',
@@ -1501,6 +1512,58 @@ Socket.prototype[kReinitializeHandle] = function reinitializeHandle(handle) {
15011512
initSocketHandle(this);
15021513
};
15031514

1515+
// A Socket can be transferred to another thread only while it is a freshly
1516+
// accepted/created TCP connection: still attached to a live handle, not
1517+
// connecting or destroyed, and with no data already buffered in either
1518+
// direction (which would otherwise be lost on the sending side).
1519+
functionassertTransferableSocket(socket){
1520+
if(isWindows){
1521+
thrownewERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED('net.Socket');
1522+
}
1523+
consthandle=socket._handle;
1524+
if(handle==null||!(handleinstanceofTCP)||
1525+
socket.destroyed||socket.connecting||
1526+
socket.bytesRead>0||socket.bytesWritten>0||
1527+
socket.readableLength>0||socket.writableLength>0||
1528+
socket.readableEncoding!=null){
1529+
thrownewERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Socket');
1530+
}
1531+
}
1532+
1533+
Socket.prototype[kTransferList]=function(){
1534+
assertTransferableSocket(this);
1535+
return[this._handle];
1536+
};
1537+
1538+
Socket.prototype[kTransfer]=function(){
1539+
assertTransferableSocket(this);
1540+
consthandle=this._handle;
1541+
constdata={
1542+
handle,
1543+
allowHalfOpen: this.allowHalfOpen,
1544+
};
1545+
// Detach the handle from this source socket; the messaging layer takes
1546+
// ownership of it via TCPWrap::TransferForMessaging(). Destroy the source so
1547+
// any further use on the sending side fails cleanly (the socket is now owned
1548+
// by the receiving thread) instead of silently dropping data.
1549+
this._handle=null;
1550+
this.destroy();
1551+
return{
1552+
data,
1553+
deserializeInfo: 'net:Socket',
1554+
};
1555+
};
1556+
1557+
Socket.prototype[kDeserialize]=function(data){
1558+
consthandle=data?.handle;
1559+
if(handle==null||!(handleinstanceofTCP)){
1560+
thrownewERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Socket');
1561+
}
1562+
this.allowHalfOpen=Boolean(data.allowHalfOpen);
1563+
this[kReinitializeHandle](handle);
1564+
this.readable=this.writable=true;
1565+
};
1566+
15041567
functionsocketToDnsFamily(family){
15051568
switch(family){
15061569
case'IPv4':
@@ -1993,6 +2056,10 @@ function Server(options, connectionListener) {
19932056

19942057
EventEmitter.call(this);
19952058

2059+
// A listening TCP Server can be moved to another thread by listing it in the
2060+
// transferList of a worker_threads postMessage() call. See [kTransfer]().
2061+
markTransferMode(this,false,true);
2062+
19962063
if(typeofoptions==='function'){
19972064
connectionListener=options;
19982065
options=kEmptyObject;
@@ -2199,6 +2266,61 @@ function setupListenHandle(address, port, addressType, backlog, fd, flags) {
21992266

22002267
Server.prototype._listen2=setupListenHandle;// legacy alias
22012268

2269+
// A listening TCP Server can be transferred to another thread, which moves the
2270+
// underlying listening socket (and its pending accept queue) to that thread's
2271+
// event loop. Only a server bound to a live TCP handle can be transferred.
2272+
functionassertTransferableServer(server){
2273+
if(isWindows){
2274+
thrownewERR_WORKER_HANDLE_TRANSFER_UNSUPPORTED('net.Server');
2275+
}
2276+
if(server._handle==null||!(server._handleinstanceofTCP)){
2277+
thrownewERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Server');
2278+
}
2279+
}
2280+
2281+
Server.prototype[kTransferList]=function(){
2282+
assertTransferableServer(this);
2283+
return[this._handle];
2284+
};
2285+
2286+
Server.prototype[kTransfer]=function(){
2287+
assertTransferableServer(this);
2288+
consthandle=this._handle;
2289+
constdata={
2290+
handle,
2291+
// Construction-time options that govern accepted sockets, so the receiving
2292+
// server reproduces the same behaviour.
2293+
allowHalfOpen: this.allowHalfOpen,
2294+
pauseOnConnect: this.pauseOnConnect,
2295+
noDelay: this.noDelay,
2296+
keepAlive: this.keepAlive,
2297+
keepAliveInitialDelay: this.keepAliveInitialDelay*1000,
2298+
highWaterMark: this.highWaterMark,
2299+
};
2300+
// Detach so the source server no longer references the handle being moved.
2301+
this._handle=null;
2302+
return{
2303+
data,
2304+
deserializeInfo: 'net:Server',
2305+
};
2306+
};
2307+
2308+
Server.prototype[kDeserialize]=function(data){
2309+
const{ handle, ...options}=data??{};
2310+
if(handle==null||!(handleinstanceofTCP)){
2311+
thrownewERR_WORKER_HANDLE_NOT_TRANSFERABLE('net.Server');
2312+
}
2313+
this.allowHalfOpen=Boolean(options.allowHalfOpen);
2314+
this.pauseOnConnect=Boolean(options.pauseOnConnect);
2315+
this.noDelay=Boolean(options.noDelay);
2316+
this.keepAlive=Boolean(options.keepAlive);
2317+
this.keepAliveInitialDelay=~~(options.keepAliveInitialDelay/1000);
2318+
this.highWaterMark=options.highWaterMark??getDefaultHighWaterMark();
2319+
// Adopt the transferred listening handle (mirrors the child_process server
2320+
// hand-off path), which re-arms accept() on this thread's event loop.
2321+
this.listen(handle);
2322+
};
2323+
22022324
functionemitErrorNT(self,err){
22032325
self.emit('error',err);
22042326
}

0 commit comments

Comments
Β (0)