Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions ext/io/event/selector/uring.c
Original file line number Diff line number Diff line change
Expand Up @@ -997,14 +997,13 @@ VALUE IO_Event_Selector_URing_io_pwrite(VALUE self, VALUE fiber, VALUE io, VALUE

static const int ASYNC_CLOSE = 1;

VALUE IO_Event_Selector_URing_io_close(VALUE self, VALUE io) {
VALUE IO_Event_Selector_URing_io_close(VALUE self, VALUE _descriptor) {
struct IO_Event_Selector_URing *selector = NULL;
TypedData_Get_Struct(self, struct IO_Event_Selector_URing, &IO_Event_Selector_URing_Type, selector);

// Ruby's fiber scheduler io_close hook may receive a raw Integer fd
// (observed on Ruby head/4.1) rather than an IO object.
int descriptor = RB_INTEGER_TYPE_P(io) ? RB_NUM2INT(io) : IO_Event_Selector_io_descriptor(io);

// Ruby's fiber scheduler `io_close` hook is invoked with a raw integer file descriptor (Ruby 4.0+); it does not pass the `IO` object.
int descriptor = RB_NUM2INT(_descriptor);

if (ASYNC_CLOSE) {
struct io_uring_sqe *sqe = io_get_sqe(selector);
io_uring_prep_close(sqe, descriptor);
Expand All @@ -1017,8 +1016,8 @@ VALUE IO_Event_Selector_URing_io_close(VALUE self, VALUE io) {
} else {
close(descriptor);
}

// We don't wait for the result of close since it has no use in pratice:
// We don't wait for the result of close since it has no use in practice:
return Qtrue;
}

Expand Down
22 changes: 22 additions & 0 deletions fixtures/io/event/test_scheduler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ module IO::Event
# end.resume
# ```
class TestScheduler
# Optional fiber scheduler hooks that we forward to the underlying selector. Mixed into the scheduler's singleton class only when the selector actually implements the corresponding method, so feature detection via `respond_to?` reflects the real backend (and Ruby falls back to its default behaviour otherwise).
module Forwarders
# Fiber scheduler hook for `IO#close`. Ruby invokes this with a raw integer file descriptor (Ruby 4.0+).
def io_close(descriptor)
@selector.io_close(descriptor)
end
end

def initialize(selector: nil, worker_pool: nil, maximum_worker_count: nil)
@selector = selector || ::IO::Event::Selector.new(Fiber.current)

Expand All @@ -42,6 +50,20 @@ def initialize(selector: nil, worker_pool: nil, maximum_worker_count: nil)
# Track the number of fibers that are blocked.
@blocked = 0
@blocking = {}

install_optional_forwarders(@selector)
end

private def install_optional_forwarders(selector)
forwarders = nil

Forwarders.instance_methods(false).each do |name|
next unless selector.respond_to?(name)
forwarders ||= Module.new
forwarders.define_method(name, Forwarders.instance_method(name))
end

singleton_class.include(forwarders) if forwarders
end

# @attribute [WorkerPool] The worker pool used for executing blocking operations.
Expand Down
25 changes: 25 additions & 0 deletions lib/io/event/debug/selector.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ module Debug
#
# You can enable this in the default selector by setting the `IO_EVENT_DEBUG_SELECTOR` environment variable. In addition, you can log all selector operations to a file by setting the `IO_EVENT_DEBUG_SELECTOR_LOG` environment variable. This is useful for debugging and understanding the behavior of the event loop.
class Selector
# Forwarders for optional selector hooks that not every backing selector implements (e.g. `io_close` is only provided by `URing`). Each method here is mixed into the wrapper's singleton class only when the wrapped selector actually defines a method of the same name, so feature detection via `respond_to?` continues to reflect the real backend.
module Forwarders
# Close a file descriptor, forwarded to the underlying selector. Ruby invokes this hook with a raw integer descriptor (Ruby 4.0+).
#
# @parameter descriptor [Integer] The raw file descriptor being closed.
def io_close(descriptor)
log("Closing file descriptor #{descriptor}")
@selector.io_close(descriptor)
end
end

# Wrap the given selector with debugging.
#
# @parameter selector [Selector] The selector to wrap.
Expand Down Expand Up @@ -40,6 +51,20 @@ def initialize(selector, log: nil)
end

@log = log

install_optional_forwarders(selector)
end

private def install_optional_forwarders(selector)
forwarders = nil

Forwarders.instance_methods(false).each do |name|
next unless selector.class.method_defined?(name)
forwarders ||= Module.new
forwarders.define_method(name, Forwarders.instance_method(name))
end

singleton_class.include(forwarders) if forwarders
end

# The idle duration of the underlying selector.
Expand Down
40 changes: 18 additions & 22 deletions test/io/event/selector/io_close.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,21 @@

require "io/event"
require "io/event/selector"
require "io/event/debug/selector"

# Ruby invokes the `io_close` fiber-scheduler hook with a raw integer file descriptor (Ruby 4.0+, see `rb_fiber_scheduler_io_close` in CRuby). Verify each selector that opts into the hook handles that contract.
IOClose = Sus::Shared("io_close") do
it "can close an IO object" do
it "can close a raw file descriptor" do
selector = subject.new(Fiber.current)
next unless selector.respond_to?(:io_close)

input, output = IO.pipe

begin
expect(selector.io_close(input)).to be_truthy
ensure
input.close rescue nil
output.close rescue nil
selector.close
end
end

# Ruby head/4.1 passes a raw Integer fd to the io_close scheduler hook
# instead of an IO object. Verify we handle both forms without raising.
it "can close a raw Integer fd" do
selector = subject.new(Fiber.current)
next unless selector.respond_to?(:io_close)

input, output = IO.pipe

# Hand ownership of the fd to io_close so Ruby won't double-close it.
# Hand ownership of the fd to `io_close` so Ruby won't double-close it.
input.autoclose = false
fd = input.fileno
descriptor = input.fileno

begin
expect(selector.io_close(fd)).to be_truthy
expect(selector.io_close(descriptor)).to be_truthy
ensure
input.close rescue nil
output.close rescue nil
Expand All @@ -47,8 +31,20 @@
IO::Event::Selector.constants.each do |name|
klass = IO::Event::Selector.const_get(name)
next unless klass.respond_to?(:new)
next unless klass.method_defined?(:io_close)

describe(klass, unique: name) do
it_behaves_like IOClose
end

# `Debug::Selector` should transparently forward `io_close` to any wrapped selector that implements it (see `Forwarders`). The shared examples build the selector via `subject.new(loop)`, so we hand them a thin factory that closes over the underlying selector class.
debug_class = Class.new do
define_singleton_method(:new) do |loop|
IO::Event::Debug::Selector.new(klass.new(loop))
end
end

describe(debug_class, unique: "Debug(#{name})") do
it_behaves_like IOClose
end
end
Loading