1
0
Fork 0
mirror of https://github.com/ruby/ruby.git synced 2022-11-09 12:17:21 -05:00
ruby--ruby/spec/ruby/core/io/select_spec.rb
normal 332b302f92 spec/ruby/core/io/select_spec.rb: workaround stuck IO.select
Under pipe page memory pressure on Linux, a pipe may only be
created with a single buffer[1].  And as of Linux v4.18, the
fs/pipe.c::pipe_poll callback does not account for merging
done in fs/pipe::pipe_write; only the number of usable buffers
in the pipe.  Thus it is possible for a pipe to be writable
(if only by a small amount) despite IO.select saying it is not.

With the default 16384 /proc/sys/fs/pipe-user-pages-soft value
and the pipe having 16 pages of buffers, this issue is trivially
reproducible by creating 1024 pipes in a background process
before running the spec:

  $ ulimit -n 2053 # or something higher
  $ ./miniruby -e '1024.times.map { IO.pipe }; sleep' &
  $ make test-spec MSPECOPT=spec/ruby/core/io/select_spec.rb

So, we create a new pipe we never write to for testing
writability of IO.select.  This may cause the test to fail with
ENFILE on an overloaded system, but at least that's an obvious
failure (unlike having a test get stuck).  This should reduce
failures on our overloaded CI machines:

   http://ci.rvm.jp/results/trunk-nopara@silicon-docker/1239426

[1] https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/fs/pipe.c?h=v4.18#n642

git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@64478 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
2018-08-20 01:43:10 +00:00

120 lines
3.7 KiB
Ruby

require_relative '../../spec_helper'
describe "IO.select" do
before :each do
@rd, @wr = IO.pipe
end
after :each do
@rd.close unless @rd.closed?
@wr.close unless @wr.closed?
end
it "blocks for duration of timeout and returns nil if there are no objects ready for I/O" do
IO.select([@rd], nil, nil, 0.001).should == nil
end
it "returns immediately all objects that are ready for I/O when timeout is 0" do
@wr.syswrite("be ready")
IO.pipe do |_, wr|
result = IO.select [@rd], [wr], nil, 0
result.should == [[@rd], [wr], []]
end
end
it "returns nil after timeout if there are no objects ready for I/O" do
result = IO.select [@rd], nil, nil, 0
result.should == nil
end
it "returns supplied objects when they are ready for I/O" do
main = Thread.current
t = Thread.new {
Thread.pass until main.status == "sleep"
@wr.write "be ready"
}
result = IO.select [@rd], nil, nil, nil
result.should == [[@rd], [], []]
t.join
end
it "leaves out IO objects for which there is no I/O ready" do
@wr.write "be ready"
platform_is :aix do
# In AIX, when a pipe is readable, select(2) returns the write side
# of the pipe as "readable", even though you cannot actually read
# anything from the write side.
result = IO.select [@wr, @rd], nil, nil, nil
result.should == [[@wr, @rd], [], []]
end
platform_is_not :aix do
# Order matters here. We want to see that @wr doesn't expand the size
# of the returned array, so it must be 1st.
result = IO.select [@wr, @rd], nil, nil, nil
result.should == [[@rd], [], []]
end
end
it "returns supplied objects correctly even when monitoring the same object in different arrays" do
filename = tmp("IO_select_pipe_file") + $$.to_s
io = File.open(filename, 'w+')
result = IO.select [io], [io], nil, 0
result.should == [[io], [io], []]
io.close
rm_r filename
end
it "invokes to_io on supplied objects that are not IO and returns the supplied objects" do
# make some data available
@wr.write("foobar")
obj = mock("read_io")
obj.should_receive(:to_io).at_least(1).and_return(@rd)
IO.select([obj]).should == [[obj], [], []]
IO.pipe do |_, wr|
obj = mock("write_io")
obj.should_receive(:to_io).at_least(1).and_return(wr)
IO.select(nil, [obj]).should == [[], [obj], []]
end
end
it "raises TypeError if supplied objects are not IO" do
lambda { IO.select([Object.new]) }.should raise_error(TypeError)
lambda { IO.select(nil, [Object.new]) }.should raise_error(TypeError)
obj = mock("io")
obj.should_receive(:to_io).any_number_of_times.and_return(nil)
lambda { IO.select([obj]) }.should raise_error(TypeError)
lambda { IO.select(nil, [obj]) }.should raise_error(TypeError)
end
it "raises a TypeError if the specified timeout value is not Numeric" do
lambda { IO.select([@rd], nil, nil, Object.new) }.should raise_error(TypeError)
end
it "raises TypeError if the first three arguments are not Arrays" do
lambda { IO.select(Object.new)}.should raise_error(TypeError)
lambda { IO.select(nil, Object.new)}.should raise_error(TypeError)
lambda { IO.select(nil, nil, Object.new)}.should raise_error(TypeError)
end
it "raises an ArgumentError when passed a negative timeout" do
lambda { IO.select(nil, nil, nil, -5)}.should raise_error(ArgumentError)
end
end
describe "IO.select when passed nil for timeout" do
it "sleeps forever and sets the thread status to 'sleep'" do
t = Thread.new do
IO.select(nil, nil, nil, nil)
end
Thread.pass while t.status && t.status != "sleep"
t.join unless t.status
t.status.should == "sleep"
t.kill
t.join
end
end