diff --git a/NEWS.md b/NEWS.md index e75aba8ff15..43db00b9e50 100644 --- a/NEWS.md +++ b/NEWS.md @@ -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 diff --git a/ext/socket/rubysocket.h b/ext/socket/rubysocket.h index d36a233d912..8691af25196 100644 --- a/ext/socket/rubysocket.h +++ b/ext/socket/rubysocket.h @@ -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" diff --git a/ext/socket/socket.c b/ext/socket/socket.c index e4125893453..97deba2e296 100644 --- a/ext/socket/socket.c +++ b/ext/socket/socket.c @@ -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. * diff --git a/ext/socket/tcpsocket.c b/ext/socket/tcpsocket.c index 5a8eb3ab111..468caf0a554 100644 --- a/ext/socket/tcpsocket.c +++ b/ext/socket/tcpsocket.c @@ -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. diff --git a/test/socket/test_tcp.rb b/test/socket/test_tcp.rb index adc8b4d9163..5d120a55bc3 100644 --- a/test/socket/test_tcp.rb +++ b/test/socket/test_tcp.rb @@ -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.