[ruby-core:116944] [Ruby master Feature#20108] Introduction of Happy Eyeballs Version 2 (RFC8305) in Socket.tcp
From:
"shioimm (Misaki Shioi) via ruby-core" <ruby-core@...>
Date:
2024-02-26 05:46:51 UTC
List:
ruby-core #116944
Issue #20108 has been updated by shioimm (Misaki Shioi).
As previously posted, I was considering a way to avoid name resolution within a thread when the host is single-stack.
However, it's clear that it will take a significant cost to associate with determining whether the host is single-stack, and there will be more additional costs for name resolution if it's not single-stack.
To avoid this, I was considering caching the results of determining whether a host is single-stack or not, but found that this could cause unexpected bugs when switching networks.
Therefore, I have decided to abandon the determination of whether the host is single-stack and proceed with initiating a thread for name resolution unless an IP address is directly specified.
Note that the fast_fallback option remains available.
This PR was merged today. Thank you very much.
----------------------------------------
Feature #20108: Introduction of Happy Eyeballs Version 2 (RFC8305) in Socket.tcp
https://bugs.ruby-lang.org/issues/20108#change-106985
* Author: shioimm (Misaki Shioi)
* Status: Open
----------------------------------------
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](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.
See https://github.com/ruby/ruby/pull/9374
### Outcome
This change eliminates a fatal defect in the following cases.
#### Case 1. One of the A or AAAA DNS queries does not return
```ruby
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
```ruby
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.224684
```
This is due to the addition of the creation of IO objects, Thread objects, etc., and calls to `IO::select` in the implementation.
--
https://bugs.ruby-lang.org/
______________________________________________
ruby-core mailing list -- [email protected]
To unsubscribe send an email to [email protected]
ruby-core info -- https://ml.ruby-lang.org/mailman3/postorius/lists/ruby-core.ml.ruby-lang.org/