[ruby-core:121220] [Ruby master Bug#21166] Fiber Scheduler is unable to be interrupted by `IO#close`.
From:
"ioquatix (Samuel Williams) via ruby-core" <ruby-core@...>
Date:
2025-03-02 23:54:11 UTC
List:
ruby-core #121220
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/
______________________________________________
ruby-core mailing list -- ruby-core@ml.ruby-lang.org
To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org
ruby-core info -- https://ml.ruby-lang.org/mailman3/lists/ruby-core.ml.ruby-lang.org/