Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 83 additions & 14 deletions ext/socket/ipsocket.c
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,17 @@ struct inetsock_arg
int type;
VALUE resolv_timeout;
VALUE connect_timeout;
VALUE open_timeout;
};

void
rsock_raise_user_specified_timeout()
{
VALUE errno_module = rb_const_get(rb_cObject, rb_intern("Errno"));
VALUE etimedout_error = rb_const_get(errno_module, rb_intern("ETIMEDOUT"));
rb_raise(etimedout_error, "user specified timeout");
}

static VALUE
inetsock_cleanup(VALUE v)
{
Expand All @@ -44,6 +53,13 @@ inetsock_cleanup(VALUE v)
return Qnil;
}

static VALUE
current_clocktime()
{
VALUE clock_monotnic_const = rb_const_get(rb_mProcess, rb_intern("CLOCK_MONOTONIC"));
return rb_funcall(rb_mProcess, rb_intern("clock_gettime"), 1, clock_monotnic_const);
}

static VALUE
init_inetsock_internal(VALUE v)
{
Expand All @@ -56,13 +72,18 @@ init_inetsock_internal(VALUE v)
const char *syscall = 0;
VALUE resolv_timeout = arg->resolv_timeout;
VALUE connect_timeout = arg->connect_timeout;
VALUE open_timeout = arg->open_timeout;
VALUE timeout;
VALUE starts_at;
unsigned int timeout_msec;

unsigned int t = NIL_P(resolv_timeout) ? 0 : rsock_value_timeout_to_msec(resolv_timeout);
timeout = NIL_P(open_timeout) ? resolv_timeout : open_timeout;
timeout_msec = NIL_P(timeout) ? 0 : rsock_value_timeout_to_msec(timeout);
starts_at = current_clocktime();

arg->remote.res = rsock_addrinfo(arg->remote.host, arg->remote.serv,
family, SOCK_STREAM,
(type == INET_SERVER) ? AI_PASSIVE : 0, t);

(type == INET_SERVER) ? AI_PASSIVE : 0, timeout_msec);

/*
* Maybe also accept a local address
Expand Down Expand Up @@ -125,8 +146,16 @@ init_inetsock_internal(VALUE v)
syscall = "bind(2)";
}

if (NIL_P(open_timeout)) {
timeout = connect_timeout;
} else {
VALUE elapsed = rb_funcall(current_clocktime(), '-', 1, starts_at);
timeout = rb_funcall(open_timeout, '-', 1, elapsed);
if (rb_funcall(timeout, '<', 1, INT2FIX(0)) == Qtrue) rsock_raise_user_specified_timeout();
}

if (status >= 0) {
status = rsock_connect(io, res->ai_addr, res->ai_addrlen, (type == INET_SOCKS), connect_timeout);
status = rsock_connect(io, res->ai_addr, res->ai_addrlen, (type == INET_SOCKS), timeout);
syscall = "connect(2)";
}
}
Expand Down Expand Up @@ -175,8 +204,16 @@ init_inetsock_internal(VALUE v)
#if FAST_FALLBACK_INIT_INETSOCK_IMPL == 0

VALUE
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)
{
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 open_timeout,
VALUE _fast_fallback, VALUE _test_mode_settings
) {
if (!NIL_P(open_timeout) && (!NIL_P(resolv_timeout) || !NIL_P(connect_timeout))) {
rb_raise(rb_eArgError, "Cannot specify open_timeout along with connect_timeout or resolv_timeout");
}

struct inetsock_arg arg;
arg.self = self;
arg.io = Qnil;
Expand All @@ -189,6 +226,7 @@ rsock_init_inetsock(VALUE self, VALUE remote_host, VALUE remote_serv, VALUE loca
arg.type = type;
arg.resolv_timeout = resolv_timeout;
arg.connect_timeout = connect_timeout;
arg.open_timeout = open_timeout;
return rb_ensure(init_inetsock_internal, (VALUE)&arg,
inetsock_cleanup, (VALUE)&arg);
}
Expand Down Expand Up @@ -224,6 +262,7 @@ struct fast_fallback_inetsock_arg
int type;
VALUE resolv_timeout;
VALUE connect_timeout;
VALUE open_timeout;

const char *hostp, *portp;
int *families;
Expand Down Expand Up @@ -383,12 +422,22 @@ select_expires_at(
struct timeval *resolution_delay,
struct timeval *connection_attempt_delay,
struct timeval *user_specified_resolv_timeout_at,
struct timeval *user_specified_connect_timeout_at
struct timeval *user_specified_connect_timeout_at,
struct timeval *user_specified_open_timeout_at
) {
if (any_addrinfos(resolution_store)) {
return resolution_delay ? resolution_delay : connection_attempt_delay;
struct timeval *delay;
delay = resolution_delay ? resolution_delay : connection_attempt_delay;

if (user_specified_open_timeout_at &&
timercmp(user_specified_open_timeout_at, delay, <)) {
return user_specified_open_timeout_at;
}
return delay;
}

if (user_specified_open_timeout_at) return user_specified_open_timeout_at;

struct timeval *timeout = NULL;

if (user_specified_resolv_timeout_at) {
Expand Down Expand Up @@ -506,6 +555,7 @@ init_fast_fallback_inetsock_internal(VALUE v)
VALUE io = arg->io;
VALUE resolv_timeout = arg->resolv_timeout;
VALUE connect_timeout = arg->connect_timeout;
VALUE open_timeout = arg->open_timeout;
VALUE test_mode_settings = arg->test_mode_settings;
struct addrinfo *remote_ai = NULL, *local_ai = NULL;
int connected_fd = -1, status = 0, local_status = 0;
Expand Down Expand Up @@ -552,8 +602,16 @@ init_fast_fallback_inetsock_internal(VALUE v)
struct timeval *user_specified_resolv_timeout_at = NULL;
struct timeval user_specified_connect_timeout_storage;
struct timeval *user_specified_connect_timeout_at = NULL;
struct timeval user_specified_open_timeout_storage;
struct timeval *user_specified_open_timeout_at = NULL;
struct timespec now = current_clocktime_ts();

if (!NIL_P(open_timeout)) {
struct timeval open_timeout_tv = rb_time_interval(open_timeout);
user_specified_open_timeout_storage = add_ts_to_tv(open_timeout_tv, now);
user_specified_open_timeout_at = &user_specified_open_timeout_storage;
}

/* start of hostname resolution */
if (arg->family_size == 1) {
arg->wait = -1;
Expand Down Expand Up @@ -854,7 +912,8 @@ init_fast_fallback_inetsock_internal(VALUE v)
resolution_delay_expires_at,
connection_attempt_delay_expires_at,
user_specified_resolv_timeout_at,
user_specified_connect_timeout_at
user_specified_connect_timeout_at,
user_specified_open_timeout_at
);
if (ends_at) {
delay = tv_to_timeout(ends_at, now);
Expand Down Expand Up @@ -1107,6 +1166,8 @@ init_fast_fallback_inetsock_internal(VALUE v)
}
}

if (is_timeout_tv(user_specified_open_timeout_at, now)) rsock_raise_user_specified_timeout();

if (!any_addrinfos(&resolution_store)) {
if (!in_progress_fds(arg->connection_attempt_fds_size) &&
resolution_store.is_all_finished) {
Expand All @@ -1128,9 +1189,7 @@ init_fast_fallback_inetsock_internal(VALUE v)
resolution_store.is_all_finished) &&
(is_timeout_tv(user_specified_connect_timeout_at, now) ||
!in_progress_fds(arg->connection_attempt_fds_size))) {
VALUE errno_module = rb_const_get(rb_cObject, rb_intern("Errno"));
VALUE etimedout_error = rb_const_get(errno_module, rb_intern("ETIMEDOUT"));
rb_raise(etimedout_error, "user specified timeout");
rsock_raise_user_specified_timeout();
}
}
}
Expand Down Expand Up @@ -1220,8 +1279,16 @@ fast_fallback_inetsock_cleanup(VALUE v)
}

VALUE
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)
{
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 open_timeout,
VALUE fast_fallback, VALUE test_mode_settings
) {
if (!NIL_P(open_timeout) && (!NIL_P(resolv_timeout) || !NIL_P(connect_timeout))) {
rb_raise(rb_eArgError, "Cannot specify open_timeout along with connect_timeout or resolv_timeout");
}

if (type == INET_CLIENT && FAST_FALLBACK_INIT_INETSOCK_IMPL == 1 && RTEST(fast_fallback)) {
struct rb_addrinfo *local_res = NULL;
char *hostp, *portp;
Expand Down Expand Up @@ -1278,6 +1345,7 @@ rsock_init_inetsock(VALUE self, VALUE remote_host, VALUE remote_serv, VALUE loca
fast_fallback_arg.type = type;
fast_fallback_arg.resolv_timeout = resolv_timeout;
fast_fallback_arg.connect_timeout = connect_timeout;
fast_fallback_arg.open_timeout = open_timeout;
fast_fallback_arg.hostp = hostp;
fast_fallback_arg.portp = portp;
fast_fallback_arg.additional_flags = additional_flags;
Expand Down Expand Up @@ -1314,6 +1382,7 @@ rsock_init_inetsock(VALUE self, VALUE remote_host, VALUE remote_serv, VALUE loca
arg.type = type;
arg.resolv_timeout = resolv_timeout;
arg.connect_timeout = connect_timeout;
arg.open_timeout = open_timeout;

return rb_ensure(init_inetsock_internal, (VALUE)&arg,
inetsock_cleanup, (VALUE)&arg);
Expand Down
2 changes: 1 addition & 1 deletion ext/socket/lib/socket.rb
Original file line number Diff line number Diff line change
Expand Up @@ -643,7 +643,7 @@ def accept_nonblock(exception: true)
#
# [:resolv_timeout] Specifies the timeout in seconds from when the hostname resolution starts.
# [: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.
# [: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.
# [: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.
# [:fast_fallback] Enables the Happy Eyeballs Version 2 algorithm (enabled by default).
#
# If a block is given, the block is called with the socket.
Expand Down
6 changes: 1 addition & 5 deletions ext/socket/raddrinfo.c
Original file line number Diff line number Diff line change
Expand Up @@ -562,11 +562,7 @@ rb_getaddrinfo(const char *hostp, const char *portp, const struct addrinfo *hint

if (need_free) free_getaddrinfo_arg(arg);

if (timedout) {
VALUE errno_module = rb_const_get(rb_cObject, rb_intern("Errno"));
VALUE etimedout_error = rb_const_get(errno_module, rb_intern("ETIMEDOUT"));
rb_raise(etimedout_error, "user specified timeout");
}
if (timedout) rsock_raise_user_specified_timeout();

// If the current thread is interrupted by asynchronous exception, the following raises the exception.
// But if the current thread is interrupted by timer thread, the following returns; we need to manually retry.
Expand Down
3 changes: 2 additions & 1 deletion ext/socket/rubysocket.h
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ int rsock_socket(int domain, int type, int proto);
int rsock_detect_cloexec(int fd);
VALUE rsock_init_sock(VALUE sock, int fd);
VALUE rsock_sock_s_socketpair(int argc, VALUE *argv, VALUE klass);
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);
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);
VALUE rsock_init_unixsock(VALUE sock, VALUE path, int server);

struct rsock_send_arg {
Expand Down Expand Up @@ -454,6 +454,7 @@ void free_fast_fallback_getaddrinfo_shared(struct fast_fallback_getaddrinfo_shar
#endif

unsigned int rsock_value_timeout_to_msec(VALUE);
NORETURN(void rsock_raise_user_specified_timeout(void));

void rsock_init_basicsocket(void);
void rsock_init_ipsocket(void);
Expand Down
2 changes: 1 addition & 1 deletion ext/socket/sockssocket.c
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ socks_init(VALUE sock, VALUE host, VALUE port)
init = 1;
}

return rsock_init_inetsock(sock, host, port, Qnil, Qnil, INET_SOCKS, Qnil, Qnil, Qfalse, Qnil);
return rsock_init_inetsock(sock, host, port, Qnil, Qnil, INET_SOCKS, Qnil, Qnil, Qnil, Qfalse, Qnil);
}

#ifdef SOCKS5
Expand Down
2 changes: 1 addition & 1 deletion ext/socket/tcpserver.c
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ tcp_svr_init(int argc, VALUE *argv, VALUE sock)
VALUE hostname, port;

rb_scan_args(argc, argv, "011", &hostname, &port);
return rsock_init_inetsock(sock, hostname, port, Qnil, Qnil, INET_SERVER, Qnil, Qnil, Qfalse, Qnil);
return rsock_init_inetsock(sock, hostname, port, Qnil, Qnil, INET_SERVER, Qnil, Qnil, Qnil, Qfalse, Qnil);
}

/*
Expand Down
22 changes: 13 additions & 9 deletions ext/socket/tcpsocket.c
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
*
* [:resolv_timeout] Specifies the timeout in seconds from when the hostname resolution starts.
* [: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.
* [: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.
* [:fast_fallback] Enables the Happy Eyeballs Version 2 algorithm (enabled by default).
*/
static VALUE
Expand All @@ -43,29 +44,32 @@ tcp_init(int argc, VALUE *argv, VALUE sock)
VALUE remote_host, remote_serv;
VALUE local_host, local_serv;
VALUE opt;
static ID keyword_ids[4];
VALUE kwargs[4];
static ID keyword_ids[5];
VALUE kwargs[5];
VALUE resolv_timeout = Qnil;
VALUE connect_timeout = Qnil;
VALUE open_timeout = Qnil;
VALUE fast_fallback = Qnil;
VALUE test_mode_settings = Qnil;

if (!keyword_ids[0]) {
CONST_ID(keyword_ids[0], "resolv_timeout");
CONST_ID(keyword_ids[1], "connect_timeout");
CONST_ID(keyword_ids[2], "fast_fallback");
CONST_ID(keyword_ids[3], "test_mode_settings");
CONST_ID(keyword_ids[2], "open_timeout");
CONST_ID(keyword_ids[3], "fast_fallback");
CONST_ID(keyword_ids[4], "test_mode_settings");
}

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

if (!NIL_P(opt)) {
rb_get_kwargs(opt, keyword_ids, 0, 4, kwargs);
rb_get_kwargs(opt, keyword_ids, 0, 5, kwargs);
if (kwargs[0] != Qundef) { resolv_timeout = kwargs[0]; }
if (kwargs[1] != Qundef) { connect_timeout = kwargs[1]; }
if (kwargs[2] != Qundef) { fast_fallback = kwargs[2]; }
if (kwargs[3] != Qundef) { test_mode_settings = kwargs[3]; }
if (kwargs[2] != Qundef) { open_timeout = kwargs[2]; }
if (kwargs[3] != Qundef) { fast_fallback = kwargs[3]; }
if (kwargs[4] != Qundef) { test_mode_settings = kwargs[4]; }
}

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

return rsock_init_inetsock(sock, remote_host, remote_serv,
local_host, local_serv, INET_CLIENT,
resolv_timeout, connect_timeout, fast_fallback,
test_mode_settings);
resolv_timeout, connect_timeout, open_timeout,
fast_fallback, test_mode_settings);
}

static VALUE
Expand Down
24 changes: 24 additions & 0 deletions test/socket/test_tcp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,30 @@ def test_initialize_resolv_timeout
end
end

def test_tcp_initialize_open_timeout
return if RUBY_PLATFORM =~ /mswin|mingw|cygwin/

server = TCPServer.new("127.0.0.1", 0)
port = server.connect_address.ip_port
server.close

assert_raise(Errno::ETIMEDOUT) do
TCPSocket.new(
"localhost",
port,
open_timeout: 0.01,
fast_fallback: true,
test_mode_settings: { delay: { ipv4: 1000 } }
)
end
end

def test_initialize_open_timeout_with_other_timeouts
assert_raise(ArgumentError) do
TCPSocket.new("localhost", 12345, open_timeout: 0.01, resolv_timeout: 0.01)
end
end

def test_initialize_connect_timeout
assert_raise(IO::TimeoutError, Errno::ENETUNREACH, Errno::EACCES) do
TCPSocket.new("192.0.2.1", 80, connect_timeout: 0)
Expand Down
11 changes: 8 additions & 3 deletions tool/mk_builtin_loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -282,16 +282,21 @@ def generate_cexpr(ofile, lineno, line_file, body_lineno, text, locals, func_nam

# Avoid generating fetches of lvars we don't need. This is imperfect as it
# will match text inside strings or other false positives.
local_candidates = text.scan(/[a-zA-Z_][a-zA-Z0-9_]*/)
local_ptrs = []
local_candidates = text.gsub(/\bLOCAL_PTR\(\K[a-zA-Z_][a-zA-Z0-9_]*(?=\))/) {
local_ptrs << $&; ''
}.scan(/[a-zA-Z_][a-zA-Z0-9_]*/)

f.puts '{'
lineno += 1
# locals is nil outside methods
locals&.reverse_each&.with_index{|param, i|
next unless Symbol === param
next unless local_candidates.include?(param.to_s)
param = param.to_s
lvar = local_candidates.include?(param)
next unless lvar or local_ptrs.include?(param)
f.puts "VALUE *const #{param}__ptr = (VALUE *)&ec->cfp->ep[#{-3 - i}];"
f.puts "MAYBE_UNUSED(const VALUE) #{param} = *#{param}__ptr;"
f.puts "MAYBE_UNUSED(const VALUE) #{param} = *#{param}__ptr;" if lvar
lineno += 1
}
f.puts "#line #{body_lineno} \"#{line_file}\""
Expand Down