KEMBAR78
Introduction of Happy Eyeballs Version 2 (RFC8305) in Socket.tcp by shioimm · Pull Request #9374 · ruby/ruby · GitHub
Skip to content

Conversation

@shioimm
Copy link
Contributor

@shioimm shioimm commented Dec 27, 2023

This is an implementation of Happy Eyeballs version 2 (RFC 8305) in Socket.tcp.

Background

Currently, Socket.tcp synchronously resolves names and makes connection attempts with Addrinfo::foreach.
This implementation has the following two problems.

  1. In hostname resolution, the program stops until the DNS server responds to all DNS queries.
  2. In a connection attempt, while an IP address is trying to connect to the destination host and is taking time, the program stops, and other resolved IP addresses cannot try to connect.

Proposal

"Happy Eyeballs" (RFC 8305) is an algorithm to solve this kind of problem. It avoids delays to the user whenever possible and also uses IPv6 preferentially.
I implemented it into Socket.tcp by using Addrinfo.getaddrinfo in each thread spawned per address family to resolve the hostname asynchronously, and using Socket::connect_nonblock to try to connect with multiple addrinfo in parallel.

Outcome

This change eliminates a fatal defect in the following cases.

Case 1. One of the A or AAAA DNS queries does not return

require 'socket'

class Addrinfo
  class << self
    # Current Socket.tcp depends on foreach
    def foreach(nodename, service, family=nil, socktype=nil, protocol=nil, flags=nil, timeout: nil, &block)
      getaddrinfo(nodename, service, Socket::AF_INET6, socktype, protocol, flags, timeout: timeout)
        .concat(getaddrinfo(nodename, service, Socket::AF_INET, socktype, protocol, flags, timeout: timeout))
        .each(&block)
    end

    def getaddrinfo(_, _, family, *_)
      case family
      when Socket::AF_INET6 then sleep
      when Socket::AF_INET then [Addrinfo.tcp("127.0.0.1", 4567)]
      end
    end
  end
end

Socket.tcp("localhost", 4567)

Because the current Socket.tcp cannot resolve IPv6 names, the program stops in this case. It cannot start to connect with IPv4 address.
Though Socket.tcp with HEv2 can promptly start a connection attempt with IPv4 address in this case.

Case 2. Server does not promptly return ack for syn of either IPv4 / IPv6 address family

require 'socket'

fork do
  socket = Socket.new(Socket::AF_INET6, :STREAM)
  socket.setsockopt(:SOCKET, :REUSEADDR, true)
  socket.bind(Socket.pack_sockaddr_in(4567, '::1'))
  sleep
  socket.listen(1)
  connection, _ = socket.accept
  connection.close
  socket.close
end

fork do
  socket = Socket.new(Socket::AF_INET, :STREAM)
  socket.setsockopt(:SOCKET, :REUSEADDR, true)
  socket.bind(Socket.pack_sockaddr_in(4567, '127.0.0.1'))
  socket.listen(1)
  connection, _ = socket.accept
  connection.close
  socket.close
end

Socket.tcp("localhost", 4567)

The current Socket.tcp tries to connect serially, so when its first name resolves an IPv6 address and initiates a connection to an IPv6 server, this server does not return an ACK, and the program stops.
Though Socket.tcp with HEv2 starts to connect sequentially and in parallel so a connection can be established promptly at the socket that attempted to connect to the IPv4 server.

In exchange, the performance of Socket.tcp with HEv2 will be degraded.

100.times { Socket.tcp("www.ruby-lang.org", 80) }
# Socket.tcp (Before) 0.123809
# Socket.tcp (After)  0.246387

This is due to the addition of the creation of IO objects, Thread objects, etc., and calls to IO::select in the implementation.

@shioimm shioimm force-pushed the Socket_tcp-hev2 branch 20 times, most recently from 5b00ede to 89f61c0 Compare December 30, 2023 06:50
@shioimm shioimm force-pushed the Socket_tcp-hev2 branch 5 times, most recently from 69edcee to 6077ffa Compare December 31, 2023 07:55
This is an implementation of Happy Eyeballs version 2 (RFC 8305) in Socket.tcp.

[Background]
Currently, `Socket.tcp` synchronously resolves names and makes connection attempts with `Addrinfo::foreach.`
This implementation has the following two problems.

1. In name resolution, the program stops until the DNS server responds to all DNS queries.
2. In a connection attempt, while an IP address is trying to connect to the destination host and is taking time, the program stops, and other resolved IP addresses cannot try to connect.

[Proposal]
"Happy Eyeballs" ([RFC 8305](https://datatracker.ietf.org/doc/html/rfc8305)) is an algorithm to solve this kind of problem. It avoids delays to the user whenever possible and also uses IPv6 preferentially.

I implemented it into `Socket.tcp` by using `Addrinfo.getaddrinfo` in each thread spawned per address family to resolve the hostname asynchronously, and using `Socket::connect_nonblock` to try to connect with multiple addrinfo in parallel.

[Outcome]

This change eliminates a fatal defect in the following cases.

Case 1. One of the A or AAAA DNS queries does not return

---
require 'socket'

class Addrinfo
  class << self
    # Current Socket.tcp depends on foreach
    def foreach(nodename, service, family=nil, socktype=nil, protocol=nil, flags=nil, timeout: nil, &block)
      getaddrinfo(nodename, service, Socket::AF_INET6, socktype, protocol, flags, timeout: timeout)
        .concat(getaddrinfo(nodename, service, Socket::AF_INET, socktype, protocol, flags, timeout: timeout))
        .each(&block)
    end

    def getaddrinfo(_, _, family, *_)
      case family
      when Socket::AF_INET6 then sleep
      when Socket::AF_INET then [Addrinfo.tcp("127.0.0.1", 4567)]
      end
    end
  end
end

Socket.tcp("localhost", 4567)
---

Because the current `Socket.tcp` cannot resolve IPv6 names, the program stops in this case. It cannot start to connect with IPv4 address.
Though `Socket.tcp` with HEv2 can promptly start a connection attempt with IPv4 address in this case.

 Case 2. Server does not promptly return ack for syn of either IPv4 / IPv6 address family

---
require 'socket'

fork do
  socket = Socket.new(Socket::AF_INET6, :STREAM)
  socket.setsockopt(:SOCKET, :REUSEADDR, true)
  socket.bind(Socket.pack_sockaddr_in(4567, '::1'))
  sleep
  socket.listen(1)
  connection, _ = socket.accept
  connection.close
  socket.close
end

fork do
  socket = Socket.new(Socket::AF_INET, :STREAM)
  socket.setsockopt(:SOCKET, :REUSEADDR, true)
  socket.bind(Socket.pack_sockaddr_in(4567, '127.0.0.1'))
  socket.listen(1)
  connection, _ = socket.accept
  connection.close
  socket.close
end

Socket.tcp("localhost", 4567)
---

The current `Socket.tcp` tries to connect serially, so when its first name resolves an IPv6 address and initiates a connection to an IPv6 server, this server does not return an ACK, and the program stops.
Though `Socket.tcp` with HEv2 starts to connect sequentially and in parallel so a connection can be established promptly at the socket that attempted to connect to the IPv4 server.

In exchange, the performance of `Socket.tcp` with HEv2 will be degraded.

---
100.times { Socket.tcp("www.ruby-lang.org", 80) }
---

This is due to the addition of the creation of IO objects, Thread objects, etc., and calls to `IO::select` in the implementation.
@shioimm shioimm force-pushed the Socket_tcp-hev2 branch 2 times, most recently from d80085a to fb0823c Compare January 2, 2024 14:26
@shioimm shioimm force-pushed the Socket_tcp-hev2 branch 7 times, most recently from 6e83c58 to 1096351 Compare January 30, 2024 22:55
I have additionally implemented the following patterns:

- If the host is single-stack, name resolution is performed in the main thread. This reduces the cost of creating threads.
- If an IP address is specified, name resolution is performed in the main thread. This also reduces the cost of creating threads.
- If only one IP address is resolved, connect is executed in blocking mode. This reduces the cost of calling IO::select.

Also, I have added a fast_fallback option for users who wish not to use HE.
Here are the results of each performance test.

```ruby
require 'socket'
require 'benchmark'

HOSTNAME = "www.ruby-lang.org"
PORT = 80

ai = Addrinfo.tcp(HOSTNAME, PORT)

Benchmark.bmbm do |x|
  x.report("Domain name") do
    30.times { Socket.tcp(HOSTNAME, PORT).close }
  end

  x.report("IP Address") do
    30.times { Socket.tcp(ai.ip_address, PORT).close }
  end

  x.report("fast_fallback: false") do
    30.times { Socket.tcp(HOSTNAME, PORT, fast_fallback: false).close }
  end
end
```

```
                           user     system      total        real
Domain name            0.015567   0.032511   0.048078 (  0.325284)
IP Address             0.004458   0.014219   0.018677 (  0.284361)
fast_fallback: false   0.005869   0.021511   0.027380 (  0.321891)
````

And this is the measurement result when executed in a single stack environment.

```
                           user     system      total        real
Domain name            0.007062   0.019276   0.026338 (  1.905775)
IP Address             0.004527   0.012176   0.016703 (  3.051192)
fast_fallback: false   0.005546   0.019426   0.024972 (  1.775798)
```

The following is the result of the run on Ruby 3.3.0.

(on Dual stack environment)

```
                 user     system      total        real
Ruby 3.3.0   0.007271   0.027410   0.034681 (  0.472510)
```

(on Single stack environment)

```
                 user     system      total        real
Ruby 3.3.0  0.005353   0.018898   0.024251 (  1.774535)
```
@shioimm
Copy link
Contributor Author

shioimm commented Jan 31, 2024

NOTE
I have added the following tests:

  • If an IPv6 address is resolved before an IPv4 address, connect using the IPv6 address.
    • test_tcp_socket_v6_hostname_resolved_earlier
  • If an IPv4 address is resolved before an IPv6 address, connect using the IPv4 address.
    • test_tcp_socket_v4_hostname_resolved_earlier
  • If an IPv4 address is resolved first, but the IPv6 address is resolved during the Resolution Delay, connect using the IPv6 address.
    • test_tcp_socket_v6_hostname_resolved_in_resolution_delay
  • If the IPv6 address is resolved first but the IPv6 server is not listening, and the IPv4 address is then resolved, connect using the IPv4 address.
    • test_tcp_socket_v6_hostname_resolved_earlier_and_v6_server_is_not_listening
  • If name resolution does not complete within resolv_timeout, an Errno::ETIMEDOUT error should occur.
    • test_tcp_socket_resolv_timeout
  • If name resolution fails for any address family, connect using the address that was successfully resolved.
    • test_tcp_socket_one_hostname_resolution_succeeded_at_least
  • If both IPv4 and IPv6 address resolutions fail, raise an exception with the error of the latter failed resolution.
    • test_tcp_socket_all_hostname_resolution_failed
  • If a resolved IPv6 address is passed as an argument, connect using the IPv6 address.
    • test_tcp_socket_v6_address_passed
  • Ensure connection is possible when passing fast_fallback: false as an argument.
    • test_tcp_socket_fast_fallback_is_false

Tests not added:

  • Connecting using an IPv4 address when a resolved IPv4 address is specified as host.
  • Raising an IPv4 connection failure exception if the connection using an IPv4 address fails after specifying a resolved IPv4 address as host.
  • Raising an IPv6 connection failure exception if the connection using an IPv6 address fails after specifying a resolved IPv6 address as host.
  • Connecting using an IPv6 address if IPv4 address resolution fails and then IPv6 address resolution succeeds.
  • Raising an IPv6 name resolution error exception if IPv4 address resolution fails and then IPv6 address resolution fails.
  • Connecting using an IPv4 address if IPv4 address resolution succeeds and then IPv6 address resolution fails during the Resolution Delay.
  • Raising an IPv4 connection failure exception if IPv4 address resolution succeeds, IPv6 address resolution fails during the Resolution Delay, and then the connection using the IPv4 address fails.
  • Raising an IPv4 connection failure exception if IPv4 address resolution succeeds, IPv6 address resolution succeeds during the Resolution Delay, but the connection using the IPv6 address fails, followed by a failed connection using the IPv4 address.
  • Connecting using the respective IPv6 address if there are unattempted IPv6 and IPv4 addresses after IPv4 address resolution succeeds, IPv6 address resolution succeeds during the Resolution Delay, but the connection using both addresses fails.
  • Connecting using the respective IPv6 address if there are unattempted IPv6 addresses after IPv4 address resolution succeeds, IPv6 address resolution succeeds during the Resolution Delay, but the connection using both addresses fails.
  • Connecting using the respective IPv4 address if there are unattempted IPv4 addresses after IPv4 address resolution succeeds, IPv6 address resolution succeeds during the Resolution Delay, but the connection using both addresses fails.
  • Connecting using an IPv6 address if IPv4 address resolution succeeds, the connection using the IPv4 address fails, but the IPv6 address resolution succeeds within the PATIENTLY_RESOLUTION_DELAY.
  • Raising an IPv6 connection failure exception if IPv4 address resolution succeeds, the connection using the IPv4 address fails, but the IPv6 address resolution succeeds within the PATIENTLY_RESOLUTION_DELAY, followed by a failed connection using the IPv6 address.
  • Connecting using the respective IPv6 address if there are unattempted IPv6 addresses after IPv4 address resolution succeeds, the connection using the IPv4 address fails, but the IPv6 address resolution succeeds within the PATIENTLY_RESOLUTION_DELAY, followed by a failed connection using the IPv6 address.
  • Raising an IPv4 connection failure exception if IPv4 address resolution succeeds, the connection using the IPv4 address fails, but IPv6 address resolution does not succeed within the PATIENTLY_RESOLUTION_DELAY.
  • Connecting using an IPv4 address if IPv6 address resolution fails and then IPv4 address resolution succeeds.
  • Raising an IPv4 name resolution error exception if IPv6 address resolution fails and then IPv4 address resolution fails.
  • Connecting using an IPv4 address if IPv6 address resolution succeeds and the connection using the IPv6 address fails, but IPv4 address resolution succeeds within the PATIENTLY_RESOLUTION_DELAY.
  • Raising an IPv6 connection failure exception if IPv6 address resolution succeeds and the connection using the IPv6 address fails, but IPv4 address resolution fails within the PATIENTLY_RESOLUTION

attr_accessor :tcp_fast_fallback

def initialize_socket
@local_ip_address_list = Socket.ip_address_list
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose caching Socket.ip_address_list permanently is not a good idea. The host may later gain or lose IP addresses on network configuration changes (e.g., Wi-Fi / cellular network) or when the OS is booting and the network interfaces are not yet ready.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your comment.
As you mentioned, persisting the cache could lead to unexpected issues, so we have decided to remove it. a3d7056

Copy link
Contributor

@hanazuki hanazuki Feb 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also support removing the dual-/single-stack check because it could result in false negatives in certain corner cases, for example,

  • When the host only has a link-local address, and /etc/hosts or mDNS allows resolving names of peer hosts to link-local addresses. In this case, the hosts can communicate using link-local addresses (APIPA network).
  • When the host doesn't have an IPv6 GUA, and tries to connect to a hostname that resolves to ::1 (e.g., "localhost6" in /etc/hosts). In this case a PF_INET6 socket should be used.

I think it's difficult to cover all of this kind of uncommon network configuration in the wild.

shioimm added a commit to shioimm/ruby that referenced this pull request Feb 15, 2024
As mentioned in the comment at ruby#9374 (comment), caching Socket.ip_address_list does not follow changes in network configuration.
But if we stop caching, it becomes necessary to check every time `Socket.tcp` is called whether it's a single stack or not, which could further degrade performance in the case of a dual stack.
From this, I've changed the approach so that when a domain name is passed, it doesn't check whether it's a single stack or not and resolves names in parallel each time.

The performance measurement results are as follows.

require 'socket'
require 'benchmark'

HOSTNAME = "www.ruby-lang.org"
PORT = 80

ai = Addrinfo.tcp(HOSTNAME, PORT)

Benchmark.bmbm do |x|
  x.report("Domain name") do
    30.times { Socket.tcp(HOSTNAME, PORT).close }
  end

  x.report("IP Address") do
    30.times { Socket.tcp(ai.ip_address, PORT).close }
  end

  x.report("fast_fallback: false") do
    30.times { Socket.tcp(HOSTNAME, PORT, fast_fallback: false).close }
  end
end

                           user     system      total        real
Domain name            0.004085   0.011873   0.015958 (  0.330097)
IP Address             0.000993   0.004400   0.005393 (  0.257286)
fast_fallback: false   0.001348   0.008266   0.009614 (  0.298626)
end

def self.specified_family_name_and_next_state(hostname)
if hostname.match?(/:/) then [:ipv6, :v6c]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just a corner case, but several platforms (at least Linux and macOS) accept a colon (:) in hostnames, and DNS also allows a domain name with a colon. For instance, Socket.tcp("x::y", 80) and Socket.tcp("1::1::1", 80) specify hostnames (not IPv6 address literals), in which case both of the IPv4 and IPv6 name resolutions should be tried; otherwise, they fail to connect if the names only have IPv4 addresses.
So I think here we need the exact pattern to match IPv6 address literals (including link-local addresses with scope). False negatives (redundant name resolutions) are better than false positives (connection failures).

As mentioned in the comment at ruby#9374 (comment), caching Socket.ip_address_list does not follow changes in network configuration.
But if we stop caching, it becomes necessary to check every time `Socket.tcp` is called whether it's a single stack or not, which could further degrade performance in the case of a dual stack.
From this, I've changed the approach so that when a domain name is passed, it doesn't check whether it's a single stack or not and resolves names in parallel each time.

The performance measurement results are as follows.

require 'socket'
require 'benchmark'

HOSTNAME = "www.ruby-lang.org"
PORT = 80

ai = Addrinfo.tcp(HOSTNAME, PORT)

Benchmark.bmbm do |x|
  x.report("Domain name") do
    30.times { Socket.tcp(HOSTNAME, PORT).close }
  end

  x.report("IP Address") do
    30.times { Socket.tcp(ai.ip_address, PORT).close }
  end

  x.report("fast_fallback: false") do
    30.times { Socket.tcp(HOSTNAME, PORT, fast_fallback: false).close }
  end
end

                           user     system      total        real
Domain name            0.004085   0.011873   0.015958 (  0.330097)
IP Address             0.000993   0.004400   0.005393 (  0.257286)
fast_fallback: false   0.001348   0.008266   0.009614 (  0.298626)
Changed from waiting only 3 seconds for name resolution when there is no fallback address available, to waiting as long as there is no resolv_timeout.
This is in accordance with the current `Socket.tcp` specification.
@nurse nurse merged commit 9ec342e into ruby:master Feb 26, 2024
shioimm added a commit to shioimm/ruby that referenced this pull request Sep 28, 2024
This is an implementation of Happy Eyeballs version 2 (RFC 8305) in `TCPSocket.new`.
See ruby#11653

1. Background
Prior to this implementation, I implemented Happy Eyeballs Version 2 (HEv2) for `Socket.tcp` in ruby#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)
```
shioimm added a commit to shioimm/ruby that referenced this pull request Sep 28, 2024
This is an implementation of Happy Eyeballs version 2 (RFC 8305) in `TCPSocket.new`.
See ruby#11653

1. Background
Prior to this implementation, I implemented Happy Eyeballs Version 2 (HEv2) for `Socket.tcp` in ruby#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)
```
shioimm added a commit to shioimm/ruby that referenced this pull request Oct 1, 2024
This is an implementation of Happy Eyeballs version 2 (RFC 8305) in `TCPSocket.new`.
See ruby#11653

1. Background
Prior to this implementation, I implemented Happy Eyeballs Version 2 (HEv2) for `Socket.tcp` in ruby#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)
```
shioimm added a commit to shioimm/ruby that referenced this pull request Oct 17, 2024
This is an implementation of Happy Eyeballs version 2 (RFC 8305) in `TCPSocket.new`.
See ruby#11653

1. Background
Prior to this implementation, I implemented Happy Eyeballs Version 2 (HEv2) for `Socket.tcp` in ruby#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)
```
shioimm added a commit that referenced this pull request Nov 12, 2024
…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 #11653

1. Background
Prior to this implementation, I implemented Happy Eyeballs Version 2 (HEv2) for `Socket.tcp` in #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>
headius added a commit to headius/jruby that referenced this pull request Mar 13, 2025
This is used by the new fast fallback algorithm for the Socket.tcp
method. This appears to be the only C change needed.

See the following PRs for the bulk of the related logic:

* ruby/ruby#9374
* ruby/ruby#11187
headius added a commit to headius/jruby that referenced this pull request Mar 13, 2025
This is used by the new fast fallback algorithm for the Socket.tcp
method. This appears to be the only C change needed.

See the following PRs for the bulk of the related logic:

* ruby/ruby#9374
* ruby/ruby#11187
* ruby/ruby#12257
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants