From f015bceb6730d7e97687da151b4f19681635950c Mon Sep 17 00:00:00 2001 From: Andrew Konchin Date: Wed, 1 May 2024 18:55:40 +0300 Subject: [PATCH] Repair IO#wait_readable, IO#wait_writable, IO#wait to be interruptible * Fixes https://github.com/oracle/truffleruby/issues/3504 --- .../fixtures/classes.rb => fixtures/io.rb | 10 +-- library/io-wait/wait_readable_spec.rb | 19 ++++++ library/io-wait/wait_spec.rb | 39 ++++++++++-- library/io-wait/wait_writable_spec.rb | 21 +++++++ optional/capi/io_spec.rb | 63 +++++++++++++++++++ 5 files changed, 143 insertions(+), 9 deletions(-) rename library/io-wait/fixtures/classes.rb => fixtures/io.rb (52%) diff --git a/library/io-wait/fixtures/classes.rb b/fixtures/io.rb similarity index 52% rename from library/io-wait/fixtures/classes.rb rename to fixtures/io.rb index 837c7edd0..87ebbbb2b 100644 --- a/library/io-wait/fixtures/classes.rb +++ b/fixtures/io.rb @@ -1,12 +1,12 @@ -module IOWaitSpec +module IOSpec def self.exhaust_write_buffer(io) written = 0 buf = " " * 4096 - begin + while true written += io.write_nonblock(buf) - rescue Errno::EAGAIN, Errno::EWOULDBLOCK - return written - end while true + end + rescue Errno::EAGAIN, Errno::EWOULDBLOCK + written end end diff --git a/library/io-wait/wait_readable_spec.rb b/library/io-wait/wait_readable_spec.rb index 06ffbda5c..c4340846b 100644 --- a/library/io-wait/wait_readable_spec.rb +++ b/library/io-wait/wait_readable_spec.rb @@ -24,4 +24,23 @@ it "waits for the IO to become readable with the given large timeout" do @io.wait_readable(365 * 24 * 60 * 60).should == @io end + + it "can be interrupted" do + rd, _wr = IO.pipe + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + + t = Thread.new do + rd.wait_readable(10) + end + + Thread.pass until t.stop? + t.kill + t.join + + finish = Process.clock_gettime(Process::CLOCK_MONOTONIC) + (finish - start).should < 9 + ensure + rd.close + _wr.close + end end diff --git a/library/io-wait/wait_spec.rb b/library/io-wait/wait_spec.rb index fc07c6a8d..5952f127f 100644 --- a/library/io-wait/wait_spec.rb +++ b/library/io-wait/wait_spec.rb @@ -1,5 +1,5 @@ require_relative '../../spec_helper' -require_relative 'fixtures/classes' +require_relative '../../fixtures/io' ruby_version_is ''...'3.2' do require 'io/wait' @@ -55,7 +55,7 @@ end it "waits for the WRITABLE event to be ready" do - written_bytes = IOWaitSpec.exhaust_write_buffer(@w) + written_bytes = IOSpec.exhaust_write_buffer(@w) @w.wait(IO::WRITABLE, 0).should == nil @r.read(written_bytes) @@ -67,7 +67,7 @@ end it "returns nil when the WRITABLE event is not ready during the timeout" do - IOWaitSpec.exhaust_write_buffer(@w) + IOSpec.exhaust_write_buffer(@w) @w.wait(IO::WRITABLE, 0).should == nil end @@ -92,7 +92,7 @@ end it "changes thread status to 'sleep' when waits for WRITABLE event" do - written_bytes = IOWaitSpec.exhaust_write_buffer(@w) + IOSpec.exhaust_write_buffer(@w) t = Thread.new { @w.wait(IO::WRITABLE, 10) } sleep 1 @@ -100,6 +100,37 @@ t.kill t.join # Thread#kill doesn't wait for the thread to end end + + it "can be interrupted when waiting for READABLE event" do + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + + t = Thread.new do + @r.wait(IO::READABLE, 10) + end + + Thread.pass until t.stop? + t.kill + t.join # Thread#kill doesn't wait for the thread to end + + finish = Process.clock_gettime(Process::CLOCK_MONOTONIC) + (finish - start).should < 9 + end + + it "can be interrupted when waiting for WRITABLE event" do + IOSpec.exhaust_write_buffer(@w) + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + + t = Thread.new do + @w.wait(IO::WRITABLE, 10) + end + + Thread.pass until t.stop? + t.kill + t.join # Thread#kill doesn't wait for the thread to end + + finish = Process.clock_gettime(Process::CLOCK_MONOTONIC) + (finish - start).should < 9 + end end context "[timeout, mode] passed" do diff --git a/library/io-wait/wait_writable_spec.rb b/library/io-wait/wait_writable_spec.rb index 8c44780d3..6012d686e 100644 --- a/library/io-wait/wait_writable_spec.rb +++ b/library/io-wait/wait_writable_spec.rb @@ -1,4 +1,5 @@ require_relative '../../spec_helper' +require_relative '../../fixtures/io' ruby_version_is ''...'3.2' do require 'io/wait' @@ -17,4 +18,24 @@ # Represents one year and is larger than a 32-bit int STDOUT.wait_writable(365 * 24 * 60 * 60).should == STDOUT end + + it "can be interrupted" do + _rd, wr = IO.pipe + IOSpec.exhaust_write_buffer(wr) + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + + t = Thread.new do + wr.wait_writable(10) + end + + Thread.pass until t.stop? + t.kill + t.join + + finish = Process.clock_gettime(Process::CLOCK_MONOTONIC) + (finish - start).should < 9 + ensure + _rd.close unless _rd.closed? + wr.close unless wr.closed? + end end diff --git a/optional/capi/io_spec.rb b/optional/capi/io_spec.rb index 870abef3e..bdec46f5e 100644 --- a/optional/capi/io_spec.rb +++ b/optional/capi/io_spec.rb @@ -1,4 +1,5 @@ require_relative 'spec_helper' +require_relative '../../fixtures/io' load_extension('io') @@ -279,6 +280,22 @@ it "raises an IOError if the IO is not initialized" do -> { @o.rb_io_maybe_wait_writable(0, IO.allocate, nil) }.should raise_error(IOError, "uninitialized stream") end + + it "can be interrupted" do + IOSpec.exhaust_write_buffer(@w_io) + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + + t = Thread.new do + @o.rb_io_maybe_wait_writable(0, @w_io, 10) + end + + Thread.pass until t.stop? + t.kill + t.join + + finish = Process.clock_gettime(Process::CLOCK_MONOTONIC) + (finish - start).should < 9 + end end end @@ -355,6 +372,21 @@ thr.join end + it "can be interrupted" do + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + + t = Thread.new do + @o.rb_io_maybe_wait_readable(0, @r_io, 10, false) + end + + Thread.pass until t.stop? + t.kill + t.join + + finish = Process.clock_gettime(Process::CLOCK_MONOTONIC) + (finish - start).should < 9 + end + it "raises an IOError if the IO is closed" do @r_io.close -> { @o.rb_io_maybe_wait_readable(0, @r_io, nil, false) }.should raise_error(IOError, "closed stream") @@ -438,6 +470,37 @@ it "raises an IOError if the IO is not initialized" do -> { @o.rb_io_maybe_wait(0, IO.allocate, IO::WRITABLE, nil) }.should raise_error(IOError, "uninitialized stream") end + + it "can be interrupted when waiting for READABLE event" do + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + + t = Thread.new do + @o.rb_io_maybe_wait(0, @r_io, IO::READABLE, 10) + end + + Thread.pass until t.stop? + t.kill + t.join + + finish = Process.clock_gettime(Process::CLOCK_MONOTONIC) + (finish - start).should < 9 + end + + it "can be interrupted when waiting for WRITABLE event" do + IOSpec.exhaust_write_buffer(@w_io) + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + + t = Thread.new do + @o.rb_io_maybe_wait(0, @w_io, IO::WRITABLE, 10) + end + + Thread.pass until t.stop? + t.kill + t.join + + finish = Process.clock_gettime(Process::CLOCK_MONOTONIC) + (finish - start).should < 9 + end end end