Skip to content

Commit ff5fc4b

Browse files
authored
Do not save the last error without sockets in the connection attempt (ruby#12153)
* Do not save the last_error if there are no sockets waiting to be connected In this implementation, the results of both name resolution and connection attempts are awaited using select(2). When it returned, the implementation attempted to check for connections even if there were no sockets currently attempting to connect, treating the absence of connected sockets as a connection failure. With this fix, it will no longer check for connections when there are no sockets waiting to be connected. Additionally, the following minor fixes have been made: * Handle failure of getsockopt(2) and removed unnecessary continue in the loop * Tweak: Use common API to check in_progress_fds * Safely call TCPServer.new in test * Set empty writefds when there is no socket waiting to be connected * Enable fast_fallback option
1 parent b305df8 commit ff5fc4b

File tree

3 files changed

+83
-56
lines changed

3 files changed

+83
-56
lines changed

ext/socket/ipsocket.c

Lines changed: 62 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,7 @@ init_fast_fallback_inetsock_internal(VALUE v)
574574
struct wait_fast_fallback_arg wait_arg;
575575
struct timeval *ends_at = NULL;
576576
struct timeval delay = (struct timeval){ -1, -1 };
577-
wait_arg.writefds = NULL;
577+
wait_arg.writefds = &writefds;
578578
wait_arg.status = 0;
579579

580580
struct hostname_resolution_store resolution_store;
@@ -859,7 +859,6 @@ init_fast_fallback_inetsock_internal(VALUE v)
859859
}
860860
arg->connection_attempt_fds[arg->connection_attempt_fds_size] = fd;
861861
(arg->connection_attempt_fds_size)++;
862-
wait_arg.writefds = &writefds;
863862

864863
set_timeout_tv(&connection_attempt_delay_strage, 250, now);
865864
connection_attempt_delay_expires_at = &connection_attempt_delay_strage;
@@ -922,8 +921,8 @@ init_fast_fallback_inetsock_internal(VALUE v)
922921
}
923922

924923
wait_arg.nfds = 0;
925-
if (arg->connection_attempt_fds_size) {
926-
FD_ZERO(wait_arg.writefds);
924+
FD_ZERO(wait_arg.writefds);
925+
if (in_progress_fds(arg->connection_attempt_fds_size)) {
927926
int n = 0;
928927
for (int i = 0; i < arg->connection_attempt_fds_size; i++) {
929928
int cfd = arg->connection_attempt_fds[i];
@@ -933,8 +932,6 @@ init_fast_fallback_inetsock_internal(VALUE v)
933932
}
934933
if (n > 0) n++;
935934
wait_arg.nfds = n;
936-
} else {
937-
wait_arg.writefds = NULL;
938935
}
939936

940937
FD_ZERO(wait_arg.readfds);
@@ -967,14 +964,39 @@ init_fast_fallback_inetsock_internal(VALUE v)
967964

968965
if (status > 0) {
969966
/* check for connection */
970-
for (int i = 0; i < arg->connection_attempt_fds_size; i++) {
971-
int fd = arg->connection_attempt_fds[i];
972-
if (fd < 0 || !FD_ISSET(fd, wait_arg.writefds)) continue;
967+
if (in_progress_fds(arg->connection_attempt_fds_size)) {
968+
for (int i = 0; i < arg->connection_attempt_fds_size; i++) {
969+
int fd = arg->connection_attempt_fds[i];
970+
if (fd < 0 || !FD_ISSET(fd, wait_arg.writefds)) continue;
971+
972+
int err;
973+
socklen_t len = sizeof(err);
974+
975+
status = getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &len);
976+
977+
if (status < 0) {
978+
last_error.type = SYSCALL_ERROR;
979+
last_error.ecode = errno;
980+
close(fd);
973981

974-
int err;
975-
socklen_t len = sizeof(err);
982+
if (any_addrinfos(&resolution_store)) continue;
983+
if (in_progress_fds(arg->connection_attempt_fds_size)) break;
984+
if (!resolution_store.is_all_finised) break;
985+
986+
if (local_status < 0) {
987+
host = arg->local.host;
988+
serv = arg->local.serv;
989+
} else {
990+
host = arg->remote.host;
991+
serv = arg->remote.serv;
992+
}
993+
if (last_error.type == RESOLUTION_ERROR) {
994+
rsock_raise_resolution_error(syscall, last_error.ecode);
995+
} else {
996+
rsock_syserr_fail_host_port(last_error.ecode, syscall, host, serv);
997+
}
998+
}
976999

977-
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &len) == 0) {
9781000
if (err == 0) { /* success */
9791001
remove_connection_attempt_fd(
9801002
arg->connection_attempt_fds,
@@ -983,42 +1005,39 @@ init_fast_fallback_inetsock_internal(VALUE v)
9831005
);
9841006
connected_fd = fd;
9851007
break;
986-
};
987-
988-
/* fail */
989-
errno = err;
990-
close(fd);
991-
remove_connection_attempt_fd(
992-
arg->connection_attempt_fds,
993-
&arg->connection_attempt_fds_size,
994-
fd
995-
);
996-
continue;
1008+
} else { /* fail */
1009+
close(fd);
1010+
remove_connection_attempt_fd(
1011+
arg->connection_attempt_fds,
1012+
&arg->connection_attempt_fds_size,
1013+
fd
1014+
);
1015+
last_error.type = SYSCALL_ERROR;
1016+
last_error.ecode = err;
1017+
}
9971018
}
998-
}
9991019

1000-
if (connected_fd >= 0) break;
1001-
last_error.type = SYSCALL_ERROR;
1002-
last_error.ecode = errno;
1020+
if (connected_fd >= 0) break;
10031021

1004-
if (!in_progress_fds(arg->connection_attempt_fds_size)) {
1005-
if (any_addrinfos(&resolution_store)) {
1006-
connection_attempt_delay_expires_at = NULL;
1007-
} else if (resolution_store.is_all_finised) {
1008-
if (local_status < 0) {
1009-
host = arg->local.host;
1010-
serv = arg->local.serv;
1011-
} else {
1012-
host = arg->remote.host;
1013-
serv = arg->remote.serv;
1014-
}
1015-
if (last_error.type == RESOLUTION_ERROR) {
1016-
rsock_raise_resolution_error(syscall, last_error.ecode);
1017-
} else {
1018-
rsock_syserr_fail_host_port(last_error.ecode, syscall, host, serv);
1022+
if (!in_progress_fds(arg->connection_attempt_fds_size)) {
1023+
if (any_addrinfos(&resolution_store)) {
1024+
connection_attempt_delay_expires_at = NULL;
1025+
} else if (resolution_store.is_all_finised) {
1026+
if (local_status < 0) {
1027+
host = arg->local.host;
1028+
serv = arg->local.serv;
1029+
} else {
1030+
host = arg->remote.host;
1031+
serv = arg->remote.serv;
1032+
}
1033+
if (last_error.type == RESOLUTION_ERROR) {
1034+
rsock_raise_resolution_error(syscall, last_error.ecode);
1035+
} else {
1036+
rsock_syserr_fail_host_port(last_error.ecode, syscall, host, serv);
1037+
}
10191038
}
1039+
user_specified_connect_timeout_at = NULL;
10201040
}
1021-
user_specified_connect_timeout_at = NULL;
10221041
}
10231042

10241043
/* check for hostname resolution */

ext/socket/tcpsocket.c

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,17 @@
2020
*
2121
* Starting from Ruby 3.4, this method operates according to the
2222
* Happy Eyeballs Version 2 ({RFC 8305}[https://datatracker.ietf.org/doc/html/rfc8305])
23-
* algorithm with *fast_fallback:true+, except on Windows.
23+
* algorithm by default, except on Windows.
24+
*
25+
* To make it behave the same as in Ruby 3.3 and earlier,
26+
* explicitly specify the option +fast_fallback:false+.
2427
*
2528
* Happy Eyeballs Version 2 is not provided on Windows,
2629
* and it behaves the same as in Ruby 3.3 and earlier.
2730
*
2831
* [:resolv_timeout] Specifies the timeout in seconds from when the hostname resolution starts.
2932
* [:connect_timeout] This method sequentially attempts connecting to all candidate destination addresses.<br>The +connect_timeout+ specifies the timeout in seconds from the start of the connection attempt to the last candidate.<br>By default, all connection attempts continue until the timeout occurs.<br>When +fast_fallback:false+ is explicitly specified,<br>a timeout is set for each connection attempt and any connection attempt that exceeds its timeout will be canceled.
30-
* [:fast_fallback] Enables the Happy Eyeballs Version 2 algorithm (disabled by default).
33+
* [:fast_fallback] Enables the Happy Eyeballs Version 2 algorithm (enabled by default).
3134
*
3235
* === Happy Eyeballs Version 2
3336
* Happy Eyeballs Version 2 ({RFC 8305}[https://datatracker.ietf.org/doc/html/rfc8305])
@@ -57,7 +60,7 @@ tcp_init(int argc, VALUE *argv, VALUE sock)
5760
VALUE kwargs[4];
5861
VALUE resolv_timeout = Qnil;
5962
VALUE connect_timeout = Qnil;
60-
VALUE fast_fallback = Qfalse;
63+
VALUE fast_fallback = Qtrue;
6164
VALUE test_mode_settings = Qnil;
6265

6366
if (!keyword_ids[0]) {

test/socket/test_tcp.rb

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ def test_accept_multithread
142142
end
143143

144144
def test_initialize_v6_hostname_resolved_earlier
145-
pend "to suppress the output of test failure logs in CI temporarily"
145+
# pend "to suppress the output of test failure logs in CI temporarily"
146146
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
147147

148148
begin
@@ -167,7 +167,7 @@ def test_initialize_v6_hostname_resolved_earlier
167167
end
168168

169169
def test_initialize_v4_hostname_resolved_earlier
170-
pend "to suppress the output of test failure logs in CI temporarily"
170+
# pend "to suppress the output of test failure logs in CI temporarily"
171171
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
172172

173173
server = TCPServer.new("127.0.0.1", 0)
@@ -188,7 +188,7 @@ def test_initialize_v4_hostname_resolved_earlier
188188
end
189189

190190
def test_initialize_v6_hostname_resolved_in_resolution_delay
191-
pend "to suppress the output of test failure logs in CI temporarily"
191+
# pend "to suppress the output of test failure logs in CI temporarily"
192192
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
193193

194194
begin
@@ -214,7 +214,7 @@ def test_initialize_v6_hostname_resolved_in_resolution_delay
214214
end
215215

216216
def test_initialize_v6_hostname_resolved_earlier_and_v6_server_is_not_listening
217-
pend "to suppress the output of test failure logs in CI temporarily"
217+
# pend "to suppress the output of test failure logs in CI temporarily"
218218
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
219219

220220
ipv4_address = "127.0.0.1"
@@ -237,7 +237,7 @@ def test_initialize_v6_hostname_resolved_earlier_and_v6_server_is_not_listening
237237
end
238238

239239
def test_initialize_v6_hostname_resolved_later_and_v6_server_is_not_listening
240-
pend "to suppress the output of test failure logs in CI temporarily"
240+
# pend "to suppress the output of test failure logs in CI temporarily"
241241
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
242242

243243
ipv4_server = Socket.new(Socket::AF_INET, :STREAM)
@@ -263,7 +263,7 @@ def test_initialize_v6_hostname_resolved_later_and_v6_server_is_not_listening
263263
end
264264

265265
def test_initialize_v6_hostname_resolution_failed_and_v4_hostname_resolution_is_success
266-
pend "to suppress the output of test failure logs in CI temporarily"
266+
# pend "to suppress the output of test failure logs in CI temporarily"
267267
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
268268

269269
server = TCPServer.new("127.0.0.1", 0)
@@ -284,10 +284,15 @@ def test_initialize_v6_hostname_resolution_failed_and_v4_hostname_resolution_is_
284284
end
285285

286286
def test_initialize_resolv_timeout_with_connection_failure
287-
pend "to suppress the output of test failure logs in CI temporarily"
287+
# pend "to suppress the output of test failure logs in CI temporarily"
288288
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
289289

290-
server = TCPServer.new("::1", 0)
290+
begin
291+
server = TCPServer.new("::1", 0)
292+
rescue Errno::EADDRNOTAVAIL # IPv6 is not supported
293+
exit
294+
end
295+
291296
port = server.connect_address.ip_port
292297
server.close
293298

@@ -303,7 +308,7 @@ def test_initialize_resolv_timeout_with_connection_failure
303308
end
304309

305310
def test_initialize_with_hostname_resolution_failure_after_connection_failure
306-
pend "to suppress the output of test failure logs in CI temporarily"
311+
# pend "to suppress the output of test failure logs in CI temporarily"
307312
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
308313

309314
begin
@@ -325,7 +330,7 @@ def test_initialize_with_hostname_resolution_failure_after_connection_failure
325330
end
326331

327332
def test_initialize_with_connection_failure_after_hostname_resolution_failure
328-
pend "to suppress the output of test failure logs in CI temporarily"
333+
# pend "to suppress the output of test failure logs in CI temporarily"
329334
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
330335

331336
server = TCPServer.new("127.0.0.1", 0)

0 commit comments

Comments
 (0)