Thread safety for your Rails
Published almost 6 years ago

Rails 2.2 marks the first release of thread safe Rails. But “thread safety” alone, without any context, doesn’t mean shit. When people say Rails is “thread safe” ( or otherwise ), they usually refer to the dispatching process of Rails. Before 2.2, Rails dispatching looked like :

@@guard.synchronize do
  dispatch_unlocked
end

And now it looks somewhat like :

dispatch_unlocked

Long story short, Rails can now serve multiple requests in more than one ruby threads ( or native threads if you’re on JRuby ) parallelly. Charles Nutter has done a good job of explaining the details here.

Should you give a flying fuck ?

You totally should if :

  • You’re using JRuby
  • You’re bold enough to play around with bleeding edge Neverblock stuff
  • Your application has a lot of long running processes, which are not heavy on blocking IO ( this would be rare I imagine )

You totally should NOT if :

  • You’re using Event based mongrel, thin or any of the event based web server in production. Event based servers don’t use Threads, so it just doesn’t matter.
  • You CBA

You may have heard a bunch of hype about how threads make everything 100x faster, this is far from the truth. Don’t believe everything the hype merchants want to sell you, test your application first and see if it helps.

Koz’s comments sums it up nicely :

I think the more interesting issue to consider is whether your application will benefit from ‘threaded dispatching’ at all.

The performance of green threads in ruby is kind of disappointing, as are the number of different options which block the interpreter. IO, regexps, calling most native libraries, etc. Odds are with matz’s ruby you’re infinitely better off using passenger + ruby enterprise edition than ruby threads.

JRuby is another matter altogether, and it’s jruby users who should be most excited about this stuff, and the most willing to help us iron out any last bugs.

Prepare your mongrels first

Currently, you’ll need to manually patch Mongrel’s built in Rails handler for testing multithreaded dispatching. I’ve submitted a patch to mongrel and hopefully there’ll be a new gem version of mongrel soon. In the mean time, monkey patch FTW.

How to enable multi threaded dispatching ?

Just put the following lines in your production.rb

config.threadsafe!

However, that’s not enough. There are some consequences if you have never made sure to write thread safe code. They are, however, simple to fix. Usually.

Ruby’s require is not atomic

What this means is, if in Thread A you require a file named whatever.rb in which defines a class called Whatever, the class Whatever can be visible from Thread B even before Thread A has finished loading whatever.rb. And because of this ruby behavior, Rails now preloads everything inside app directory.

config.threadsafe! also disables automatic loading by ActiveSupport::Dependencies.

ActiveSupport::Dependencies uses ruby’s const_missing hook to load files automatically for you, whenever possible. For example, if you have following file inside your application’s lib/ directory :

# hello.rb
class Hello
  def world
    "hello world"
  end
end

Rails has traditionally saved you the trouble of requring that file manually inside your application. Whenever you access Hello ( Hello.new for example ) constant for the first time, ActiveSupport::Dependencies loads hello.rb for you automatically. Note that this is only possible if the file name matches the class name that it defines.

But as this behavior is disabled when you calls config.threadsafe!, you’ll now need to require the file hello.rb manually before Rails starts serving the requests ( typically inside environment.rb or an initializer ).

Alternatively, you can just add lib/ directory to eager load paths. The following inside production.rb will do that :

config.eager_load_paths << "#{RAILS_ROOT}/lib"

And that will make Rails preload everything inside lib/ directory.

Don’t mess with class variables

Imagine your controller having a code that does :

class HomeController < ApplicationController
  @@visits = 0
  
  def index
    @@visits += 1
    render :text => @@visits
  end
end

This code is not safe if you enable multi threaded dispatching. All your instance methods ( actions in case of controllers ) should only read global values ( $vars, @@vars, class instance variables ) and never modify them.

Here’s a better example which would explains the consequences as well :

class HomeController < ApplicationController
  before_filter :set_site
  
  def index
  end
  
  private
  
  def set_site
    @site = Site.find_by_subdomain(request.subdomains.first)
    if @site.layout?
      self.class.layout(@site.layout_name)
    else
      self.class.layout('default_lay')
    end
  end
end

What happens here is :

  • Before filter set_site uses subdomain to populate @site instance variable
  • It also sets the layout to @site.layout_name is not nil

Imagine your application has two possible subdomains :

  • foo – has a layout called ‘foo_lay’
  • bar – has no layout. Uses default layout ‘default_lay’

When you call self.class.layout(value), Rails will store the value inside a class variable @@layout_, which causes a race condition if called from multiple instance methods in different threads. Wikipedia pagecondition will do a better job of explaining what is a race condition if you have never bothered about it before.

Let us assume that two users are accessing the application : UserA and UserB. UserA’s request is served by Thread1 and UserB’s request is served by Thread2. Here, numbers also represent the order in which these events occur :

  1. Thread1 : UserA visits http://foo.site.com/home
  2. Thread1 : HomeController#set_site calls self.class.layout(@site.layout_name)
  3. Thread1 : This sets HomeController#@@layout to ‘foo_lay’
  4. Thread2 : UserB visits http://bar.site.com/home
  5. Thread2 : HomeController#set_site calls self.class.layout(‘default_lay’)
  6. Thread2 : This sets HomeController#@@layout to ‘default_lay’
  7. Thread1 : Request is done executing action code. Time to send back the response to UserA.
  8. Thread1 : Rails calls HomeController#render
  9. Thread1 : HomeController#render uses the value of HomeController#@@layout to render the final output html
  10. Thread1 : As the value of HomeController#@@layout was modified by #6 to ‘default_lay’, #9 will uses ‘default_lay’ even if the expected layout was ‘foo_lay’

The thread safe way to write this code is :

class HomeController < ApplicationController
  before_filter :set_site
  layout :site_layout

  def index
  end
  
  private
  
  def set_site
    @site = Site.find_by_subdomain(request.subdomains.first)
  end
  
  def site_layout
    if @site.layout?
      @site.layout_name
    else
      'default_lay'
    end
  end
end

When you use layout :site_layout, Rails will use the return value of site_layout instance method to determine the layout, which makes it a thread safe way. Please note that this is not the same as calling layout ‘something’. If you pass a string to the class method layout, Rails will use the passed value as the layout.

( Example inspired from Dynamic Layouts Railscast )

Getting dirty with Thread.current

if you must, you can always use Thread local variables as the last resort. Ruby provides you with a magical hash Thread.current[] inside any executing thread, where you can store variables accessible anywhere from inside that specific thread. Really, you can check this docs

The following code :

threads = []

threads << Thread.new do
  Thread.current[:hello] = 1
  sleep 2
  puts "From T1 : #{Thread.current[:hello]}"
end

threads << Thread.new do
  Thread.current[:hello] = 10
  puts "From T2 : #{Thread.current[:hello]}"
end

threads.each {|t| t.join }

will produce :

From T2 : 10
From T1 : 1

You might have seen this in use in with any current_user hacks : Here or here. But it’s still a hack.

If you’re familiar with Rails source ( of interested in being familiar ), you can find Rails using Thread.current[] at several places : Thread.current[:time_zone] or Thread.current[‘query_cache’]. I18 gem uses Thread.current[:locale] to store the value of locale specific to the thread.

But as I said earlier, Thread.current should be used as a last resort only.

Good ol’ Mutex

There is always the big fat mutex which can be slapped around a piece of code that you want to execute exclusively per thread. You should check the wikipedia page if you’re looking for some explanation :

class HomeController < ApplicationController
  @@lock = Mutex.new

  def index
    @@lock.synchronize do
      thread_unsafe_code
    end
  end
  
  private
  
  def thread_unsafe_code
    if @@something == 'hello'
      do_hello
    elsif @@something == 'world'
      do_world
    else
      @@something = 'nothing'
    end
  end
end

This ensures that only one thread can be executing thread_unsafe_code() method at any given point in time. Other threads will block and wait indefinitely for the executing thread to release the lock acquired by @@lock.synchronize.

Common concerns

Adam Hooper raised three valid concerns :

  • What is the likelihood that there is broken code in the Rails core, despite the word “threadsafe!”? If Java frameworks (engineered in a language which, unlike Ruby, was built from the ground up with threads in mind) can provide premonitions, is it not safe to assume Rails is painfully buggy in this regard?

Chances of thread unsafe code being in Rails are close to none. There have never been anything inherently thread unsafe about Rails codebase. If some people had you think otherwise, you listened to the wrong bunch of people/FUD. We’ve had a list of thread unsafe code inside Rails for a long time, and it was a small list.

However, thread safety is like a Random Number Generator – You can never be sure

  • All existing plug-ins are thread-unsafe until proven otherwise, right? (And IMO Rails developers should broadcast this at the top of their voices, because promising thread-safety and relying on other people to provide it is shooting oneself in the foot….)

Jeremy says : No. They are not. Most, in fact, are probably threadsafe. Your claiming this is a major issue is a fairly good indicator that you don’t actually know the core issues with thread safety in Ruby/Rails.

Me : That’s not true. At least all the plugins that I use, are thread safe. Having said that, you should never use a plugin without getting yourself familiar with it’s code base.

  • Does anybody think thread-safe Rails will ever be suitable for production use? It’s hard enough to convince a project manager to consider it already, with its current, much, much, much, MUCH simpler model.

Short answer, don’t jump the ship if you can’t be bothered about ensuring your code is thread safe. Always stick to “Simplest thing that works” motto IMO. You could just spend some time researching if running multithreaded Rails is going to benefit your application/business at all or not and evaluate that against the risk/time involved.

But that doesn’t make threadsafe Rails unsuitable for production use. It makes your specific application/team unsuitable for using thread-safe Rails in production mode. Multi threaded programming has never been easy. However, if you write good OO code, thread safety usually comes for free.

UPDATES :

  1. Added Mutex section.
  2. Added ‘Common concerns’
  3. Added ‘Should you give a flying fuck ?’
  4. Added ‘Prepare your mongrels first’