Bug #21166
openFiber Scheduler is unable to be interrupted by `IO#close`.
Description
Background¶
Ruby's IO#close
can cause IO#read
, IO#write
, IO#wait
, IO#wait_readable
and IO#wait_writable
to be interrupted with an IOError: stream closed in another thread
. For reference, IO#select
cannot be interrupted in this way.
r, w = IO.pipe
thread = Thread.new do
r.read(1)
end
Thread.pass until thread.status == "sleep"
r.close
thread.join
# ./test.rb:6:in 'IO#read': stream closed in another thread (IOError)
Problem¶
The fiber scheduler provides hooks for io_read
, io_write
and io_wait
which are used by IO#read
, IO#write
, IO#wait
, IO#wait_readable
and IO#wait_writable
, but those hooks are not interrupted when IO#close
is invoked. That is because rb_notify_fd_close
is not scheduler aware, and the fiber scheduler is unable to register itself into the "waiting file descriptor" list.
#!/usr/bin/env ruby
require 'async'
r, w = IO.pipe
thread = Thread.new do
Async do
r.wait_readable
end
end
Thread.pass until thread.status == "sleep"
r.close
thread.join
In this test program, rb_notify_fd_close
will incorrectly terminate the entire fiber scheduler thread:
#<Thread:0x00007faa5b161bf8 /home/samuel/Developer/socketry/io-event/test.rb:7 run> terminated with exception (report_on_exception is true):
/home/samuel/Developer/socketry/io-event/lib/io/event/selector/select.rb:470:in 'IO.select': closed stream (IOError)
from /home/samuel/Developer/socketry/io-event/lib/io/event/selector/select.rb:470:in 'block in IO::Event::Selector::Select#select'
from /home/samuel/Developer/socketry/io-event/lib/io/event/selector/select.rb:468:in 'Thread.handle_interrupt'
from /home/samuel/Developer/socketry/io-event/lib/io/event/selector/select.rb:468:in 'IO::Event::Selector::Select#select'
from /home/samuel/.gem/ruby/3.4.1/gems/async-2.23.0/lib/async/scheduler.rb:396:in 'Async::Scheduler#run_once!'
...
Solution¶
We need a mechanism to ensure fibers are treated the same as threads, and interrupted correctly. We do this by:
- Introducing
VALUE rb_thread_io_interruptible_operation(VALUE self, VALUE(*function)(VALUE), VALUE argument)
which allows us to execute a callback that may be interrupted. Internally, this registers the current execution context into the existingwaiting_fds
list. - We update all the relevant fiber scheduler hooks to use
rb_thread_io_interruptible_operation
, e.g.io_wait
,io_read
,io_write
and so on. - We introduce
VALUE rb_fiber_scheduler_fiber_interrupt(VALUE scheduler, VALUE fiber, VALUE exception)
which can be used to interrupt a fiber, e.g. with an IOError exception. -
rb_notify_fd_close
is modified to correctly interrupt fibers using the new rb_fiber_scheduler_fiber_interrupt` function.
See https://github.com/ruby/ruby/pull/12839 for the proposed implementation.