Skip to content

Commit

Permalink
lib: add UV_UDP_REUSEPORT for udp
Browse files Browse the repository at this point in the history
  • Loading branch information
theanarkh committed Oct 17, 2024
1 parent 87da1f3 commit 42298db
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 3 deletions.
16 changes: 14 additions & 2 deletions doc/api/dgram.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,8 @@ 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. If creating a `dgram.Socket` with `reusePort`
flag, `exclusive` will always be `true` when call `socket.bind()` with a port.

A bound datagram socket keeps the Node.js process running to receive
datagram messages.
Expand Down Expand Up @@ -916,6 +917,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 +939,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
address, even if another process has already bound a socket on it. Incoming
datagrams are distributed across the receiving sockets. The flag 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 function will
return an error.
**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
25 changes: 25 additions & 0 deletions test/common/udp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'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('don not support reusePort flag', err);
socket.close();
reject();
});
});
}

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();
}
}));
}
}));
}, process.exit);
return;
}

const socket = dgram.createSocket(options);

socket.bind(+process.env.port, common.mustCall(() => {
socket.close(common.mustCall(() => {
process.exit(0);
}));
})).on('error', common.mustNotCall());
37 changes: 37 additions & 0 deletions test/parallel/test-cluster-dgram-reuseport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'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);
}));
}, process.exit);
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());
19 changes: 19 additions & 0 deletions test/parallel/test-dgram-reuseport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'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, process.exit);

0 comments on commit 42298db

Please sign in to comment.