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
5 changes: 3 additions & 2 deletions lib/async/scheduler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions releases.md
Original file line number Diff line number Diff line change
@@ -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`.
Expand Down
12 changes: 12 additions & 0 deletions test/async/scheduler/io.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading