Gotchas

Eloy Durán edited this page Feb 4, 2016 · 37 revisions

Known Issues

General Considerations

Celluloid is fundamentally a messaging system which uses thread-safe proxies to manage all inter-object communication in the system. While the goal of these proxies is to make it simple for you to write concurrent programs by applying the uniform access principle to thread-safe inter-object messaging, you can't simply forget they're there.

The thread-safety guarantees Celluloid provides around synchronizing access to instance variables only work so long as all access to actors go through the proxy objects. If the real objects that Celluloid is wrapping in an actor manage to leak out of the system, all hell will break loose. See also Thread Safety.

Here are a few rules you can follow to keep this from happening:

Basic rules

NEVER RETURN self (or pass self as an argument to other actors)

in cases where you want to pass an actor around to other actors or threads, use Celluloid::Actor.current, or if you're within an actor itself, you can just call Actor.current as you've included the Celluloid module.

This is a problem that Celluloid shares with Akka (see the warning about 'this' part way down the page) and Go.

If you really need to get a hold of self in order to add instance-specific behavior, e.g for metaprogramming purposes or adding stubs during tests, call MyActor#wrapped_object to obtain the actual object an actor is wrapping. This isn't thread safe in general but is perfectly fine to do if you've just created an actor, hold the only reference to it, and want to do some metaprogramming before using it as part of a test.

Don't mutate the state of objects you've passed as arguments in calls to other actors.

This means you must think about data in one of two different ways: either you "fire and forget" the data, leaving it for other actors to do with what they will, or you must treat it as immutable if you have any plans of sharing it with other actors. If you're paranoid (and when you're dealing with concurrency, there's nothing wrong with being paranoid), you can freeze objects so you can detect subsequent mutations (or rather, turn attempts at mutation into errors). If you want to use freeze, be aware that freeze is shallow and will not freeze all objects referenced by the object you're freezing.

Don't mix Ruby thread primitives and calls to other actors

If you make a call to another actor with a mutex held, you're doing it wrong.

While it's perfectly fine and strongly encouraged to call out to thread safe libraries from Celluloid actors, if you're using libraries that acquire mutexes and then execute callbacks (e.g. they take a block while they're holding a mutex) the guarantees that Celluloid provides will become weak and you may encounter deadlocks.

Use Fibers at your own risk

Celluloid employs Fibers as an intrinsic part of how it implements actors. While it's possible for certain uses of Fibers to cooperatively work alongside how Celluloid behaves, in most cases you'll be writing a check you can't afford. So please ask yourself: why are you using Fibers, and why can't it be solved by a block? If you've got a really good reason and you're feeling lucky, knock yourself out.

Do not create actors prior to forking the process

Please fork prior to creating actors. If you don't, Ruby semantics are both undefined, broken, and can lead to crashes. Specifically it will lead to all actor threads being dead after forking.

Tools such as Rails Spring and DelayedJob employ this pattern to speed-up load time, thus defer creating actors util after preloading has finished.

Blocks

The semantics of blocks in Celluloid can be confusing. Please see the section on blocks for more information.

Tasks & JRuby

Each method call running concurrently on an actor creates a new Task with associated Fiber. E.g.:

class Foo
  def initialize(bar)
    @bar = bar
  end

  def work
    @bar.work # Actor that blocks for 5 seconds.
  end
end

f = Foo.new(some_bar)
5.times { f.async.work } # Now you have 5 tasks.

Celluloid uses this to avoid deadlocks and to run concurrent tasks more effectively (you can disable it with Celluloid.exclusive(&block)). For example celluloid-io uses this concept to manage multiple connected clients in a non-blocking evented fashion.

However due to how Fibers are implemented in JRuby, each Fiber actually spawns new kernel-level thread. Which means:

You have separate Thread.current for each Task.

This is a bit of strange JRuby errata and most likely should be considered a JRuby bug.

This poses funny problems for libs which use it to store thread-local data. In example: ActiveRecord stores connection id in there. That means each Task will check out new connection from connection pool, never checking it in if you don't handle it properly.

I/O Operations you expect not to block are blocking

Include Celluloid::IO if you intend to use evented IO in an actor

By default, a Celluloid::IO socket will fall back to Kernel#select unless the hosting actor has included Celluloid::IO. If you find that you have an IO operation blocking that you don't expect to, make sure that the calling actor is including Celluloid::IO or else it's completely normal that your otherwise-normally-blocking IO operation will block. Just having a handle on a Celluloid::IO socket isn't sufficient for the nio4r non-blocking operations to operate.

You can tell that this is happening if the Task reports itself as :running instead of :iowait. A nio4r-based IO task should appear as :iowait when it's awaiting I/O. If you see the task :running, then it's most likely you forgot to include Celluloid::IO

Testing

RSpec Magic

RSpec caches things created in :let blocks and the subject variable it automatically provides. This can cause some serious confusion as it helps state to leak between tests. It also means that something can be cached with a reference to an Actor which gets terminated, leading to a wide variety of errors.

The best ways to avoid these issues are:

  • Always create/retrieve the Actor under test at the start of the test; don't rely on subject or even an actor reference created in a Before block.
  • Before each test, call Celluloid.shutdown; Celluloid.boot
  • Look at the notes elsewhere on the wiki about using #wrapped_object to get a reference to actors and not their proxy objects.
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.