Skip to content

Commit

Permalink
Repair IO#wait_readable, IO#wait_writable, IO#wait to be interruptible
Browse files Browse the repository at this point in the history
* Fixes #3504
  • Loading branch information
andrykonchin committed May 10, 2024
1 parent 2651959 commit a838307
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Bug fixes:
* Fix `rb_gc_register_mark_object()` for `Float` and bignum values (#3502, @eregon, @andrykonchin).
* Fix parsing literal floats when the locale does not use `.` for the decimal separator (e.g. `LANG=fr_FR.UTF-8`) (#3512, @eregon).
* Fix `IO#{read_nonblock,readpartial,sysread}`, `BasicSocket#{recv,recv_nonblock}`, `{Socket,UDPSocket}#recvfrom_nonblock`, `UnixSocket#recvfrom` and preserve a provided buffer's encoding (#3506, @andrykonchyn).
* Repair `IO#{wait_readable,wait_writable,wait}` to be interruptible (#3504, @andrykonchin).

Compatibility:

Expand Down
Original file line number Diff line number Diff line change
@@ -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
19 changes: 19 additions & 0 deletions spec/ruby/library/io-wait/wait_readable_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
39 changes: 35 additions & 4 deletions spec/ruby/library/io-wait/wait_spec.rb
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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)
Expand All @@ -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

Expand All @@ -92,14 +92,45 @@
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
t.status.should == 'sleep'
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
Expand Down
21 changes: 21 additions & 0 deletions spec/ruby/library/io-wait/wait_writable_spec.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require_relative '../../spec_helper'
require_relative '../../fixtures/io'

ruby_version_is ''...'3.2' do
require 'io/wait'
Expand All @@ -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
63 changes: 63 additions & 0 deletions spec/ruby/optional/capi/io_spec.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require_relative 'spec_helper'
require_relative '../../fixtures/io'

load_extension('io')

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions src/main/ruby/truffleruby/core/posix.rb
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,9 @@ def self.attach_function_eagerly(native_name, argument_types, return_type,
# We should capture the non-lazy method
attach_function_eagerly :poll, [:pointer, :nfds_t, :int], :int, LIBC, false, :poll, self
POLL = method(:poll)

attach_function_eagerly :truffleposix_poll_single_fd, [:int, :int, :int], :int, LIBTRUFFLEPOSIX, false, :truffleposix_poll_single_fd, self
POLL_SINGLE_FD = method(:truffleposix_poll_single_fd)
end
end

Expand Down
4 changes: 3 additions & 1 deletion src/main/ruby/truffleruby/core/truffle/io_operations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,9 @@ def self.poll(io, event_mask, timeout)
status = Primitive.thread_set_status(Thread.current, :sleep)

begin
returned_events = Truffle::POSIX.truffleposix_poll_single_fd(Primitive.io_fd(io), event_mask, remaining_timeout)
returned_events = Primitive.thread_run_blocking_nfi_system_call(
Truffle::POSIX::POLL_SINGLE_FD,
[Primitive.io_fd(io), event_mask, remaining_timeout])
ensure
Primitive.thread_set_status(Thread.current, status)
end
Expand Down

0 comments on commit a838307

Please sign in to comment.