
Issue #21166 has been updated by ioquatix (Samuel Williams).
when you can accomplish the same thing by allowing the io operation to be interrupted by a close in every case like the regular thread scheduler does
Because the point at which IO operations become interruptible is not the entire operation, but usually the point at where the fiber yields back to the event loop, e.g. <https://github.com/socketry/io-event/pull/130/files#diff-3e7a68b220a9360ead1d2e7a5ec23e7d36de591eab138721efdc61b565fc5194R552> - adding interrupts around the entire operation is unlikely to be safe nor desirable. Maybe you can propose in more detail how your suggestion would work. I suppose you are suggesting to wrap the scheduler code in `rb_fiber_scheduler_io_wait` and so on? ---------------------------------------- Bug #21166: Fiber Scheduler is unable to be interrupted by `IO#close`. https://bugs.ruby-lang.org/issues/21166#change-112162 * Author: ioquatix (Samuel Williams) * Status: Open * Backport: 3.1: UNKNOWN, 3.2: UNKNOWN, 3.3: UNKNOWN, 3.4: UNKNOWN ---------------------------------------- ## 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. ```ruby 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. ```ruby #!/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 This PR introduces some new functions: - `VALUE rb_io_interruptible_operation(VALUE self, VALUE(*function)(VALUE), VALUE argument)` for wrapping user IO operations so they can be interrupted. - `IO#interruptable_operation(&block)` the same as above. - `VALUE rb_fiber_scheduler_fiber_interrupt(VALUE scheduler, VALUE fiber, VALUE exception)` for interrupting a specific fiber on the fiber scheduler. `rb_notify_fd_close` is modified so that it is fiber scheduler aware and uses `rb_fiber_scheduler_fiber_interrupt` to interrupt a fiber. In addition, we also change the internal `struct waiting_fd` to track the `rb_execution_context_t` rather than just the `rb_thread_t` instance, so that we can correctly wake up either the waiting thread or fiber. The public interface `rb_io_interruptible_operation` and `IO#interruptible_operation` are introduced so that the scheduler implementation can wrap IO operations that should be interruptible, e.g. ```ruby Fiber.schedule do io.interruptible_operation do io.wait_readable end end # Will interrupt above fiber: io.close ``` See <https://github.com/ruby/ruby/pull/12585> for the proposed implementation and <https://github.com/socketry/io-event/pull/130> for example of how `io-event` gem uses both the C and Ruby interfaces. -- https://bugs.ruby-lang.org/