Project

General

Profile

Actions

Feature #19472

open

Ractor::Selector to wait multiple ractors

Added by ko1 (Koichi Sasada) almost 2 years ago. Updated 8 months ago.

Status:
Assigned
Target version:
[ruby-core:112656]

Description

This ticket propose Ractor::Selector API to wait multiple ractor events.

Now, if we want to wait for taking from r1, r2 and r3, we can use Ractor.select() like that.

r, v = Ractor.select(r1, r2, r3)
p "taking an object #{v} from #{r}"

With proposed Ractor::Selector API, we can write the following:

selector = Ractor::Selector.new(r1, r2) # make a waiting set with r1 and r2
selector.add(r3) # we can add r3 to the waiting set after that.
selector.add(r4)
selector.remove(r4) # we can remove r4 from the waiting set.

r, v = selector.wait
p "taking an object #{v} from #{r}"
  • Ractor::Selector.new(*ractors): creates a selector.
  • Ractor::Selector#add(r): adds r to the waiting set.
  • Ractor::Selector#remove(r): removes r from the waiting set.
  • Ractor::Selector#clear: remove all ractors from the waiting set.
  • Ractor::Selector#empty?: returns if the waiting set is empty or not.
  • Ractor::Selector#wait: waits for the ractor events from the waiting set.

https://github.com/ruby/ruby/blob/master/ractor.rb#L380

The advantages comparing with Ractor.select are:

  • (1) (API design) We can preset the waiting set before waiting. Providing unified way to manage a waiting set seems better.
  • (2) (Performance) It is lighter than passing an array object to the Ractor.select(*rs) if rs is bigger and bigger.

For (2), it is important to supervise thousands of ractors.

Ractor::Selector#wait also has additional features:

  • wait(receive: true) also waits receiving.
    • Ractor.select(*rs, Ractor.current) does same, but I believe receive: true keyword is more direct to understand.
  • wait(yield_value: obj, move: true/false) also waits yielding.
    • Same as Ractor.select(yield_value: obj, move: true/false)
  • If a ractor r is closing, then #wait removes r automatically.
  • If there is no waiting ractors, it raises an exception (now Ractor::Error is raised but it should be a better exception class)

With automatic removing, we can write the code to wait n tasks.

rs = n.times.map{ Ractor.new{ do_task } }
selector = Ractor::Selector.new(*rs)

loop do
  r, v = selector.wait
  handle_answers(r, v)
rescue Ractor::Error
  p :all_tasks_done
end

Without auto removing, we can write the following code.

rs = n.times.map{ Ractor.new{ do_task } }
selector = Ractor::Selector.new(*rs)

loop do
  r, v = selector.wait
  handle_answers(r, v)
rescue Ractor::ClosedError => e
  selector.remove e.ractor
rescue Ractor::Error
  p :all_tasks_done
end

# or on this case worker ractors only yield one value (at exit) so the following code works as well.

loop do
  r, v = selector.wait
  handle_answers(r, v)
  selector.remove r
rescue Ractor::Error
  p :all_tasks_done
end

I already merged it but I want to discuss about the spec.

Discussion:

  • The name Selector is acceptable?
  • Auto-removing seems convenient but it can hide the behavior.
    • allow auto-removing
    • allow auto-removing as configurable option
      • per ractor or per selector
      • which is default?
    • disallow auto-removing
  • What happens on no taking ractors
    • raise an exception (which exception?)
    • return nil simply

maybe and more...

Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like1Like0Like0Like0Like0Like0