With the introduction of Happy Eyeballs Version 2 to `Socket::tcp`, the following areas have been mainly enhanced:
- How the value specified for `connect_timeout` works
- How Socket.tcp operates with Happy Eyeballs Version 2
A description for the new option `fast_fallback` has been added in https://github.com/ruby/ruby/pull/11813.
[Feature #20646]Improve Socket.tcp
This is a proposed improvement to `Socket.tcp`, which has implemented Happy Eyeballs version 2 (RFC8305) in PR9374.
1. Background
I implemented Happy Eyeballs version 2 (HEv2) for Socket.tcp in PR9374, but several issues have been identified:
- `IO.select` waits for name resolution or connection establishment in v46w, but it does not consider the case where both events occur simultaneously when it returns a value.
- In this case, Socket.tcp can only capture one event and needs to execute an unnecessary loop to capture the other one, calling `IO.select` one extra time.
- `IO.select` waits for both IPv6/IPv4 name resolution (in start), but when it returns a value, it doesn't consider the case where name resolution for both address families is complete.
- In this case, `Socket.tcp` can only obtain the addresses of one address family and needs to execute an unnecessary loop obtain the other addresses, calling `IO.select` one extra time.
- The consideration for `connect_timeout` was insufficient. After initiating one or more connections, it raises a 'user specified timeout' after the `connect_timeout` period even if there were addresses that have been resolved and have not yet tried to connect.
- It does not retry with another address in case of a connection failure.
- It executes unnecessary state transitions even when an IP address is passed as the `host` argument.
- The regex for IP addresses did not correctly specify the start and end.
2. Proposal & Outcome
To overcome the aforementioned issues, this PR introduces the following changes:
- Previously, each loop iteration represented a single state transition. This has been changed to execute all processes that meet the execution conditions within a single loop iteration.
- This prevents unnecessary repeated loops and calling `IO.select`
- Introduced logic to determine the timeout value set for `IO.select`. During the Resolution Delay and Connection Attempt Delay, the user-specified timeout is ignored. Otherwise, the timeout value is set to the larger of `resolv_timeout` and `connect_timeout`.
- This ensures that the `connect_timeout` is only detected after attempting to connect to all resolved addresses.
- Retry with another address in case of a connection failure.
- This prevents unnecessary repeated loops upon connection failure.
- Call `tcp_without_fast_fallback` when an IP address is passed as the host argument.
- This prevents unnecessary state transitions when an IP address is passed.
- Fixed regex for IP addresses.
Additionally, the code has been reduced by over 100 lines, and redundancy has been minimized, which is expected to improve readability.
3. Performance
No significant performance changes were observed in the happy case before and after the improvement.
However, improvements in state transition deficiencies are expected to enhance performance in edge cases.
```ruby
require 'socket'
require 'benchmark'
Benchmark.bmbm do |x|
x.report('fast_fallback: true') do
30.times { Socket.tcp("www.ruby-lang.org", 80) }
end
x.report('fast_fallback: false') do # Ruby3.3時点と同じ
30.times { Socket.tcp("www.ruby-lang.org", 80, fast_fallback: false) }
end
end
```
Before:
```
~/s/build ❯❯❯ ../install/bin/ruby ../ruby/test.rb
user system total real
fast_fallback: true 0.021315 0.040723 0.062038 ( 0.504866)
fast_fallback: false 0.007553 0.026248 0.033801 ( 0.533211)
```
After:
```
~/s/build ❯❯❯ ../install/bin/ruby ../ruby/test.rb
user system total real
fast_fallback: true 0.023081 0.040525 0.063606 ( 0.406219)
fast_fallback: false 0.007302 0.025515 0.032817 ( 0.418680)
```
* Introduction of Happy Eyeballs Version 2 (RFC8305) in Socket.tcp
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.
* Avoid NameError of Socket::EAI_ADDRFAMILY in MinGW
* Support Windows with SO_CONNECT_TIME
* Improve performance
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)
```
* Do not cache `Socket.ip_address_list`
As mentioned in the comment at https://github.com/ruby/ruby/pull/9374#discussion_r1482269186, 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)
* Wait forever if fallback addresses are unresolved, unless resolv_timeout
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.
* Use exact pattern to match IPv6 address format for specify address family
* Windows: Fix warning about undefined if_indextoname()
* Windows: Fix UNIXSocket on MINGW and make .pair more reliable
* Windows: Use nonblock=true for read tests with scheduler
* Windows: Move socket detection from File.socket? to File.stat
Add S_IFSOCK to Windows and interpret reparse points accordingly.
Enable tests that work now.
* Windows: Use wide-char functions to UNIXSocket
This fixes behaviour with non-ASCII characters.
It also fixes deletion of temporary UNIXSocket.pair files.
* Windows: Add UNIXSocket tests for specifics of Windows impl.
* Windows: fix VC build due to missing _snwprintf
Avoid usage of _snwprintf, since it fails linking ruby.dll like so:
linking shared-library x64-vcruntime140-ruby320.dll
x64-vcruntime140-ruby320.def : error LNK2001: unresolved external symbol snwprintf
x64-vcruntime140-ruby320.def : error LNK2001: unresolved external symbol vsnwprintf_l
whereas linking miniruby.exe succeeds.
This patch uses snprintf on the UTF-8 string instead.
Also remove branch GetWindowsDirectoryW, since it doesn't work.
* Windows: Fix dangling symlink test failures
Co-authored-by: Lars Kanis <kanis@comcard.de>
Addrinfo.getaddrinfo and .foreach now accepts :timeout in seconds as
a keyword argument. If getaddrinfo_a(3) is available, the timeout will be
applied for name resolution. Otherwise, it will be ignored.
Socket.tcp accepts :resolv_timeout to use this feature.
This commit is retry of 6382f5cc91.
Test was failed on Solaris machines which don't have "http" in
/etc/services. In this commit, use "ssh" instead.
Addrinfo.getaddrinfo and .foreach now accepts :timeout in seconds as
a keyword argument. If getaddrinfo_a(3) is available, the timeout will be
applied for name resolution. Otherwise, it will be ignored.
Socket.tcp accepts :resolv_timeout to use this feature.
* ext/socket/lib/socket.rb (Addrinfo#connect_internal): make
protected for Addrinfo#connect_to, instead of private and send.
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@62449 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
IO#read_nonblock and IO#write_nonblock take into account
buffered data, so the Linux-only BasicSocket#read_nonblock
and BasicSocket#write_nonblock methods must, too.
This bug was only introduced in r58400
("socket: avoid fcntl for read/write_nonblock on Linux")
and does not affect any stable release.
* ext/socket/basicsocket.c (rsock_init_basicsocket):
* ext/socket/init.c (rsock_s_recvfrom_nonblock):
* ext/socket/init.c (rsock_init_socket_init):
* ext/socket/lib/socket.rb (def read_nonblock):
* ext/socket/lib/socket.rb (def write_nonblock):
* ext/socket/rubysocket.h (static inline void rsock_maybe_wait_fd):
* test/socket/test_basicsocket.rb (def test_read_write_nonblock):
[Feature #13362]
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@60496 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
On platforms where MSG_DONTWAIT works reliably on all sockets
(so far, I know of Linux), we can avoid fcntl syscalls and
implement IO#write_nonblock and IO#read_nonblock in terms of the
socket-specific send and recv family of syscalls.
This avoids side effects on the socket, and also encourages
generic code to be written in cases where IO wrappers like
OpenSSL::SSL::SSLSocket are used.
Perhaps in the future, side-effect-free non-blocking I/O can
be standard on all files and OSes: https://cr.yp.to/unix/nonblock.html
* ext/socket/lib/socket.rb (read_nonblock, write_nonblock):
Linux-specific wrapper without side effects
[ruby-core:80780] [Feature #13362]
* test/socket/test_basicsocket.rb (test_read_write_nonblock):
new test
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@58400 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
Symbol proc is shorter human and machine code;
and also avoids needing to name variables.
* ext/socket/lib/socket.rb (Socket.udp_server_sockets): use symbol proc
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@58320 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
* ext/socket/lib/socket.rb (Socket.udp_server_sockets): remove duplicated
addresses before passing it to ip_sockets_port0 because it causes
Errno::EADDRINUSE and retry forever.
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@58293 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
Update docs to reflect EOF behavior change of read_nonblock and
write_nonblock when using `exception: false`.
[Fix GH-1527]
Author: Russell Davis <russell-stripe@users.noreply.github.com>
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@57690 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
Made possible by r56795, this reduces human and byte code size.
* ext/socket/lib/socket.rb (self.ip_sockets_port0,
self.tcp_server_sockets_port0,
self.tcp_server_sockets,
self.udp_server_sockets): use symbol proc
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@56867 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
* ext/socket/lib/socket.rb: remove unnecessary closed checks,
close on closed socket no longer raises an exception.
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@56795 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
* ext/socket/lib/socket.rb (UDPSocket#recvfrom_nonblock): [DOC] Remove
a false statement "If _maxlen_ is omitted, its default value is
65536." maxlen, the first parameter, cannot be omitted as the method
signature indicates. This hasn't changed ever since it was first
implemented.
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@56499 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
buffer fullness is more robust to detect the message is too big for
the buffer.
AIX 7.1 recvmsg doesn't set MSG_TRUNC for rflags when MSG_PEEK is
given.
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@52673 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
of clen must be nil.
* ext/socket/ancdata.c (bsock_sendmsg_internal): handle nil of clen.
fixes test errors introduced at r52602.
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@52610 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
* ext/socket/socket.c (sock_connect_nonblock):
avoid argument parsing in C.
[ruby-core:71439] [Feature #11339]
* ext/socket/lib/socket.rb (Socket#connect_nonblock):
new wrapper for private method, move RDoc
target 0: a (ruby 2.3.0dev (2015-11-12 trunk 52540) [x86_64-linux])
target 1: b (ruby 2.3.0dev (2015-11-12 avoid-kwarg-capi 52540) [x86_64-linux]
-----------------------------------------------------------
connect_nonblock
require 'tempfile'
require 'socket'
require 'io/wait'
nr = 500000
Tempfile.create(%w(connect_nonblock .sock)) do |tmp|
path = tmp.path
File.unlink(path)
s = UNIXServer.new(path)
addr = Socket.sockaddr_un(path).freeze
nr.times do
c = Socket.new(Socket::AF_UNIX, Socket::SOCK_STREAM)
while c.connect_nonblock(addr, exception: false) == :wait_writable
c.wait_writable
end
s.accept.close
c.close
end
end
-----------------------------------------------------------
raw data:
[["connect_nonblock",
[[4.014209181070328,
3.8479955345392227,
3.981342639774084,
4.471840236335993,
3.7867715656757355],
[3.639054525643587,
3.58337214961648,
3.525284394621849,
3.52646067738533,
3.511393066495657]]]]
Elapsed time: 37.889623996 (sec)
-----------------------------------------------------------
benchmark results:
minimum results in each 5 measurements.
Execution time (sec)
name a b
connect_nonblock 3.787 3.511
Speedup ratio: compare with the result of `a' (greater is better)
name b
connect_nonblock 1.078
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@52600 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
* ext/socket/init.c (rsock_s_recvfrom_nonblock):
avoid arg parsing with C API
[ruby-core:71439] [Feature #11339]
* ext/socket/basicsocket.c (bsock_recv_nonblock):
adjust for above change, make private
* ext/socket/socket.c (sock_recvfrom_nonblock): ditto
* ext/socket/udpsocket.c (udp_recvfrom_nonblock): ditto
* ext/socket/lib/socket.rb (BasicSocket#recv_nonblock):
new wrapper for private method, move RDoc
(Socket#recvfrom_nonblock): ditto
(UDPSocket#recvfrom_nonblock): ditto
Note, not adding bm_recv_nonblock.rb to benchmark/ directory
since it is non-portable. It is only in this commit message.
Benchmark results + code
target 0: a (ruby 2.3.0dev (2015-11-12 trunk 52540) [x86_64-linux])
target 1: b (ruby 2.3.0dev (2015-11-12 avoid-kwarg-capi 52540) [x86_64-linux]
-----------------------------------------------------------
recv_nonblock
require 'socket'
nr = 1000000
msg = 'hello world'
buf = ''
size = msg.bytesize
UNIXSocket.pair(:SEQPACKET) do |a, b|
nr.times do
a.sendmsg(msg)
b.recv_nonblock(size, 0, buf, exception: false)
end
end
-----------------------------------------------------------
raw data:
[["recv_nonblock",
[[1.83511221408844,
1.8703329525887966,
1.8448856547474861,
1.859263762831688,
1.8331583738327026],
[1.5637447573244572,
1.4062932096421719,
1.4247371144592762,
1.4108827747404575,
1.4802536629140377]]]]
Elapsed time: 16.530452496 (sec)
-----------------------------------------------------------
benchmark results:
minimum results in each 5 measurements.
Execution time (sec)
name a b
recv_nonblock 1.833 1.406
Speedup ratio: compare with the result of `a' (greater is better)
name b
recv_nonblock 1.304
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@52598 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
In case a process encounters high-numbered FDs, this allows
consistent performance on systems with ppoll support.
[ruby-core:35572]
* ext/socket/lib/socket.rb (connect_nonblock): use IO#wait_writable
* lib/drb/drb.rb (DRB::DRbTCPSocket#alive?): use IO#wait_readable
* lib/webrick/httpserver.rb (run): ditto
* lib/resolv.rb (request): ditto for single socket case
[ruby-core:68943] [Feature #11081]
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@50432 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
Errno::EISCONN and IO::WaitReadable exceptions are common,
expensive, and noisy under normal use. Avoid raising on them
since they are not exceptional.
* ext/socket/lib/socket.rb (connect_internal): avoid common exceptions
from connect_nonblock. [ruby-core:68909]
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@50363 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
it may be set before the body.
Reported by ko1 and mrkn. [ruby-core:59088] [Bug #9247]
* lib/cgi/core.rb: Ditto.
* lib/drb/ssl.rb: Ditto.
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@44184 b2dd03c8-39d4-4d8f-98ff-823fe69b080e