Project

General

Profile

Actions

Bug #21166

open

Fiber Scheduler is unable to be interrupted by `IO#close`.

Added by ioquatix (Samuel Williams) 4 days ago. Updated 3 days ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:121215]

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

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.

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.

Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like0