Line by line explanation of Ruby 3 Ractor - worker pool implementation
Tags: ruby, distributedsystems, ractor
Published: 2021-05-31 18:02:17

Ruby 3 has an exciting update about its concurrency support: Ractors.

First, please read https://github.com/ruby/ruby/blob/master/doc/ractor.md for "What it is" and "What it can do".

If you're trying to achieve some sort of concurrent programming and ❤️ Ruby language, you should definitely give it a try.

## Worker pool's implementation in Ractor

Below's the copy of the code explained in Ractor's official documentation. It's slightly modified to provide more granular explanation on its behaviour.

I've also annotated the code heavily, so that you can read the intent of each line with me.

Let's decipher this line by line, so that you can properly assess what you can do with Ractor.

For the basic API of Ractor, please refer to https://ruby-doc.org/core-3.0.0/Ractor.html 


### Code with line-by-line explanation

```ruby
# This example computes if the given numbers is a prime number or not, using 10 parallel workers. 

# to make `num.prime?` possible.
require 'prime'

# this ractor will keep listening to the sent messages, and yield them to whomever want to take the value.
pipe = Ractor.new do
  loop do
    Ractor.yield Ractor.receive
  end
end

# Let's check the prime numbers from 1 upto 1000
N = 1000
# we'll use 10 workers to do this work.
RN = 10
# worker ractors initialization.
# each worker ractor takes pipe as its sharable object (Ractor has some synchronization mechanism according to [this doc](https://github.com/ruby/ruby/blob/master/doc/ractor.md#shareable-objects)) 
# then, each worker Ractor reads the input through the pipe (multiplexing) to utilize 10 workers
workers = (1..RN).map.with_index do |i|
  Ractor.new pipe, i do |pipe, i|
    # Ractor#take is a blocking call, and waits till pipe yields something
    while n = pipe.take
      # Worker, then in turn, computes something (expensive :) and yields to whomever willing to listen to this ractor
      Ractor.yield ["worker ##{i}}", n, n.prime?]
    end
  end
end

# sending 1000 numbers to the pipe, worker ractors are ready to consume by now.
(1..N).each{|i|
  pipe << i
}

# main process (main process itself is a running Ractor) calls
# Ractor#select, which can listen to a list of ractors
# because it's called (1..N) times, it'll hit all of the computed values by the time pp prints them.
pp (1..N).map{
  _r, (wid, n, b) = Ractor.select(*workers)
  [wid, n, b]
}.sort_by{|(wid, n, b)| n}
```

## Conclusion

I hope this illustration together with https://github.com/ruby/ruby/blob/master/doc/ractor.md has given you a better understanding of what you can do with Ractor.

In my opinion, Ruby finally providing basic primitives for doing actor based concurrency is very exciting.

You won't have to focus too much on the performance gain, or even the current status of all the APIs (they explicitly state its an experimental feature, as of Ruby 3.0.1).

still, I'm confident that we can already start learning how to write distributed systems all within the favourite language of our choice :)

I hope you'll dig a bit deeper by yourself, as well! Happy coding 🙌