Exclusive

Hinrik Örn Sigurðsson edited this page Sep 12, 2013 · 16 revisions

NOTE: Exclusive mode is a power user feature that can potentially lead to deadlocks if used improperly. Use at your own risk!

By default, Celluloid will pipeline method execution, i.e. Celluloid will continue processing incoming method requests even if the methods that are currently executing are blocked on things like requests to other actors, timers, or signals. This behavior is known as ATOM Mode.

This pipelining prevents most deadlocks at the cost of a loss of guarantees around ordering. If you absolutely must enforce a particular ordering of events, you can use the exclusive method within Celluloid to enable Erlang Mode receive, where events that may block the current actor indefinitely suspend all execution of other methods until they complete.

Exclusive methods or blocks currently do not run within a Task, thus celluloid-io or celluloid-zmq probably won't work in this context.

Example

class Worker
  include Celluloid
  include Celluloid::Logger

  def initialize(name)
    @name = name
  end

  def work
    info "Working really hard."
  end

  def wait(name, worker)
    info "Issuing blocking call to #{name}."
    worker.wait_for(1)
    info "Blocking call returned."
  end

  def wait_for(time)
    info "Waiting for #{time} seconds."
    sleep time
    info "Done waiting."
  end
end

w1 = Worker.new("worker_1")
w2 = Worker.new("worker_2")
w1.async.wait("worker_2", w2)
w1.async.work

sleep 3 # Wait until all threads finish.

Running this code gives us:

14:44:53 [worker_1] Issuing blocking call to worker_2.
14:44:53 [worker_1] Working really hard.
14:44:53 [worker_2] Waiting for 1 seconds.
14:44:54 [worker_2] Done waiting.
14:44:54 [worker_1] Blocking call returned.

As you can see, worker_1 starts working immediately after first message issues a blocking call. This can be unexpected if you come from a different background, like Akka or Erlang.

To make actor block until that synchronous request is done, we need to use an #exclusive block. All synchronous requests within #exclusive block actor totally until that sync call returns.

Lets modify part of our code:

def wait(name, worker)
  exclusive do
    log "Issuing blocking call to #{name}."
    worker.wait_for(1)
    log "Blocking call returned."
  end
end

And run it again:

14:47:58 [worker_1] Issuing blocking call to worker_2.
14:47:58 [worker_2] Waiting for 1 seconds.
14:47:59 [worker_2] Done waiting.
14:47:59 [worker_1] Blocking call returned.
14:47:59 [worker_1] Working really hard.

As we can see worker_1 started working only after blocking call has returned.

Different Styles

The exclusive functionality can be used several ways.

  1. At the class level for all methods.
  2. At the class level for specifically named methods.
  3. Within a method as a block.

All Methods

Some classes require that all methods are run in exclusive mode.

class Helper
  include Celluloid

  exclusive

  def initialize
    @array = []
  end

  def add(obj)
    @array << obj
  end

  def delete(obj)
    @array.delete(obj)
  end
end

Named Methods

Sometimes a class wants to present an "exclusive" public interface to the world but it wants to retain the flexibility of calling itself without blocking or causing a deadlock.

class Container
  exclusive :select, :add, :delete

  def initialize
    @array = []
  end

  def select(opts, predicate)
    internal_select(opts, predicate)
  end

  def add(obj)
    @array << obj
  end

  def delete(obj)
    predicate = Proc.new { |s| s == obj }
    @array = @array - internal_select({}, predicate)
  end

  private

  def internal_select(options, predicate)
    name = options[:name] || :any
    size = options[:size] || :any

    @array.select do |obj|
      (name == obj.name || :any == name) &&
      (size == obj.size || :any == size) &&
      predicate.call(obj)
    end
  end
end

Using a list of specific method names seems like a reasonable technique to achieve some functional programming goals (flesh out this idea).

You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.