Improve APIs for Globally Enabling/Disabling fast_fallback in Socket (#12257)

This change includes the following updates:
- Added an environment variable `RUBY_TCP_NO_FAST_FALLBACK` to control enabling/disabling fast_fallback
- Updated documentation and man pages
- Revised the implementation of Socket.tcp_fast_fallback= and Socket.tcp_fast_fallback, which previously performed dynamic name resolution of constants and variables. As a result, the following performance improvements were achieved:

(Case of 1000 executions of `TCPSocket.new` to the local host)

Rehearsal -----------------------------------------
before   0.031462   0.147946   0.179408 (  0.249279)
after    0.031164   0.146839   0.178003 (  0.346935)
-------------------------------- total: 0.178003sec

            user     system      total        real
before   0.027584   0.138712   0.166296 (  0.233356)
after    0.025953   0.127608   0.153561 (  0.237971)
This commit is contained in:
Misaki Shioi 2024-12-14 15:51:19 +09:00 committed by GitHub
parent 77016a7b43
commit 9f924e2f13
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
Notes: git 2024-12-14 06:51:36 +00:00
Merged-By: shioimm <shioi.mm@gmail.com>
6 changed files with 117 additions and 49 deletions

View File

@ -617,12 +617,6 @@ class Socket < BasicSocket
IPV6_ADRESS_FORMAT = /\A(?i:(?:(?:[0-9A-F]{1,4}:){7}(?:[0-9A-F]{1,4}|:)|(?:[0-9A-F]{1,4}:){6}(?:[0-9A-F]{1,4}|:(?:[0-9A-F]{1,4}:){1,5}[0-9A-F]{1,4}|:)|(?:[0-9A-F]{1,4}:){5}(?:(?::[0-9A-F]{1,4}){1,2}|:(?:[0-9A-F]{1,4}:){1,4}[0-9A-F]{1,4}|:)|(?:[0-9A-F]{1,4}:){4}(?:(?::[0-9A-F]{1,4}){1,3}|:(?:[0-9A-F]{1,4}:){1,3}[0-9A-F]{1,4}|:)|(?:[0-9A-F]{1,4}:){3}(?:(?::[0-9A-F]{1,4}){1,4}|:(?:[0-9A-F]{1,4}:){1,2}[0-9A-F]{1,4}|:)|(?:[0-9A-F]{1,4}:){2}(?:(?::[0-9A-F]{1,4}){1,5}|:(?:[0-9A-F]{1,4}:)[0-9A-F]{1,4}|:)|(?:[0-9A-F]{1,4}:){1}(?:(?::[0-9A-F]{1,4}){1,6}|:(?:[0-9A-F]{1,4}:){0,5}[0-9A-F]{1,4}|:)|(?:::(?:[0-9A-F]{1,4}:){0,7}[0-9A-F]{1,4}|::)))(?:%.+)?\z/
private_constant :IPV6_ADRESS_FORMAT
@tcp_fast_fallback = true
class << self
attr_accessor :tcp_fast_fallback
end
# :call-seq:
# Socket.tcp(host, port, local_host=nil, local_port=nil, [opts]) {|socket| ... }
# Socket.tcp(host, port, local_host=nil, local_port=nil, [opts])
@ -633,8 +627,13 @@ class Socket < BasicSocket
# Happy Eyeballs Version 2 ({RFC 8305}[https://datatracker.ietf.org/doc/html/rfc8305])
# algorithm by default.
#
# For details on Happy Eyeballs Version 2,
# see {Socket.tcp_fast_fallback=}[rdoc-ref:Socket#tcp_fast_fallback=].
#
# To make it behave the same as in Ruby 3.3 and earlier,
# explicitly specify the option +fast_fallback:false+.
# explicitly specify the option fast_fallback:false.
# Or, setting Socket.tcp_fast_fallback=false will disable
# Happy Eyeballs Version 2 not only for this method but for all Socket globally.
#
# If local_host:local_port is given,
# the socket is bound to it.
@ -657,24 +656,6 @@ class Socket < BasicSocket
# sock.close_write
# puts sock.read
# }
#
# === 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:
#
# 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.
def self.tcp(host, port, local_host = nil, local_port = nil, connect_timeout: nil, resolv_timeout: nil, fast_fallback: tcp_fast_fallback, &) # :yield: socket
sock = if fast_fallback && !(host && ip_address?(host))
tcp_with_fast_fallback(host, port, local_host, local_port, connect_timeout:, resolv_timeout:)

View File

@ -501,6 +501,8 @@ void rsock_make_fd_nonblock(int fd);
int rsock_is_dgram(rb_io_t *fptr);
extern ID tcp_fast_fallback;
#if !defined HAVE_INET_NTOP && ! defined _WIN32
const char *inet_ntop(int, const void *, char *, size_t);
#elif defined __MINGW32__

View File

@ -14,6 +14,8 @@ static VALUE sym_wait_writable;
static VALUE sock_s_unpack_sockaddr_in(VALUE, VALUE);
ID tcp_fast_fallback;
void
rsock_sys_fail_host_port(const char *mesg, VALUE host, VALUE port)
{
@ -1854,6 +1856,67 @@ socket_s_ip_address_list(VALUE self)
#define socket_s_ip_address_list rb_f_notimplement
#endif
/*
* call-seq:
* Socket.tcp_fast_fallback -> true or false
*
* Returns whether Happy Eyeballs Version 2 ({RFC 8305}[https://datatracker.ietf.org/doc/html/rfc8305]),
* which is provided starting from Ruby 3.4 when using TCPSocket.new and Socket.tcp,
* is enabled or disabled.
*
* If true, it is enabled for TCPSocket.new and Socket.tcp.
* (Note: Happy Eyeballs Version 2 is not provided when using TCPSocket.new on Windows.)
*
* If false, Happy Eyeballs Version 2 is disabled.
*
* For details on Happy Eyeballs Version 2,
* see {Socket.tcp_fast_fallback=}[rdoc-ref:Socket#tcp_fast_fallback=].
*/
VALUE socket_s_tcp_fast_fallback(VALUE self) {
return rb_ivar_get(rb_cSocket, tcp_fast_fallback);
}
/*
* call-seq:
* Socket.tcp_fast_fallback= -> true or false
*
* Enable or disable Happy Eyeballs Version 2 ({RFC 8305}[https://datatracker.ietf.org/doc/html/rfc8305])
* globally, which is provided starting from Ruby 3.4 when using TCPSocket.new and Socket.tcp.
*
* When set to true, the feature is enabled for both `TCPSocket.new` and `Socket.tcp`.
* (Note: This feature is not available when using TCPSocket.new on Windows.)
*
* When set to false, the behavior reverts to that of Ruby 3.3 or earlier.
*
* The default value is true if no value is explicitly set by calling this method.
* However, when the environment variable RUBY_TCP_NO_FAST_FALLBACK=1 is set,
* the default is false.
*
* To control the setting on a per-method basis, use the fast_fallback keyword argument for each method.
*
* === 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:
*
* 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.
*/
VALUE socket_s_tcp_fast_fallback_set(VALUE self, VALUE value) {
rb_ivar_set(rb_cSocket, tcp_fast_fallback, value);
return value;
}
void
Init_socket(void)
{
@ -1982,6 +2045,16 @@ Init_socket(void)
rsock_init_socket_init();
const char *tcp_no_fast_fallback_config = getenv("RUBY_TCP_NO_FAST_FALLBACK");
VALUE fast_fallback_default;
if (tcp_no_fast_fallback_config == NULL || strcmp(tcp_no_fast_fallback_config, "0") == 0) {
fast_fallback_default = Qtrue;
} else {
fast_fallback_default = Qfalse;
}
tcp_fast_fallback = rb_intern_const("tcp_fast_fallback");
rb_ivar_set(rb_cSocket, tcp_fast_fallback, fast_fallback_default);
rb_define_method(rb_cSocket, "initialize", sock_initialize, -1);
rb_define_method(rb_cSocket, "connect", sock_connect, 1);
@ -2025,6 +2098,9 @@ Init_socket(void)
rb_define_singleton_method(rb_cSocket, "ip_address_list", socket_s_ip_address_list, 0);
rb_define_singleton_method(rb_cSocket, "tcp_fast_fallback", socket_s_tcp_fast_fallback, 0);
rb_define_singleton_method(rb_cSocket, "tcp_fast_fallback=", socket_s_tcp_fast_fallback_set, 1);
#undef rb_intern
sym_wait_writable = ID2SYM(rb_intern("wait_writable"));
}

View File

@ -22,33 +22,20 @@
* 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+.
* For details on Happy Eyeballs Version 2,
* see {Socket.tcp_fast_fallback=}[rdoc-ref:Socket#tcp_fast_fallback=].
*
* Happy Eyeballs Version 2 is not provided on Windows,
* To make it behave the same as in Ruby 3.3 and earlier,
* explicitly specify the option fast_fallback:false.
* Or, setting Socket.tcp_fast_fallback=false will disable
* Happy Eyeballs Version 2 not only for this method but for all Socket globally.
*
* When using TCPSocket.new on Windows, Happy Eyeballs Version 2 is not provided,
* 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)
@ -82,9 +69,7 @@ tcp_init(int argc, VALUE *argv, VALUE sock)
}
if (fast_fallback == Qnil) {
VALUE socket_class = rb_const_get(rb_cObject, rb_intern("Socket"));
ID var_id = rb_intern("@tcp_fast_fallback");
fast_fallback = rb_ivar_get(socket_class, var_id);
fast_fallback = rb_ivar_get(rb_cSocket, tcp_fast_fallback);
if (fast_fallback == Qnil) fast_fallback = Qtrue;
}

View File

@ -776,6 +776,17 @@ a program (or script) that is to be executed.
.Pp
The pipe template is split on spaces into an argument list before the
template parameters are expanded.
.Sh MISC ENVIRONMENT
.Pp
.Bl -hang -compact -width "RUBY_TCP_NO_FAST_FALLBACK"
.It Ev RUBY_TCP_NO_FAST_FALLBACK
If set to
.Li 1 ,
disables the fast fallback feature by default in TCPSocket.new and Socket.tcp.
When set to
.Li 0
or left unset, the fast fallback feature is enabled.
Introduced in Ruby 3.4, default: unset.
.Sh SEE ALSO
.Bl -hang -compact -width "https://www.ruby-toolbox.com/"
.It Lk https://www.ruby-lang.org/

View File

@ -1017,4 +1017,17 @@ class TestSocket < Test::Unit::TestCase
server.close
socket.close if socket && !socket.closed?
end
def test_tcp_fast_fallback
opts = %w[-rsocket -W1]
assert_separately opts, <<~RUBY
assert_true(Socket.tcp_fast_fallback)
Socket.tcp_fast_fallback = false
assert_false(Socket.tcp_fast_fallback)
Socket.tcp_fast_fallback = true
assert_true(Socket.tcp_fast_fallback)
RUBY
end
end if defined?(Socket)