Skip to content

Commit 176fca1

Browse files
authored
Add support for IO#timeout in io_read, io_write and io_wait. (#296)
1 parent 087f78f commit 176fca1

File tree

2 files changed

+70
-3
lines changed

2 files changed

+70
-3
lines changed

lib/async/scheduler.rb

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,29 +165,66 @@ def address_resolve(hostname)
165165
::Resolv.getaddresses(hostname)
166166
end
167167

168+
169+
if IO.method_defined?(:timeout)
170+
private def get_timeout(io)
171+
io.timeout
172+
end
173+
else
174+
private def get_timeout(io)
175+
nil
176+
end
177+
end
178+
168179
# @asynchronous May be non-blocking..
169180
def io_wait(io, events, timeout = nil)
170181
fiber = Fiber.current
171182

172183
if timeout
184+
# If an explicit timeout is specified, we expect that the user will handle it themselves:
173185
timer = @timers.after(timeout) do
174186
fiber.transfer
175187
end
188+
elsif timeout = get_timeout(io)
189+
# Otherwise, if we default to the io's timeout, we raise an exception:
190+
timer = @timers.after(timeout) do
191+
fiber.raise(::IO::TimeoutError, "Timeout while waiting for IO to become ready!")
192+
end
176193
end
177194

178195
return @selector.io_wait(fiber, io, events)
179196
ensure
180197
timer&.cancel
181198
end
182-
199+
183200
if ::IO::Event::Support.buffer?
184201
def io_read(io, buffer, length, offset = 0)
185-
@selector.io_read(Fiber.current, io, buffer, length, offset)
202+
fiber = Fiber.current
203+
204+
if timeout = get_timeout(io)
205+
timer = @timers.after(timeout) do
206+
fiber.raise(::IO::TimeoutError, "execution expired")
207+
end
208+
end
209+
210+
@selector.io_read(fiber, io, buffer, length, offset)
211+
ensure
212+
timer&.cancel
186213
end
187214

188215
if RUBY_ENGINE != "ruby" || RUBY_VERSION >= "3.3.0"
189216
def io_write(io, buffer, length, offset = 0)
190-
@selector.io_write(Fiber.current, io, buffer, length, offset)
217+
fiber = Fiber.current
218+
219+
if timeout = get_timeout(io)
220+
timer = @timers.after(timeout) do
221+
fiber.raise(::IO::TimeoutError, "execution expired")
222+
end
223+
end
224+
225+
@selector.io_write(fiber, io, buffer, length, offset)
226+
ensure
227+
timer&.cancel
191228
end
192229
end
193230
end

test/io.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,35 @@
3030
input.close
3131
output.close
3232
end
33+
34+
it "can read with timeout" do
35+
skip_unless_constant_defined(:TimeoutError, IO)
36+
37+
input, output = IO.pipe
38+
input.timeout = 0.001
39+
40+
expect do
41+
line = input.gets
42+
end.to raise_exception(::IO::TimeoutError)
43+
end
44+
45+
it "can wait readable with default timeout" do
46+
skip_unless_constant_defined(:TimeoutError, IO)
47+
48+
input, output = IO.pipe
49+
input.timeout = 0.001
50+
51+
expect do
52+
# This behaviour is not consistent with non-fiber scheduler IO.
53+
# However, this is the best we can do without fixing CRuby.
54+
input.wait_readable
55+
end.to raise_exception(::IO::TimeoutError)
56+
end
57+
58+
it "can wait readable with explicit timeout" do
59+
input, output = IO.pipe
60+
61+
expect(input.wait_readable(0)).to be_nil
62+
end
3363
end
3464
end

0 commit comments

Comments
 (0)