ruby-changes:73909
From: Samuel <ko1@a...>
Date: Fri, 7 Oct 2022 17:49:05 +0900 (JST)
Subject: [ruby-changes:73909] e4f91bbdba (master): Add IO#timeout attribute and use it for blocking IO operations. (#5653)
https://git.ruby-lang.org/ruby.git/commit/?id=e4f91bbdba From e4f91bbdbaa6ab3125f24967414ac5300bb244f5 Mon Sep 17 00:00:00 2001 From: Samuel Williams <samuel.williams@o...> Date: Fri, 7 Oct 2022 21:48:38 +1300 Subject: Add IO#timeout attribute and use it for blocking IO operations. (#5653) --- NEWS.md | 11 + ext/openssl/extconf.rb | 1 + ext/openssl/ossl_ssl.c | 14 +- ext/socket/ancdata.c | 4 +- ext/socket/basicsocket.c | 2 +- ext/socket/init.c | 4 +- ext/socket/socket.c | 4 + ext/socket/udpsocket.c | 2 +- gc.c | 1 + include/ruby/io.h | 50 +++- io.c | 336 ++++++++++++++++++----- scheduler.c | 4 +- spec/ruby/library/socket/tcpsocket/shared/new.rb | 10 +- test/ruby/test_io.rb | 3 + test/ruby/test_io_timeout.rb | 64 +++++ test/socket/test_tcp.rb | 2 +- thread.c | 11 + 17 files changed, 426 insertions(+), 97 deletions(-) create mode 100644 test/ruby/test_io_timeout.rb diff --git a/NEWS.md b/NEWS.md index d1b25f9444..eb1ffbe438 100644 --- a/NEWS.md +++ b/NEWS.md @@ -102,6 +102,16 @@ Note that each entry is kept to a minimum, see links for details. https://github.com/ruby/ruby/blob/trunk/NEWS.md#L102 Note: We're only listing outstanding class updates. +* IO + * Introduce `IO#timeout=` and `IO#timeout` which can cause + `IO::TimeoutError` to be raised if a blocking operation exceeds the + specified timeout. [[Feature #18630]] + + ```ruby + STDIN.timeout = 1 + STDIN.read # => Blocking operation timed out! (IO::TimeoutError) + ``` + * Data * New core class to represent simple immutable value object. The class is similar to `Struct` and partially shares an implementation, but has more @@ -332,3 +342,4 @@ The following deprecated APIs are removed. https://github.com/ruby/ruby/blob/trunk/NEWS.md#L342 [Feature #19008]: https://bugs.ruby-lang.org/issues/19008 [Feature #19026]: https://bugs.ruby-lang.org/issues/19026 [Feature #16122]: https://bugs.ruby-lang.org/issues/16122 +[Feature #18630]: https://bugs.ruby-lang.org/issues/18630 diff --git a/ext/openssl/extconf.rb b/ext/openssl/extconf.rb index cc2b1f8ba2..a856646fe5 100644 --- a/ext/openssl/extconf.rb +++ b/ext/openssl/extconf.rb @@ -27,6 +27,7 @@ if with_config("debug") or enable_config("debug") https://github.com/ruby/ruby/blob/trunk/ext/openssl/extconf.rb#L27 end have_func("rb_io_maybe_wait") # Ruby 3.1 +have_func("rb_io_timeout") # Ruby 3.2 Logging::message "=== Checking for system dependent stuff... ===\n" have_library("nsl", "t_open") diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index 6e1a50fd6d..605591efe5 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -1641,11 +1641,21 @@ no_exception_p(VALUE opts) https://github.com/ruby/ruby/blob/trunk/ext/openssl/ossl_ssl.c#L1641 return 0; } +inline static +VALUE io_timeout() +{ +#ifdef HAVE_RB_IO_TIMEOUT + return Qundef; +#else + return Qnil; +#endif +} + static void io_wait_writable(rb_io_t *fptr) { #ifdef HAVE_RB_IO_MAYBE_WAIT - rb_io_maybe_wait_writable(errno, fptr->self, Qnil); + rb_io_maybe_wait_writable(errno, fptr->self, io_timeout()); #else rb_io_wait_writable(fptr->fd); #endif @@ -1655,7 +1665,7 @@ static void https://github.com/ruby/ruby/blob/trunk/ext/openssl/ossl_ssl.c#L1665 io_wait_readable(rb_io_t *fptr) { #ifdef HAVE_RB_IO_MAYBE_WAIT - rb_io_maybe_wait_readable(errno, fptr->self, Qnil); + rb_io_maybe_wait_readable(errno, fptr->self, io_timeout()); #else rb_io_wait_readable(fptr->fd); #endif diff --git a/ext/socket/ancdata.c b/ext/socket/ancdata.c index 071e3323bb..0ab3a8da47 100644 --- a/ext/socket/ancdata.c +++ b/ext/socket/ancdata.c @@ -1285,7 +1285,7 @@ bsock_sendmsg_internal(VALUE sock, VALUE data, VALUE vflags, https://github.com/ruby/ruby/blob/trunk/ext/socket/ancdata.c#L1285 if (ss == -1) { int e; - if (!nonblock && rb_io_maybe_wait_writable(errno, fptr->self, Qnil)) { + if (!nonblock && rb_io_maybe_wait_writable(errno, fptr->self, fptr->timeout)) { rb_io_check_closed(fptr); goto retry; } @@ -1557,7 +1557,7 @@ bsock_recvmsg_internal(VALUE sock, https://github.com/ruby/ruby/blob/trunk/ext/socket/ancdata.c#L1557 if (ss == -1) { int e; - if (!nonblock && rb_io_maybe_wait_readable(errno, fptr->self, Qnil)) { + if (!nonblock && rb_io_maybe_wait_readable(errno, fptr->self, fptr->timeout)) { rb_io_check_closed(fptr); goto retry; } diff --git a/ext/socket/basicsocket.c b/ext/socket/basicsocket.c index 93196c924d..66c2537cbb 100644 --- a/ext/socket/basicsocket.c +++ b/ext/socket/basicsocket.c @@ -601,7 +601,7 @@ rsock_bsock_send(int argc, VALUE *argv, VALUE socket) https://github.com/ruby/ruby/blob/trunk/ext/socket/basicsocket.c#L601 if (n >= 0) return SSIZET2NUM(n); - if (rb_io_maybe_wait_writable(errno, socket, Qnil)) { + if (rb_io_maybe_wait_writable(errno, socket, fptr->timeout)) { continue; } diff --git a/ext/socket/init.c b/ext/socket/init.c index 0cff3d6794..e60dd32264 100644 --- a/ext/socket/init.c +++ b/ext/socket/init.c @@ -189,7 +189,7 @@ rsock_s_recvfrom(VALUE socket, int argc, VALUE *argv, enum sock_recv_type from) https://github.com/ruby/ruby/blob/trunk/ext/socket/init.c#L189 if (slen >= 0) break; - if (!rb_io_maybe_wait_readable(errno, socket, Qnil)) + if (!rb_io_maybe_wait_readable(errno, socket, Qundef)) rb_sys_fail("recvfrom(2)"); } @@ -705,7 +705,7 @@ rsock_s_accept(VALUE klass, VALUE io, struct sockaddr *sockaddr, socklen_t *len) https://github.com/ruby/ruby/blob/trunk/ext/socket/init.c#L705 retry = 1; goto retry; default: - if (!rb_io_maybe_wait_readable(error, io, Qnil)) break; + if (!rb_io_maybe_wait_readable(error, io, Qundef)) break; retry = 0; goto retry; } diff --git a/ext/socket/socket.c b/ext/socket/socket.c index b1965deb9e..5cf0835062 100644 --- a/ext/socket/socket.c +++ b/ext/socket/socket.c @@ -28,6 +28,10 @@ rsock_syserr_fail_host_port(int err, const char *mesg, VALUE host, VALUE port) https://github.com/ruby/ruby/blob/trunk/ext/socket/socket.c#L28 message = rb_sprintf("%s for %+"PRIsVALUE" port % "PRIsVALUE"", mesg, host, port); + if (err == ETIMEDOUT) { + rb_exc_raise(rb_exc_new3(rb_eIOTimeoutError, message)); + } + rb_syserr_fail_str(err, message); } diff --git a/ext/socket/udpsocket.c b/ext/socket/udpsocket.c index 3500107972..5b878b4a95 100644 --- a/ext/socket/udpsocket.c +++ b/ext/socket/udpsocket.c @@ -170,7 +170,7 @@ udp_send_internal(VALUE v) https://github.com/ruby/ruby/blob/trunk/ext/socket/udpsocket.c#L170 if (n >= 0) return RB_SSIZE2NUM(n); - if (rb_io_maybe_wait_writable(errno, fptr->self, Qnil)) { + if (rb_io_maybe_wait_writable(errno, fptr->self, fptr->timeout)) { goto retry; } } diff --git a/gc.c b/gc.c index c1b06a76e7..916584ed35 100644 --- a/gc.c +++ b/gc.c @@ -7303,6 +7303,7 @@ gc_mark_children(rb_objspace_t *objspace, VALUE obj) https://github.com/ruby/ruby/blob/trunk/gc.c#L7303 gc_mark(objspace, any->as.file.fptr->writeconv_pre_ecopts); gc_mark(objspace, any->as.file.fptr->encs.ecopts); gc_mark(objspace, any->as.file.fptr->write_lock); + gc_mark(objspace, any->as.file.fptr->timeout); } break; diff --git a/include/ruby/io.h b/include/ruby/io.h index dc4c8becf6..b91ecd00cb 100644 --- a/include/ruby/io.h +++ b/include/ruby/io.h @@ -63,6 +63,11 @@ RBIMPL_SYMBOL_EXPORT_BEGIN() https://github.com/ruby/ruby/blob/trunk/include/ruby/io.h#L63 struct stat; struct timeval; +/** + * Indicates that a timeout has occurred while performing an IO operation. + */ +RUBY_EXTERN VALUE rb_eIOTimeoutError; + /** * Type of events that an IO can wait. * @@ -214,6 +219,11 @@ typedef struct rb_io_t { https://github.com/ruby/ruby/blob/trunk/include/ruby/io.h#L219 * This of course doesn't help inter-process IO interleaves, though. */ VALUE write_lock; + + /** + * The timeout associated with this IO when performing blocking operations. + */ + VALUE timeout; } rb_io_t; /** @alias{rb_io_enc_t} */ @@ -844,11 +854,33 @@ int rb_io_wait_writable(int fd); https://github.com/ruby/ruby/blob/trunk/include/ruby/io.h#L854 */ int rb_wait_for_single_fd(int fd, int events, struct timeval *tv); +/** + * Get the timeout associated with the specified io object. + * + * @param[in] io An IO object. + * @retval RUBY_Qnil There is no associated timeout. + * @retval Otherwise The timeout value. + */ +VALUE rb_io_timeout(VALUE io); + +/** + * Set the timeout associated with the specified io object. This timeout is + * used as a best effort timeout to prevent operations from blocking forever. + * + * @param[in] io An IO object. + * @param[in] timeout A timeout value. Must respond to #to_f. + * @ + */ +VALUE rb_io_set_timeout(VALUE io, VALUE timeout); + /** * Blocks until the passed IO is ready for the passed events. The "events" * here is a Ruby level integer, which is an OR-ed value of `IO::READABLE`, * `IO::WRITable`, and `IO::PRIORITY`. * + * If timeout is `Qundef`, it will use the default timeout as given by + * `rb_io_timeout(io)`. + * * @p (... truncated) -- ML: ruby-changes@q... Info: http://www.atdot.net/~ko1/quickml/