https://bugs.ruby-lang.org/https://bugs.ruby-lang.org/favicon.ico?17113305112022-03-14T02:43:55ZRuby Issue Tracking SystemRuby master - Feature #18630: Introduce general `IO#timeout` and `IO#timeout=` for blocking operations.https://bugs.ruby-lang.org/issues/18630?journal_id=968262022-03-14T02:43:55Zioquatix (Samuel Williams)samuel@oriontransfer.net
<ul><li><strong>Description</strong> updated (<a title="View differences" href="/journals/96826/diff?detail_id=62167">diff</a>)</li></ul> Ruby master - Feature #18630: Introduce general `IO#timeout` and `IO#timeout=` for blocking operations.https://bugs.ruby-lang.org/issues/18630?journal_id=968272022-03-14T02:44:38Zioquatix (Samuel Williams)samuel@oriontransfer.net
<ul><li><strong>Description</strong> updated (<a title="View differences" href="/journals/96827/diff?detail_id=62168">diff</a>)</li></ul> Ruby master - Feature #18630: Introduce general `IO#timeout` and `IO#timeout=` for blocking operations.https://bugs.ruby-lang.org/issues/18630?journal_id=968282022-03-14T04:28:41Zmatz (Yukihiro Matsumoto)matz@ruby.or.jp
<ul></ul><p>I am positive about introducing safer timeout feature.<br>
But I have several concerns:</p>
<ul>
<li>time-out may happen from I/O blocking operations or other CPU bound operation. This proposal only covers I/O blocking. Is it OK?</li>
<li>time-out may not be caused by a single I/O operation, but by multiple operations. This proposal should be updated to address it (maybe specifying time-bound by the specific time point).</li>
<li>some I/O operation methods takes <code>timeout</code> keyword argument. We need to confirm they are consistent with this proposal.</li>
</ul>
<p>Matz.</p> Ruby master - Feature #18630: Introduce general `IO#timeout` and `IO#timeout=` for blocking operations.https://bugs.ruby-lang.org/issues/18630?journal_id=968302022-03-14T09:05:50ZEregon (Benoit Daloze)
<ul></ul><p>I'm not sure a timeout per IO instance makes sense, some IO operations might take longer e.g. reading many bytes at once and so it seems unclear whether any timeout value would be sensible there.</p>
<p>The proposal should also mention this can only work for non-blocking IOs (and maybe raise if called on a blocking IO?)</p>
<p>I thought Timeout.timeout is already good enough when there is a scheduler and has similar semantics, why would we need this? Is there a concrete example?</p> Ruby master - Feature #18630: Introduce general `IO#timeout` and `IO#timeout=` for blocking operations.https://bugs.ruby-lang.org/issues/18630?journal_id=968322022-03-14T14:52:57Zioquatix (Samuel Williams)samuel@oriontransfer.net
<ul></ul><blockquote>
<p>I'm not sure a timeout per IO instance makes sense, some IO operations might take longer e.g. reading many bytes at once and so it seems unclear whether any timeout value would be sensible there.</p>
</blockquote>
<p>I think it's reasonable that no single IO operation should take more than, say, several seconds on a healthy system. However it's definitely a hard problem. I'm also considering whether we can have a general default, e.g. set by an environment variable or global within Ruby, e.g. <code>IO.timeout</code>. We could also consider adding keyword arguments to <code>File.open</code> and so on. A timeout is really a way of protecting code from hanging indefinitely, e.g. because of deadlocks or DoS hacks. A lot of programs monkey patch such functionality but none of it is compatible with each other. I think introducing a simple, standard interface here makes sense.</p>
<blockquote>
<p>The proposal should also mention this can only work for non-blocking IOs (and maybe raise if called on a blocking IO?)</p>
</blockquote>
<p>This only matters for general blocking Ruby. When using the fiber scheduler with io_uring, this limitation goes away.</p>
<p>We could fall back to the blocking <code>Timeout.timeout</code> semantics in non-scheduler blocking case, I just don't know if it's a good idea to over-complexity the implementation. The vast majority of IO in Ruby now is non-blocking by default. This mostly just applies to <code>stdin</code>, <code>stdout</code>, and <code>stderr</code>. I think we could even consider making <code>stdin</code> non-blocking by default.</p>
<p>In order to state this more clearly, we could document this limitation as "Timeouts are best effort and are not always guaranteed to be enforced or accurate." which is totally reasonable in my mind given the nature of timeouts.</p>
<blockquote>
<p>I thought Timeout.timeout is already good enough when there is a scheduler and has similar semantics, why would we need this? Is there a concrete example?</p>
</blockquote>
<p><code>Timeout.timeout</code> is hard to implement and I'm not sure there is any general easy implementation. There is a fiber scheduler hook for <code>Timeout.timeout</code> which can be a little bit safer in practice at the expense of only interrupting non-blocking IO.</p>
<p><code>IO#timeout</code> is more like the default timeout to use when internal wait mechanisms are invoked, like <code>nogvl_wait_for</code> or <code>rb_io_wait</code>. This is much more predictable and robust.</p> Ruby master - Feature #18630: Introduce general `IO#timeout` and `IO#timeout=` for blocking operations.https://bugs.ruby-lang.org/issues/18630?journal_id=968832022-03-17T02:08:44Zioquatix (Samuel Williams)samuel@oriontransfer.net
<ul></ul><p><a class="user active user-mention" href="https://bugs.ruby-lang.org/users/13">@matz (Yukihiro Matsumoto)</a> thanks for your comments.</p>
<blockquote>
<p>time-out may happen from I/O blocking operations or other CPU bound operation. This proposal only covers I/O blocking. Is it OK?</p>
</blockquote>
<p>I believe it's acceptable, since it only impacts IO operations.</p>
<blockquote>
<p>time-out may not be caused by a single I/O operation, but by multiple operations. This proposal should be updated to address it (maybe specifying time-bound by the specific time point).</p>
</blockquote>
<p>We still retain <code>Timeout.timeout</code> which is sufficient for these cases.</p>
<blockquote>
<p>some I/O operation methods takes timeout keyword argument. We need to confirm they are consistent with this proposal.</p>
</blockquote>
<p>I agree to introduce consistency here where possible. Some method like <code>gets</code> and <code>puts</code> would be hard to update, but others like <code>read</code> and <code>write</code> should be possible to add individual timeouts.</p>
<p>Later we could consider adding <code>IO#read_timeout</code> and <code>IO#write_timeout</code> if there is a valid use case.</p> Ruby master - Feature #18630: Introduce general `IO#timeout` and `IO#timeout=` for blocking operations.https://bugs.ruby-lang.org/issues/18630?journal_id=968992022-03-17T12:06:31ZEregon (Benoit Daloze)
<ul></ul><p>This part wasn't answered:</p>
<blockquote>
<p>Is there a concrete/practical example (for this new timeout)?</p>
</blockquote>
<p>I'm not sure it's valuable to have per-IO timeout. It's kind of global state, especially if it's set on some globally-shared IO like STDIN/STDERR/STDOUT.<br>
Also this approach can't work when creating an IO, e.g. creating a new TCPSocket connecting to some address+port.</p>
<p>I think this might be redundant and add additional complexity and global state compared to a new way to do timeout that only interrupts IO and blocking operations (like Queue#pop, Mutex#lock, sleep, etc).<br>
i.e., same as Timeout.timeout except it would only interrupt blocking operations (blocking IO is considered a blocking operation too), not <code>Thread#raise</code> on some random Ruby line.<br>
So we'd have <code>Timeout.blocking_timeout(5) { ... }</code> or so, and that's no global state, more general and more useful, isn't it?<br>
And that should of course interrupt blocking IO via SIGVTALRM (and interruptible blocking regions in C exts) as already done for other functionality like <code>Timeout.timeout</code>.</p> Ruby master - Feature #18630: Introduce general `IO#timeout` and `IO#timeout=` for blocking operations.https://bugs.ruby-lang.org/issues/18630?journal_id=969072022-03-17T15:42:32Zioquatix (Samuel Williams)samuel@oriontransfer.net
<ul></ul><blockquote>
<p>I'm not sure it's valuable to have per-IO timeout.</p>
</blockquote>
<p><code>async-io</code> has used it for years successfully as a protection against slowloris attacks. It ensures that no matter who calls the IO operations, some timeout is associated with it.</p>
<p>There is a difference between IO which is usually externally facing and things like Queue, Thread which are internal. I personally have no problem adding timeouts to all those interfaces, and even adding <code>Thread::Queue#timeout</code> if that makes sense.</p>
<p>However, this PR is mostly just addressing the issue of making non-blocking IO safer in the presence of external malicious actors.</p> Ruby master - Feature #18630: Introduce general `IO#timeout` and `IO#timeout=` for blocking operations.https://bugs.ruby-lang.org/issues/18630?journal_id=969592022-03-21T15:52:17Zbyroot (Jean Boussier)byroot@ruby-lang.org
<ul></ul><blockquote>
<p>I'm not sure a timeout per IO instance makes sense,</p>
</blockquote>
<p>I think it does for various network clients. See for example <a href="https://github.com/ruby/net-protocol/blob/088e52609a8768c1e6156fa6c5609bbfadd44320/lib/net/protocol.rb#L115-L302" class="external"><code>net-protocol</code>'s <code>BufferedIO</code> class</a> which backs Net::HTTP and others.</p>
<p>If you want to have a timeout on your network client, you are basically to <code>read_nonblock / write_nonblock</code>, so for most line based protocols (such as HTTP but also memcached, redis, etc) you can't use <code>IO#gets</code>. This means you need to buffer your reads to find the newline, so now you have the Ruby internal 16KiB IO buffer, plus the <code>BufferedIO</code> 16KiB buffer, and you end up calling <code>String#slice!</code> repeatedly on that Ruby String which is atrocious for performance.</p>
<p>I recently had to do <a href="https://github.com/redis-rb/redis-client/blob/a9b198ec1f312c30f5c5b94be3131c228a950aa8/lib/redis_client/buffered_io.rb" class="external">the same thing for a new Redis client I'm working on</a>, but to save on performance I don't <code>slice!</code> the buffer, but keep a numeric <code>@offset</code> alongside.</p>
<p>If I understand this proposal correctly, it would allow me to directly call <code>IO#gets</code> with the timeout I desire, which would allow me to not have my own buffer and solely rely on the internal one.</p>
<p>That being said, if there was some kind of <code>IO.timeout(timeout) {}</code> it would work too. I guess both have their pros and cons.</p> Ruby master - Feature #18630: Introduce general `IO#timeout` and `IO#timeout=` for blocking operations.https://bugs.ruby-lang.org/issues/18630?journal_id=969632022-03-21T16:59:49ZEregon (Benoit Daloze)
<ul></ul><p>I'm still confused, in which cases does this timeout apply?<br>
Only non-blocking IO, right?</p>
<p>The issue title says:</p>
<blockquote>
<p>Introduce general <code>IO#timeout</code> and <code>IO#timeout=</code>for all (non-)blocking operations.</p>
</blockquote>
<p>The description says:</p>
<blockquote>
<p>I would like us to consider introducing a general timeout for all blocking operations.</p>
</blockquote>
<p>And the PR says:<br>
<a href="https://github.com/ruby/ruby/pull/5653/files#diff-92194f057884b3287a3a6bf84e6e3b2bf433a556b68562799252a091744e7854R857" class="external">https://github.com/ruby/ruby/pull/5653/files#diff-92194f057884b3287a3a6bf84e6e3b2bf433a556b68562799252a091744e7854R857</a></p>
<blockquote>
<ul>
<li>Set the internal timeout to the specified duration or nil. The timeout</li>
<li>applies to all non-blocking operations unless otherwise specified.</li>
<li>
<li>This affects the following methods (but is not limited to): #gets, #puts,</li>
<li>#read, #write, #wait_readable and #wait_writable. This also affects</li>
<li>non-blocking socket operations like Socket#accept and Socket#connect.</li>
</ul>
</blockquote>
<p>But those are not "non-blocking methods" or "non-blocking operations", or at least that's not intuitive.</p>
<p>Is the actual condition depending on <code>IO#nonblock?</code>, if true possible to timeout all IO operations on it, if false none of them?</p> Ruby master - Feature #18630: Introduce general `IO#timeout` and `IO#timeout=` for blocking operations.https://bugs.ruby-lang.org/issues/18630?journal_id=969642022-03-21T17:11:45ZEregon (Benoit Daloze)
<ul></ul><p>I guess one way to express it is this new IO#timeout would apply to: blocking IO methods on non-blocking IO instances.</p> Ruby master - Feature #18630: Introduce general `IO#timeout` and `IO#timeout=` for blocking operations.https://bugs.ruby-lang.org/issues/18630?journal_id=969652022-03-21T19:30:43Zioquatix (Samuel Williams)samuel@oriontransfer.net
<ul><li><strong>Description</strong> updated (<a title="View differences" href="/journals/96965/diff?detail_id=62237">diff</a>)</li></ul><p>Sorry if the description is confusing. This is a general timeout for all non-blocking IO operations, specified per IO instance.</p> Ruby master - Feature #18630: Introduce general `IO#timeout` and `IO#timeout=` for blocking operations.https://bugs.ruby-lang.org/issues/18630?journal_id=969662022-03-21T19:31:09Zioquatix (Samuel Williams)samuel@oriontransfer.net
<ul><li><strong>Description</strong> updated (<a title="View differences" href="/journals/96966/diff?detail_id=62238">diff</a>)</li></ul> Ruby master - Feature #18630: Introduce general `IO#timeout` and `IO#timeout=` for blocking operations.https://bugs.ruby-lang.org/issues/18630?journal_id=970632022-03-28T10:24:18ZEregon (Benoit Daloze)
<ul></ul><p>Per <a href="https://bugs.ruby-lang.org/issues/17837?next_issue_id=17835&prev_issue_id=17843#note-44" class="external">https://bugs.ruby-lang.org/issues/17837?next_issue_id=17835&prev_issue_id=17843#note-44</a>, <code>class IO::TimeoutError < StandardError</code> seems best (should not subclass Exception for "non-fatal exceptions") and consistent (with Regexp::TimeoutError).</p> Ruby master - Feature #18630: Introduce general `IO#timeout` and `IO#timeout=` for blocking operations.https://bugs.ruby-lang.org/issues/18630?journal_id=970772022-03-29T21:58:18Zioquatix (Samuel Williams)samuel@oriontransfer.net
<ul></ul><p><a class="user active user-mention" href="https://bugs.ruby-lang.org/users/772">@Eregon (Benoit Daloze)</a> it makes sense to me to introduce <code>IO::Timeout</code> or <code>IO::TimeoutError</code> (not sure timeout is strictly an error?).</p>
<p>However, how should we handle <code>Errno::ETIMEDOUT</code> which can also occur in some system calls.</p>
<p>Should we map all <code>Errno::ETIMEDOUT</code> -> <code>IO::Timeout</code>?</p>
<p>The current implementation just uses <code>Errno::ETIMEDOUT</code> which is the smallest most compatible change (since technically people should already be expecting IO operations to potentially return <code>ETIMEDOUT</code>), e.g. from <code>recv</code> man page:</p>
<blockquote>
<p>ETIMEDOUT<br>
The connection timed out during connection establishment, or due to a transmission timeout on active connection.</p>
</blockquote>
<p>A quick glance at Node.JS seems to be they just use <code>ETIMEDOUT</code>.</p>
<p>For me, the biggest reason to use a different class would be because we want a more consistent handling of timeouts, across all blocking operations (not just IO ones). It sounds like maybe that ship has sailed because we now have several different exceptions for timeouts, and no common root AFAIK.</p>
<p>Maybe we should have:</p>
<pre><code>class TimeoutError < StandardError # or Exception?
end
class Regexp::TimeoutError < ::TimeoutError
end
class IO::TimeoutError :: TimeoutError
end
</code></pre>
<p>etc.</p>
<p>I hesitate to make this <code>< StandardError</code> as I feel that timeouts are often not strictly "errors" although a timeout can cause an error.</p> Ruby master - Feature #18630: Introduce general `IO#timeout` and `IO#timeout=` for blocking operations.https://bugs.ruby-lang.org/issues/18630?journal_id=971122022-03-31T10:58:19ZEregon (Benoit Daloze)
<ul></ul><p>ioquatix (Samuel Williams) wrote in <a href="#note-15">#note-15</a>:</p>
<blockquote>
<p><a class="user active user-mention" href="https://bugs.ruby-lang.org/users/772">@Eregon (Benoit Daloze)</a> it makes sense to me to introduce <code>IO::Timeout</code> or <code>IO::TimeoutError</code> (not sure timeout is strictly an error?).</p>
</blockquote>
<p>Basically all exception classes end with <code>Exception</code> or <code>Error</code> or some other indication of it (see <code>ri Exception</code>).<br>
So I think <code>IO::Timeout</code> is not a good fit, because from the name it's unclear it's an exception class.<br>
And the other two timeout exception classes have <code>Timeout</code> + <code>Error</code> in their full name, so <code>IO::TimeoutError</code> seems an obvious fit.</p>
<p><code>ETIMEDOUT</code> is not e.g. in read/select/epoll_wait/... man pages as far as I can see locally and I don't even see it in the my local <code>recv</code> man page.<br>
It seems specific to socket connections, but the timeout we are talking about is more general (e.g., applies to pipes).<br>
I think <code>ETIMEDOUT</code> is a bad fit, because it is <em>not</em> what is actually returned by the syscalls to which we apply the timeout, isn't it?<br>
Also I'd consider <code>Errno::E*</code> to be low-level exceptions, and not something we'd want for higher-level functionality.</p>
<blockquote>
<p>For me, the biggest reason to use a different class would be because we want a more consistent handling of timeouts, across all blocking operations (not just IO ones).</p>
</blockquote>
<p>If that is useful (so far it seems not to me), that could always be done by a module included by all timeout exception classes.</p>
<blockquote>
<p>I hesitate to make this < StandardError as I feel that timeouts are often not strictly "errors" although a timeout can cause an error.</p>
</blockquote>
<p><code>StandardError</code> is not about being "an error" or not (that's just consistent naming for exception classes), it's whether it's something that is sensible to rescue "as a general failure" / to recover from or not.<br>
It is sensible to rescue <code>IO::TimeoutError</code> as a general "something failed during the operation" (at least in some cases) and keep going,<br>
while it is e.g. never (or almost never) sensible to rescue (without reraising them) <code>NoMemoryError</code>/<code>SystemStackError</code>.</p>
<blockquote>
<p>Maybe we should have: <code>class TimeoutError</code></p>
</blockquote>
<p>That's a deprecated alias for Timeout::Error:</p>
<pre><code>$ ruby -w -rtimeout -e 'p TimeoutError'
-e:1: warning: constant ::TimeoutError is deprecated
Timeout::Error
</code></pre> Ruby master - Feature #18630: Introduce general `IO#timeout` and `IO#timeout=` for blocking operations.https://bugs.ruby-lang.org/issues/18630?journal_id=971142022-03-31T11:25:35Zioquatix (Samuel Williams)samuel@oriontransfer.net
<ul></ul><p>I am fine with <code>IO::TimeoutError</code> but again, I'm not sure it's strictly an error. Not all exceptions or errors have those naming conventions, <code>Errno::E*</code> being an obvious one, but there are several others:</p>
<pre><code>> Exception.subclasses
=>
[IRB::Abort,
ErrorHighlight::Spotter::NonAscii,
SystemStackError,
NoMemoryError,
SecurityError,
ScriptError,
StandardError,
SignalException,
fatal,
SystemExit]
</code></pre>
<pre><code>> SignalException.subclasses
=> [Interrupt]
</code></pre>
<pre><code>> StandardError.subclasses.map(&:to_s).grep_v(/Error/)
=>
["IRB::IllegalRCGenerator",
"IRB::UndefinedPromptMode",
"IRB::CantChangeBinding",
"IRB::CantShiftToMultiIrbMode",
"IRB::NoSuchJob",
"IRB::IrbSwitchedToCurrentThread",
"IRB::IrbAlreadyDead",
"IRB::IllegalParameter",
"IRB::CantReturnToNormalMode",
"IRB::UnrecognizedSwitch",
"RubyLex::TerminateLineInput",
"Gem::TSort::Cyclic"]
</code></pre>
<p>Basically, I think there are two questions:</p>
<ol>
<li>Should it be called <code>IO::Timeout</code> or <code>IO::TimeoutError</code>.</li>
<li>Should it be <code>< Exception</code> or <code>< StandardError</code>.</li>
</ol>
<p>I don't have strong opinion about either.</p>
<p>For (1) I guess it's matter of taste and opinion whether a timeout is an error or not. To me, whether it's an error depends on context. But for most cases, it could be considered exceptional except when a timeout is a reasonable possibility. <code>read</code> with timeout is exceptional, but <code>wait_readable</code> with timeout is not, since it has clear existing semantics for dealing with "exceeding the given timeout". So, with that in mind, it probably makes sense that in most cases, the timeout would be unexpected and thus an error.</p>
<p>For (2) I've gone both ways on this in the past and finally settled on <code>< StandardError</code> being more reasonable. However it's true that in many cases, a timeout is significantly different from a normal "error". It's the reason why <code>SystemExit</code> is an exception and not an error, is the same motivating factor for why timeouts should be an exception and not an error.</p>
<p>I would personally like to hear if <a class="user active user-mention" href="https://bugs.ruby-lang.org/users/13">@matz (Yukihiro Matsumoto)</a> has an opinion on this. Basically, I think we agree on <code>IO::TimeoutError < StandardError</code> being the most obvious, but I don't have a strong opinion on this... the closest analogue would be <code>Interrupt</code> which is also not <code>StandardError</code> which makes me think we need to consider this more carefully.</p> Ruby master - Feature #18630: Introduce general `IO#timeout` and `IO#timeout=` for blocking operations.https://bugs.ruby-lang.org/issues/18630?journal_id=971152022-03-31T11:46:22Zioquatix (Samuel Williams)samuel@oriontransfer.net
<ul></ul><p>By the way, this still doesn't address that some operations can return <code>Errno::ETIMEDOUT</code> and this class of errors is practically speaking the same.</p>
<p>I don't have a strong opinion about whether we should force users to write <code>rescue IO::TimeoutError, Errno::ETIMEDOUT</code> but it seems more difficult for users.</p>
<p>Regarding which operations can give <code>Errno::ETIMEDOUT</code> you can check your local man pages: <code>man -K ETIMEDOUT</code>. Surprisingly macOS has a ton of hits, even some non-socket cases. I can check Linux too.</p>
<p>I guess we need to decide whether we try to incorporate <code>Errno::ETIMEDOUT</code> or ignore it. Or maybe another option is to just have a shared module which is included in all of them.</p> Ruby master - Feature #18630: Introduce general `IO#timeout` and `IO#timeout=` for blocking operations.https://bugs.ruby-lang.org/issues/18630?journal_id=971212022-03-31T17:15:33ZEregon (Benoit Daloze)
<ul></ul><p>AFAIK Errno::* is only meant for what the libc/syscalls would return, nothing else, so doesn't fit here.<br>
As said above, many of functions involved above don't ever return ETIMEDOUT, so it would be incorrect/surprising to reuse it for that.</p>
<p>Re naming <code>Errno::E*</code>, the "Error" is in the name, twice, it's "Error numero::ERROR abbreviation" ;)<br>
Most of the other exceptions have something that is closely related to error/exception in their name.</p>
<p>It's not because there is "Error" in the name that the exception class should be treated "as an error".<br>
There is simply no such relation, and as you can see there are exception classes with Error in the name both under StandardError and above it too.<br>
They are all exceptions, and whether it is "an error" almost always depends on the context.<br>
The only meaningful distinction AFAIK in Ruby is < StandardError or not. And since it's meant to be rescued in some cases, and for consistency will all other timeout exceptions, it should be < StandardError.</p> Ruby master - Feature #18630: Introduce general `IO#timeout` and `IO#timeout=` for blocking operations.https://bugs.ruby-lang.org/issues/18630?journal_id=971232022-03-31T22:52:06Zioquatix (Samuel Williams)samuel@oriontransfer.net
<ul></ul><blockquote>
<p>many of functions involved above don't ever return ETIMEDOUT, so it would be incorrect/surprising to reuse it for that.</p>
</blockquote>
<p>The reality is, some functions <strong>do</strong> return this and so we should consider how it impacts users who want to handle <strong>all</strong> IO timeouts. Having two distinct exceptions seems a little messy to me. I'm happy to accept these are different, but I'm also suggesting we should consider carefully the implications of this choice.</p>
<p>Basically, do we want users to write:</p>
<pre><code>rescue IO::TimeoutError, Errno::ETIMEDOUT
</code></pre>
<p>???</p> Ruby master - Feature #18630: Introduce general `IO#timeout` and `IO#timeout=` for blocking operations.https://bugs.ruby-lang.org/issues/18630?journal_id=973632022-04-21T09:20:50ZEregon (Benoit Daloze)
<ul></ul><p>In relation to <a class="issue tracker-2 status-1 priority-4 priority-default" title="Feature: Merge `io-nonblock` gems into core (Open)" href="https://bugs.ruby-lang.org/issues/18668">#18668</a>, this proposal needs IO#nonblock= and IO#nonblock? because IO#timeout= only applies to IO#nonblock?=true IO instances. And so to use it sucessfully, a user needs to ensure the relevant IO is in nonblock=true mode + some other conditions:</p>
<p><a href="https://github.com/ruby/ruby/pull/5653/files#diff-92194f057884b3287a3a6bf84e6e3b2bf433a556b68562799252a091744e7854R856-R867" class="external">https://github.com/ruby/ruby/pull/5653/files#diff-92194f057884b3287a3a6bf84e6e3b2bf433a556b68562799252a091744e7854R856-R867</a> says:</p>
<pre><code> * call-seq:
* timeout = duration -> duration
* timeout = nil -> nil
*
* Set the internal timeout to the specified duration or nil. The timeout
* applies to all blocking operations provided the IO is in non-blocking mode:
* +io.nonblock? => true+.
*
* This affects the following methods (but is not limited to): #gets, #puts,
* #read, #write, #wait_readable and #wait_writable. This also affects
* blocking socket operations like Socket#accept and Socket#connect.
</code></pre> Ruby master - Feature #18630: Introduce general `IO#timeout` and `IO#timeout=` for blocking operations.https://bugs.ruby-lang.org/issues/18630?journal_id=973652022-04-21T09:36:40ZEregon (Benoit Daloze)
<ul></ul><p>Looks like I didn't reply to:</p>
<blockquote>
<p>Basically, do we want users to write:</p>
</blockquote>
<p>Yes, I think it's fine because it's very rarely needed.<br>
It's like cases of <code>rescue IO::WaitReadable, ...</code> which often includes multiple exception types.<br>
It can be useful to distinguish both timeouts, <code>IO::TimeoutError</code> is the per-IO timeout and <code>Errno::ETIMEDOUT</code> is a per-operation timeout (only for some operations, most operations can't raise ETIMEDOUT) and the timeout durations can be different of course.</p> Ruby master - Feature #18630: Introduce general `IO#timeout` and `IO#timeout=` for blocking operations.https://bugs.ruby-lang.org/issues/18630?journal_id=983882022-07-20T10:13:16Zioquatix (Samuel Williams)samuel@oriontransfer.net
<ul><li><strong>Subject</strong> changed from <i>Introduce general `IO#timeout` and `IO#timeout=`for all (non-)blocking operations.</i> to <i>Introduce general `IO#timeout` and `IO#timeout=` for blocking operations.</i></li></ul><p>I think we can make this for blocking operations too, but the code path will be a little different if a timeout is set.</p> Ruby master - Feature #18630: Introduce general `IO#timeout` and `IO#timeout=` for blocking operations.https://bugs.ruby-lang.org/issues/18630?journal_id=983922022-07-20T11:48:56Zioquatix (Samuel Williams)samuel@oriontransfer.net
<ul></ul><p>Okay, I was able to make it work:</p>
<pre><code class="ruby syntaxhl" data-language="ruby"><span class="no">STDIN</span><span class="p">.</span><span class="nf">timeout</span> <span class="o">=</span> <span class="mi">1</span>
<span class="n">pp</span> <span class="no">STDIN</span><span class="p">.</span><span class="nf">read</span>
</code></pre>
<p>Output:</p>
<pre><code>/Users/samuel/Projects/ioquatix/ruby/build/../test.rb:3:in `read': Operation timed out @ io_fread - <STDIN> (Errno::ETIMEDOUT)
from /Users/samuel/Projects/ioquatix/ruby/build/../test.rb:4:in `<main>'
</code></pre>
<p>The final step is to decide what kind of exception we should use.</p> Ruby master - Feature #18630: Introduce general `IO#timeout` and `IO#timeout=` for blocking operations.https://bugs.ruby-lang.org/issues/18630?journal_id=992172022-09-20T22:49:57Zioquatix (Samuel Williams)samuel@oriontransfer.net
<ul></ul><p>I'm proposing to introduce the following:</p>
<pre><code class="ruby syntaxhl" data-language="ruby"><span class="k">class</span> <span class="nc">TimeoutError</span> <span class="o"><</span> <span class="no">StandardError</span>
<span class="k">end</span>
<span class="k">class</span> <span class="nc">IO::TimeoutError</span> <span class="o"><</span> <span class="no">TimeoutError</span>
<span class="k">end</span>
</code></pre>
<p>This is practical and simple, and I suggest other timeout errors follow the same way, e.g.</p>
<pre><code class="ruby syntaxhl" data-language="ruby"><span class="k">class</span> <span class="nc">Regexp::TimeoutError</span> <span class="o"><</span> <span class="no">TimeoutError</span>
<span class="k">end</span>
<span class="c1"># etc</span>
</code></pre>
<p>I think this is easy to use and understand.</p>
<p>However, it can introduce some compatibility issues with existing code, e.g. <code>Socket.tcp(..., connect_timeout)</code> can raise <code>Errno::ETIMEDOUT</code>. I think this interface is a mistake and should be fixed. In my PR, it now raises <code>IO::TimeoutError</code>. Not all timeouts will be associated with <code>errno</code> and thus we should avoid exposing users to this internal implementation detail of <code>errno</code>.</p>
<p>If we want to retain the existing semantics, we could try with modules, but I think it's confusing, so I don't support such an approach.</p>
<pre><code class="ruby syntaxhl" data-language="ruby"><span class="k">module</span> <span class="nn">TimeoutError</span>
<span class="k">end</span>
<span class="k">class</span> <span class="nc">IO::TimeoutError</span> <span class="o"><</span> <span class="no">StandardError</span>
<span class="kp">include</span> <span class="no">TimeoutError</span>
<span class="k">end</span>
<span class="k">class</span> <span class="nc">Errno::ETIMEDOUT</span> <span class="o"><</span> <span class="o">...</span>
<span class="kp">include</span> <span class="no">TimeoutError</span>
<span class="k">end</span>
</code></pre>
<p>I don't think this is as good/clean design. We already have this issue with <code>IOError</code>, <code>EOFError</code>, <code>Errno::EPIPE</code>, etc. This is very confusing and complicated for user.</p> Ruby master - Feature #18630: Introduce general `IO#timeout` and `IO#timeout=` for blocking operations.https://bugs.ruby-lang.org/issues/18630?journal_id=992472022-09-22T10:22:07Zioquatix (Samuel Williams)samuel@oriontransfer.net
<ul></ul><p><a class="user active user-mention" href="https://bugs.ruby-lang.org/users/17">@ko1 (Koichi Sasada)</a> I've updated the PR with documentation to address your concerns. Do you have any other concerns?</p> Ruby master - Feature #18630: Introduce general `IO#timeout` and `IO#timeout=` for blocking operations.https://bugs.ruby-lang.org/issues/18630?journal_id=992832022-09-23T06:35:00Zioquatix (Samuel Williams)samuel@oriontransfer.net
<ul></ul><p><a class="user active user-mention" href="https://bugs.ruby-lang.org/users/13">@matz (Yukihiro Matsumoto)</a> <a class="user active user-mention" href="https://bugs.ruby-lang.org/users/5">@naruse (Yui NARUSE)</a> do you agree with the current PR?</p>
<pre><code class="ruby syntaxhl" data-language="ruby"><span class="k">class</span> <span class="nc">TimeoutError</span> <span class="o"><</span> <span class="no">StandardError</span>
<span class="k">end</span>
<span class="k">class</span> <span class="nc">IO::TimeoutError</span> <span class="o"><</span> <span class="no">TimeoutError</span>
<span class="k">end</span>
</code></pre>
<p>Or do you think it makes more sense:</p>
<pre><code class="ruby syntaxhl" data-language="ruby"><span class="k">module</span> <span class="nn">TimeoutError</span>
<span class="k">end</span>
<span class="k">class</span> <span class="nc">IO::TimeoutError</span> <span class="o"><</span> <span class="no">IOError</span>
<span class="kp">include</span> <span class="no">TimeoutError</span>
<span class="k">end</span>
<span class="k">class</span> <span class="nc">Errno::ETIMEDOUT</span>
<span class="kp">include</span> <span class="no">TimeoutError</span>
<span class="k">end</span>
<span class="k">class</span> <span class="nc">Regexp::TimeoutError</span> <span class="o"><</span> <span class="no">RegexpError</span>
<span class="kp">include</span> <span class="no">TimeoutError</span>
<span class="k">end</span>
</code></pre>
<p>and so on. Using a module is more flexible but also more complicated design.</p> Ruby master - Feature #18630: Introduce general `IO#timeout` and `IO#timeout=` for blocking operations.https://bugs.ruby-lang.org/issues/18630?journal_id=995042022-10-07T02:28:43Zioquatix (Samuel Williams)samuel@oriontransfer.net
<ul></ul><p><a class="user active user-mention" href="https://bugs.ruby-lang.org/users/13">@matz (Yukihiro Matsumoto)</a> I've updated the PR so it has the following structure:</p>
<pre><code class="ruby syntaxhl" data-language="ruby"><span class="k">class</span> <span class="nc">IO::TimeoutError</span> <span class="o"><</span> <span class="no">IOError</span>
<span class="k">end</span>
</code></pre>
<p>Is that acceptable?</p>
<p>We can decide in separate issue to introduce general top level <code>TimeoutError</code>.</p>
<p>Since we no longer have top level <code>TimeoutError</code> to consider, everything else was acceptable, so can we merge this feature?</p>
<p>Thanks!</p> Ruby master - Feature #18630: Introduce general `IO#timeout` and `IO#timeout=` for blocking operations.https://bugs.ruby-lang.org/issues/18630?journal_id=995092022-10-07T08:32:22Zmatz (Yukihiro Matsumoto)matz@ruby.or.jp
<ul></ul><p>I am OK with it. Let us put aside having aggregating timeout errors (like <code>TimeoutError</code> module) for the future.</p>
<p>Matz.</p> Ruby master - Feature #18630: Introduce general `IO#timeout` and `IO#timeout=` for blocking operations.https://bugs.ruby-lang.org/issues/18630?journal_id=995182022-10-08T07:34:20Zioquatix (Samuel Williams)samuel@oriontransfer.net
<ul><li><strong>Status</strong> changed from <i>Open</i> to <i>Closed</i></li><li><strong>Assignee</strong> set to <i>ioquatix (Samuel Williams)</i></li></ul><p>It was merged.</p>