Skip to content

Commit a838307

Browse files
committed
Repair IO#wait_readable, IO#wait_writable, IO#wait to be interruptible
* Fixes #3504
1 parent 2651959 commit a838307

File tree

8 files changed

+150
-10
lines changed

8 files changed

+150
-10
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Bug fixes:
1111
* Fix `rb_gc_register_mark_object()` for `Float` and bignum values (#3502, @eregon, @andrykonchin).
1212
* Fix parsing literal floats when the locale does not use `.` for the decimal separator (e.g. `LANG=fr_FR.UTF-8`) (#3512, @eregon).
1313
* 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).
14+
* Repair `IO#{wait_readable,wait_writable,wait}` to be interruptible (#3504, @andrykonchin).
1415

1516
Compatibility:
1617

Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
module IOWaitSpec
1+
module IOSpec
22
def self.exhaust_write_buffer(io)
33
written = 0
44
buf = " " * 4096
55

6-
begin
6+
while true
77
written += io.write_nonblock(buf)
8-
rescue Errno::EAGAIN, Errno::EWOULDBLOCK
9-
return written
10-
end while true
8+
end
9+
rescue Errno::EAGAIN, Errno::EWOULDBLOCK
10+
written
1111
end
1212
end

spec/ruby/library/io-wait/wait_readable_spec.rb

+19
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,23 @@
2424
it "waits for the IO to become readable with the given large timeout" do
2525
@io.wait_readable(365 * 24 * 60 * 60).should == @io
2626
end
27+
28+
it "can be interrupted" do
29+
rd, _wr = IO.pipe
30+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
31+
32+
t = Thread.new do
33+
rd.wait_readable(10)
34+
end
35+
36+
Thread.pass until t.stop?
37+
t.kill
38+
t.join
39+
40+
finish = Process.clock_gettime(Process::CLOCK_MONOTONIC)
41+
(finish - start).should < 9
42+
ensure
43+
rd.close
44+
_wr.close
45+
end
2746
end

spec/ruby/library/io-wait/wait_spec.rb

+35-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
require_relative '../../spec_helper'
2-
require_relative 'fixtures/classes'
2+
require_relative '../../fixtures/io'
33

44
ruby_version_is ''...'3.2' do
55
require 'io/wait'
@@ -55,7 +55,7 @@
5555
end
5656

5757
it "waits for the WRITABLE event to be ready" do
58-
written_bytes = IOWaitSpec.exhaust_write_buffer(@w)
58+
written_bytes = IOSpec.exhaust_write_buffer(@w)
5959
@w.wait(IO::WRITABLE, 0).should == nil
6060

6161
@r.read(written_bytes)
@@ -67,7 +67,7 @@
6767
end
6868

6969
it "returns nil when the WRITABLE event is not ready during the timeout" do
70-
IOWaitSpec.exhaust_write_buffer(@w)
70+
IOSpec.exhaust_write_buffer(@w)
7171
@w.wait(IO::WRITABLE, 0).should == nil
7272
end
7373

@@ -92,14 +92,45 @@
9292
end
9393

9494
it "changes thread status to 'sleep' when waits for WRITABLE event" do
95-
written_bytes = IOWaitSpec.exhaust_write_buffer(@w)
95+
IOSpec.exhaust_write_buffer(@w)
9696

9797
t = Thread.new { @w.wait(IO::WRITABLE, 10) }
9898
sleep 1
9999
t.status.should == 'sleep'
100100
t.kill
101101
t.join # Thread#kill doesn't wait for the thread to end
102102
end
103+
104+
it "can be interrupted when waiting for READABLE event" do
105+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
106+
107+
t = Thread.new do
108+
@r.wait(IO::READABLE, 10)
109+
end
110+
111+
Thread.pass until t.stop?
112+
t.kill
113+
t.join # Thread#kill doesn't wait for the thread to end
114+
115+
finish = Process.clock_gettime(Process::CLOCK_MONOTONIC)
116+
(finish - start).should < 9
117+
end
118+
119+
it "can be interrupted when waiting for WRITABLE event" do
120+
IOSpec.exhaust_write_buffer(@w)
121+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
122+
123+
t = Thread.new do
124+
@w.wait(IO::WRITABLE, 10)
125+
end
126+
127+
Thread.pass until t.stop?
128+
t.kill
129+
t.join # Thread#kill doesn't wait for the thread to end
130+
131+
finish = Process.clock_gettime(Process::CLOCK_MONOTONIC)
132+
(finish - start).should < 9
133+
end
103134
end
104135

105136
context "[timeout, mode] passed" do

spec/ruby/library/io-wait/wait_writable_spec.rb

+21
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require_relative '../../spec_helper'
2+
require_relative '../../fixtures/io'
23

34
ruby_version_is ''...'3.2' do
45
require 'io/wait'
@@ -17,4 +18,24 @@
1718
# Represents one year and is larger than a 32-bit int
1819
STDOUT.wait_writable(365 * 24 * 60 * 60).should == STDOUT
1920
end
21+
22+
it "can be interrupted" do
23+
_rd, wr = IO.pipe
24+
IOSpec.exhaust_write_buffer(wr)
25+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
26+
27+
t = Thread.new do
28+
wr.wait_writable(10)
29+
end
30+
31+
Thread.pass until t.stop?
32+
t.kill
33+
t.join
34+
35+
finish = Process.clock_gettime(Process::CLOCK_MONOTONIC)
36+
(finish - start).should < 9
37+
ensure
38+
_rd.close unless _rd.closed?
39+
wr.close unless wr.closed?
40+
end
2041
end

spec/ruby/optional/capi/io_spec.rb

+63
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require_relative 'spec_helper'
2+
require_relative '../../fixtures/io'
23

34
load_extension('io')
45

@@ -279,6 +280,22 @@
279280
it "raises an IOError if the IO is not initialized" do
280281
-> { @o.rb_io_maybe_wait_writable(0, IO.allocate, nil) }.should raise_error(IOError, "uninitialized stream")
281282
end
283+
284+
it "can be interrupted" do
285+
IOSpec.exhaust_write_buffer(@w_io)
286+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
287+
288+
t = Thread.new do
289+
@o.rb_io_maybe_wait_writable(0, @w_io, 10)
290+
end
291+
292+
Thread.pass until t.stop?
293+
t.kill
294+
t.join
295+
296+
finish = Process.clock_gettime(Process::CLOCK_MONOTONIC)
297+
(finish - start).should < 9
298+
end
282299
end
283300
end
284301

@@ -355,6 +372,21 @@
355372
thr.join
356373
end
357374

375+
it "can be interrupted" do
376+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
377+
378+
t = Thread.new do
379+
@o.rb_io_maybe_wait_readable(0, @r_io, 10, false)
380+
end
381+
382+
Thread.pass until t.stop?
383+
t.kill
384+
t.join
385+
386+
finish = Process.clock_gettime(Process::CLOCK_MONOTONIC)
387+
(finish - start).should < 9
388+
end
389+
358390
it "raises an IOError if the IO is closed" do
359391
@r_io.close
360392
-> { @o.rb_io_maybe_wait_readable(0, @r_io, nil, false) }.should raise_error(IOError, "closed stream")
@@ -438,6 +470,37 @@
438470
it "raises an IOError if the IO is not initialized" do
439471
-> { @o.rb_io_maybe_wait(0, IO.allocate, IO::WRITABLE, nil) }.should raise_error(IOError, "uninitialized stream")
440472
end
473+
474+
it "can be interrupted when waiting for READABLE event" do
475+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
476+
477+
t = Thread.new do
478+
@o.rb_io_maybe_wait(0, @r_io, IO::READABLE, 10)
479+
end
480+
481+
Thread.pass until t.stop?
482+
t.kill
483+
t.join
484+
485+
finish = Process.clock_gettime(Process::CLOCK_MONOTONIC)
486+
(finish - start).should < 9
487+
end
488+
489+
it "can be interrupted when waiting for WRITABLE event" do
490+
IOSpec.exhaust_write_buffer(@w_io)
491+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
492+
493+
t = Thread.new do
494+
@o.rb_io_maybe_wait(0, @w_io, IO::WRITABLE, 10)
495+
end
496+
497+
Thread.pass until t.stop?
498+
t.kill
499+
t.join
500+
501+
finish = Process.clock_gettime(Process::CLOCK_MONOTONIC)
502+
(finish - start).should < 9
503+
end
441504
end
442505
end
443506

src/main/ruby/truffleruby/core/posix.rb

+3
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,9 @@ def self.attach_function_eagerly(native_name, argument_types, return_type,
242242
# We should capture the non-lazy method
243243
attach_function_eagerly :poll, [:pointer, :nfds_t, :int], :int, LIBC, false, :poll, self
244244
POLL = method(:poll)
245+
246+
attach_function_eagerly :truffleposix_poll_single_fd, [:int, :int, :int], :int, LIBTRUFFLEPOSIX, false, :truffleposix_poll_single_fd, self
247+
POLL_SINGLE_FD = method(:truffleposix_poll_single_fd)
245248
end
246249
end
247250

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,9 @@ def self.poll(io, event_mask, timeout)
265265
status = Primitive.thread_set_status(Thread.current, :sleep)
266266

267267
begin
268-
returned_events = Truffle::POSIX.truffleposix_poll_single_fd(Primitive.io_fd(io), event_mask, remaining_timeout)
268+
returned_events = Primitive.thread_run_blocking_nfi_system_call(
269+
Truffle::POSIX::POLL_SINGLE_FD,
270+
[Primitive.io_fd(io), event_mask, remaining_timeout])
269271
ensure
270272
Primitive.thread_set_status(Thread.current, status)
271273
end

0 commit comments

Comments
 (0)