Skip to content

Commit 490dfbb

Browse files
committed
Adding IO#timeout.
1 parent 3e9d8d8 commit 490dfbb

File tree

5 files changed

+178
-7
lines changed

5 files changed

+178
-7
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Compatibility:
2020
* Add `String#bytesplice` (#3039, @itarato).
2121
* Add `String#byteindex` and `String#byterindex` (#3039, @itarato).
2222
* Add implementations of `rb_proc_call_with_block`, `rb_proc_call_kw`, `rb_proc_call_with_block_kw` and `rb_funcall_with_block_kw` (#3068, @andrykonchin).
23+
* Adding `IO#timeout` and `IO#timeout=` (#3039, @itarato).
2324

2425
Performance:
2526

spec/ruby/core/io/timeout_spec.rb

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# -*- encoding: utf-8 -*-
2+
require_relative '../../spec_helper'
3+
4+
describe "IO#timeout" do
5+
before :each do
6+
@fname = tmp("io_timeout.txt")
7+
@file = File.open(@fname, "a+")
8+
9+
@rpipe, @wpipe = IO.pipe
10+
# There is no strict long term standard for pipe limits (2**16 bytes currently). This is an attempt to set a safe
11+
# enough size to test a full pipe.
12+
@more_than_pipe_limit = 1 << 18
13+
end
14+
15+
after :each do
16+
@rpipe.close
17+
@wpipe.close
18+
19+
@file.close
20+
rm_r @fname
21+
end
22+
23+
ruby_version_is "3.2" do
24+
it "files have timeout attribute" do
25+
@fname = tmp("io_timeout_attribute.txt")
26+
touch(@fname)
27+
28+
@file.timeout.should == nil
29+
30+
@file.timeout = 1.23
31+
@file.timeout.should == 1.23
32+
end
33+
34+
it "IO instances have timeout attribute" do
35+
@rpipe.timeout.should == nil
36+
@wpipe.timeout.should == nil
37+
38+
@rpipe.timeout = 1.23
39+
@wpipe.timeout = 4.56
40+
41+
@rpipe.timeout.should == 1.23
42+
@wpipe.timeout.should == 4.56
43+
end
44+
45+
it "raises IO::TimeoutError when timeout is exceeded for .read" do
46+
@rpipe.timeout = 0.01
47+
-> { @rpipe.read.should }.should raise_error(IO::TimeoutError)
48+
end
49+
50+
it "raises IO::TimeoutError when timeout is exceeded for .read(n)" do
51+
@rpipe.timeout = 0.01
52+
-> { @rpipe.read(3) }.should raise_error(IO::TimeoutError)
53+
end
54+
55+
it "raises IO::TimeoutError when timeout is exceeded for .gets" do
56+
@rpipe.timeout = 0.01
57+
-> { @rpipe.gets }.should raise_error(IO::TimeoutError)
58+
end
59+
60+
it "raises IO::TimeoutError when timeout is exceeded for .write" do
61+
@wpipe.timeout = 0.01
62+
-> { @wpipe.write("x" * @more_than_pipe_limit) }.should raise_error(IO::TimeoutError)
63+
end
64+
65+
it "raises IO::TimeoutError when timeout is exceeded for .puts" do
66+
@wpipe.timeout = 0.01
67+
-> { @wpipe.puts("x" * @more_than_pipe_limit) }.should raise_error(IO::TimeoutError)
68+
end
69+
70+
it "times out with .read when there is no EOF" do
71+
@wpipe.write("hello")
72+
@rpipe.timeout = 0.01
73+
74+
-> { @rpipe.read }.should raise_error(IO::TimeoutError)
75+
end
76+
77+
it "returns content with .read when there is EOF" do
78+
@wpipe.write("hello")
79+
@wpipe.close
80+
81+
@rpipe.timeout = 0.01
82+
83+
@rpipe.read.should == "hello"
84+
end
85+
86+
it "times out with .read(N) when there is not enough bytes" do
87+
@wpipe.write("hello")
88+
@rpipe.timeout = 0.01
89+
90+
@rpipe.read(2).should == "he"
91+
-> { @rpipe.read(5) }.should raise_error(IO::TimeoutError)
92+
end
93+
94+
it "returns partial content with .read(N) when there is not enough bytes but there is EOF" do
95+
@wpipe.write("hello")
96+
@rpipe.timeout = 0.01
97+
98+
@rpipe.read(2).should == "he"
99+
100+
@wpipe.close
101+
@rpipe.read(5).should == "llo"
102+
end
103+
end
104+
end

spec/tags/truffle/methods_tags.txt

+2
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,5 @@ fails:Public methods on UnboundMethod should include private?
114114
fails:Public methods on UnboundMethod should include protected?
115115
fails:Public methods on UnboundMethod should include public?
116116
fails:Public methods on String should not include bytesplice
117+
fails:Public methods on IO should not include timeout
118+
fails:Public methods on IO should not include timeout=

src/main/ruby/truffleruby/core/io.rb

+14
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ class IO
3838

3939
include Enumerable
4040

41+
class TimeoutError < IOError; end
42+
4143
module WaitReadable; end
4244
module WaitWritable; end
4345

@@ -1648,6 +1650,18 @@ def printf(fmt, *args)
16481650
write sprintf(fmt, *args)
16491651
end
16501652

1653+
def timeout
1654+
@timeout || nil
1655+
end
1656+
1657+
def timeout=(new_timeout)
1658+
if Primitive.nil?(timeout) ^ Primitive.nil?(new_timeout)
1659+
self.nonblock = !Primitive.nil?(new_timeout)
1660+
end
1661+
1662+
@timeout = new_timeout
1663+
end
1664+
16511665
def read(length = nil, buffer = nil)
16521666
ensure_open_and_readable
16531667
buffer = StringValue(buffer) if buffer

src/main/ruby/truffleruby/core/posix.rb

+57-7
Original file line numberDiff line numberDiff line change
@@ -401,10 +401,10 @@ def self.read_string_nonblock(io, count, exception)
401401
# by IO#sysread
402402

403403
def self.read_string_native(io, length)
404-
fd = io.fileno
405404
buffer = Primitive.io_thread_buffer_allocate(length)
406405
begin
407-
bytes_read = Truffle::POSIX.read(fd, buffer, length)
406+
bytes_read = execute_posix_read(io, buffer, length)
407+
408408
if bytes_read < 0
409409
bytes_read, errno = bytes_read, Errno.errno
410410
elsif bytes_read == 0 # EOF
@@ -425,11 +425,62 @@ def self.read_string_native(io, length)
425425
end
426426
end
427427

428-
def self.read_to_buffer_native(io, length)
428+
def self.execute_posix_read(io, buffer, length)
429+
fd = io.fileno
430+
return Truffle::POSIX.read(fd, buffer, length) unless io.timeout
431+
432+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + io.timeout
433+
434+
loop do
435+
current_timeout = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
436+
raise IO::TimeoutError if current_timeout < 0
437+
438+
poll_result = Truffle::IOOperations.poll(io, Truffle::IOOperations::POLLIN, current_timeout)
439+
if poll_result == 0
440+
raise IO::TimeoutError
441+
elsif poll_result == -1
442+
Errno.handle_errno(Errno.errno)
443+
end
444+
445+
if (bytes_read = Truffle::POSIX.read(fd, buffer, length)) == -1
446+
continue if Errno.errno == Errno::EAGAIN
447+
break
448+
end
449+
450+
return bytes_read
451+
end
452+
end
453+
454+
def self.execute_posix_write(io, buffer, length)
429455
fd = io.fileno
456+
return Truffle::POSIX.write(fd, buffer, length) unless io.timeout
457+
458+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + io.timeout
459+
460+
loop do
461+
current_timeout = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
462+
raise IO::TimeoutError if current_timeout < 0
463+
464+
poll_result = Truffle::IOOperations.poll(io, Truffle::IOOperations::POLLOUT, current_timeout)
465+
if poll_result == 0
466+
raise IO::TimeoutError
467+
elsif poll_result == -1
468+
Errno.handle_errno(Errno.errno)
469+
end
470+
471+
if (bytes_written = Truffle::POSIX.write(fd, buffer, length)) == -1
472+
continue if Errno.errno == Errno::EAGAIN
473+
break
474+
end
475+
476+
return bytes_written
477+
end
478+
end
479+
480+
def self.read_to_buffer_native(io, length)
430481
buffer = Primitive.io_thread_buffer_allocate(length)
431482
begin
432-
bytes_read = Truffle::POSIX.read(fd, buffer, length)
483+
bytes_read = execute_posix_read(io, buffer, length)
433484
if bytes_read < 0
434485
bytes_read, errno = bytes_read, Errno.errno
435486
elsif bytes_read == 0 # EOF
@@ -495,7 +546,7 @@ def self.write_string_native(io, string, continue_on_eagain)
495546

496547
written = 0
497548
while written < length
498-
ret = Truffle::POSIX.write(fd, buffer + written, length - written)
549+
ret = execute_posix_write(io, buffer + written, length - written)
499550
if ret < 0
500551
errno = Errno.errno
501552
if errno == EAGAIN_ERRNO
@@ -540,12 +591,11 @@ def self.write_string_polyglot(io, string, continue_on_eagain)
540591
# #write_string_nonblock_polylgot) is called by IO#write_nonblock
541592

542593
def self.write_string_nonblock_native(io, string)
543-
fd = io.fileno
544594
length = string.bytesize
545595
buffer = Primitive.io_thread_buffer_allocate(length)
546596
begin
547597
buffer.write_bytes string
548-
written = Truffle::POSIX.write(fd, buffer, length)
598+
written = execute_posix_write(io, buffer, length)
549599

550600
if written < 0
551601
errno = Errno.errno

0 commit comments

Comments
 (0)