Skip to content
Open
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
19 changes: 17 additions & 2 deletions lib/async/scheduler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -309,10 +309,12 @@ def address_resolve(hostname)
# @parameter timeout [Float | Nil] The maximum time to wait, or if nil, indefinitely.
def io_wait(io, events, timeout = nil)
fiber = Fiber.current
expired = false

if timeout
# If an explicit timeout is specified, we expect that the user will handle it themselves:
timer = @timers.after(timeout) do
expired = true
fiber.transfer
end
elsif timeout = io.timeout
Expand All @@ -322,7 +324,13 @@ def io_wait(io, events, timeout = nil)
end
end

return @selector.io_wait(fiber, io, events)
loop do

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's slightly more efficient to use while true for these loops, or better yet, maybe something like until expired or something like that.

if result = @selector.io_wait(fiber, io, events)
return result
elsif expired
return nil
end
end
ensure
timer&.cancel!
end
Expand Down Expand Up @@ -415,7 +423,14 @@ def fiber_interrupt(fiber, exception)
# @returns [Process::Status] A process status instance.
# @asynchronous May be non-blocking..
def process_wait(pid, flags)
return @selector.process_wait(Fiber.current, pid, flags)
fiber = Fiber.current

loop do
status = @selector.process_wait(fiber, pid, flags)

# `false` indicates the wake-up was spurious, e.g. a stale {unblock}
return status unless status == false
end
end

# Wait for the specified IOs to become ready for the specified events.
Expand Down
40 changes: 40 additions & 0 deletions test/async/scheduler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,46 @@
end

with "#block" do
it "ignores stale wake-ups from previous blocking operations" do
input, output = IO.pipe
duration = nil

Sync do |parent|
queue = Thread::Queue.new

child = parent.async do |task|
begin
task.with_timeout(0.02) do
queue.pop
end
rescue Async::TimeoutError
# Expected - the item was pushed after the timeout already expired.
end

# The deferred wake-up from `queue.push` must not spuriously interrupt a subsequent IO operation:
duration = Async::Clock.measure do
input.wait_readable(0.02)
end
end

producer = parent.async do
sleep(0.01)
queue.push(:wakeup)
end

# Prevent the event loop from running until both the producer's sleep and the child's timeout are overdue, so that the wake-up from `queue.push` is still pending when the timeout fires:
Fiber.blocking{sleep(0.03)}

child.wait
producer.wait
end

expect(duration).to be >= 0.02
ensure
input&.close
output&.close
end

it "can block and unblock the scheduler after closing" do
scheduler = Async::Scheduler.new

Expand Down
Loading