mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
332b302f92
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
120 lines
3.7 KiB
Ruby
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
|