Project

General

Profile

Bug #21166

Updated by ioquatix (Samuel Williams) 4 days ago

## 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. implementation.

Back