Skip to content

Commit 337f61f

Browse files
theanarkhruyadorno
authored andcommitted
lib: add UV_UDP_REUSEPORT for udp
PR-URL: #55403 Refs: libuv/libuv#4419 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 3d8e3a6 commit 337f61f

7 files changed

+141
-3
lines changed

doc/api/dgram.md

+15-2
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,9 @@ used when using `dgram.Socket` objects with the [`cluster`][] module. When
343343
`exclusive` is set to `false` (the default), cluster workers will use the same
344344
underlying socket handle allowing connection handling duties to be shared.
345345
When `exclusive` is `true`, however, the handle is not shared and attempted
346-
port sharing results in an error.
346+
port sharing results in an error. Creating a `dgram.Socket` with the `reusePort`
347+
option set to `true` causes `exclusive` to always be `true` when `socket.bind()`
348+
is called.
347349

348350
A bound datagram socket keeps the Node.js process running to receive
349351
datagram messages.
@@ -916,6 +918,9 @@ chained.
916918
<!-- YAML
917919
added: v0.11.13
918920
changes:
921+
- version: REPLACEME
922+
pr-url: https://github.com/nodejs/node/pull/55403
923+
description: The `reusePort` option is supported.
919924
- version: v15.8.0
920925
pr-url: https://github.com/nodejs/node/pull/37026
921926
description: AbortSignal support was added.
@@ -935,7 +940,15 @@ changes:
935940
* `type` {string} The family of socket. Must be either `'udp4'` or `'udp6'`.
936941
Required.
937942
* `reuseAddr` {boolean} When `true` [`socket.bind()`][] will reuse the
938-
address, even if another process has already bound a socket on it.
943+
address, even if another process has already bound a socket on it, but
944+
only one socket can receive the data.
945+
**Default:** `false`.
946+
* `reusePort` {boolean} When `true` [`socket.bind()`][] will reuse the
947+
port, even if another process has already bound a socket on it. Incoming
948+
datagrams are distributed to listening sockets. The option is available
949+
only on some platforms, such as Linux 3.9+, DragonFlyBSD 3.6+, FreeBSD 12.0+,
950+
Solaris 11.4, and AIX 7.2.5+. On unsupported platforms this option raises an
951+
an error when the socket is bound.
939952
**Default:** `false`.
940953
* `ipv6Only` {boolean} Setting `ipv6Only` to `true` will
941954
disable dual-stack support, i.e., binding to address `::` won't make

lib/dgram.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ const {
7272
const { UV_UDP_REUSEADDR } = internalBinding('constants').os;
7373

7474
const {
75-
constants: { UV_UDP_IPV6ONLY },
75+
constants: { UV_UDP_IPV6ONLY, UV_UDP_REUSEPORT },
7676
UDP,
7777
SendWrap,
7878
} = internalBinding('udp_wrap');
@@ -129,6 +129,7 @@ function Socket(type, listener) {
129129
connectState: CONNECT_STATE_DISCONNECTED,
130130
queue: undefined,
131131
reuseAddr: options?.reuseAddr, // Use UV_UDP_REUSEADDR if true.
132+
reusePort: options?.reusePort,
132133
ipv6Only: options?.ipv6Only,
133134
recvBufferSize,
134135
sendBufferSize,
@@ -344,6 +345,10 @@ Socket.prototype.bind = function(port_, address_ /* , callback */) {
344345
flags |= UV_UDP_REUSEADDR;
345346
if (state.ipv6Only)
346347
flags |= UV_UDP_IPV6ONLY;
348+
if (state.reusePort) {
349+
exclusive = true;
350+
flags |= UV_UDP_REUSEPORT;
351+
}
347352

348353
if (cluster.isWorker && !exclusive) {
349354
bindServerHandle(this, {

src/udp_wrap.cc

+1
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ void UDPWrap::Initialize(Local<Object> target,
231231
Local<Object> constants = Object::New(isolate);
232232
NODE_DEFINE_CONSTANT(constants, UV_UDP_IPV6ONLY);
233233
NODE_DEFINE_CONSTANT(constants, UV_UDP_REUSEADDR);
234+
NODE_DEFINE_CONSTANT(constants, UV_UDP_REUSEPORT);
234235
target->Set(context,
235236
env->constants_string(),
236237
constants).Check();

test/common/udp.js

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use strict';
2+
const dgram = require('dgram');
3+
4+
const options = { type: 'udp4', reusePort: true };
5+
6+
function checkSupportReusePort() {
7+
return new Promise((resolve, reject) => {
8+
const socket = dgram.createSocket(options);
9+
socket.bind(0);
10+
socket.on('listening', () => {
11+
socket.close(resolve);
12+
});
13+
socket.on('error', (err) => {
14+
console.log('The `reusePort` option is not supported:', err.message);
15+
socket.close();
16+
reject(err);
17+
});
18+
});
19+
}
20+
21+
module.exports = {
22+
checkSupportReusePort,
23+
options,
24+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use strict';
2+
const common = require('../common');
3+
const { checkSupportReusePort, options } = require('../common/udp');
4+
const assert = require('assert');
5+
const child_process = require('child_process');
6+
const dgram = require('dgram');
7+
8+
if (!process.env.isWorker) {
9+
checkSupportReusePort().then(() => {
10+
const socket = dgram.createSocket(options);
11+
socket.bind(0, common.mustCall(() => {
12+
const port = socket.address().port;
13+
const workerOptions = { env: { ...process.env, isWorker: 1, port } };
14+
let count = 2;
15+
for (let i = 0; i < 2; i++) {
16+
const worker = child_process.fork(__filename, workerOptions);
17+
worker.on('exit', common.mustCall((code) => {
18+
assert.strictEqual(code, 0);
19+
if (--count === 0) {
20+
socket.close();
21+
}
22+
}));
23+
}
24+
}));
25+
}, () => {
26+
common.skip('The `reusePort` is not supported');
27+
});
28+
return;
29+
}
30+
31+
const socket = dgram.createSocket(options);
32+
33+
socket.bind(+process.env.port, common.mustCall(() => {
34+
socket.close();
35+
})).on('error', common.mustNotCall());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use strict';
2+
const common = require('../common');
3+
if (common.isWindows)
4+
common.skip('dgram clustering is currently not supported on windows.');
5+
6+
const { checkSupportReusePort, options } = require('../common/udp');
7+
const assert = require('assert');
8+
const cluster = require('cluster');
9+
const dgram = require('dgram');
10+
11+
if (cluster.isPrimary) {
12+
checkSupportReusePort().then(() => {
13+
cluster.fork().on('exit', common.mustCall((code) => {
14+
assert.strictEqual(code, 0);
15+
}));
16+
}, () => {
17+
common.skip('The `reusePort` option is not supported');
18+
});
19+
return;
20+
}
21+
22+
let waiting = 2;
23+
function close() {
24+
if (--waiting === 0)
25+
cluster.worker.disconnect();
26+
}
27+
28+
// Test if the worker requests the main process to create a socket
29+
cluster._getServer = common.mustNotCall();
30+
31+
const socket1 = dgram.createSocket(options);
32+
const socket2 = dgram.createSocket(options);
33+
34+
socket1.bind(0, () => {
35+
socket2.bind(socket1.address().port, () => {
36+
socket1.close(close);
37+
socket2.close(close);
38+
}).on('error', common.mustNotCall());
39+
}).on('error', common.mustNotCall());

test/parallel/test-dgram-reuseport.js

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
'use strict';
2+
const common = require('../common');
3+
const { checkSupportReusePort, options } = require('../common/udp');
4+
const dgram = require('dgram');
5+
6+
function test() {
7+
const socket1 = dgram.createSocket(options);
8+
const socket2 = dgram.createSocket(options);
9+
socket1.bind(0, common.mustCall(() => {
10+
socket2.bind(socket1.address().port, common.mustCall(() => {
11+
socket1.close();
12+
socket2.close();
13+
}));
14+
}));
15+
socket1.on('error', common.mustNotCall());
16+
socket2.on('error', common.mustNotCall());
17+
}
18+
19+
checkSupportReusePort().then(test, () => {
20+
common.skip('The `reusePort` option is not supported');
21+
});

0 commit comments

Comments
 (0)