Skip to content

Commit ba49005

Browse files
authored
[Feature #21347] Add open_timeout as an overall timeout option for TCPSocket.new (#13909)
* [Feature #21347] Add `open_timeout` as an overall timeout option for `TCPSocket.new` With this change, `TCPSocket.new` now accepts the `open_timeout` option. This option raises an exception if the specified number of seconds has elapsed since the start of the method call, even if the operation is still in the middle of name resolution or connection attempts. The addition of this option follows the same intent as the previously merged change to `Socket.tcp`. [Feature #21347](https://bugs.ruby-lang.org/issues/21347) #13368 * Tidy up: Extract rsock_raise_user_specified_timeout() * Added a note to the documentation of `Socket.tcp` * Fix `rsock_init_inetsock` for `FAST_FALLBACK_INIT_INETSOCK_IMPL`
1 parent 98aa2a6 commit ba49005

File tree

8 files changed

+126
-32
lines changed

8 files changed

+126
-32
lines changed

ext/socket/ipsocket.c

Lines changed: 83 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,17 @@ struct inetsock_arg
2323
int type;
2424
VALUE resolv_timeout;
2525
VALUE connect_timeout;
26+
VALUE open_timeout;
2627
};
2728

29+
void
30+
rsock_raise_user_specified_timeout()
31+
{
32+
VALUE errno_module = rb_const_get(rb_cObject, rb_intern("Errno"));
33+
VALUE etimedout_error = rb_const_get(errno_module, rb_intern("ETIMEDOUT"));
34+
rb_raise(etimedout_error, "user specified timeout");
35+
}
36+
2837
static VALUE
2938
inetsock_cleanup(VALUE v)
3039
{
@@ -44,6 +53,13 @@ inetsock_cleanup(VALUE v)
4453
return Qnil;
4554
}
4655

56+
static VALUE
57+
current_clocktime()
58+
{
59+
VALUE clock_monotnic_const = rb_const_get(rb_mProcess, rb_intern("CLOCK_MONOTONIC"));
60+
return rb_funcall(rb_mProcess, rb_intern("clock_gettime"), 1, clock_monotnic_const);
61+
}
62+
4763
static VALUE
4864
init_inetsock_internal(VALUE v)
4965
{
@@ -56,13 +72,18 @@ init_inetsock_internal(VALUE v)
5672
const char *syscall = 0;
5773
VALUE resolv_timeout = arg->resolv_timeout;
5874
VALUE connect_timeout = arg->connect_timeout;
75+
VALUE open_timeout = arg->open_timeout;
76+
VALUE timeout;
77+
VALUE starts_at;
78+
unsigned int timeout_msec;
5979

60-
unsigned int t = NIL_P(resolv_timeout) ? 0 : rsock_value_timeout_to_msec(resolv_timeout);
80+
timeout = NIL_P(open_timeout) ? resolv_timeout : open_timeout;
81+
timeout_msec = NIL_P(timeout) ? 0 : rsock_value_timeout_to_msec(timeout);
82+
starts_at = current_clocktime();
6183

6284
arg->remote.res = rsock_addrinfo(arg->remote.host, arg->remote.serv,
6385
family, SOCK_STREAM,
64-
(type == INET_SERVER) ? AI_PASSIVE : 0, t);
65-
86+
(type == INET_SERVER) ? AI_PASSIVE : 0, timeout_msec);
6687

6788
/*
6889
* Maybe also accept a local address
@@ -125,8 +146,16 @@ init_inetsock_internal(VALUE v)
125146
syscall = "bind(2)";
126147
}
127148

149+
if (NIL_P(open_timeout)) {
150+
timeout = connect_timeout;
151+
} else {
152+
VALUE elapsed = rb_funcall(current_clocktime(), '-', 1, starts_at);
153+
timeout = rb_funcall(open_timeout, '-', 1, elapsed);
154+
if (rb_funcall(timeout, '<', 1, INT2FIX(0)) == Qtrue) rsock_raise_user_specified_timeout();
155+
}
156+
128157
if (status >= 0) {
129-
status = rsock_connect(io, res->ai_addr, res->ai_addrlen, (type == INET_SOCKS), connect_timeout);
158+
status = rsock_connect(io, res->ai_addr, res->ai_addrlen, (type == INET_SOCKS), timeout);
130159
syscall = "connect(2)";
131160
}
132161
}
@@ -175,8 +204,16 @@ init_inetsock_internal(VALUE v)
175204
#if FAST_FALLBACK_INIT_INETSOCK_IMPL == 0
176205

177206
VALUE
178-
rsock_init_inetsock(VALUE self, VALUE remote_host, VALUE remote_serv, VALUE local_host, VALUE local_serv, int type, VALUE resolv_timeout, VALUE connect_timeout, VALUE _fast_fallback, VALUE _test_mode_settings)
179-
{
207+
rsock_init_inetsock(
208+
VALUE self, VALUE remote_host, VALUE remote_serv,
209+
VALUE local_host, VALUE local_serv, int type,
210+
VALUE resolv_timeout, VALUE connect_timeout, VALUE open_timeout,
211+
VALUE _fast_fallback, VALUE _test_mode_settings
212+
) {
213+
if (!NIL_P(open_timeout) && (!NIL_P(resolv_timeout) || !NIL_P(connect_timeout))) {
214+
rb_raise(rb_eArgError, "Cannot specify open_timeout along with connect_timeout or resolv_timeout");
215+
}
216+
180217
struct inetsock_arg arg;
181218
arg.self = self;
182219
arg.io = Qnil;
@@ -189,6 +226,7 @@ rsock_init_inetsock(VALUE self, VALUE remote_host, VALUE remote_serv, VALUE loca
189226
arg.type = type;
190227
arg.resolv_timeout = resolv_timeout;
191228
arg.connect_timeout = connect_timeout;
229+
arg.open_timeout = open_timeout;
192230
return rb_ensure(init_inetsock_internal, (VALUE)&arg,
193231
inetsock_cleanup, (VALUE)&arg);
194232
}
@@ -224,6 +262,7 @@ struct fast_fallback_inetsock_arg
224262
int type;
225263
VALUE resolv_timeout;
226264
VALUE connect_timeout;
265+
VALUE open_timeout;
227266

228267
const char *hostp, *portp;
229268
int *families;
@@ -383,12 +422,22 @@ select_expires_at(
383422
struct timeval *resolution_delay,
384423
struct timeval *connection_attempt_delay,
385424
struct timeval *user_specified_resolv_timeout_at,
386-
struct timeval *user_specified_connect_timeout_at
425+
struct timeval *user_specified_connect_timeout_at,
426+
struct timeval *user_specified_open_timeout_at
387427
) {
388428
if (any_addrinfos(resolution_store)) {
389-
return resolution_delay ? resolution_delay : connection_attempt_delay;
429+
struct timeval *delay;
430+
delay = resolution_delay ? resolution_delay : connection_attempt_delay;
431+
432+
if (user_specified_open_timeout_at &&
433+
timercmp(user_specified_open_timeout_at, delay, <)) {
434+
return user_specified_open_timeout_at;
435+
}
436+
return delay;
390437
}
391438

439+
if (user_specified_open_timeout_at) return user_specified_open_timeout_at;
440+
392441
struct timeval *timeout = NULL;
393442

394443
if (user_specified_resolv_timeout_at) {
@@ -506,6 +555,7 @@ init_fast_fallback_inetsock_internal(VALUE v)
506555
VALUE io = arg->io;
507556
VALUE resolv_timeout = arg->resolv_timeout;
508557
VALUE connect_timeout = arg->connect_timeout;
558+
VALUE open_timeout = arg->open_timeout;
509559
VALUE test_mode_settings = arg->test_mode_settings;
510560
struct addrinfo *remote_ai = NULL, *local_ai = NULL;
511561
int connected_fd = -1, status = 0, local_status = 0;
@@ -552,8 +602,16 @@ init_fast_fallback_inetsock_internal(VALUE v)
552602
struct timeval *user_specified_resolv_timeout_at = NULL;
553603
struct timeval user_specified_connect_timeout_storage;
554604
struct timeval *user_specified_connect_timeout_at = NULL;
605+
struct timeval user_specified_open_timeout_storage;
606+
struct timeval *user_specified_open_timeout_at = NULL;
555607
struct timespec now = current_clocktime_ts();
556608

609+
if (!NIL_P(open_timeout)) {
610+
struct timeval open_timeout_tv = rb_time_interval(open_timeout);
611+
user_specified_open_timeout_storage = add_ts_to_tv(open_timeout_tv, now);
612+
user_specified_open_timeout_at = &user_specified_open_timeout_storage;
613+
}
614+
557615
/* start of hostname resolution */
558616
if (arg->family_size == 1) {
559617
arg->wait = -1;
@@ -854,7 +912,8 @@ init_fast_fallback_inetsock_internal(VALUE v)
854912
resolution_delay_expires_at,
855913
connection_attempt_delay_expires_at,
856914
user_specified_resolv_timeout_at,
857-
user_specified_connect_timeout_at
915+
user_specified_connect_timeout_at,
916+
user_specified_open_timeout_at
858917
);
859918
if (ends_at) {
860919
delay = tv_to_timeout(ends_at, now);
@@ -1107,6 +1166,8 @@ init_fast_fallback_inetsock_internal(VALUE v)
11071166
}
11081167
}
11091168

1169+
if (is_timeout_tv(user_specified_open_timeout_at, now)) rsock_raise_user_specified_timeout();
1170+
11101171
if (!any_addrinfos(&resolution_store)) {
11111172
if (!in_progress_fds(arg->connection_attempt_fds_size) &&
11121173
resolution_store.is_all_finished) {
@@ -1128,9 +1189,7 @@ init_fast_fallback_inetsock_internal(VALUE v)
11281189
resolution_store.is_all_finished) &&
11291190
(is_timeout_tv(user_specified_connect_timeout_at, now) ||
11301191
!in_progress_fds(arg->connection_attempt_fds_size))) {
1131-
VALUE errno_module = rb_const_get(rb_cObject, rb_intern("Errno"));
1132-
VALUE etimedout_error = rb_const_get(errno_module, rb_intern("ETIMEDOUT"));
1133-
rb_raise(etimedout_error, "user specified timeout");
1192+
rsock_raise_user_specified_timeout();
11341193
}
11351194
}
11361195
}
@@ -1220,8 +1279,16 @@ fast_fallback_inetsock_cleanup(VALUE v)
12201279
}
12211280

12221281
VALUE
1223-
rsock_init_inetsock(VALUE self, VALUE remote_host, VALUE remote_serv, VALUE local_host, VALUE local_serv, int type, VALUE resolv_timeout, VALUE connect_timeout, VALUE fast_fallback, VALUE test_mode_settings)
1224-
{
1282+
rsock_init_inetsock(
1283+
VALUE self, VALUE remote_host, VALUE remote_serv,
1284+
VALUE local_host, VALUE local_serv, int type,
1285+
VALUE resolv_timeout, VALUE connect_timeout, VALUE open_timeout,
1286+
VALUE fast_fallback, VALUE test_mode_settings
1287+
) {
1288+
if (!NIL_P(open_timeout) && (!NIL_P(resolv_timeout) || !NIL_P(connect_timeout))) {
1289+
rb_raise(rb_eArgError, "Cannot specify open_timeout along with connect_timeout or resolv_timeout");
1290+
}
1291+
12251292
if (type == INET_CLIENT && FAST_FALLBACK_INIT_INETSOCK_IMPL == 1 && RTEST(fast_fallback)) {
12261293
struct rb_addrinfo *local_res = NULL;
12271294
char *hostp, *portp;
@@ -1278,6 +1345,7 @@ rsock_init_inetsock(VALUE self, VALUE remote_host, VALUE remote_serv, VALUE loca
12781345
fast_fallback_arg.type = type;
12791346
fast_fallback_arg.resolv_timeout = resolv_timeout;
12801347
fast_fallback_arg.connect_timeout = connect_timeout;
1348+
fast_fallback_arg.open_timeout = open_timeout;
12811349
fast_fallback_arg.hostp = hostp;
12821350
fast_fallback_arg.portp = portp;
12831351
fast_fallback_arg.additional_flags = additional_flags;
@@ -1314,6 +1382,7 @@ rsock_init_inetsock(VALUE self, VALUE remote_host, VALUE remote_serv, VALUE loca
13141382
arg.type = type;
13151383
arg.resolv_timeout = resolv_timeout;
13161384
arg.connect_timeout = connect_timeout;
1385+
arg.open_timeout = open_timeout;
13171386

13181387
return rb_ensure(init_inetsock_internal, (VALUE)&arg,
13191388
inetsock_cleanup, (VALUE)&arg);

ext/socket/lib/socket.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -643,7 +643,7 @@ def accept_nonblock(exception: true)
643643
#
644644
# [:resolv_timeout] Specifies the timeout in seconds from when the hostname resolution starts.
645645
# [: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.
646-
# [:open_timeout] Specifies the timeout in seconds from the start of the method execution.<br>If this timeout is reached while there are still addresses that have not yet been attempted for connection, no further attempts will be made.
646+
# [:open_timeout] Specifies the timeout in seconds from the start of the method execution.<br>If this timeout is reached while there are still addresses that have not yet been attempted for connection, no further attempts will be made.<br>If this option is specified together with other timeout options, an +ArgumentError+ will be raised.
647647
# [:fast_fallback] Enables the Happy Eyeballs Version 2 algorithm (enabled by default).
648648
#
649649
# If a block is given, the block is called with the socket.

ext/socket/raddrinfo.c

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -562,11 +562,7 @@ rb_getaddrinfo(const char *hostp, const char *portp, const struct addrinfo *hint
562562

563563
if (need_free) free_getaddrinfo_arg(arg);
564564

565-
if (timedout) {
566-
VALUE errno_module = rb_const_get(rb_cObject, rb_intern("Errno"));
567-
VALUE etimedout_error = rb_const_get(errno_module, rb_intern("ETIMEDOUT"));
568-
rb_raise(etimedout_error, "user specified timeout");
569-
}
565+
if (timedout) rsock_raise_user_specified_timeout();
570566

571567
// If the current thread is interrupted by asynchronous exception, the following raises the exception.
572568
// But if the current thread is interrupted by timer thread, the following returns; we need to manually retry.

ext/socket/rubysocket.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ int rsock_socket(int domain, int type, int proto);
355355
int rsock_detect_cloexec(int fd);
356356
VALUE rsock_init_sock(VALUE sock, int fd);
357357
VALUE rsock_sock_s_socketpair(int argc, VALUE *argv, VALUE klass);
358-
VALUE rsock_init_inetsock(VALUE sock, VALUE remote_host, VALUE remote_serv, VALUE local_host, VALUE local_serv, int type, VALUE resolv_timeout, VALUE connect_timeout, VALUE fast_fallback, VALUE test_mode_settings);
358+
VALUE rsock_init_inetsock(VALUE sock, VALUE remote_host, VALUE remote_serv, VALUE local_host, VALUE local_serv, int type, VALUE resolv_timeout, VALUE connect_timeout, VALUE open_timeout, VALUE fast_fallback, VALUE test_mode_settings);
359359
VALUE rsock_init_unixsock(VALUE sock, VALUE path, int server);
360360

361361
struct rsock_send_arg {
@@ -454,6 +454,7 @@ void free_fast_fallback_getaddrinfo_shared(struct fast_fallback_getaddrinfo_shar
454454
#endif
455455

456456
unsigned int rsock_value_timeout_to_msec(VALUE);
457+
void rsock_raise_user_specified_timeout(void);
457458

458459
void rsock_init_basicsocket(void);
459460
void rsock_init_ipsocket(void);

ext/socket/sockssocket.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ socks_init(VALUE sock, VALUE host, VALUE port)
3535
init = 1;
3636
}
3737

38-
return rsock_init_inetsock(sock, host, port, Qnil, Qnil, INET_SOCKS, Qnil, Qnil, Qfalse, Qnil);
38+
return rsock_init_inetsock(sock, host, port, Qnil, Qnil, INET_SOCKS, Qnil, Qnil, Qnil, Qfalse, Qnil);
3939
}
4040

4141
#ifdef SOCKS5

ext/socket/tcpserver.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ tcp_svr_init(int argc, VALUE *argv, VALUE sock)
3636
VALUE hostname, port;
3737

3838
rb_scan_args(argc, argv, "011", &hostname, &port);
39-
return rsock_init_inetsock(sock, hostname, port, Qnil, Qnil, INET_SERVER, Qnil, Qnil, Qfalse, Qnil);
39+
return rsock_init_inetsock(sock, hostname, port, Qnil, Qnil, INET_SERVER, Qnil, Qnil, Qnil, Qfalse, Qnil);
4040
}
4141

4242
/*

ext/socket/tcpsocket.c

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
*
3636
* [:resolv_timeout] Specifies the timeout in seconds from when the hostname resolution starts.
3737
* [: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.
38+
* [:open_timeout] Specifies the timeout in seconds from the start of the method execution.<br>If this timeout is reached while there are still addresses that have not yet been attempted for connection, no further attempts will be made.<br>If this option is specified together with other timeout options, an +ArgumentError+ will be raised.
3839
* [:fast_fallback] Enables the Happy Eyeballs Version 2 algorithm (enabled by default).
3940
*/
4041
static VALUE
@@ -43,29 +44,32 @@ tcp_init(int argc, VALUE *argv, VALUE sock)
4344
VALUE remote_host, remote_serv;
4445
VALUE local_host, local_serv;
4546
VALUE opt;
46-
static ID keyword_ids[4];
47-
VALUE kwargs[4];
47+
static ID keyword_ids[5];
48+
VALUE kwargs[5];
4849
VALUE resolv_timeout = Qnil;
4950
VALUE connect_timeout = Qnil;
51+
VALUE open_timeout = Qnil;
5052
VALUE fast_fallback = Qnil;
5153
VALUE test_mode_settings = Qnil;
5254

5355
if (!keyword_ids[0]) {
5456
CONST_ID(keyword_ids[0], "resolv_timeout");
5557
CONST_ID(keyword_ids[1], "connect_timeout");
56-
CONST_ID(keyword_ids[2], "fast_fallback");
57-
CONST_ID(keyword_ids[3], "test_mode_settings");
58+
CONST_ID(keyword_ids[2], "open_timeout");
59+
CONST_ID(keyword_ids[3], "fast_fallback");
60+
CONST_ID(keyword_ids[4], "test_mode_settings");
5861
}
5962

6063
rb_scan_args(argc, argv, "22:", &remote_host, &remote_serv,
6164
&local_host, &local_serv, &opt);
6265

6366
if (!NIL_P(opt)) {
64-
rb_get_kwargs(opt, keyword_ids, 0, 4, kwargs);
67+
rb_get_kwargs(opt, keyword_ids, 0, 5, kwargs);
6568
if (kwargs[0] != Qundef) { resolv_timeout = kwargs[0]; }
6669
if (kwargs[1] != Qundef) { connect_timeout = kwargs[1]; }
67-
if (kwargs[2] != Qundef) { fast_fallback = kwargs[2]; }
68-
if (kwargs[3] != Qundef) { test_mode_settings = kwargs[3]; }
70+
if (kwargs[2] != Qundef) { open_timeout = kwargs[2]; }
71+
if (kwargs[3] != Qundef) { fast_fallback = kwargs[3]; }
72+
if (kwargs[4] != Qundef) { test_mode_settings = kwargs[4]; }
6973
}
7074

7175
if (fast_fallback == Qnil) {
@@ -75,8 +79,8 @@ tcp_init(int argc, VALUE *argv, VALUE sock)
7579

7680
return rsock_init_inetsock(sock, remote_host, remote_serv,
7781
local_host, local_serv, INET_CLIENT,
78-
resolv_timeout, connect_timeout, fast_fallback,
79-
test_mode_settings);
82+
resolv_timeout, connect_timeout, open_timeout,
83+
fast_fallback, test_mode_settings);
8084
}
8185

8286
static VALUE

test/socket/test_tcp.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,30 @@ def test_initialize_resolv_timeout
7373
end
7474
end
7575

76+
def test_tcp_initialize_open_timeout
77+
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/
78+
79+
server = TCPServer.new("127.0.0.1", 0)
80+
port = server.connect_address.ip_port
81+
server.close
82+
83+
assert_raise(Errno::ETIMEDOUT) do
84+
TCPSocket.new(
85+
"localhost",
86+
port,
87+
open_timeout: 0.01,
88+
fast_fallback: true,
89+
test_mode_settings: { delay: { ipv4: 1000 } }
90+
)
91+
end
92+
end
93+
94+
def test_initialize_open_timeout_with_other_timeouts
95+
assert_raise(ArgumentError) do
96+
TCPSocket.new("localhost", 12345, open_timeout: 0.01, resolv_timeout: 0.01)
97+
end
98+
end
99+
76100
def test_initialize_connect_timeout
77101
assert_raise(IO::TimeoutError, Errno::ENETUNREACH, Errno::EACCES) do
78102
TCPSocket.new("192.0.2.1", 80, connect_timeout: 0)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy