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

net: enable autoSelectFamily by default #46790

Merged
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
13 changes: 10 additions & 3 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -462,14 +462,20 @@ added: v6.0.0
Enable FIPS-compliant crypto at startup. (Requires Node.js to be built
against FIPS-compatible OpenSSL.)

### `--enable-network-family-autoselection`
### `--no-network-family-autoselection`

<!-- YAML
added: v19.4.0
ShogunPanda marked this conversation as resolved.
Show resolved Hide resolved
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/46790
description: The flag was renamed from `--no-enable-network-family-autoselection`
to `--no-network-family-autoselection`. The old name can still work as
an alias.
-->

Enables the family autoselection algorithm unless connection options explicitly
disables it.
Disables the family autoselection algorithm unless connection options explicitly
enables it.

### `--enable-source-maps`

Expand Down Expand Up @@ -2125,6 +2131,7 @@ Node.js options that are allowed are:
* `--no-extra-info-on-fatal-exception`
* `--no-force-async-hooks-checks`
* `--no-global-search-paths`
* `--no-network-family-autoselection`
* `--no-warnings`
* `--node-memory-debug`
* `--openssl-config`
Expand Down
7 changes: 7 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -2593,6 +2593,13 @@ An attempt was made to operate on an already closed socket.
When calling [`net.Socket.write()`][] on a connecting socket and the socket was
closed before the connection was established.

<a id="ERR_SOCKET_CONNECTION_TIMEOUT"></a>

### `ERR_SOCKET_CONNECTION_TIMEOUT`

The socket was unable to connect to any address returned by the DNS within the
allowed timeout when using the family autoselection algorithm.

<a id="ERR_SOCKET_DGRAM_IS_CONNECTED"></a>

### `ERR_SOCKET_DGRAM_IS_CONNECTED`
Expand Down
19 changes: 14 additions & 5 deletions doc/api/net.md
Original file line number Diff line number Diff line change
Expand Up @@ -870,6 +870,12 @@ behavior.
<!-- YAML
added: v0.1.90
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/46790
description: The default value for the autoSelectFamily option is now true.
The `--enable-network-family-autoselection` CLI flag has been renamed
to `--network-family-autoselection`. The old name is now an
alias but it is discouraged.
- version: v19.4.0
pr-url: https://github.com/nodejs/node/pull/45777
description: The default value for autoSelectFamily option can be changed
Expand Down Expand Up @@ -936,12 +942,12 @@ For TCP connections, available `options` are:
option before timing out and trying the next address.
Ignored if the `family` option is not `0` or if `localAddress` is set.
Connection errors are not emitted if at least one connection succeeds.
**Default:** initially `false`, but it can be changed at runtime using [`net.setDefaultAutoSelectFamily(value)`][]
or via the command line option `--enable-network-family-autoselection`.
If all connections attempts fails, a single `AggregateError` with all failed attempts is emitted.
**Default:** [`net.getDefaultAutoSelectFamily()`][]
* `autoSelectFamilyAttemptTimeout` {number}: The amount of time in milliseconds to wait
for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option.
If set to a positive integer less than `10`, then the value `10` will be used instead.
**Default:** initially `250`, but it can be changed at runtime using [`net.setDefaultAutoSelectFamilyAttemptTimeout(value)`][]
**Default:** [`net.getDefaultAutoSelectFamilyAttemptTimeout()`][]

For [IPC][] connections, available `options` are:

Expand Down Expand Up @@ -1629,6 +1635,8 @@ added: v19.4.0
-->

Gets the current default value of the `autoSelectFamily` option of [`socket.connect(options)`][].
The initial default value is `true`, unless the command line option
`--no-network-family-autoselection` is provided.

* Returns: {boolean} The current default value of the `autoSelectFamily` option.

Expand All @@ -1649,6 +1657,7 @@ added: v19.8.0
-->

Gets the current default value of the `autoSelectFamilyAttemptTimeout` option of [`socket.connect(options)`][].
The initial default value is `250`.

* Returns: {number} The current default value of the `autoSelectFamilyAttemptTimeout` option.

Expand Down Expand Up @@ -1747,8 +1756,8 @@ net.isIPv6('fhqwhgads'); // returns false
[`net.createConnection(path)`]: #netcreateconnectionpath-connectlistener
[`net.createConnection(port, host)`]: #netcreateconnectionport-host-connectlistener
[`net.createServer()`]: #netcreateserveroptions-connectionlistener
[`net.setDefaultAutoSelectFamily(value)`]: #netsetdefaultautoselectfamilyvalue
[`net.setDefaultAutoSelectFamilyAttemptTimeout(value)`]: #netsetdefaultautoselectfamilyattempttimeoutvalue
[`net.getDefaultAutoSelectFamily()`]: #netgetdefaultautoselectfamily
[`net.getDefaultAutoSelectFamilyAttemptTimeout()`]: #netgetdefaultautoselectfamilyattempttimeout
[`new net.Socket(options)`]: #new-netsocketoptions
[`readable.setEncoding()`]: stream.md#readablesetencodingencoding
[`server.close()`]: #serverclosecallback
Expand Down
2 changes: 2 additions & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1567,6 +1567,8 @@ E('ERR_SOCKET_CLOSED', 'Socket is closed', Error);
E('ERR_SOCKET_CLOSED_BEFORE_CONNECTION',
'Socket closed before the connection was established',
Error);
E('ERR_SOCKET_CONNECTION_TIMEOUT',
'Socket connection timeout', Error);
E('ERR_SOCKET_DGRAM_IS_CONNECTED', 'Already connected', Error);
E('ERR_SOCKET_DGRAM_NOT_CONNECTED', 'Not connected', Error);
E('ERR_SOCKET_DGRAM_NOT_RUNNING', 'Not running', Error);
Expand Down
2 changes: 1 addition & 1 deletion lib/internal/net.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ function makeSyncWrite(fd) {
}

module.exports = {
kReinitializeHandle: Symbol('reinitializeHandle'),
kReinitializeHandle: Symbol('kReinitializeHandle'),
isIP,
isIPv4,
isIPv6,
Expand Down
70 changes: 54 additions & 16 deletions lib/net.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

const {
ArrayIsArray,
ArrayPrototypeIncludes,
ArrayPrototypeIndexOf,
ArrayPrototypePush,
Boolean,
Expand Down Expand Up @@ -97,6 +98,7 @@ const {
ERR_INVALID_HANDLE_TYPE,
ERR_SERVER_ALREADY_LISTEN,
ERR_SERVER_NOT_RUNNING,
ERR_SOCKET_CONNECTION_TIMEOUT,
ERR_SOCKET_CLOSED,
ERR_SOCKET_CLOSED_BEFORE_CONNECTION,
ERR_MISSING_ARGS,
Expand Down Expand Up @@ -127,7 +129,7 @@ let cluster;
let dns;
let BlockList;
let SocketAddress;
let autoSelectFamilyDefault = getOptionValue('--enable-network-family-autoselection');
let autoSelectFamilyDefault = getOptionValue('--network-family-autoselection');
let autoSelectFamilyAttemptTimeoutDefault = 250;

const { clearTimeout, setTimeout } = require('timers');
Expand Down Expand Up @@ -1092,6 +1094,11 @@ function internalConnectMultiple(context, canceled) {

// All connections have been tried without success, destroy with error
if (canceled || context.current === context.addresses.length) {
if (context.errors.length === 0) {
self.destroy(new ERR_SOCKET_CONNECTION_TIMEOUT());
return;
}

self.destroy(aggregateErrors(context.errors));
return;
}
Expand Down Expand Up @@ -1322,6 +1329,7 @@ function lookupAndConnect(self, options) {
options,
dnsopts,
port,
localAddress,
localPort,
autoSelectFamilyAttemptTimeout,
);
Expand Down Expand Up @@ -1364,7 +1372,9 @@ function lookupAndConnect(self, options) {
});
}

function lookupAndConnectMultiple(self, async_id_symbol, lookup, host, options, dnsopts, port, localPort, timeout) {
function lookupAndConnectMultiple(
self, async_id_symbol, lookup, host, options, dnsopts, port, localAddress, localPort, timeout,
) {
defaultTriggerAsyncIdScope(self[async_id_symbol], function emitLookup() {
lookup(host, dnsopts, function emitLookup(err, addresses) {
// It's possible we were destroyed while looking this up.
Expand All @@ -1385,6 +1395,7 @@ function lookupAndConnectMultiple(self, async_id_symbol, lookup, host, options,
// Filter addresses by only keeping the one which are either IPv4 or IPV6.
// The first valid address determines which group has preference on the
// alternate family sorting which happens later.
const validAddresses = [[], []];
const validIps = [[], []];
let destinations;
for (let i = 0, l = addresses.length; i < l; i++) {
Expand All @@ -1397,12 +1408,19 @@ function lookupAndConnectMultiple(self, async_id_symbol, lookup, host, options,
destinations = addressType === 6 ? { 6: 0, 4: 1 } : { 4: 0, 6: 1 };
}

ArrayPrototypePush(validIps[destinations[addressType]], address);
const destination = destinations[addressType];

// Only try an address once
if (!ArrayPrototypeIncludes(validIps[destination], ip)) {
ArrayPrototypePush(validAddresses[destination], address);
ArrayPrototypePush(validIps[destination], ip);
}
}
}


// When no AAAA or A records are available, fail on the first one
if (!validIps[0].length && !validIps[1].length) {
if (!validAddresses[0].length && !validAddresses[1].length) {
const { address: firstIp, family: firstAddressType } = addresses[0];

if (!isIP(firstIp)) {
Expand All @@ -1420,16 +1438,36 @@ function lookupAndConnectMultiple(self, async_id_symbol, lookup, host, options,

// Sort addresses alternating families
const toAttempt = [];
for (let i = 0, l = MathMax(validIps[0].length, validIps[1].length); i < l; i++) {
if (i in validIps[0]) {
ArrayPrototypePush(toAttempt, validIps[0][i]);
for (let i = 0, l = MathMax(validAddresses[0].length, validAddresses[1].length); i < l; i++) {
if (i in validAddresses[0]) {
ArrayPrototypePush(toAttempt, validAddresses[0][i]);
}
if (i in validIps[1]) {
ArrayPrototypePush(toAttempt, validIps[1][i]);
if (i in validAddresses[1]) {
ArrayPrototypePush(toAttempt, validAddresses[1][i]);
}
}

if (toAttempt.length === 1) {
debug('connect/multiple: only one address found, switching back to single connection');
const { address: ip, family: addressType } = toAttempt[0];

self._unrefTimer();
defaultTriggerAsyncIdScope(
self[async_id_symbol],
internalConnect,
self,
ip,
port,
addressType,
localAddress,
localPort,
);

return;
}

self.autoSelectFamilyAttemptedAddresses = [];
debug('connect/multiple: will try the following addresses', toAttempt);

const context = {
socket: self,
Expand Down Expand Up @@ -1543,6 +1581,13 @@ function afterConnect(status, handle, req, readable, writable) {
}

function afterConnectMultiple(context, status, handle, req, readable, writable) {
// One of the connection has completed and correctly dispatched but after timeout, ignore this one
if (context[kTimeoutTriggered]) {
debug('connect/multiple: ignoring successful but timedout connection to %s:%s', req.address, req.port);
handle.close();
return;
}

const self = context.socket;

// Make sure another connection is not spawned
Expand Down Expand Up @@ -1571,13 +1616,6 @@ function afterConnectMultiple(context, status, handle, req, readable, writable)
return;
}

// One of the connection has completed and correctly dispatched but after timeout, ignore this one
if (context[kTimeoutTriggered]) {
debug('connect/multiple: ignoring successful but timedout connection to %s:%s', req.address, req.port);
handle.close();
return;
}

if (context.current > 1 && self[kReinitializeHandle]) {
self[kReinitializeHandle](handle);
handle = self._handle;
Expand Down
11 changes: 7 additions & 4 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -358,10 +358,13 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"returned)",
&EnvironmentOptions::dns_result_order,
kAllowedInEnvvar);
AddOption("--enable-network-family-autoselection",
"Enable network address family autodetection algorithm",
&EnvironmentOptions::enable_network_family_autoselection,
kAllowedInEnvvar);
AddOption("--network-family-autoselection",
"Disable network address family autodetection algorithm",
&EnvironmentOptions::network_family_autoselection,
kAllowedInEnvvar,
true);
AddAlias("--enable-network-family-autoselection",
"--network-family-autoselection");
AddOption("--enable-source-maps",
"Source Map V3 support for stack traces",
&EnvironmentOptions::enable_source_maps,
Expand Down
2 changes: 1 addition & 1 deletion src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ class EnvironmentOptions : public Options {
bool frozen_intrinsics = false;
int64_t heap_snapshot_near_heap_limit = 0;
std::string heap_snapshot_signal;
bool enable_network_family_autoselection = false;
bool network_family_autoselection = true;
uint64_t max_http_header_size = 16 * 1024;
bool deprecation = true;
bool force_async_hooks_checks = true;
Expand Down
10 changes: 10 additions & 0 deletions test/common/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const process = global.process; // Some tests tamper with the process global.
const assert = require('assert');
const { exec, execSync, spawn, spawnSync } = require('child_process');
const fs = require('fs');
const net = require('net');
// Do not require 'os' until needed so that test-os-checked-function can
// monkey patch it. If 'os' is required here, that test will fail.
const path = require('path');
Expand Down Expand Up @@ -137,6 +138,14 @@ const isPi = (() => {

const isDumbTerminal = process.env.TERM === 'dumb';

// When using high concurrency or in the CI we need much more time for each connection attempt
const defaultAutoSelectFamilyAttemptTimeout = platformTimeout(2500);
// Since this is also used by tools outside of the test suite,
// make sure setDefaultAutoSelectFamilyAttemptTimeout
if (typeof net.setDefaultAutoSelectFamilyAttemptTimeout === 'function') {
net.setDefaultAutoSelectFamilyAttemptTimeout(platformTimeout(defaultAutoSelectFamilyAttemptTimeout));
}

const buildType = process.config.target_defaults ?
process.config.target_defaults.default_configuration :
'Release';
Expand Down Expand Up @@ -886,6 +895,7 @@ const common = {
canCreateSymLink,
childShouldThrowAndAbort,
createZeroFilledFile,
defaultAutoSelectFamilyAttemptTimeout,
expectsError,
expectWarning,
gcUntil,
Expand Down
4 changes: 0 additions & 4 deletions test/internet/test-tls-autoselectfamily-servername.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,8 @@ if (!common.hasCrypto) {
common.skip('missing crypto');
}

const { setDefaultAutoSelectFamilyAttemptTimeout } = require('net');
const { connect } = require('tls');

// Some of the windows machines in the CI need more time to establish connection
setDefaultAutoSelectFamilyAttemptTimeout(common.platformTimeout(common.isWindows ? 1500 : 250));

// Test that TLS connecting works without autoSelectFamily
{
const socket = connect({
Expand Down
4 changes: 0 additions & 4 deletions test/parallel/test-http-autoselectfamily.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,9 @@ const assert = require('assert');
const dgram = require('dgram');
const { Resolver } = require('dns');
const { request, createServer } = require('http');
const { setDefaultAutoSelectFamilyAttemptTimeout } = require('net');

// Test that happy eyeballs algorithm is properly implemented when using HTTP.

// Some of the windows machines in the CI need more time to establish connection
setDefaultAutoSelectFamilyAttemptTimeout(common.platformTimeout(common.isWindows ? 1500 : 250));

function _lookup(resolver, hostname, options, cb) {
resolver.resolve(hostname, 'ANY', (err, replies) => {
assert.notStrictEqual(options.family, 4);
Expand Down
11 changes: 1 addition & 10 deletions test/parallel/test-http2-ping-settings-heapdump.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,6 @@ const v8 = require('v8');
// after it is destroyed, either because they are detached from it or have been
// destroyed themselves.

// We use an higher autoSelectFamilyAttemptTimeout in this test as the v8.getHeapSnapshot().resume()
// will slow the connection flow and we don't want the second connection attempt to start.
let autoSelectFamilyAttemptTimeout = common.platformTimeout(1000);
if (common.isWindows) {
// Some of the windows machines in the CI need more time to establish connection
autoSelectFamilyAttemptTimeout = common.platformTimeout(10000);
}

for (const variant of ['ping', 'settings']) {
const server = http2.createServer();
server.on('session', common.mustCall((session) => {
Expand All @@ -38,8 +30,7 @@ for (const variant of ['ping', 'settings']) {
}));

server.listen(0, common.mustCall(() => {
const client = http2.connect(`http://localhost:${server.address().port}`, { autoSelectFamilyAttemptTimeout },
common.mustCall());
const client = http2.connect(`http://localhost:${server.address().port}`, common.mustCall());
client.on('error', (err) => {
// We destroy the session so it's possible to get ECONNRESET here.
if (err.code !== 'ECONNRESET')
Expand Down
Loading