Skip to content

Commit

Permalink
lib: add UV_UDP_REUSEPORT for udp
Browse files Browse the repository at this point in the history
PR-URL: #55403
Refs: libuv/libuv#4419
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
  • Loading branch information
theanarkh authored and ruyadorno committed Nov 27, 2024
1 parent 3d8e3a6 commit 337f61f
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 3 deletions.
17 changes: 15 additions & 2 deletions doc/api/dgram.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,9 @@ used when using `dgram.Socket` objects with the [`cluster`][] module. When
`exclusive` is set to `false` (the default), cluster workers will use the same
underlying socket handle allowing connection handling duties to be shared.
When `exclusive` is `true`, however, the handle is not shared and attempted
port sharing results in an error.
port sharing results in an error. Creating a `dgram.Socket` with the `reusePort`
option set to `true` causes `exclusive` to always be `true` when `socket.bind()`
is called.

A bound datagram socket keeps the Node.js process running to receive
datagram messages.
Expand Down Expand Up @@ -916,6 +918,9 @@ chained.
<!-- YAML
added: v0.11.13
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/55403
description: The `reusePort` option is supported.
- version: v15.8.0
pr-url: https://github.com/nodejs/node/pull/37026
description: AbortSignal support was added.
Expand All @@ -935,7 +940,15 @@ changes:
* `type` {string} The family of socket. Must be either `'udp4'` or `'udp6'`.
Required.
* `reuseAddr` {boolean} When `true` [`socket.bind()`][] will reuse the
address, even if another process has already bound a socket on it.
address, even if another process has already bound a socket on it, but
only one socket can receive the data.
**Default:** `false`.
* `reusePort` {boolean} When `true` [`socket.bind()`][] will reuse the
port, even if another process has already bound a socket on it. Incoming
datagrams are distributed to listening sockets. The option is available
only on some platforms, such as Linux 3.9+, DragonFlyBSD 3.6+, FreeBSD 12.0+,
Solaris 11.4, and AIX 7.2.5+. On unsupported platforms this option raises an
an error when the socket is bound.
**Default:** `false`.
* `ipv6Only` {boolean} Setting `ipv6Only` to `true` will
disable dual-stack support, i.e., binding to address `::` won't make
Expand Down
7 changes: 6 additions & 1 deletion lib/dgram.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const {
const { UV_UDP_REUSEADDR } = internalBinding('constants').os;

const {
constants: { UV_UDP_IPV6ONLY },
constants: { UV_UDP_IPV6ONLY, UV_UDP_REUSEPORT },
UDP,
SendWrap,
} = internalBinding('udp_wrap');
Expand Down Expand Up @@ -129,6 +129,7 @@ function Socket(type, listener) {
connectState: CONNECT_STATE_DISCONNECTED,
queue: undefined,
reuseAddr: options?.reuseAddr, // Use UV_UDP_REUSEADDR if true.
reusePort: options?.reusePort,
ipv6Only: options?.ipv6Only,
recvBufferSize,
sendBufferSize,
Expand Down Expand Up @@ -344,6 +345,10 @@ Socket.prototype.bind = function(port_, address_ /* , callback */) {
flags |= UV_UDP_REUSEADDR;
if (state.ipv6Only)
flags |= UV_UDP_IPV6ONLY;
if (state.reusePort) {
exclusive = true;
flags |= UV_UDP_REUSEPORT;
}

if (cluster.isWorker && !exclusive) {
bindServerHandle(this, {
Expand Down
1 change: 1 addition & 0 deletions src/udp_wrap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ void UDPWrap::Initialize(Local<Object> target,
Local<Object> constants = Object::New(isolate);
NODE_DEFINE_CONSTANT(constants, UV_UDP_IPV6ONLY);
NODE_DEFINE_CONSTANT(constants, UV_UDP_REUSEADDR);
NODE_DEFINE_CONSTANT(constants, UV_UDP_REUSEPORT);
target->Set(context,
env->constants_string(),
constants).Check();
Expand Down
24 changes: 24 additions & 0 deletions test/common/udp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use strict';
const dgram = require('dgram');

const options = { type: 'udp4', reusePort: true };

function checkSupportReusePort() {
return new Promise((resolve, reject) => {
const socket = dgram.createSocket(options);
socket.bind(0);
socket.on('listening', () => {
socket.close(resolve);
});
socket.on('error', (err) => {
console.log('The `reusePort` option is not supported:', err.message);
socket.close();
reject(err);
});
});
}

module.exports = {
checkSupportReusePort,
options,
};
35 changes: 35 additions & 0 deletions test/parallel/test-child-process-dgram-reuseport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use strict';
const common = require('../common');
const { checkSupportReusePort, options } = require('../common/udp');
const assert = require('assert');
const child_process = require('child_process');
const dgram = require('dgram');

if (!process.env.isWorker) {
checkSupportReusePort().then(() => {
const socket = dgram.createSocket(options);
socket.bind(0, common.mustCall(() => {
const port = socket.address().port;
const workerOptions = { env: { ...process.env, isWorker: 1, port } };
let count = 2;
for (let i = 0; i < 2; i++) {
const worker = child_process.fork(__filename, workerOptions);
worker.on('exit', common.mustCall((code) => {
assert.strictEqual(code, 0);
if (--count === 0) {
socket.close();
}
}));
}
}));
}, () => {
common.skip('The `reusePort` is not supported');
});
return;
}

const socket = dgram.createSocket(options);

socket.bind(+process.env.port, common.mustCall(() => {
socket.close();
})).on('error', common.mustNotCall());
39 changes: 39 additions & 0 deletions test/parallel/test-cluster-dgram-reuseport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use strict';
const common = require('../common');
if (common.isWindows)
common.skip('dgram clustering is currently not supported on windows.');

const { checkSupportReusePort, options } = require('../common/udp');
const assert = require('assert');
const cluster = require('cluster');
const dgram = require('dgram');

if (cluster.isPrimary) {
checkSupportReusePort().then(() => {
cluster.fork().on('exit', common.mustCall((code) => {
assert.strictEqual(code, 0);
}));
}, () => {
common.skip('The `reusePort` option is not supported');
});
return;
}

let waiting = 2;
function close() {
if (--waiting === 0)
cluster.worker.disconnect();
}

// Test if the worker requests the main process to create a socket
cluster._getServer = common.mustNotCall();

const socket1 = dgram.createSocket(options);
const socket2 = dgram.createSocket(options);

socket1.bind(0, () => {
socket2.bind(socket1.address().port, () => {
socket1.close(close);
socket2.close(close);
}).on('error', common.mustNotCall());
}).on('error', common.mustNotCall());
21 changes: 21 additions & 0 deletions test/parallel/test-dgram-reuseport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use strict';
const common = require('../common');
const { checkSupportReusePort, options } = require('../common/udp');
const dgram = require('dgram');

function test() {
const socket1 = dgram.createSocket(options);
const socket2 = dgram.createSocket(options);
socket1.bind(0, common.mustCall(() => {
socket2.bind(socket1.address().port, common.mustCall(() => {
socket1.close();
socket2.close();
}));
}));
socket1.on('error', common.mustNotCall());
socket2.on('error', common.mustNotCall());
}

checkSupportReusePort().then(test, () => {
common.skip('The `reusePort` option is not supported');
});

0 comments on commit 337f61f

Please sign in to comment.