[ruby-core:121441] [Ruby Bug#21166] Fiber Scheduler is unable to be interrupted by `IO#close`.
From:
"ioquatix (Samuel Williams) via ruby-core" <ruby-core@...>
Date:
2025-03-25 20:36:47 UTC
List:
ruby-core #121441
Issue #21166 has been updated by ioquatix (Samuel Williams).
After discussing this PR with @ko1:
1. waiting_fds -> ractor local
2. document fiber_interrupt can be called in another thread
3. `rb_thread_io_interruptible_operation(func)` ->
`rb_thread_io_with_waiting_file_descriptor(func)`
(Internal function, not public API)
4. Update the PR for per-IO waiting_fd (separate follow up).
----------------------------------------
Bug #21166: Fiber Scheduler is unable to be interrupted by `IO#close`.
https://bugs.ruby-lang.org/issues/21166#change-112426
* Author: ioquatix (Samuel Williams)
* Status: Open
* Assignee: ioquatix (Samuel Williams)
* 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
We need a mechanism to ensure fibers are treated the same as threads, and interrupted correctly. We do this by:
1. 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 existing `waiting_fds` list.
2. 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.
3. 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.
4. `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.
--
https://bugs.ruby-lang.org/
______________________________________________
ruby-core mailing list -- [email protected]
To unsubscribe send an email to [email protected]
ruby-core info -- https://ml.ruby-lang.org/mailman3/lists/ruby-core.ml.ruby-lang.org/