6 Comments

Making Refs in Ruby Using Celluloid Actors

I am currently working on a project developing a JRuby server process. The server is in constant communication with multiple front-end clients and back-end processes. The development team decided to make use of the Celluloid Actor library so we could use concurrent objects as a way to deal with the complexity that comes with developing a multithreaded program.

Heavily inspired by some Rich Hickey talks and our experiments with Clojure, we have been making a concerted effort to use immutable data structures and to keep the application state from being spread out across many objects. To this end, we have been using Hamster for its immutable data structures.

But how to manage the immutable application state? Again, turning to Clojure for inspiration, we have been experimenting with using a Celluloid Actor as a kind of poor man’s Ref:

… transactional references (Refs) ensure safe shared use of mutable storage locations via a software transactional memory (STM) system. Refs are bound to a single storage location for their lifetime, and only allow mutation of that location to occur within a transaction.

We aren’t trying to write an STM system, but using a Celluloid Actor to wrap an immutable data structure can cheaply provide a way to avoid concurrent modification without having to write any locking code yourself.

The implementation we have been using is an Actor with two methods, current and update. The current method just returns the current value to the caller. It’s immutable, so no worries about someone modifying the application state unexpectedly or concurrently.

The update method is the transaction. It yields the current value to a block and commits the new value that is returned from the block. The update method is marked as exclusive to prevent the possibility of multiple transactions occurring simultaneously. (Supporting simultaneous transactions is certainly something that could be done, but we haven’t had the need for it yet.)

Here is an example implementation of a Ref Actor:

class Ref
  include Celluloid
 
  def initialize(data = Hamster.hash)
    @data = data
  end
 
  def update
    old_data = @data
    begin
      @data = yield old_data
    rescue => ex
      puts "Error occurred in Ref#update: #{ex.message}"
    end
    @data
  end
  exclusive :update
 
  def current
    @data
  end
end

This could be used as follows:

ref = Ref.new
 
ref.update do |hash|
  hash.put(:temperature, 27).put(:unit, "F")
end
 
data = ref.current
puts "The current temperature is: #{data[:temperature]} #{data[:unit]}"

If multiple threads are trying to update the Ref at the same time, they will get queued up in the Actor’s mailbox, with only one thread at a time being able to update the value.

This simple example does not include some of the nicer features of a Clojure Ref like validation and the ability to be notified when the value of the Ref is updated. But it wouldn’t be much work to add either of these things to the Ref Actor.

So far this experiment has been working very well, and I suspect we will be using more of these Ref-like Actors as the project continues.