diff --git a/lib/async/scheduler.rb b/lib/async/scheduler.rb index f128896d..ffdd9e17 100644 --- a/lib/async/scheduler.rb +++ b/lib/async/scheduler.rb @@ -307,6 +307,7 @@ def address_resolve(hostname) # @parameter io [IO] The IO object to wait on. # @parameter events [Integer] The events to wait for, e.g. `IO::READABLE`, `IO::WRITABLE`, etc. # @parameter timeout [Float | Nil] The maximum time to wait, or if nil, indefinitely. + # @returns [Integer | false] The subset of events that are ready, or `false` if the timeout expires. def io_wait(io, events, timeout = nil) fiber = Fiber.current expired = false @@ -326,8 +327,8 @@ def io_wait(io, events, timeout = nil) # A selector wait may return a falsy result when the fiber is resumed without the requested IO becoming ready. For example, a deferred unblock from a previous blocking operation may arrive after the fiber has moved on to this wait. Retry these stale or spurious wake-ups without resetting the original timer. until result = @selector.io_wait(fiber, io, events) - # If the original timer resumed the fiber, the falsy result represents the timeout rather than a spurious wake-up: - return nil if expired + # If the original timer resumed the fiber, the false result represents the timeout rather than a spurious wake-up: + return false if expired end return result diff --git a/releases.md b/releases.md index dc0ff4bf..158857c0 100644 --- a/releases.md +++ b/releases.md @@ -1,5 +1,9 @@ # Releases +## Unreleased + + - Fixed `Scheduler#io_wait` returning `nil` instead of `false` when an explicit timeout expired. Native callers such as `Socket#connect` with `connect_timeout:` distinguish a timeout by checking for `false`, so the `nil` caused `TypeError: no implicit conversion from nil to integer` instead of the intended `IO::TimeoutError`. + ## v2.45.0 - Fixed scheduler I/O and process waits returning prematurely after stale or interrupted wake-ups. I/O waits now preserve their original timeout, while blocking process waits retry and non-blocking `Process::WNOHANG` waits still return `nil`. diff --git a/test/async/scheduler/io.rb b/test/async/scheduler/io.rb index 3a7389dd..cc2aa8fa 100644 --- a/test/async/scheduler/io.rb +++ b/test/async/scheduler/io.rb @@ -24,6 +24,18 @@ s2.close end + it "returns false from io_wait when an explicit timeout expires" do + s1, s2 = Socket.pair :UNIX, :STREAM, 0 + + # Native callers (e.g. `wait_connectable` in `Socket#connect` with `connect_timeout:`) distinguish a timeout from a readiness mask by checking for `false`, matching the non-scheduler `rb_io_wait`: + result = reactor.io_wait(s1, IO::READABLE, 0.001) + + expect(result).to be == false + ensure + s1.close + s2.close + end + it "can read a single character" do s1, s2 = Socket.pair :UNIX, :STREAM, 0