« Back to Index

Spawning Actors in Celluloid

View original Gist on GitHub

Spawning Actors in Celluloid.md

// Taken from http://uberfork.com/post/30510463110/spawning-actors-in-celluloid

There are four built-in classes that help managing Actor instances:

  1. Actor
  2. Supervisor
  3. PoolManager
  4. SupervisionGroup
class Cat
  include Celluloid
  class DeadCatError < StandardError; end
 
  def initialize name=nil
    @name = name
    @lives_left = 9
  end
  
  def spray
    † if @lives_left == 0
 
    # spraying is dangerous
    # http://www.youtube.com/watch?v=uIbkLjjlMV8 
    @lives_left -= 1
  end
 
  def †
    who = if name
      "known as '#{@name}'"
    else
      "unknown vagrant"
    end
    raise DeadCatError, "Yet another cat in heaven.. (#{who})"
  end
end

// Each time a cat sprays it will loose one of its lives. When a cat sprays too often, it will just die*.

The simpliest example to use an Actor is just to call Actor.new, like using a plain Ruby object. This works totally fine, until the Actor thread dies, because then it won’t come back by itself. Instead it is up to you to catch such a case and deal with it.

# instantiate a cat object named 'Garfield' within its own actor thread
cat = Cat.new 'Garfield'
 
# blocking (the main thread waits until the cat has finished..)
cat.spray
 
# non-blocking
cat.spray!
 
# cats do what cats do
7.times { cat.spray } 
 
# this will kill the actor thread (because spraying ain't petty crime!)
# however, when using the non-blocking call the main thread won't receive
# any exceptions raised within the actor thread (even a dead actor call is not
# harmful)
cat.spray!
 
# false, the actor thread died
puts cat.alive?

When you don’t like to loose your Actor instance and you want to have it respawn automatically you can just use the Actor.supervise method. This will spawn a second thread that respawns the Actor thread every time it dies.

# here there are two threads, one for the actor and one for the supervisor, that
# will respawn the actor if the actor dies
Cat.supervise_as :cat, 'Garfield'
 
cats_died = 0
50.times do
  begin
    Celluloid::Actor[:cat].spray
  rescue Cat::DeadCatError => e
    cats_died += 1
    sleep 0.05 # wait a bit to let respawn take place
  end
end
 
# when cat actors die, the supervisor will respawn..
puts cats_died # more than 1

Sometimes you may not want just one Actor instance but whole group. This can be useful when you want to gain performance by parallelization. You can do this easily by using the Actor.pool method to spawn a PoolManager instance. The PoolManager will then spawn multiple Actor threads of the same Actor class. Similar to using the Supervisor the PoolManager will also respawn Actor threads if they fail. For maximum performance the default pool size matches the size of CPU cores your system provides.

# a pool of actors spawns one additional thread for the pool manager. the pool
# manager then spawns (and respawns in case of failure) the actor threads.
# per default the pool size (the number of actor threads) is equal to the number
# of cpu cores.
gang_of_cats = Cat.pool size: 2
 
cats_died = 0
50.times do
  begin
    gang_of_cats.spray
  rescue Cat::DeadCatError => e
    cats_died += 1
    sleep 0.05
  end
end
 
puts cats_died # more than 1

Finally there is the SupervisionGroup. In contrast to the previous examples the SupervisionGroup is designed to be used as a base class. It offers a declarative approach to combine a set of actors to a group together and make them depend on each other. So if one member of the group dies, the whole group will die. Of course, you can also add whole pool of Actors to the group. Hereby the group would only become dependent on the PoolManager itself. So when the PoolManager dies, the group dies. But when a member of the pool dies, it is respawned by the PoolManager. The following examples illustrates this.

# define a gang of cats with a leader
class CatGangWithLeader < Celluloid::SupervisionGroup
  # this
  supervise Cat, as: :alpha_leader, args: 'Garfield'
  supervise Cat, as: :gang_of_cats, size: 2, method: 'pool'
end
 
# this starts 5 threads in total:
#   - 2x for the alpha leader (supervisor + actor)
#   - 3x for the gang member (supervisor + 2x actor)
CatGangWithLeader.run!
sleep 0.05
 
alpha_leader_died = 0
member_died = 0
 
200.times do
  begin
    # most of the time a member will be used
    if rand > 0.1
      Celluloid::Actor[:gang_of_cats].spray
    else
      Celluloid::Actor[:alpha_leader].spray
    end
 
  # as soon as the alpha leader dies, the whole supervision group is dead
  # then this error will happen and we can stop the loop
  rescue Celluloid::DeadActorError => e
    break
  rescue Cat::DeadCatError => e
    if e.message =~ /Garfield/
      alpha_leader_died += 1
    else
      member_died += 1
    end
    sleep 0.01
  end
end
 
# this is a strange or interesting behaviour. death of a vagrant (a pool member)
# usally happens more than 1 time (max. 20). but as soon as the leader dies, the 
# whole supervision group dies and the loop ends. so the typical ratio is 9:1.
puts "The gang members died #{member_died} times.\n"\
     "The leader (Garfield) died #{alpha_leader_died} times."