Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dgram: add UV_UDP_REUSEPORT for udp #55403

Merged
merged 1 commit into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -74,7 +74,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 @@ -130,6 +130,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 @@ -345,6 +346,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');
});
Loading