Build the fast fallback of TCPSocket.new on mswin

Every piece the Happy Eyeballs Version 2 path needed from pthread, pipe(2) and select(2) now has a Windows counterpart, so a host that answers with both families no longer waits for the IPv6 attempt to time out there. MinGW stays out, since its pthread emulation was never tested against this path, and its tests are omitted instead of passing by connecting sequentially.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hiroshi SHIBATA 2026-09-13 14:39:35 +09:00
parent d81710bca7
commit 30fd927272
Notes: git 2026-11-06 01:45:09 +00:00
5 changed files with 56 additions and 24 deletions

12
NEWS.md
View File

@ -466,17 +466,23 @@ Ruby 4.0 bundled RubyGems and Bundler version 4. see the following links for det
* Socket
* `TCPSocket.new` now connects through Happy Eyeballs Version 2 on
mswin as well, so a host that answers with both an IPv6 and an IPv4
address no longer waits for the IPv6 attempt to time out. When it
resolves the IPv6 and IPv4 addresses in parallel, the hostname
resolution, which used to ignore `resolv_timeout` and `open_timeout`
there, now raises `IO::TimeoutError` once the timeout expires.
* On Windows, a connection that is refused or unreachable now raises the
matching `Errno` class as soon as Winsock reports it, instead of
`Errno::ETIMEDOUT` once the whole `connect_timeout` has passed. Code
rescuing `Errno::ETIMEDOUT` there has to rescue the real error instead.
[[Bug #18661]]
* On Windows, `BasicSocket#getsockopt(:SOCKET, :ERROR)` now reports an
errno as it does on the other platforms, instead of the raw WinSock
error code. Code comparing it with a `WSAE*` value has to compare it
with the matching `Errno::*::Errno` instead.
[[Bug #18661]]
with the matching `Errno::*::Errno` instead. [[Bug #18661]]
## C API updates

View File

@ -420,7 +420,9 @@ char *raddrinfo_port_str(VALUE port, char *pbuf, size_t pbuflen, int *flags_ptr)
int raddrinfo_thread_create(void *(*start_routine) (void *), void *arg);
#ifndef FAST_FALLBACK_INIT_INETSOCK_IMPL
# if !defined(HAVE_PTHREAD_CREATE) || !defined(HAVE_PTHREAD_DETACH) || defined(__MINGW32__) || defined(__MINGW64__)
/* _WIN32 spawns the resolution threads with _beginthreadex(3) instead. */
# if (!defined(_WIN32) && (!defined(HAVE_PTHREAD_CREATE) || !defined(HAVE_PTHREAD_DETACH))) || \
defined(__MINGW32__) || defined(__MINGW64__)
# define FAST_FALLBACK_INIT_INETSOCK_IMPL 0
# else
# include "ruby/thread_native.h"

View File

@ -1738,7 +1738,8 @@ socket_s_ip_address_list(VALUE self)
* 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.)
* (Note: Happy Eyeballs Version 2 is not provided when using TCPSocket.new on MinGW builds of Windows.
* On mswin builds of Windows, it is provided starting from Ruby 4.1.)
*
* If false, Happy Eyeballs Version 2 is disabled.
*
@ -1757,7 +1758,8 @@ VALUE socket_s_tcp_fast_fallback(VALUE self) {
* 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.)
* (Note: This feature is not available when using TCPSocket.new on MinGW builds of Windows.
* On mswin builds of Windows, it is available starting from Ruby 4.1.)
*
* When set to false, the behavior reverts to that of Ruby 3.3 or earlier.
*

View File

@ -20,7 +20,8 @@
*
* Starting from Ruby 3.4, this method operates according to the
* Happy Eyeballs Version 2 ({RFC 8305}[https://datatracker.ietf.org/doc/html/rfc8305])
* algorithm by default, except on Windows.
* algorithm by default, except on MinGW builds of Windows.
* On mswin builds of Windows, it does so starting from Ruby 4.1.
*
* For details on Happy Eyeballs Version 2,
* see {Socket.tcp_fast_fallback=}[rdoc-ref:Socket.tcp_fast_fallback=].
@ -30,7 +31,7 @@
* 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,
* When using TCPSocket.new on MinGW builds of 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.

View File

@ -74,8 +74,26 @@ class TestSocket_TCPSocket < Test::Unit::TestCase
end
end
def test_tcp_initialize_resolv_timeout
omit_unless_fast_fallback
server = TCPServer.new("127.0.0.1", 0)
port = server.connect_address.ip_port
server.close
assert_raise(IO::TimeoutError) do
TCPSocket.new(
"localhost",
port,
resolv_timeout: 0.01,
fast_fallback: true,
test_mode_settings: { delay: { ipv6: 1000, ipv4: 1000 } }
)
end
end
def test_tcp_initialize_open_timeout
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
omit_unless_fast_fallback
server = TCPServer.new("127.0.0.1", 0)
port = server.connect_address.ip_port
@ -180,7 +198,7 @@ class TestSocket_TCPSocket < Test::Unit::TestCase
end
def test_initialize_v6_hostname_resolved_earlier
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
omit_unless_fast_fallback
begin
# Verify that "localhost" can be resolved to an IPv6 address
@ -209,7 +227,7 @@ class TestSocket_TCPSocket < Test::Unit::TestCase
end
def test_initialize_v4_hostname_resolved_earlier
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
omit_unless_fast_fallback
server = TCPServer.new("127.0.0.1", 0)
port = server.addr[1]
@ -253,7 +271,7 @@ class TestSocket_TCPSocket < Test::Unit::TestCase
end
def test_initialize_v6_hostname_resolved_in_resolution_delay
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
omit_unless_fast_fallback
begin
# Verify that "localhost" can be resolved to an IPv6 address
@ -287,7 +305,7 @@ class TestSocket_TCPSocket < Test::Unit::TestCase
end
def test_initialize_v6_hostname_resolved_earlier_and_v6_server_is_not_listening
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
omit_unless_fast_fallback
ipv4_address = "127.0.0.1"
server = Socket.new(Socket::AF_INET, :STREAM)
@ -315,7 +333,7 @@ class TestSocket_TCPSocket < Test::Unit::TestCase
end
def test_initialize_v6_hostname_resolved_later_and_v6_server_is_not_listening
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
omit_unless_fast_fallback
server = Socket.new(Socket::AF_INET, :STREAM)
server.bind(Socket.pack_sockaddr_in(0, "127.0.0.1"))
@ -342,7 +360,7 @@ class TestSocket_TCPSocket < Test::Unit::TestCase
end
def test_initialize_v6_hostname_resolution_failed_and_v4_hostname_resolution_is_success
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
omit_unless_fast_fallback
server = TCPServer.new("127.0.0.1", 0)
port = server.addr[1]
@ -386,7 +404,12 @@ class TestSocket_TCPSocket < Test::Unit::TestCase
end
def test_initialize_resolv_timeout_with_connection_failure
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
omit_unless_fast_fallback
if RUBY_PLATFORM =~ /mswin|cygwin/
omit "Windows reports a refused loopback connection two seconds after " \
"connect(2), so the IPv6 attempt is still in flight when " \
"resolv_timeout expires"
end
begin
server = TCPServer.new("::1", 0)
@ -409,7 +432,7 @@ class TestSocket_TCPSocket < Test::Unit::TestCase
end
def test_initialize_with_hostname_resolution_failure_after_connection_failure
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
omit_unless_fast_fallback
begin
server = TCPServer.new("::1", 0)
@ -431,7 +454,7 @@ class TestSocket_TCPSocket < Test::Unit::TestCase
end
def test_initialize_with_connection_failure_after_hostname_resolution_failure
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
omit_unless_fast_fallback
server = TCPServer.new("127.0.0.1", 0)
port = server.connect_address.ip_port
@ -463,8 +486,6 @@ class TestSocket_TCPSocket < Test::Unit::TestCase
end
def test_initialize_v6_connected_socket_with_v6_address
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
begin
server = TCPServer.new("::1", 0)
rescue Errno::EADDRNOTAVAIL # IPv6 is not supported
@ -485,8 +506,6 @@ class TestSocket_TCPSocket < Test::Unit::TestCase
end
def test_initialize_v4_connected_socket_with_v4_address
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
server = TCPServer.new("127.0.0.1", 0)
accepted = nil
server_thread = Thread.new { accepted = server.accept }
@ -502,8 +521,6 @@ class TestSocket_TCPSocket < Test::Unit::TestCase
end
def test_initialize_fast_fallback_is_false
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
server = TCPServer.new("127.0.0.1", 0)
_, port, = server.addr
accepted = nil
@ -520,6 +537,10 @@ class TestSocket_TCPSocket < Test::Unit::TestCase
private
def omit_unless_fast_fallback
omit "TCPSocket.new does not provide Happy Eyeballs Version 2 on MinGW" if RUBY_PLATFORM =~ /mingw/
end
# On success, wait for the accept thread to finish. If the test failed
# before the client connected, `server.accept` never returns, so kill
# the thread instead of waiting for it.