summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMisaki Shioi <[email protected]>2024-12-14 15:51:19 +0900
committerGitHub <[email protected]>2024-12-14 15:51:19 +0900
commit9f924e2f13992241c447190a9eb139bf46dcb8d9 (patch)
treeac6acd2fa5b2fe57d895266a762999343c3ae160
parent77016a7b4341d861b83e222c18333204b63f480b (diff)
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)
Notes
Notes: Merged-By: shioimm <[email protected]>
-rw-r--r--ext/socket/lib/socket.rb31
-rw-r--r--ext/socket/rubysocket.h2
-rw-r--r--ext/socket/socket.c76
-rw-r--r--ext/socket/tcpsocket.c31
-rw-r--r--man/ruby.111
-rw-r--r--test/socket/test_socket.rb13
6 files changed, 116 insertions, 48 deletions
diff --git a/ext/socket/lib/socket.rb b/ext/socket/lib/socket.rb
index 39fd51b92a..78958c377b 100644
--- a/ext/socket/lib/socket.rb
+++ b/ext/socket/lib/socket.rb
@@ -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:)
diff --git a/ext/socket/rubysocket.h b/ext/socket/rubysocket.h
index 7f92599079..932ed8e022 100644
--- a/ext/socket/rubysocket.h
+++ b/ext/socket/rubysocket.h
@@ -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__
diff --git a/ext/socket/socket.c b/ext/socket/socket.c
index 487a293c4e..2713ef3735 100644
--- a/ext/socket/socket.c
+++ b/ext/socket/socket.c
@@ -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"));
}
diff --git a/ext/socket/tcpsocket.c b/ext/socket/tcpsocket.c
index e091d75e5d..93618d2d90 100644
--- a/ext/socket/tcpsocket.c
+++ b/ext/socket/tcpsocket.c
@@ -22,33 +22,20 @@
* Happy Eyeballs Version 2 ({RFC 8305}[https://datatracker.ietf.org/doc/html/rfc8305])
* algorithm by default, except on Windows.
*
+ * 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.
*
- * Happy Eyeballs Version 2 is not provided on Windows,
+ * 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;
}
diff --git a/man/ruby.1 b/man/ruby.1
index b1b8e42401..b058acab97 100644
--- a/man/ruby.1
+++ b/man/ruby.1
@@ -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/
diff --git a/test/socket/test_socket.rb b/test/socket/test_socket.rb
index 6a057e866f..5fa07039d4 100644
--- a/test/socket/test_socket.rb
+++ b/test/socket/test_socket.rb
@@ -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)