Skip to content

Commit 365a678

Browse files
committed
[GR-18163] Implement changes of ruby-3.0 to IO#wait (#2953)
PullRequest: truffleruby/3724
2 parents fae89b3 + 9fef30b commit 365a678

File tree

9 files changed

+270
-25
lines changed

9 files changed

+270
-25
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ Compatibility:
120120
* Upgrading `UNICODE` version to 13.0.0 and `EMOJI` version to 13.1 (#2733, @horakivo).
121121
* Add `rb_io_maybe_wait_readable`, `rb_io_maybe_wait_writable` and `rb_io_maybe_wait` functions (#2733, @andrykonchin).
122122
* `StringIO#set_encoding` should coerce the argument to an Encoding (#2954, @eregon).
123+
* Implement changes of Ruby 3.0 to `IO#wait` (#2953, @larskanis).
123124

124125
Performance:
125126

lib/truffle/io/wait.rb

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# truffleruby_primitives: true
2+
13
# Copyright (c) 2017, 2023 Oracle and/or its affiliates. All rights reserved. This
24
# code is released under a tri EPL/GPL/LGPL license. You can use it,
35
# redistribute it and/or modify it under the terms of the:
@@ -14,17 +16,77 @@ def nread
1416

1517
def ready?
1618
ensure_open_and_readable
17-
Truffle::IOOperations.poll(self, Truffle::IOOperations::POLLIN, 0)
19+
Truffle::IOOperations.poll(self, IO::READABLE, 0) > 0
1820
end
1921

2022
def wait_readable(timeout = nil)
2123
ensure_open_and_readable
22-
Truffle::IOOperations.poll(self, Truffle::IOOperations::POLLIN, timeout) ? self : nil
24+
Truffle::IOOperations.poll(self, IO::READABLE, timeout) > 0 ? self : nil
2325
end
24-
alias_method :wait, :wait_readable
2526

2627
def wait_writable(timeout = nil)
2728
ensure_open_and_writable
28-
Truffle::IOOperations.poll(self, Truffle::IOOperations::POLLOUT, timeout) ? self : nil
29+
Truffle::IOOperations.poll(self, IO::WRITABLE, timeout) > 0 ? self : nil
30+
end
31+
32+
def wait_priority(timeout = nil)
33+
ensure_open_and_readable
34+
Truffle::IOOperations.poll(self, IO::PRIORITY, timeout) > 0 ? self : nil
35+
end
36+
37+
38+
# call-seq:
39+
# io.wait(events, timeout) -> event mask, false or nil
40+
# io.wait(timeout = nil, mode = :read) -> self, true, or false
41+
#
42+
# Waits until the IO becomes ready for the specified events and returns the
43+
# subset of events that become ready, or a falsy value when times out.
44+
#
45+
# The events can be a bit mask of +IO::READABLE+, +IO::WRITABLE+ or
46+
# +IO::PRIORITY+.
47+
#
48+
# Returns a truthy value immediately when buffered data is available.
49+
#
50+
# Optional parameter +mode+ is one of +:read+, +:write+, or
51+
# +:read_write+.
52+
def wait(*args)
53+
ensure_open
54+
55+
if args.size != 2 || Primitive.object_kind_of?(args[0], Symbol) || Primitive.object_kind_of?(args[1], Symbol)
56+
# Slow/messy path:
57+
timeout = :undef
58+
events = 0
59+
args.each do |arg|
60+
if Primitive.object_kind_of?(arg, Symbol)
61+
events |= case arg
62+
when :r, :read, :readable then IO::READABLE
63+
when :w, :write, :writable then IO::WRITABLE
64+
when :rw, :read_write, :readable_writable then IO::READABLE | IO::WRITABLE
65+
else
66+
raise ArgumentError, "unsupported mode: #{arg}"
67+
end
68+
69+
elsif timeout == :undef
70+
timeout = arg
71+
else
72+
raise ArgumentError, 'timeout given more than once'
73+
end
74+
end
75+
76+
timeout = nil if timeout == :undef
77+
78+
events = IO::READABLE if events == 0
79+
80+
res = Truffle::IOOperations.poll(self, events, timeout)
81+
res == 0 ? nil : self
82+
else
83+
# args.size == 2 and neither are symbols
84+
# This is the fast path and the new interface:
85+
events, timeout = *args
86+
raise ArgumentError, 'Events must be positive integer!' if events <= 0
87+
res = Truffle::IOOperations.poll(self, events, timeout)
88+
# return events as bit mask
89+
res == 0 ? nil : res
90+
end
2991
end
3092
end
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
module IOWaitSpec
2+
def self.exhaust_write_buffer(io)
3+
written = 0
4+
buf = " " * 4096
5+
6+
begin
7+
written += io.write_nonblock(buf)
8+
rescue Errno::EAGAIN, Errno::EWOULDBLOCK
9+
return written
10+
end while true
11+
end
12+
end
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
require_relative '../../spec_helper'
2+
require_relative 'fixtures/classes'
3+
4+
ruby_version_is ''...'3.2' do
5+
require 'io/wait'
6+
end
7+
8+
describe "IO#wait" do
9+
before :each do
10+
@io = File.new(__FILE__ )
11+
12+
if /mswin|mingw/ =~ RUBY_PLATFORM
13+
@r, @w = Socket.pair(Socket::AF_INET, Socket::SOCK_STREAM, 0)
14+
else
15+
@r, @w = IO.pipe
16+
end
17+
end
18+
19+
after :each do
20+
@io.close unless @io.closed?
21+
22+
@r.close unless @r.closed?
23+
@w.close unless @w.closed?
24+
end
25+
26+
ruby_version_is "3.0" do
27+
context "[events, timeout] passed" do
28+
ruby_version_is "3.0"..."3.2" do
29+
it "returns self when the READABLE event is ready during the timeout" do
30+
@w.write('data to read')
31+
@r.wait(IO::READABLE, 2).should.equal?(@r)
32+
end
33+
34+
it "returns self when the WRITABLE event is ready during the timeout" do
35+
@w.wait(IO::WRITABLE, 0).should.equal?(@w)
36+
end
37+
end
38+
39+
ruby_version_is "3.2" do
40+
it "returns events mask when the READABLE event is ready during the timeout" do
41+
@w.write('data to read')
42+
@r.wait(IO::READABLE, 2).should == IO::READABLE
43+
end
44+
45+
it "returns events mask when the WRITABLE event is ready during the timeout" do
46+
@w.wait(IO::WRITABLE, 0).should == IO::WRITABLE
47+
end
48+
end
49+
50+
ruby_version_is "3.0" do
51+
it "waits for the READABLE event to be ready" do
52+
queue = Queue.new
53+
thread = Thread.new { queue.pop; sleep 1; @w.write('data to read') };
54+
55+
queue.push('signal');
56+
@r.wait(IO::READABLE, 2).should_not == nil
57+
58+
thread.join
59+
end
60+
61+
it "waits for the WRITABLE event to be ready" do
62+
written_bytes = IOWaitSpec.exhaust_write_buffer(@w)
63+
64+
queue = Queue.new
65+
thread = Thread.new { queue.pop; sleep 1; @r.read(written_bytes) };
66+
67+
queue.push('signal');
68+
@w.wait(IO::WRITABLE, 2).should_not == nil
69+
70+
thread.join
71+
end
72+
73+
it "returns nil when the READABLE event is not ready during the timeout" do
74+
@w.wait(IO::READABLE, 0).should == nil
75+
end
76+
77+
it "returns nil when the WRITABLE event is not ready during the timeout" do
78+
IOWaitSpec.exhaust_write_buffer(@w)
79+
@w.wait(IO::WRITABLE, 0).should == nil
80+
end
81+
82+
it "raises IOError when io is closed (closed stream (IOError))" do
83+
@io.close
84+
-> { @io.wait(IO::READABLE, 0) }.should raise_error(IOError, "closed stream")
85+
end
86+
87+
ruby_version_is "3.2" do
88+
it "raises ArgumentError when events is not positive" do
89+
-> { @w.wait(0, 0) }.should raise_error(ArgumentError, "Events must be positive integer!")
90+
-> { @w.wait(-1, 0) }.should raise_error(ArgumentError, "Events must be positive integer!")
91+
end
92+
end
93+
end
94+
end
95+
end
96+
97+
context "[timeout, mode] passed" do
98+
it "accepts :r, :read, :readable mode to check READABLE event" do
99+
@io.wait(0, :r).should == @io
100+
@io.wait(0, :read).should == @io
101+
@io.wait(0, :readable).should == @io
102+
end
103+
104+
it "accepts :w, :write, :writable mode to check WRITABLE event" do
105+
@io.wait(0, :w).should == @io
106+
@io.wait(0, :write).should == @io
107+
@io.wait(0, :writable).should == @io
108+
end
109+
110+
it "accepts :rw, :read_write, :readable_writable mode to check READABLE and WRITABLE events" do
111+
@io.wait(0, :rw).should == @io
112+
@io.wait(0, :read_write).should == @io
113+
@io.wait(0, :readable_writable).should == @io
114+
end
115+
116+
it "accepts a list of modes" do
117+
@io.wait(0, :r, :w, :rw).should == @io
118+
end
119+
120+
# It works at least since 2.7 but by some reason may fail on 3.1
121+
ruby_version_is "3.2" do
122+
it "accepts timeout and mode in any order" do
123+
@io.wait(0, :r).should == @io
124+
@io.wait(:r, 0).should == @io
125+
@io.wait(:r, 0, :w).should == @io
126+
end
127+
end
128+
129+
it "raises ArgumentError when passed wrong Symbol value as mode argument" do
130+
-> { @io.wait(0, :wrong) }.should raise_error(ArgumentError, "unsupported mode: wrong")
131+
end
132+
133+
# It works since 3.0 but by some reason may fail on 3.1
134+
ruby_version_is "3.2" do
135+
it "raises ArgumentError when several Integer arguments passed" do
136+
-> { @w.wait(0, 10, :r) }.should raise_error(ArgumentError, "timeout given more than once")
137+
end
138+
end
139+
140+
ruby_version_is "3.2" do
141+
it "raises IOError when io is closed (closed stream (IOError))" do
142+
@io.close
143+
-> { @io.wait(0, :r) }.should raise_error(IOError, "closed stream")
144+
end
145+
end
146+
end
147+
end
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fails:IO#wait [events, timeout] passed returns self when the READABLE event is ready during the timeout
2+
fails:IO#wait [events, timeout] passed returns self when the WRITABLE event is ready during the timeout
3+
fails:IO#wait [events, timeout] passed waits for the READABLE event to be ready

src/main/c/truffleposix/truffleposix.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ int truffleposix_poll(int fd, int events, int timeout_ms) {
117117
fds.fd = fd;
118118
fds.events = events;
119119

120-
return poll(&fds, 1, timeout_ms);
120+
return poll(&fds, 1, timeout_ms) >= 0 ? fds.revents : -1;
121121
}
122122

123123
int truffleposix_select(int nread, int *readfds, int nwrite, int *writefds,

src/main/ruby/truffleruby/core/truffle/io_operations.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,9 @@ def self.select(readables, readable_ios, writables, writable_ios, errorables, er
205205

206206
end
207207

208-
# This method will return a true if poll returned without error
209-
# with an event within the timeout, false if the timeout expired,
210-
# or raises an exception for an errno.
208+
# This method will return an event mask if poll returned without error.
209+
# The event mask is > 0 when an event occurred within the timeout, 0 if the timeout expired.
210+
# Raises an exception for an errno.
211211
def self.poll(io, event_mask, timeout)
212212
if (event_mask & POLLIN) != 0
213213
return 1 unless io.__send__(:buffer_empty?)
@@ -257,7 +257,7 @@ def self.poll(io, event_mask, timeout)
257257
Errno.handle_errno(errno)
258258
end
259259
else
260-
primitive_result > 0
260+
primitive_result
261261
end
262262
end while result == :retry
263263

test/mri/excludes/TestIOWait.rb

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,4 @@
1-
exclude :test_nread, "needs investigation"
2-
exclude :test_nread_buffered, "needs investigation"
3-
exclude :test_wait_buffered, "needs investigation"
4-
exclude :test_wait_readable_buffered, "needs investigation"
5-
exclude :test_wait_readwrite, "needs investigation"
6-
exclude :test_wait_readwrite_timeout, "needs investigation"
7-
exclude :test_nread, "needs investigation"
8-
exclude :test_nread_buffered, "needs investigation"
9-
exclude :test_wait_buffered, "needs investigation"
10-
exclude :test_wait_readable_buffered, "needs investigation"
11-
exclude :test_wait_readwrite, "needs investigation"
12-
exclude :test_wait_readwrite_timeout, "needs investigation"
1+
exclude :test_nread, "buffered reads are not implemented"
2+
exclude :test_nread_buffered, "buffered reads are not implemented"
3+
exclude :test_wait_buffered, "buffered reads are not implemented"
4+
exclude :test_wait_readable_buffered, "buffered reads are not implemented"

test/mri/tests/io/wait/test_io_wait.rb

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@
33
require 'test/unit'
44
require 'timeout'
55
require 'socket'
6-
begin
7-
require 'io/wait'
8-
rescue LoadError
9-
end
6+
7+
# For `IO#ready?` and `IO#nread`:
8+
require 'io/wait'
109

1110
class TestIOWait < Test::Unit::TestCase
1211

@@ -50,6 +49,7 @@ def test_buffered_ready?
5049
end
5150

5251
def test_wait
52+
omit 'unstable on MinGW' if /mingw/ =~ RUBY_PLATFORM
5353
assert_nil @r.wait(0)
5454
@w.syswrite "."
5555
sleep 0.1
@@ -161,6 +161,34 @@ def test_wait_readwrite_timeout
161161
assert_equal @w, @w.wait(0.01, :read_write)
162162
end
163163

164+
def test_wait_mask_writable
165+
omit("Missing IO::WRITABLE!") unless IO.const_defined?(:WRITABLE)
166+
assert_equal IO::WRITABLE, @w.wait(IO::WRITABLE, 0)
167+
end
168+
169+
def test_wait_mask_readable
170+
omit("Missing IO::READABLE!") unless IO.const_defined?(:READABLE)
171+
@w.write("Hello World\n" * 3)
172+
assert_equal IO::READABLE, @r.wait(IO::READABLE, 0)
173+
174+
@r.gets
175+
assert_equal IO::READABLE, @r.wait(IO::READABLE, 0)
176+
end
177+
178+
def test_wait_mask_zero
179+
omit("Missing IO::WRITABLE!") unless IO.const_defined?(:WRITABLE)
180+
assert_raise(ArgumentError) do
181+
@w.wait(0, 0)
182+
end
183+
end
184+
185+
def test_wait_mask_negative
186+
omit("Missing IO::WRITABLE!") unless IO.const_defined?(:WRITABLE)
187+
assert_raise(ArgumentError) do
188+
@w.wait(-6, 0)
189+
end
190+
end
191+
164192
private
165193

166194
def fill_pipe

0 commit comments

Comments
 (0)