[Feature #120782] Introduction of Happy Eyeballs Version 2 (RFC8305) in TCPSocket.new (#11653)

* Introduction of Happy Eyeballs Version 2 (RFC8305) in TCPSocket.new

This is an implementation of Happy Eyeballs version 2 (RFC 8305) in `TCPSocket.new`.
See https://github.com/ruby/ruby/pull/11653

1. Background
Prior to this implementation, I implemented Happy Eyeballs Version 2 (HEv2) for `Socket.tcp` in https://github.com/ruby/ruby/pull/9374.
HEv2 is an algorithm defined in [RFC 8305](https://datatracker.ietf.org/doc/html/rfc8305), aimed at improving network connectivity.
For more details on the specific cases that HEv2 helps, please refer to https://bugs.ruby-lang.org/issues/20108.

2. Proposal & Outcome
This proposal implements the same HEv2 algorithm in `TCPSocket.new`.
Since `TCPSocket.new` is used more widely than `Socket.tcp`, this change is expected to broaden the impact of HEv2's benefits.
Like `Socket.tcp`, I have also added `fast_fallback` keyword argument to `TCPSocket.new`.
This option is set to true by default, enabling the HEv2 functionality.
However, users can explicitly set it to false to disable HEv2 and use the previous behavior of `TCPSocket.new`.

It should be noted that HEv2 is enabled only in environments where pthreads are available.
This specification follows the approach taken in https://bugs.ruby-lang.org/issues/19965 , where name resolution can be interrupted.
(In environments where pthreads are not available, the `fast_fallback` option is ignored.)

3. Performance
Below is the benchmark of 100 requests to `www.ruby-lang.org` with the fast_fallback option set to true and false, respectively.
While there is a slight performance degradation when HEv2 is enabled, the degradation is smaller compared to that seen in `Socket.tcp`.

```
~/s/build ❯❯❯ ../install/bin/ruby ../ruby/test.rb
Rehearsal --------------------------------------------------------
fast_fallback: true    0.017588   0.097045   0.114633 (  1.460664)
fast_fallback: false   0.014033   0.078984   0.093017 (  1.413951)
----------------------------------------------- total: 0.207650sec

                           user     system      total        real
fast_fallback: true    0.020891   0.124054   0.144945 (  1.473816)
fast_fallback: false   0.018392   0.110852   0.129244 (  1.466014)
```

* Update debug prints

Co-authored-by: Nobuyoshi Nakada <nobu.nakada@gmail.com>

* Remove debug prints

* misc

* Disable HEv2 in Win

* Raise resolution error with hostname resolution

* Fix to handle errors

* Remove warnings

* Errors that do not need to be handled

* misc

* Improve doc

* Fix bug on cancellation

* Avoid EAI_ADDRFAMILY for resolving IPv6

* Follow upstream

* misc

* Refactor connection_attempt_fds management

- Introduced allocate_connection_attempt_fds and reallocate_connection_attempt_fds for improved memory allocation of connection_attempt_fds
- Added remove_connection_attempt_fd to resize connection_attempt_fds dynamically.
- Simplified the in_progress_fds function to only check the size of connection_attempt_fds.

* Rename do_pthread_create to raddrinfo_pthread_create to avoid conflicting

---------

Co-authored-by: Nobuyoshi Nakada <nobu.nakada@gmail.com>
This commit is contained in:
Misaki Shioi 2024-11-12 10:06:48 +09:00 committed by GitHub
parent 821a5b966f
commit 4c270200db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
Notes: git 2024-11-12 01:07:04 +00:00
Merged-By: shioimm <shioi.mm@gmail.com>
7 changed files with 1549 additions and 16 deletions

File diff suppressed because it is too large Load Diff

View File

@ -469,8 +469,8 @@ cancel_getaddrinfo(void *ptr)
rb_nativethread_lock_unlock(&arg->lock);
}
static int
do_pthread_create(pthread_t *th, void *(*start_routine) (void *), void *arg)
int
raddrinfo_pthread_create(pthread_t *th, void *(*start_routine) (void *), void *arg)
{
int limit = 3, ret;
do {
@ -505,7 +505,7 @@ start:
}
pthread_t th;
if (do_pthread_create(&th, fork_safe_do_getaddrinfo, arg) != 0) {
if (raddrinfo_pthread_create(&th, fork_safe_do_getaddrinfo, arg) != 0) {
int err = errno;
free_getaddrinfo_arg(arg);
errno = err;
@ -726,7 +726,7 @@ start:
}
pthread_t th;
if (do_pthread_create(&th, do_getnameinfo, arg) != 0) {
if (raddrinfo_pthread_create(&th, do_getnameinfo, arg) != 0) {
int err = errno;
free_getnameinfo_arg(arg);
errno = err;
@ -822,7 +822,7 @@ str_is_number(const char *p)
((ptr)[0] == name[0] && \
rb_strlen_lit(name) == (len) && memcmp(ptr, name, len) == 0)
static char*
char*
host_str(VALUE host, char *hbuf, size_t hbuflen, int *flags_ptr)
{
if (NIL_P(host)) {
@ -861,7 +861,7 @@ host_str(VALUE host, char *hbuf, size_t hbuflen, int *flags_ptr)
}
}
static char*
char*
port_str(VALUE port, char *pbuf, size_t pbuflen, int *flags_ptr)
{
if (NIL_P(port)) {
@ -3024,6 +3024,106 @@ rsock_io_socket_addrinfo(VALUE io, struct sockaddr *addr, socklen_t len)
UNREACHABLE_RETURN(Qnil);
}
#if FAST_FALLBACK_INIT_INETSOCK_IMPL == 1
void
free_fast_fallback_getaddrinfo_shared(struct fast_fallback_getaddrinfo_shared **shared)
{
free((*shared)->node);
(*shared)->node = NULL;
free((*shared)->service);
(*shared)->service = NULL;
close((*shared)->notify);
close((*shared)->wait);
rb_nativethread_lock_destroy((*shared)->lock);
free(*shared);
*shared = NULL;
}
void
free_fast_fallback_getaddrinfo_entry(struct fast_fallback_getaddrinfo_entry **entry)
{
if ((*entry)->ai) {
freeaddrinfo((*entry)->ai);
(*entry)->ai = NULL;
}
free(*entry);
*entry = NULL;
}
void *
do_fast_fallback_getaddrinfo(void *ptr)
{
struct fast_fallback_getaddrinfo_entry *entry = (struct fast_fallback_getaddrinfo_entry *)ptr;
struct fast_fallback_getaddrinfo_shared *shared = entry->shared;
int err = 0, need_free = 0, shared_need_free = 0;
err = numeric_getaddrinfo(shared->node, shared->service, &entry->hints, &entry->ai);
if (err != 0) {
err = getaddrinfo(shared->node, shared->service, &entry->hints, &entry->ai);
#ifdef __linux__
/* On Linux (mainly Ubuntu 13.04) /etc/nsswitch.conf has mdns4 and
* it cause getaddrinfo to return EAI_SYSTEM/ENOENT. [ruby-list:49420]
*/
if (err == EAI_SYSTEM && errno == ENOENT)
err = EAI_NONAME;
#endif
}
/* for testing HEv2 */
if (entry->test_sleep_ms > 0) {
struct timespec sleep_ts;
sleep_ts.tv_sec = entry->test_sleep_ms / 1000;
sleep_ts.tv_nsec = (entry->test_sleep_ms % 1000) * 1000000L;
if (sleep_ts.tv_nsec >= 1000000000L) {
sleep_ts.tv_sec += sleep_ts.tv_nsec / 1000000000L;
sleep_ts.tv_nsec = sleep_ts.tv_nsec % 1000000000L;
}
nanosleep(&sleep_ts, NULL);
}
if (entry->test_ecode != 0) {
err = entry->test_ecode;
if (entry->ai) {
freeaddrinfo(entry->ai);
entry->ai = NULL;
}
}
rb_nativethread_lock_lock(shared->lock);
{
entry->err = err;
if (*shared->cancelled) {
if (entry->ai) {
freeaddrinfo(entry->ai);
entry->ai = NULL;
}
} else {
const char notification = entry->family == AF_INET6 ?
IPV6_HOSTNAME_RESOLVED : IPV4_HOSTNAME_RESOLVED;
if ((write(shared->notify, &notification, strlen(&notification))) < 0) {
entry->err = errno;
entry->has_syserr = true;
}
}
if (--(entry->refcount) == 0) need_free = 1;
if (--(shared->refcount) == 0) shared_need_free = 1;
}
rb_nativethread_lock_unlock(shared->lock);
if (need_free && entry) {
free_fast_fallback_getaddrinfo_entry(&entry);
}
if (shared_need_free && shared) {
free_fast_fallback_getaddrinfo_shared(&shared);
}
return 0;
}
#endif
/*
* Addrinfo class
*/

View File

@ -354,7 +354,7 @@ int rsock_socket(int domain, int type, int proto);
int rsock_detect_cloexec(int fd);
VALUE rsock_init_sock(VALUE sock, int fd);
VALUE rsock_sock_s_socketpair(int argc, VALUE *argv, VALUE klass);
VALUE rsock_init_inetsock(VALUE sock, VALUE remote_host, VALUE remote_serv, VALUE local_host, VALUE local_serv, int type, VALUE resolv_timeout, VALUE connect_timeout);
VALUE rsock_init_inetsock(VALUE sock, VALUE remote_host, VALUE remote_serv, VALUE local_host, VALUE local_serv, int type, VALUE resolv_timeout, VALUE connect_timeout, VALUE fast_fallback, VALUE test_mode_settings);
VALUE rsock_init_unixsock(VALUE sock, VALUE path, int server);
struct rsock_send_arg {
@ -413,6 +413,45 @@ ssize_t rsock_recvmsg(int socket, struct msghdr *message, int flags);
void rsock_discard_cmsg_resource(struct msghdr *mh, int msg_peek_p);
#endif
char *host_str(VALUE host, char *hbuf, size_t hbuflen, int *flags_ptr);
char *port_str(VALUE port, char *pbuf, size_t pbuflen, int *flags_ptr);
#ifndef FAST_FALLBACK_INIT_INETSOCK_IMPL
# if !defined(HAVE_PTHREAD_CREATE) || !defined(HAVE_PTHREAD_DETACH) || defined(__MINGW32__) || defined(__MINGW64__)
# define FAST_FALLBACK_INIT_INETSOCK_IMPL 0
# else
# include "ruby/thread_native.h"
# define FAST_FALLBACK_INIT_INETSOCK_IMPL 1
# define IPV6_HOSTNAME_RESOLVED '1'
# define IPV4_HOSTNAME_RESOLVED '2'
# define SELECT_CANCELLED '3'
struct fast_fallback_getaddrinfo_shared
{
int wait, notify, refcount, connection_attempt_fds_size;
int *connection_attempt_fds, *cancelled;
char *node, *service;
rb_nativethread_lock_t *lock;
};
struct fast_fallback_getaddrinfo_entry
{
int family, err, refcount;
struct addrinfo hints;
struct addrinfo *ai;
struct fast_fallback_getaddrinfo_shared *shared;
int has_syserr;
long test_sleep_ms;
int test_ecode;
};
int raddrinfo_pthread_create(pthread_t *th, void *(*start_routine) (void *), void *arg);
void *do_fast_fallback_getaddrinfo(void *ptr);
void free_fast_fallback_getaddrinfo_entry(struct fast_fallback_getaddrinfo_entry **entry);
void free_fast_fallback_getaddrinfo_shared(struct fast_fallback_getaddrinfo_shared **shared);
# endif
#endif
void rsock_init_basicsocket(void);
void rsock_init_ipsocket(void);
void rsock_init_tcpsocket(void);

View File

@ -34,7 +34,7 @@ socks_init(VALUE sock, VALUE host, VALUE port)
init = 1;
}
return rsock_init_inetsock(sock, host, port, Qnil, Qnil, INET_SOCKS, Qnil, Qnil);
return rsock_init_inetsock(sock, host, port, Qnil, Qnil, INET_SOCKS, Qnil, Qnil, Qfalse, Qnil);
}
#ifdef SOCKS5

View File

@ -36,7 +36,7 @@ tcp_svr_init(int argc, VALUE *argv, VALUE sock)
VALUE hostname, port;
rb_scan_args(argc, argv, "011", &hostname, &port);
return rsock_init_inetsock(sock, hostname, port, Qnil, Qnil, INET_SERVER, Qnil, Qnil);
return rsock_init_inetsock(sock, hostname, port, Qnil, Qnil, INET_SERVER, Qnil, Qnil, Qfalse, Qnil);
}
/*

View File

@ -12,13 +12,43 @@
/*
* call-seq:
* TCPSocket.new(remote_host, remote_port, local_host=nil, local_port=nil, connect_timeout: nil)
* TCPSocket.new(remote_host, remote_port, local_host=nil, local_port=nil, resolv_timeout: nil, connect_timeout: nil, fast_fallback: true)
*
* Opens a TCP connection to +remote_host+ on +remote_port+. If +local_host+
* and +local_port+ are specified, then those parameters are used on the local
* end to establish the connection.
*
* [:connect_timeout] specify the timeout in seconds.
* Starting from Ruby 3.4, this method operates according to the
* Happy Eyeballs Version 2 ({RFC 8305}[https://datatracker.ietf.org/doc/html/rfc8305])
* algorithm by default, except on Windows.
*
* To make it behave the same as in Ruby 3.3 and earlier,
* explicitly specify the option +fast_fallback:false+.
*
* Happy Eyeballs Version 2 is not provided on Windows,
* and it behaves the same as in Ruby 3.3 and earlier.
*
* [:resolv_timeout] Specifies the timeout in seconds from when the hostname resolution starts.
* [:connect_timeout] This method sequentially attempts connecting to all candidate destination addresses.<br>The +connect_timeout+ specifies the timeout in seconds from the start of the connection attempt to the last candidate.<br>By default, all connection attempts continue until the timeout occurs.<br>When +fast_fallback:false+ is explicitly specified,<br>a timeout is set for each connection attempt and any connection attempt that exceeds its timeout will be canceled.
* [:fast_fallback] Enables the Happy Eyeballs Version 2 algorithm (enabled by default).
*
* === Happy Eyeballs Version 2
* Happy Eyeballs Version 2 ({RFC 8305}[https://datatracker.ietf.org/doc/html/rfc8305])
* is an algorithm designed to improve client socket connectivity.<br>
* It aims for more reliable and efficient connections by performing hostname resolution
* and connection attempts in parallel, instead of serially.
*
* Starting from Ruby 3.4, this method operates as follows with this algorithm except on Windows:
*
* 1. Start resolving both IPv6 and IPv4 addresses concurrently.
* 2. Start connecting to the one of the addresses that are obtained first.<br>If IPv4 addresses are obtained first,
* the method waits 50 ms for IPv6 name resolution to prioritize IPv6 connections.
* 3. After starting a connection attempt, wait 250 ms for the connection to be established.<br>
* If no connection is established within this time, a new connection is started every 250 ms<br>
* until a connection is established or there are no more candidate addresses.<br>
* (Although RFC 8305 strictly specifies sorting addresses,<br>
* this method only alternates between IPv6 / IPv4 addresses due to the performance concerns)
* 4. Once a connection is established, all remaining connection attempts are canceled.
*/
static VALUE
tcp_init(int argc, VALUE *argv, VALUE sock)
@ -26,28 +56,35 @@ tcp_init(int argc, VALUE *argv, VALUE sock)
VALUE remote_host, remote_serv;
VALUE local_host, local_serv;
VALUE opt;
static ID keyword_ids[2];
VALUE kwargs[2];
static ID keyword_ids[4];
VALUE kwargs[4];
VALUE resolv_timeout = Qnil;
VALUE connect_timeout = Qnil;
VALUE fast_fallback = Qtrue;
VALUE test_mode_settings = Qnil;
if (!keyword_ids[0]) {
CONST_ID(keyword_ids[0], "resolv_timeout");
CONST_ID(keyword_ids[1], "connect_timeout");
CONST_ID(keyword_ids[2], "fast_fallback");
CONST_ID(keyword_ids[3], "test_mode_settings");
}
rb_scan_args(argc, argv, "22:", &remote_host, &remote_serv,
&local_host, &local_serv, &opt);
if (!NIL_P(opt)) {
rb_get_kwargs(opt, keyword_ids, 0, 2, kwargs);
rb_get_kwargs(opt, keyword_ids, 0, 4, kwargs);
if (kwargs[0] != Qundef) { resolv_timeout = kwargs[0]; }
if (kwargs[1] != Qundef) { connect_timeout = kwargs[1]; }
if (kwargs[2] != Qundef) { fast_fallback = kwargs[2]; }
if (kwargs[3] != Qundef) { test_mode_settings = kwargs[3]; }
}
return rsock_init_inetsock(sock, remote_host, remote_serv,
local_host, local_serv, INET_CLIENT,
resolv_timeout, connect_timeout);
resolv_timeout, connect_timeout, fast_fallback,
test_mode_settings);
}
static VALUE

View File

@ -140,4 +140,238 @@ class TestSocket_TCPSocket < Test::Unit::TestCase
server_threads.each(&:join)
end
end
def test_initialize_v6_hostname_resolved_earlier
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
opts = %w[-rsocket -W1]
assert_separately opts, "#{<<-"begin;"}\n#{<<-'end;'}"
begin;
begin
server = TCPServer.new("::1", 0)
rescue Errno::EADDRNOTAVAIL # IPv6 is not supported
exit
end
server_thread = Thread.new { server.accept }
port = server.addr[1]
socket = TCPSocket.new("localhost", port, test_mode_settings: { delay: { ipv4: 1000 } })
assert_true(socket.remote_address.ipv6?)
server_thread.value.close
server.close
socket.close if socket && !socket.closed?
end;
end
def test_initialize_v4_hostname_resolved_earlier
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
opts = %w[-rsocket -W1]
assert_separately opts, "#{<<-"begin;"}\n#{<<-'end;'}"
begin;
server = TCPServer.new("127.0.0.1", 0)
port = server.addr[1]
server_thread = Thread.new { server.accept }
socket = TCPSocket.new("localhost", port, test_mode_settings: { delay: { ipv6: 1000 } })
assert_true(socket.remote_address.ipv4?)
server_thread.value.close
server.close
socket.close if socket && !socket.closed?
end;
end
def test_initialize_v6_hostname_resolved_in_resolution_delay
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
opts = %w[-rsocket -W1]
assert_separately opts, "#{<<-"begin;"}\n#{<<-'end;'}"
begin;
begin
server = TCPServer.new("::1", 0)
rescue Errno::EADDRNOTAVAIL # IPv6 is not supported
exit
end
port = server.addr[1]
delay_time = 25 # Socket::RESOLUTION_DELAY (private) is 50ms
server_thread = Thread.new { server.accept }
socket = TCPSocket.new("localhost", port, test_mode_settings: { delay: { ipv6: delay_time } })
assert_true(socket.remote_address.ipv6?)
server_thread.value.close
server.close
socket.close if socket && !socket.closed?
end;
end
def test_initialize_v6_hostname_resolved_earlier_and_v6_server_is_not_listening
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
opts = %w[-rsocket -W1]
assert_separately opts, "#{<<-"begin;"}\n#{<<-'end;'}"
begin;
ipv4_address = "127.0.0.1"
ipv4_server = Socket.new(Socket::AF_INET, :STREAM)
ipv4_server.bind(Socket.pack_sockaddr_in(0, ipv4_address))
port = ipv4_server.connect_address.ip_port
ipv4_server_thread = Thread.new { ipv4_server.listen(1); ipv4_server.accept }
socket = TCPSocket.new("localhost", port, test_mode_settings: { delay: { ipv4: 10 } })
assert_equal(ipv4_address, socket.remote_address.ip_address)
accepted, _ = ipv4_server_thread.value
accepted.close
ipv4_server.close
socket.close if socket && !socket.closed?
end;
end
def test_initialize_v6_hostname_resolved_later_and_v6_server_is_not_listening
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
opts = %w[-rsocket -W1]
assert_separately opts, "#{<<-"begin;"}\n#{<<-'end;'}"
begin;
ipv4_server = Socket.new(Socket::AF_INET, :STREAM)
ipv4_server.bind(Socket.pack_sockaddr_in(0, "127.0.0.1"))
port = ipv4_server.connect_address.ip_port
ipv4_server_thread = Thread.new { ipv4_server.listen(1); ipv4_server.accept }
socket = TCPSocket.new("localhost", port, test_mode_settings: { delay: { ipv6: 25 } })
assert_equal(
socket.remote_address.ipv4?,
true
)
accepted, _ = ipv4_server_thread.value
accepted.close
ipv4_server.close
socket.close if socket && !socket.closed?
end;
end
def test_initialize_v6_hostname_resolution_failed_and_v4_hostname_resolution_is_success
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
opts = %w[-rsocket -W1]
assert_separately opts, "#{<<-"begin;"}\n#{<<-'end;'}"
begin;
server = TCPServer.new("127.0.0.1", 0)
port = server.addr[1]
server_thread = Thread.new { server.accept }
socket = TCPSocket.new("localhost", port, test_mode_settings: { delay: { ipv4: 10 }, error: { ipv6: Socket::EAI_FAIL } })
assert_true(socket.remote_address.ipv4?)
server_thread.value.close
server.close
socket.close if socket && !socket.closed?
end;
end
def test_initialize_resolv_timeout_with_connection_failure
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
opts = %w[-rsocket -W1]
assert_separately opts, "#{<<-"begin;"}\n#{<<-'end;'}"
server = TCPServer.new("::1", 0)
port = server.connect_address.ip_port
server.close
begin;
assert_raise(Errno::ETIMEDOUT) do
TCPSocket.new("localhost", port, resolv_timeout: 0.01, test_mode_settings: { delay: { ipv4: 1000 } })
end
end;
end
def test_initialize_with_hostname_resolution_failure_after_connection_failure
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
opts = %w[-rsocket -W1]
assert_separately opts, "#{<<-"begin;"}\n#{<<-'end;'}"
begin
server = TCPServer.new("::1", 0)
rescue Errno::EADDRNOTAVAIL # IPv6 is not supported
exit
end
port = server.connect_address.ip_port
server.close
begin;
assert_raise(Socket::ResolutionError) do
TCPSocket.new("localhost", port, test_mode_settings: { delay: { ipv4: 100 }, error: { ipv4: Socket::EAI_FAIL } })
end
end;
end
def test_initialize_with_connection_failure_after_hostname_resolution_failure
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
opts = %w[-rsocket -W1]
assert_separately opts, "#{<<-"begin;"}\n#{<<-'end;'}"
server = TCPServer.new("127.0.0.1", 0)
port = server.connect_address.ip_port
server.close
begin;
assert_raise(Errno::ECONNREFUSED) do
TCPSocket.new("localhost", port, test_mode_settings: { delay: { ipv4: 100 }, error: { ipv6: Socket::EAI_FAIL } })
end
end;
end
def test_initialize_v6_connected_socket_with_v6_address
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
opts = %w[-rsocket -W1]
assert_separately opts, "#{<<-"begin;"}\n#{<<-'end;'}"
begin;
begin
server = TCPServer.new("::1", 0)
rescue Errno::EADDRNOTAVAIL # IPv6 is not supported
exit
end
server_thread = Thread.new { server.accept }
port = server.addr[1]
socket = TCPSocket.new("::1", port)
assert_true(socket.remote_address.ipv6?)
server_thread.value.close
server.close
socket.close if socket && !socket.closed?
end;
end
def test_initialize_v4_connected_socket_with_v4_address
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
opts = %w[-rsocket -W1]
assert_separately opts, "#{<<-"begin;"}\n#{<<-'end;'}"
begin;
server = TCPServer.new("127.0.0.1", 0)
server_thread = Thread.new { server.accept }
port = server.addr[1]
socket = TCPSocket.new("127.0.0.1", port)
assert_true(socket.remote_address.ipv4?)
server_thread.value.close
server.close
socket.close if socket && !socket.closed?
end;
end
def test_initialize_fast_fallback_is_false
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
server = TCPServer.new("127.0.0.1", 0)
_, port, = server.addr
server_thread = Thread.new { server.accept }
socket = TCPSocket.new("127.0.0.1", port, fast_fallback: false)
assert_true(socket.remote_address.ipv4?)
server_thread.value.close
server.close
socket.close if socket && !socket.closed?
end
end if defined?(TCPSocket)